Skip to content

[design] Capability constraints — declare what an agent may PASS, and enforce it at the tool boundary #404

Description

@serge-ivo

The primitive underneath #402 and #75. Direction written up in docs/capability-constraints.md (this ticket is the build).

The gap in one line

An agent's capabilities can say which tools it has. They cannot say what it may pass to a tool it has — and every existing constraint is enforced by taking the tool away.

Declaration Enforced by
capabilities.tools toolNamesFor — undeclared tool not offered, refused if called
surfaceOptions.coding.drive: false agent-do-tools.ts:200 removes send_to_cli/read_terminal from the set
surfaceOptions.coding.repos: "single" the console hides add-repo
write consent (#90) runRegistryTool refuses at dispatch

Withholding is a complete answer only while "may this agent do X" and "does it have tool T" are the same question. They stop being the same the moment one tool serves several resources:

Build

1. Vocabulary — extend surfaceOptions, do not invent a sibling. lib/surface-options.ts is already the creator-declared, sanitised, persisted, surface-gated constraint map, and its header argues the case for that shape. Add per-connector constraint specs; absent = today's behaviour byte-for-byte.

2. Resolution — with the rest of capabilities. sanitizeDeclaredCapabilities validates against a closed vocabulary per connector (the same discipline as steps, behaviour fields, stats sources, board actions). A creator may narrow; a subscriber may never widen — the ceiling is a catalog claim and lintAgentClaims (#362) checks descriptions against it.

3. Enforcement — runRegistryTool, at dispatch. Beside the write-consent gate, which is already the place a call is refused for reasons the model does not control.

This point is genuinely new, not a repair: optionsFor is consulted in exactly four places today (agent-do-tools.ts:200,215, agent-self-description.ts:162, coding-brains.ts:185,204) and none of them is runRegistryTool — because every existing constraint is applied before dispatch is reached.

It must be server-side. Two independent reminders this month that a boundary a model can talk around is not a boundary: the agent that wrote its own tool results and was believed (#395), and the mute guard whose first proposed fix would have deleted the capability it protected (ADR 0001). A prompt is a request; a gate is a fact.

4. First consumer in the same PR — terminal backends. Per the extraction rule this codebase already follows (work-card.ts: "extracted when the second domain arrived, not before"). Do not merge the primitive with no consumer.

Acceptance

  • A creator declares a constraint; it is sanitised, persisted, and resolved with capabilities.
  • A subscriber can narrow or bind within it and cannot widen it — with a test for the widen attempt.
  • A call carrying an out-of-scope argument is refused in runRegistryTool, with a message naming which constraint applied — tested at the dispatcher, not at the console.
  • Absent constraints leave every existing agent behaving identically.
  • Consent and constraints compose: consent granted + argument out of scope = refused.

Explicitly not

Blocks/serves: #402 (terminal ceiling + single-target binding), #75 (browser domain/action allowlist — second consumer, demand-gated; it should inherit this rather than grow its own).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions