/** * Capability ids: their grammar, and the two authority predicates over them. * * Split out of `delegate.ts` when ADR-0024 pushed that file past the 400-line ceiling and * `test/file-size.test.ts` refused it — for the second time, and answered the same way as the first: split * the file the way the failure message says rather than raise the cap on the guard. * * The seam is not arbitrary. Three modules outside `delegate.ts` already imported these * (`extensions/session.ts` for `DELEGATE_CAPABILITY`, `extensions/delegation.ts` and * `extensions/run-delegation.ts` for `maySpawnDefinition`), which is the evidence that they were a separate * concern living in the wrong file: *what an id means and who may use it* is a different question from * *what one delegation does*. */ import { type Capability } from "./resolve.ts"; /** The capability that authorises spawning a definition (ADR-0017). `tool:*` satisfies any of them. */ export declare const agentCapability: (name: string) => Capability; /** * May this grant spawn that definition? (ADR-0017.) * * `resolve()` is exact-match plus subsumption and has no wildcard rule — a wildcard session works only * because `deriveOwnGrant` *enumerates* its observed tools alongside `tool:*`. Definitions are not tools, * so nothing enumerates them, and the wildcard has to be honoured here explicitly. Without that an * UNGOVERNED session would stop being able to spawn, and "governance is opt-in" is the one rule this * package must never break by accident. */ export declare function maySpawnDefinition(ownGrant: Capability[], name: string): boolean; /** The tool name that confers the ability to delegate further. */ export declare const DELEGATE_CAPABILITY: Capability; /** * Every namespace a capability id may carry, as its literal prefix. * * **One list, because there were three and adding a namespace only updated some of them.** ADR-0035 taught * `normaliseCapability` about `workspace:` and left `ceilingForDefinition` (which mangled it into * `tool:workspace:`) and `isSafeCapability` (which called it malformed) behind. The two places that parse * an id's namespace read this, so a fifth namespace is one entry rather than a third divergence. * * **Not every prefix decision in the package.** Inline `startsWith("` tests on a capability namespace are * scattered across `src/` and `extensions/`; this list is read by two of them. **No number is written here, * and the history is why:** SPEC first claimed the list was "what every site reads"; a correction said "six" * while its own enumeration listed seven; this docstring then said "nine" in the sentence claiming it gave no * number; and the correction to *that* said "fourteen", which counted every file containing any * `startsWith("` — CLI flags, `git worktree list` parsing, an assurance sentinel and node_modules entries * among them. Four numbers, four wrong. The command, so nobody has to quote a predecessor: * * grep -rl 'startsWith("\(tool:\|ext:\|agent:\|skill:\|workspace:\)' src extensions * * Consolidating the sites is a separate change, and a count nobody re-derives is the defect this list exists * to prevent. * * The README's grammar section is the prose statement of the same list and is kept in step with it. */ export declare const CAPABILITY_NAMESPACE_PREFIXES: readonly ["tool:", "ext:", "skill:", "agent:", "workspace:", "context:"]; /** Accept `read` or `tool:read` or `ext:pkg/tool` and normalise to a capability id. */ export declare function normaliseCapability(raw: string): Capability; /** The capability that authorises routing a child to one registered workspace (ADR-0035). */ export declare function workspaceCapability(workspaceId: string): Capability; /** * May this session route a child to workspace `id`? * * Mirrors `maySpawnDefinition` deliberately — same shape, same wildcard handling, same reason. Routing was * the one governance dimension that did not attenuate (R-131, measured in * probe `g36-workspace-attenuation`): the registry inherited into every child and nothing checked the * caller's authority, so a child routed to `staging` could route its grandchild to `prod`. * * `tool:*` satisfies it because governance is opt-in — an ungoverned session holds the wildcard and must * keep working exactly as before. */ export declare function mayRouteToWorkspace(ownGrant: readonly Capability[], workspaceId: string): boolean; /** False when an id would be read as more than one capability by any channel that carries it. */ export declare function isWellFormedCapability(id: string): boolean; /** * The STRICT grammar: a whitelist of what a capability id may look like, not a blocklist of what it may not. * * **Two consumers, and they are two different reasons for the same rule.** It lives here rather than in * either of them because it shipped in one and was needed by both: * * 1. `skill-packages.ts` — the boundary that GENERATES a grant. R-77/R-78: a package declaring * `allowed-tools: Read,ext:x";touch /tmp/pwned;PI_DADDY_GRANT="` produced a `.pi/grants.env` that ran * arbitrary code the moment an operator sourced the line `init` prints. * 2. `workspace.ts` — the operator registry, since ADR-0035 made a registry id the tail of a capability id * (`workspace:`). That made the registry an input to this grammar, and it got the LOOSE * `isWellFormedCapability` blocklist instead, which review showed costs three things: * an id of literally `*` loaded and minted `WORKSPACE_WILDCARD`, so an operator naming ONE worktree held * routing authority over every id in the registry including ones added later; * an id containing a space became TWO capabilities, because `ceilingForDefinition` splits `allowed-tools` * on `[\s,]+` — `allowed-tools: read, workspace:prod bash` measured as * `['tool:bash','tool:read','workspace:prod']`, i.e. routing over production plus a shell, neither typed * by anyone (0.18.1's comma, one namespace over); * and quote/`$()`/backtick ids reached the `ROUTABLE WORKSPACES` block of the generated file, whose own * instructions tell the operator to paste them into `PI_DADDY_GRANT`. * * **A blocklist was the wrong shape for this and the comment on `isWellFormedCapability` says why it is the * right shape THERE**: that one guards the enforcement path, which must keep accepting whatever ids operators * already have. A registry is a file the operator writes and can rename, so refusing a hostile id costs * nothing — and `grant-env.ts`'s standing warning applies to it exactly: *"the third channel — whatever it * turns out to be — should cost a refusal rather than an injection."* The registry was the third channel. */ /** * A workspace registry id, which is NOT a tool name and needed its own rule. * * `isSafeCapability` was reused here first, and that was the error: it is the grammar for a *tool* name, and * it refused ids that published 0.18.1 accepted — `feature/x` above all. Git worktrees are routinely named * after their branch, so a slash is the ordinary case, and a slash splits nothing: not the comma-separated * grant, not `allowed-tools`' `[\s,]+`. Refusing it bought no safety and broke the common setup. * * What is refused, and why each one earns it: * - **whitespace** — `ceilingForDefinition` splits `allowed-tools` on `[\s,]+`, so `workspace:prod bash` * measured as `['tool:bash','tool:read','workspace:prod']`: routing over production plus a shell, neither * typed by anyone. 0.18.1's comma, one namespace over. * - **comma, CR, LF, NUL** — split a grant. * - **`*`** — collides with `WORKSPACE_WILDCARD`, so registering a worktree as `*` and granting * `workspace:*` believing it named that one root minted routing authority over the whole registry. * - **quotes, `$`, backticks, `;`, `&`, `|`, `<`, `>`, `(`, `)`, `\`, `#`** — reach the ROUTABLE WORKSPACES * block of a generated `.pi/grants.env`, whose own instructions tell the operator to paste the id into * `PI_DADDY_GRANT`. Sourcing the file was safe; following its instructions executed. R-77/R-78. * - **control characters and non-ASCII** — the generated file is reviewed in an editor and `/grants` prints * these; backspace and ANSI escapes let one id render as another. This is the one refusal that costs a * legitimate user something (a non-English worktree name), and it is a deliberate trade recorded as * breaking rather than asserted to cost nothing. */ export declare function isSafeWorkspaceId(id: string): boolean; export declare function isSafeCapability(id: Capability): boolean; /** * The write-side backstop, for every channel that joins capabilities into one string. * * `covered()` already refuses to GRANT a malformed id, so this should be unreachable — which is exactly * why it exists. `grant-env.ts` says it better than I can: "the third channel — whatever it turns out to * be — should cost a refusal rather than an injection. A guard that depends on my enumeration being * complete is not a guard." The propagation channel turned out to be that third channel, and it had no * backstop while the file-writing channel did. * * Loud rather than silently filtered: a malformed id here means something upstream admitted one, and * dropping it quietly would hide the defect that produced it. */ export declare function assertCapabilitiesArePropagatable(grant: readonly Capability[]): void; //# sourceMappingURL=capabilities.d.ts.map