# Agent Configuration — Contributor Rules
Scope: `desktop/src/features/agents/` (config surfaces, shared config renderer,
and the agent config core). Read this before changing how harness / provider /
model / effort configuration is modeled, rendered, persisted, or applied.
Plan of record: `Buzz/Harness-Provider-Model.md` in Morgan's Obsidian vault
(PR sequence, decisions log). PRs: #2140 (rename), #2148 (flag reduction),
#2156 (honest model states), #2158 (Agent Config Core).
## The one rule
**Harness capability facts have exactly one source: the Rust runtime catalog.**
`KnownAcpRuntime` (`desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs`)
declares each harness's model/provider/effort env keys and capabilities. Spawn
applies them; `AcpRuntimeCatalogEntry` exposes them over IPC; and
`lib/agentConfigCore.ts` projects them into field descriptors. The frontend
never maintains a rival copy of this table. Setup guidance follows the same
rule: `requires_external_cli` is derived from `KnownAcpRuntime` and projected
to the UI rather than inferred from a runtime ID in a component.
**Second metadata source: command-keyed execution policy.**
`harness_max_parallelism` (`managed_agents/parallelism.rs`) maps the harness's
static command string to a spawn-time cap (`OPENCLAW_MAX_PARALLELISM = 5` for
OpenClaw). This cap is not a `KnownAcpRuntime` field because it applies to
preset harnesses (like OpenClaw) that are not in the builtin catalog. It is
projected onto `AcpRuntimeCatalogEntry.max_parallelism` by all four
catalog-producing constructors (builtin discovery, preset catalog, custom
discovery, custom-save response) using the **static definition command**, not
the resolved `entry.command` (which may be `null` for unavailable entries).
The frontend reads `maxParallelism` from the catalog entry and never keeps a
separate constant.
If you need a new capability fact (a new env key, a native option, a "supports
X" flag): add it to `KnownAcpRuntime` first, expose it on
`AcpRuntimeCatalogEntry`, then project it through the core. Do not shortcut
with a TypeScript lookup table or an id comparison in a component.
## Rules
1. **No hardcoded harness-ID checks in render code.** `runtime.id === "claude"`
belongs in `deriveAgentConfigFieldModel` (once, with a named reason), never
in a component. Components ask the field model what exists
(`hasRenderableAgentConfigField`, `getRenderableEffortField`).
2. **Effort reads/writes go through the descriptor.** Use the effort
descriptor's `currentPersistence` key — never a raw
`BUZZ_AGENT_THINKING_EFFORT` literal in UI code. `currentPersistence` is
where the value lives *today*; `targetApplication` is how the harness
*should* receive it. They intentionally differ until PR 2.7 migrates
Goose/Claude — do not "fix" one to match the other without doing the
migration work.
3. **Field absence has a named reason, not a boolean.** Codex effort is
`ownedByModelId`; Claude effort is `deferredUntilNativeOptionsAvailable`.
New absences get new named reasons in `AgentConfigOmission` /
`render` — never a `showX` prop.
4. **The clearing policy is the named types.** `onContextChange:
"resetDependentValues"` (user changed harness/provider → dependent values
reset everywhere) vs `onCatalogMismatch: "explainOnly" | "onboardingCleanup"`
(an async catalog miss never silently erases saved state outside
onboarding's named cleanup). Do not add mutation booleans like
`clearInvalidModel`; extend the policy types.
5. **"Metadata unknown" ≠ "harness lacks the capability".** Passing
`runtime: undefined` to the core means fields won't render. Surfaces must
gate on the runtime catalog query settling (loading/error states) rather
than letting fields silently vanish — see `AgentDefaultsEditor` /
`DefaultConfigStep` for the pattern.
6. **One canonical behavior, disclosure presets for visibility.** Behavior
flags were deliberately killed in #2148 (`CANONICAL_CONFIG_BEHAVIORS`).
Surface differences are expressed via the `disclosure` preset, not new
boolean props. **Exception:** `onboarding-essential` hides happy-path
helper copy (provider/model descriptions) but a non-null model-discovery
status always bypasses the preset and renders the status line — enforced
via `shouldShowModelStatusMessage()` (`AgentConfigFields.tsx`).
Additionally, a successful discovery response that yields no usable options
(`supportsSwitching:false` or empty model list) synthesizes a warning status
via `synthesizeEmptyDiscoveryStatus()` and is intentionally **not cached**
so that closing → reopening the dialog re-runs discovery after the user
installs or signs into the CLI (`isCacheableDiscoveryResponse()`).
7. **Onboarding setup detects readiness; it does not select defaults.** The
setup page derives visible and ready harnesses from the runtime catalog and
only offers install or sign-in actions. The following defaults page is the
sole onboarding surface that chooses `preferred_runtime`. Its complete draft
lives in machine-onboarding session state, so Back performs no write and
restores even incomplete edits when the user returns. Skip abandons that
draft and advances with zero config writes. Next is the only persistence
boundary: it consumes the shared renderer's `onValidityChange` signal,
disables editing while awaiting `set_global_agent_config`, advances only on
success, and leaves the draft in place with a retryable inline error on
failure. A harness selection alone does not enable Next when the harness
requires provider/model/credential config (e.g. buzz-agent with no
provider). Baked build env and runtime-file config satisfy the gate. Drafts
intentionally do not survive an app restart.
`onboarding-agent-defaults.spec.ts` is the acceptance gate for anything
touching this flow or the shared renderer.
8. **Omit the Model control only after a confirmed successful empty
discovery on an optional-model harness.** When the field model marks model
as `acpNative` (Claude Code / Codex), `shouldRenderModelControl` hides the
picker while discovery is in flight and after IPC resolves with no usable
options (`modelDiscoverySuccessfulEmpty` / `isSuccessfulEmptyDiscovery`).
A thrown or unavailable discovery keeps the control so #2246 failure UI can
render, and must not heal/clear persisted model or effort. Full disclosure
still shows the control when Custom model is available. Required-model
harnesses always keep the field. Gate: `defaults hides model when optional
harness has empty discovery` (and the failed-discovery counterpart) in
`onboarding-agent-defaults.spec.ts`.
9. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
preset therefore begins at Provider for Buzz Agent, then reveals Model,
Effort, and Advanced only after a provider is configured. Harnesses whose
runtime metadata has no provider field skip that gate. Reveals animate their
height through Motion and become immediate when reduced motion is requested.
Once the Advanced toggle is visible, its expanded state is exclusively
user-controlled: provider, harness, and required-env changes must never
open it automatically in defaults, create, or edit flows. In Create mode,
`Run on` belongs in Advanced directly after **Who can send instructions**;
keep it out of the basic create fields. The defaults summary follows
preferred-harness changes saved while the dialog is open, and its configured
state includes required credentials as well as provider/model values. If no
available harness can resolve, Create starts in Customize and lets unavailable
catalog entries be selected only to expose their setup guidance; submission
remains blocked.
Advanced-only required credentials and incomplete remote **Run on** setup
mark the collapsed Advanced toggle without opening it, and block incomplete
saves.
Runtime-file credentials satisfy Global Defaults just as they do Create and
Edit. In Edit,
selecting Custom command keeps its required command field beside the harness
picker rather than hiding it in Advanced.
10. **Catalog visibility is community-scoped relay state, never a global
definition field.** `AgentDefinition.shared` is only the active
relay+owner projection returned to the UI. Durable heads and pending
publications live in the scoped retention database, and explicit share
toggles await relay acceptance before the UI claims that an agent was
published or removed. A queued update must stay visibly queued, and the
catalog itself must render only relay-confirmed publications — never an
optimistic local persona.
11. **Shared agent access names the consequence where it is selected.** The
shared respond-to field shows a persistent warning whenever `anyone` **or**
`allowlist` is selected — both hand the host's access to someone other than
the owner, so both disclose it and only the audience phrase differs. This
covers persona-backed create and edit surfaces. Keep that disclosure in
the shared field instead of adding surface-specific flags. It renders
directly below the selector for `anyone` but *after* the people picker for
`allowlist`, so it never sits between the user and the selection they came
to make. The copy leads with the audience ("Anyone can use this agent to
access…") so it reads as a warning rather than an explanation, and stays one
sentence — don't split the mechanism into a second sentence. Both the machine
and the stakes it names come from `lib/agentAccessWarning.ts`, keyed on an
optional `runLocation`: instance surfaces resolve it from
`ManagedAgent.backend` via `runLocationForBackend`, and the create flow from
`WhereToRunDraft.runOn` via `runLocationForRunOn`. `AgentDialog` is the one
place that resolves it for dialog surfaces and publishes it through
`ui/AgentRunLocationContext.tsx`; the field reads that context and lets an
explicit `runLocation` prop win. Do **not** thread the value as a prop
through `AgentDefinitionDialog` / `AgentInstanceEditDialog` — both are
already over the 1000-line ceiling, and neither uses the value itself.
Surfaces rendered outside `AgentDialog` (e.g. `EditRespondToDialog`) pass the
prop directly. Local names "your
computer, including files, accounts, and connected tools"; remote names "the
server it runs on, including any accounts and tools available there" —
deliberately *not* the owner's files, which aren't theirs to describe on a
host they don't own. **An unknown location falls back to the local wording —
never hedge with "computer or server".** A remote host requires an
installed `buzz-backend-*` provider, and without one `WhereToRunSection`
never renders, so "server" would name a concept the owner has never been
shown; when it *is* remote they picked that host from the selector
themselves. Never synthesize a run location a surface doesn't have. Don't
expose `respond-to`, `allowlist`, Nostr, or harness jargon in primary UI
copy. **The owner-only-access build capability is backend-independent.** When
`getAgentAccessOwnerOnly()` is true, every managed agent's access control is
locked to owner-only, including provider-backed agents. A provider backend
does not prove remote execution and must never create a policy carve-out.
12. **Shared instructions must be reviewable byte-for-byte.** Agent definitions
execute their `system_prompt` verbatim, so catalog and snapshot review
surfaces render the literal prompt, never the chat Markdown projection
(which can conceal spoilers, link destinations, and image sources). Reject
Unicode default-ignorable, bidirectional-formatting, and non-layout control
characters at both the untrusted catalog parser and the Rust persistence /
import boundary. Do not silently strip them: rejection keeps the reviewed
string identical to the executed string. New sharing paths must reuse the
same validation before they persist or activate a definition.
13. **Profile runtime sections render only reported agent data.** Missing
runtime, model, status, command, MCP, advanced, or diagnostics values stay
absent in every build mode. Do not fill profile or agent-panel gaps with
development/staging examples, preview controls, or synthetic configuration;
those values can be mistaken for the viewed agent's real configuration.
Configuration rows show the effective value regardless of whether it came
from an explicit choice, global default, config file, or runtime override.
Do not add provenance lines, shadowed/struck-through values, pre-start
placeholders, or whole-section dimming; use an em dash for an unknown value.
Info, activity, agent-configuration, and model-setting rows use the same bare
16px leading-icon treatment as agent management actions. Keep semantic icons
visible in profile variants and do not wrap them in background shapes. An
owned agent profile is entry-point invariant: opening the same deployed
agent from Agents, a DM, or a channel must expose the same actions, tabs,
fields, and profile-wide activity selection. Caller context may control the
panel shell or return navigation, but must not filter or replace profile
content.
## The tests that enforce this
- `lib/agentConfigCore.test.mjs` — field model per harness × scope, clearing
policy. Update when the capability model changes.
- `ui/agentConfigFieldsContract.test.mjs` — canonical behaviors + disclosure
presets + `shouldShowModelStatusMessage` status-bypass +
`shouldRenderModelControl` (successful-empty omit vs failure keep). If this
fails, you probably reintroduced a per-surface flag or conflated empty with
failed discovery.
- `ui/usePersonaModelDiscovery.test.mjs` — `synthesizeEmptyDiscoveryStatus`,
`isCacheableDiscoveryResponse`, `deriveModelDiscoveryPending`,
`isSuccessfulEmptyDiscovery`. If the "reopen to retry" copy becomes inert
again, these tests will catch it.
- `ui/respondToFieldContract.test.mjs` — plain-language mode labels, the
persistent warning contract for shared agent access, and its two render
positions (after the people picker for `allowlist`).
- `lib/agentAccessWarning.test.mjs` — every mode × run-location copy variant
plus both resolvers, including unknown-reads-as-local and
blank-`runOn`-is-not-a-provider.
- `lib/personaCatalogRelay.test.mjs` and
`ui/personaCatalogOwnerLabel.test.mjs` — reject invisible definition text
and keep Markdown concealment syntax literal in the review surface.
- `../profile/ui/UserProfileRuntimeContent.test.mjs` — profile runtime panels
cannot reintroduce build-mode previews or synthetic fallback controls.
- `desktop/tests/e2e/profile.spec.ts` — the owned-agent parity flow compares
every profile tab when opened from Agents and from the agent's DM.
- `ui/AgentConfigPanelPresentation.test.mjs` — shared profile/agent config rows
show only effective values, with an em dash for unknown values.
- `desktop/tests/e2e/onboarding-agent-defaults.spec.ts` — onboarding behavior
acceptance coverage for readiness, failure states, defaults, session-draft
restoration, zero-write Skip, Next save failure/retry, navigation, and
successful-empty vs failed optional-model discovery.
- Rust: `runtime_metadata_env_vars` tests pin spawn-time key application.
- Rust: persona sharing/retention tests pin relay+owner scoping, durable
enqueue errors, relay rejection/unavailability, and accepted publication.
- Rust: `definition_validation` and inbound persona tests pin the shared
Unicode/control-character policy at local, import, publish, and sync gates.
## Keep this file true
**If you change how agent configuration is modeled, rendered, persisted,
applied, or cleared — update this file in the same PR.** A rule that no longer
matches the code is worse than no rule; a new pattern that isn't written down
here will be broken by the next agent that never learns it existed. Reviewers:
treat a config-behavior diff without a matching AGENTS.md diff (or an explicit
"no rules changed" note) as incomplete.