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

@@ -16,6 +16,6 @@ Scoped-context registration primitive. `createScope(ctx, key)` mints a Cordis co
## Design contract
Ownership and visibility derive from ONE fact — which context a registration went through. An explicit `{ scope }` registration parameter could express "visible to X, disposed with Y", which is almost always a bug; the scoped context makes it unrepresentable. Rationale and alternatives: [the agent-scope RFC](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md).
Ownership and visibility derive from ONE fact — which context a registration went through. An explicit `{ scope }` registration parameter could express "visible to X, disposed with Y", which is almost always a bug; the scoped context makes it unrepresentable. This is trusted registration and listener routing, not sandboxing or an authority hierarchy: a same-process plugin is not confined, and a child scope need not be a subset of its parent's view. Rationale, alternatives, and the security non-goal: [the agent-scope RFC](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-explicit-non-goals).
Handing out a scoped context hands out the minting plugin's service-resolution capability (resolution walks the minting fiber's dependency chain, not the holder's) — mint scopes from a plugin whose `inject` surface is what scope holders should reach.
Handing out a scoped context hands out the minting plugin's service-resolution surface (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve.

View File

@@ -27,8 +27,8 @@ import { Context as CordisContext } from 'cordis'
// Capture the invocation primordials once. A carrier holder can reach the
// composed Context.filter function, so neither that function's mutable
// property surface nor a base filter's own `.call` may choose how isolation
// predicates are invoked.
// property surface nor a base filter's own `.call` may choose how listener-
// selection predicates are invoked.
const reflectApply = Reflect.apply
// eslint-disable-next-line @typescript-eslint/unbound-method
const functionCall = Function.prototype.call
@@ -131,7 +131,7 @@ function scope(): void {}
* Service resolution through the scoped context flows through the minting
* plugin's dependency chain (the fiber walk), regardless of what the eventual
* holder's own fiber injected — handing out the scoped context hands out that
* capability; see `Agent.ctx` in `@deepseek-ai/dsh-agent` for the harness's
* dependency surface; see `Agent.ctx` in `@deepseek-ai/dsh-agent` for the harness's
* contract.
* @param ctx - the context to mount the scope under; its fiber must be active
* (a disposing owner throws Cordis's INACTIVE_EFFECT), and its plugin's
@@ -201,7 +201,7 @@ function isConstructable(value: (...args: unknown[]) => unknown): boolean {
* compatibility default: plain plugin listeners see every subject), or
* - its tag IS `key` (a scoped listener seeing exactly its own subject),
*
* AND `base`'s own filter (a Cordis `Service`'s isolation check) also admits
* AND `base`'s own filter (a Cordis `Service`'s listener-filter check) also admits
* it. Both the captured base filter and the composed filter are invoked
* through captured JavaScript primordials, so mutating either function's
* public `.call` property cannot bypass either predicate. Dispatching with
@@ -263,7 +263,7 @@ export function scopeTarget<T extends object>(base: T, key: ScopeKey | undefined
// construction, a base-target proxy would therefore silently replace the
// composed scope predicate with the caller's filter. The surrogate owns the
// two immutable overlay slots, so later descriptor changes on `base` cannot
// affect isolation. It shares the base prototype and delegates ordinary
// affect listener selection. It shares the base prototype and delegates ordinary
// reads/writes/keys to preserve the supported transparent shape. Callable
// targets use native bound built-ins so V8 contributes no user-code surface;
// the chosen built-in matches whether `base` has [[Construct]], and the traps