fix(scope): align trust and input boundaries

Rewrite the agent-scope RFC with executable examples and an explicit security non-goal. Harden subagent scalar and depth validation, and pin live tool-filter semantics across code, tests, and generated docs.
This commit is contained in:
Tianyi Cui
2026-07-12 11:17:57 +08:00
parent d427478c44
commit cb03c8c284
29 changed files with 545 additions and 105 deletions

View File

@@ -78,7 +78,7 @@ interface SubagentRun {
## The provider seam: `SubagentProvider`
One transport for running a child agent. Implementations register under a unique name via `SubagentService.registerProvider`; multiple coexist in one context. The service validates every requested start-time capability before calling `start`, so an implementation may assume e.g. `request.maxDepth` is honorable when present. `inheritsParentContext` is a DESCRIPTIVE fact beside the capabilities (nothing validates against it): whether a child sees the parent conversation (`fork`: true, `spawn`/`acp`: false) — the model-facing consumer derives truthful tool wording from it.
One transport for running a child agent. Implementations register under a unique name via `SubagentService.registerProvider`; multiple coexist in one context. The service validates every requested start-time capability before calling `start`, so an implementation may assume e.g. `request.maxDepth` is honorable when present. `inheritsParentContext` is a DESCRIPTIVE fact beside the capabilities (nothing validates against it): whether a child sees the parent conversation (`fork`: true, `spawn`/`acp`: false) — the model-facing consumer derives truthful tool wording from it. It describes conversation history only, not tool registrations, injected services, or authority inheritance.
```ts type-equiv
interface SubagentProvider {
@@ -93,7 +93,7 @@ The service (`ctx.subagents`) emits `subagent/start` only after `run.started` fu
## In-process backends: depth and seed
The two in-process backends ([dsh-subagent-spawn](../../packages/subagent/subagent-spawn) fresh, [dsh-subagent-fork](../../packages/subagent/subagent-fork) seeded) run the child as a child `Agent` on the same application. They synchronously snapshot caller-owned data, install provider ownership before attaching the abort listener, create one run-owner fiber under `parent.ctx`, and invoke the factory through that fiber: parent teardown, provider teardown, and manual run disposal share the same pre-publication ownership and quiescence boundary, while the child still receives a flat new scope rather than inheriting the parent's capabilities. Their `started` promise projects the factory's successful publication and the result driver awaits that same promise before sending the prompt. Two pieces of vocabulary ride on the existing agent/session types rather than new core types:
The two in-process backends ([dsh-subagent-spawn](../../packages/subagent/subagent-spawn) fresh, [dsh-subagent-fork](../../packages/subagent/subagent-fork) seeded) run the child as a child `Agent` on the same application. They synchronously snapshot caller-owned data, install provider ownership before attaching the abort listener, create one run-owner fiber under `parent.ctx`, and invoke the factory through that fiber: parent teardown, provider teardown, and manual run disposal share the same pre-publication ownership and quiescence boundary, while the child still receives a flat new scope rather than inheriting the parent's registrations. Their `started` promise projects the factory's successful publication and the result driver awaits that same promise before sending the prompt. Two pieces of vocabulary ride on the existing agent/session types rather than new core types:
- **Delegation depth** is a merge-extensible `AgentOptions.subagentDepth` field (`0` for a top-level agent, parent + 1 for a child). The seam owns it — the loop neither sets nor reads it — so a nested spawn reads its parent's depth from `parent.options.subagentDepth` and the `depthLimit` capability caps the tree by refusing a child whose depth would exceed `request.maxDepth`.
- **Delegation depth** is a merge-extensible `AgentOptions.subagentDepth` field (`0` for a top-level agent, parent + 1 for a child). Only `undefined` means top level; every stored present value must be a non-negative safe integer. The seam owns it — the loop neither sets nor reads it — so a nested spawn validates its parent's stored depth, rejects a derived child depth outside the safe-integer domain, and applies a defined absolute `request.maxDepth` cap to that child.
- **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `resume` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).