--- id: subagents title: A subagent is a context, so it gets an account status: open crates: [didbot-pds, didbot-agentd] dependsOn: [credentials, node] exitCriterion: > Two subagents of one session write records signed by two different accounts, each account's registration names the context that spawned it, and a deployment that wants one account per session instead sets that in configuration rather than in code. --- # subagents One session is one context. A subagent's context is its own and ends with its task, so it is a different entity from its parent and from its siblings — not a role its parent plays. It gets an account. This reverses what [credentials](credentials.md) and [node](node.md) each stated: that the custody boundary is the session and subagent identity is attribution. That boundary was never a claim about trust. It was a claim about delivery — session-scoped environment cannot narrow below a session, so a credential could not either, and the rule described the mechanism rather than the model. [cred-delivery](cred-delivery.md) removes the mechanism's limit. ## What an account buys, and what it does not - **Revocability.** A context's binding is dropped in the daemon without ending the session that spawned it. - **Attribution that survives.** The writer is the signer, so nothing has to be tagged onto each record and nothing has to be believed. Lineage is stated once, at provisioning, in `bot.did.registration`'s `#agent` member, which already carries `harness`, `agentType`, `session` and `parent`. - **Not narrower authority.** An agent's credential is all-or-nothing over its own repository; what a context may write is decided by [policy](policy.md). A subagent account restricts what a subagent *is handed*, never what it could reach by acting as the session instead. That last point answers this epic's old open question, and it holds whatever harness is underneath: a subagent runs inside its parent's process and filesystem, so the boundary is reported rather than enforced. ## Reported, not enforced — and that was always true `agent_id` arrives on the harness's own report of what it spawned. Nothing proves the subagent process is isolated from its parent's memory or its siblings'. What is worth keeping is that `session_id` is exactly the same kind of claim from exactly the same bookkeeping. Sessions were never better attested than subagents, so admitting subagents lowers nothing. What both rest on is the node the [handshake](handshake.md) admitted, and `didbot-attest` says as much: read a provenance as "a machine we admitted asked for this", never as "this agent is who it says it is". ## The identifier is not reliable, and disk is The harness's `agent_id` has been observed arriving wrong: one subagent's consecutive tool calls under two different ids, and a single stray id collecting calls from more than one real subagent. A stray id would mint an account for a context that never existed, or worse, sign several agents' work under one name. - [ ] **Confirm a context against the harness's own transcript before minting for it.** The harness writes each subagent's calls to that subagent's own file, so which agent made a given call is recoverable from disk with certainty — including for two byte-identical concurrent commands, where nothing derived from the command itself can tell them apart. Measured: the hook's `agent_id` agreed with the transcript on every call observed, and the row landed within a tenth of a second of the hook firing. - [ ] **Decide what happens when they disagree.** Disk is authoritative, so the write is either attributed to the agent disk names or refused. What it must not be is attributed to the agent the hook named. - [ ] **Record disagreements before enforcing on them.** The rate is unknown outside a single report, and a first deployment that blocks on it measures nothing. ## Cost, measured Two days on the owner's machine: 16 sessions, 167 subagent invocations, all of one type. Only five sessions spawned any, and 161 of the invocations came from one project — bursty, and concentrated where the work is. - [ ] **Bound what is in flight, and publish the number.** A fan-out of parallel subagents all acting at once is not allowed to become an unbounded provisioning queue. The cap doubles as the bound on how much work an unadmitted caller can ask for, and the depth is a health number rather than a constant nobody can see. See [node](node.md). - [ ] **Per-type accounts are the wrong grain.** One account reused across every subagent of a type is cheaper and denotes nothing: two invocations share no context, so they are not one entity. - [ ] **Granularity is configuration, with per-context the default.** [account-types](account-types.md) already has the kind supply defaults and the account carry values, so a deployment that wants one account per session sets it rather than patching this decision out. The instability above is a second reason to want that switch, and a reason its default should be revisited once there are numbers. ## Done - [x] **The decision this epic existed to make.** A subagent gets its own DID and its own account. What tipped it was not cost — a wildcard DNS record already removed the per-account DNS write this epic priced, and a monotonic name suffix removes the pool limit — but that the alternative denotes something untrue. A parent's account signing a subagent's work records the wrong author. - [x] **What a subagent account is a credential for.** Revocation and attribution, not scope. Narrowing is [policy](policy.md)'s, at the write. - [x] **Whether a harness lets a subagent's identity be enforced.** No harness examined does: a subagent shares its parent's process and filesystem, and what arrives is the harness's report. Recorded here rather than left as a question, because every design above is built on top of it.