# Tool Catalog, Grant Semantics, and Multi-Agent Protocol

This page specifies the _content_ of an agent's authority in the V1 Agentic
Studio: the enumerated tool catalog, the per-tool grant scopes and revocation
cascade, the multi-agent hand-off protocol, the runtime admission gate that
makes those governance decisions act, and the concrete cross-domain pipeline
patterns that compose them. It is the companion to
[Agent Registry, Job Orchestration, and Multi-Agent Plans](./agentic-registry-jobs-plans.md)
(the families, lifecycle, and plan DAGs) and
[Agent Invocation, Budgets, Memory, and Feedback Loops](./agentic-invocation-budgets-grants.md)
(invocation surfaces, cost budgets, memory, and champion-challenger). Without
the material here, an `AgentRun` is a black box — this is the layer that says
exactly what a run is allowed to do, how that authority is granted and revoked,
and how it is enforced when a tool is actually about to fire.

A reality note worth stating up front, in the spirit of the rest of these docs:
the governance and orchestration _brain_ is real, pure-TypeScript, and
value-tested (271 tests across 16 files in `@oshun/agentic-studio` v0.1.0). The
tool _execution_ itself — the real `web.fetch`, `sophia.ground`, and so on — is
injected at the application boundary and **fail-closed by default**. The catalog
declares and validates authority; the deployable binds (or withholds) the real
tool implementations. Where a piece is a data/query layer rather than a shipped
admin UI, or is provider-gated, this page says so plainly.

## Where this lives in the codebase

| Concern                             | Module                                                                                                          |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Tool catalog (contract)             | `libs/contracts/src/agent/tools.ts`                                                                             |
| Grant record (contract)             | `libs/contracts/src/agent/tool-grants.ts`                                                                       |
| Grant resolver + revocation cascade | `libs/oshun/agentic-studio/src/grants/resolver.ts`                                                              |
| Multi-agent hand-off protocol       | `libs/oshun/agentic-studio/src/handoff/handoff-protocol.ts`                                                     |
| Run envelope + statuses             | `libs/oshun/agentic-studio/src/runs/agent-run.ts`                                                               |
| Run controls                        | `libs/oshun/agentic-studio/src/runs/run-controls.ts`                                                            |
| Runtime admission gate              | `libs/oshun/agentic-studio/src/runs/executor.ts`                                                                |
| Guarded dispatcher / orchestrator   | `libs/oshun/agentic-studio/src/runs/dispatcher.ts`, `runs/orchestrator.ts`                                      |
| Kill switches                       | `libs/oshun/agentic-studio/src/budgets/kill-switch.ts`                                                          |
| Budgets                             | `libs/oshun/agentic-studio/src/budgets/budgets.ts`                                                              |
| Cost-quality modes                  | `libs/oshun/agentic-studio/src/modes/modes.ts`                                                                  |
| Tool isolation / SSRF / secrets     | `libs/oshun/agentic-studio/src/capabilities/tool-isolation.ts`                                                  |
| Adversarial test catalog            | `libs/oshun/agentic-studio/src/capabilities/adversarial-tests.ts`                                               |
| Champion-challenger                 | `libs/oshun/agentic-studio/src/feedback/champion-challenger.ts`                                                 |
| Pipelines                           | `libs/oshun/agentic-studio/src/pipelines/v1-pipelines.ts`, `pipeline-registry.ts`, `pipelines/observability.ts` |
| Governed-run HTTP surface           | `apps/oshun/bff/src/agentic/runs-route.ts`                                                                      |

The package's barrel (`libs/oshun/agentic-studio/src/index.ts`) re-exports
twelve subsystems: `registry`, `runs`, `plans`, `dashboard`, `invocation`,
`budgets`, `capabilities`, `feedback`, `modes`, `pipelines`, `grants`, and
`handoff`.

## Tool catalog

Every tool an agent can invoke is enumerated in a versioned, Zod-validated
catalog (`AgentToolCatalog`) exported from `libs/contracts/src/agent/tools.ts`.
The V1 catalog declares `catalogVersion: '1.0.0'`,
`publishedAt: '2026-05-11T00:00:00Z'`, and exactly **21 entries**
(`V1_TOOL_IDS`). Additions go through the Agent Registry release-gate review.

The precise count matters: `generate.image`, `generate.video`, and
`generate.audio` are three distinct entries; `memory.read` / `memory.write` and
`calendar.read` / `calendar.write` are separate read/write entries. A
prose-grouped reading can make the catalog look like "about seventeen" tools,
but `V1_TOOL_IDS.length === 21` is the contract.

| Tool id               | Display name        | Required scope        | What it does / hard limits                                                                                                                                          |
| --------------------- | ------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `web.fetch`           | Web Fetch           | `web.egress`          | HTTP through the egress allowlist; SSRF-guarded (see below); per-request audit. Read-only network.                                                                  |
| `source.fetch`        | Source Fetch        | `sources.read`        | Read a registered Sophia `SourceSet` entry by id; scope-checked against the run's source set; bumps a usage counter.                                                |
| `sophia.ground`       | Sophia Ground       | `sophia.ground`       | Full retrieve → rank → ground → cite envelope (see [Sophia Grounding](./sophia-grounding.md)); may return an ungrounded/abstained verdict rather than synthesizing. |
| `sophia.fact_check`   | Sophia Fact-Check   | `sophia.fact_check`   | Run the fact-check loop on a claim set.                                                                                                                             |
| `memory.read`         | Memory Read         | `memory.read`         | Iris recall within the run's declared memory scope.                                                                                                                 |
| `memory.write`        | Memory Write        | `memory.write`        | Iris write; sensitive categories gated by explicit consent.                                                                                                         |
| `persona.invoke`      | Persona Invoke      | `persona.invoke`      | Render under a registered persona/voice; Lilith policy applies; cannot widen the run's tone-band ceiling.                                                           |
| `generate.image`      | Image Generate      | `generate.image`      | Dispatch through Isis under the run's audacity ceiling; provenance bundle returned.                                                                                 |
| `generate.video`      | Video Generate      | `generate.video`      | As above for video.                                                                                                                                                 |
| `generate.audio`      | Audio Generate      | `generate.audio`      | As above for audio/narration.                                                                                                                                       |
| `composition.suggest` | Composition Suggest | `composition.suggest` | Living-Scenes Compose Assist over an artifact set; ranked techniques + rationale.                                                                                   |
| `themis.adjudicate`   | Themis Adjudicate   | `themis.adjudicate`   | Submit an evidence pack for a Themis verdict (Metis academic-integrity surface).                                                                                    |
| `code.exec`           | Code Execute        | `code.exec`           | Sandboxed code execution.                                                                                                                                           |
| `file.read`           | File Read           | `file.read`           | Read a tenant/customer-scoped file by id.                                                                                                                           |
| `file.write`          | File Write          | `file.write`          | Write into the run's output bundle (never directly into a user-facing surface; publication is a separate gate).                                                     |
| `calendar.read`       | Calendar Read       | `calendar.read`       | Read calendar entries within consent scope.                                                                                                                         |
| `calendar.write`      | Calendar Write      | `calendar.write`      | Write calendar entries within consent scope.                                                                                                                        |
| `notify.send`         | Notify Send         | `notify.send`         | Send through a delivery channel adapter.                                                                                                                            |
| `handoff`             | Hand-off            | `handoff`             | Dispatch a sub-agent with bounded budget and authority (see Multi-agent protocol).                                                                                  |
| `approval.request`    | Approval Request    | `approval.request`    | Pause the run pending human approval.                                                                                                                               |
| `agent.terminate`     | Agent Terminate     | `agent.terminate`     | Terminate the current run (or a sub-agent).                                                                                                                         |

Each `AgentToolCatalogEntry` carries more than an id: a `semver` (the V1 set is
uniformly `1.0.0`), a `capabilityModifiers` descriptor (schema name and
JSON-schema shape — the V1 entries declare the empty `NoModifiers` descriptor,
with the real per-tool modifier schemas registered in the runtime), an array of
`scopeRequirements` (each with `scope`, `required`, and a `rationale` string for
the audit trail), at least one `auditHook` (e.g., `audit.web.fetch` capturing
the `args` key), a `dependencies` array of `(toolId, semverRange)` pairs, and a
nullable `deprecation` record.

### Tool versioning and deprecation surfacing

The catalog backs the "tool versioning / deprecations surfaced at
registry-review time" claim with real code, not prose:

- **`resolveToolDependencies(catalog, rootToolId)`** walks the dependency graph
  with a genuine semver-range matcher (`semverInRange`) supporting `^`, `~`,
  `>=`, `<=`, `=`, and bare-exact ranges, including the caret's `0.x` special
  cases. It returns either the resolved entry set or the first unsatisfied
  dependency, with a typed reason: `unknown-tool` or `semver-range-no-match`
  (plus the offending `toolId` and `requestedRange`).
- **`surfaceDeprecations({ catalog, dependentToolsByToolId, nowUnixSeconds, reviewWindowSeconds })`**
  scans for entries whose `deprecation.deprecatedAt` falls within the review
  window and returns, per tool, the deprecation date, the `replacementToolId`,
  and the dependent tool ids that still rely on it — the payload an Agent
  Registry reviewer sees before approving a release.

## Grant scope and revocation cascade

Tool grants are scoped, time-bound, and revocable. Crucially, the grant
vocabulary is the same vocabulary the `AgentRunEnvelope` uses, so audit logs and
operator surfaces speak one language.

### The grant record

`AgentToolGrantSchema` (`libs/contracts/src/agent/tool-grants.ts`) defines a
grant as:

```json
{
  "grantId": "grant-…",
  "agentId": "veritas.fact-checker",
  "toolId": "sophia.fact_check",
  "scope": "per-pipeline-instance",
  "scopeRef": "pipeline-instance-abc",
  "capabilityModifiers": { "egressDomains": ["*.un.org"] },
  "expiresAt": "2026-06-30T00:00:00Z",
  "grantedBy": { "actorId": "op-7", "actorRole": "platform-operator" },
  "rationale": "weekly briefing pack run",
  "grantedAt": "2026-06-23T00:00:00Z",
  "revokedAt": null,
  "revokedByActorId": null
}
```

The `capabilityModifiers` field is per-tool and free-form: for `web.fetch` it
carries the egress-domain allowlist; for `generate.image` the audacity ceiling;
for `memory.write` the allowed memory scopes. The `grantedBy.actorRole` is
constrained to `platform-operator | tenant-operator | creator | customer`.

### Scopes and specificity

`GrantScopeSchema` enumerates exactly five scopes. Each grant carries exactly
one. When a tool call is evaluated, the resolver picks the **most specific
live** grant by a fixed priority (`GRANT_SCOPE_PRIORITY` in
`grants/resolver.ts`):

| Scope                   | Priority | Lifetime / `scopeRef` matches                                                                                 |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `per-run`               | 100      | A single `AgentRun`; `scopeRef === runId`. Expires at terminal state.                                         |
| `per-pipeline-instance` | 80       | A cross-domain pipeline run; `scopeRef === pipelineInstanceId`. Narrower than per-user, broader than per-run. |
| `per-session`           | 60       | A logical session (a Studio authoring session, a Metis tutor session); `scopeRef === sessionId`.              |
| `per-user`              | 40       | Durable for a user account; `scopeRef === userId`.                                                            |
| `per-tenant`            | 20       | Durable for a tenant; `scopeRef === tenantId`. Until revoked.                                                 |

The resolver (`resolveGrant`) is a per-call authorization step: it filters
grants to the matching `(agentId, toolId)` and a matching scope ref, drops any
that are revoked or expired, and then `reduce`s the survivors to the
highest-priority one (ties broken by most-recent `grantedAtUnixSeconds`). When
nothing remains, it returns a typed `GrantDenialReason`: `no-matching-grant`,
`all-matches-expired`, or `all-matches-revoked` — distinguishing "you never had
this" from "your grant lapsed" from "it was pulled".

### Revocation, expiry, and the cascade

- **Queued runs re-check at dispatch.** `recheckQueuedRunAtDispatch` re-runs
  `resolveGrant` immediately before a queued run starts, so a grant revoked or
  expired _while the run waited_ is enforced at the boundary — a queued run
  never inherits a stale "allowed" decision.
- **In-flight cascade.** `emitRevocationCascade` performs a breadth-first walk
  of active runs: every run whose `grantsInUse` includes the revoked grant gets
  a `ToolRevokedEvent`, and every sub-agent (`parentRunId` chain) inherits the
  revocation transitively, with `cascadeDepth` recorded. Runs cannot evade
  revocation by re-issuing the same call within a single invocation.
- **Expiry is revocation.** `emitExpiryCascade` finds grants expiring within a
  horizon and routes each through the same cascade machinery (with
  `revokedByActorId: 'system.expiry'`). Time-bound and operator-pulled authority
  are treated identically.
- **Renewal requires fresh rationale and audit.** `renewGrant` refuses with
  `rationale-required` if the rationale is blank and `audit-required` if no
  audit event id is bound; on success it mints a _new_ `grantId` rather than
  silently extending the old authority.
- **Kill-switch cascade.** `emitToolKillSwitchCascade` propagates a per-tool
  kill switch (platform-wide when `tenantId === null`, else tenant-scoped)
  across all in-flight runs using the tool, again BFS over sub-agents. Each
  emitted `KillSwitchPropagationEvent` records `propagationLatencySeconds` and a
  `withinFiveSecondSlo` boolean (`latency <= 5`) — the data backing the
  "platform kill propagates ≤ 5 s" claim.

## Kill switches and the scope hierarchy

Kill switches (`budgets/kill-switch.ts`) are the safety primitive that overrides
everything else. `KILL_SWITCH_SCOPES` are `agent`, `family`, `tenant`,
`provider`, `region`, `tool`, and `global`. `decideExecution(switches, target)`
collects every active switch that matches an `ExecutionTarget` (tenant / agent /
family / provider / region / tool) and, when more than one is active, "blames"
the broadest one for the user-visible copy by this priority:

```
global 6 > region 5 > provider 4 > tenant 3 > family 2 > agent 1 > tool 0
```

The blamed switch supplies the `userVisibleStatusCopy` and an optional
`safeDegradationModeId` so a tripped switch can degrade to a safe mode rather
than simply erroring.

## The runtime admission gate — where decisions act

The audit found a sharp distinction worth naming: the budget, kill-switch, and
throttle modules each _decide_ (a pure verdict), but for that to mean anything
something must _consume_ those verdicts at a tool-call boundary and actually
stop a run. That seam is `runs/executor.ts` → `runs/dispatcher.ts` →
`runs/orchestrator.ts`, and it is the concrete enforcement story the
higher-level docs gesture at.

- **`admitToolCall(input)`** (`executor.ts`) runs the checks in **safety → cost
  → rate** order:
  1. **Kill switch** (`decideExecution`) — if any switch is active, the run is
     `kill`ed (terminal `killed`), with the operator's status copy and an audit
     event. This is the check that honors the ≤ 5 s propagation budget.
  2. **Budget** (`checkBudget`) — an `exceeded` verdict is a hard stop (terminal
     `killed`); `warn` / `grace` verdicts admit the call but surface as
     `BudgetWarning`s (degradation/notification is policy, not a hard stop).
  3. **Rate** (`acquireToken`) — a failed token acquisition is back-pressure:
     the call is deferred with a `retryAfterSeconds`; the run is **not**
     terminated.
  4. Otherwise it returns `admit` with the token-deducted throttle state and any
     budget warnings. The executor never invokes the tool — it is purely the
     enforcement primitive.
- **`dispatchGuardedToolCall(input, runTool)`** (`dispatcher.ts`) calls
  `admitToolCall` and invokes the injected `runTool` **only** on `admit`. A kill
  switch or exhausted budget terminates the run and the tool never runs; a
  throttle defers; a terminal run refuses.
- **`runGuardedToolPlan({ run, plan, killSwitches, budgets, … })`**
  (`orchestrator.ts`) threads the envelope through a sequence of
  `PlannedToolCall`s; the moment a call is killed or throttled, the orchestrator
  **stops** and the remaining planned calls never execute.

### The governed-run HTTP surface

`POST /v1/agentic/runs/execute` (`apps/oshun/bff/src/agentic/runs-route.ts`)
mounts the guard as the deployable's run surface. The design choices are the
honest, fail-closed ones:

- **Fail-closed by default.** The real agent tools are injected at the app
  boundary via an `AgentToolRegistry` (`toolId → operation`). The default is
  `notConfiguredAgentToolRegistry()` — an empty map — so any plan referencing an
  unconfigured tool returns **503 `agent_tools_not_configured`**. The gate is
  real; the tools are bound by the deployable. With nothing configured, the
  route never fabricates a run.
- **Server-authoritative identity.** The actor is the authenticated principal
  and the tenant comes _only_ from the verified auth claim
  (`authContext.tenantId`) — a body-supplied tenant would let a caller dodge
  tenant-scoped kill switches.
- **Malformed → 400.** A missing `runId` / `tenantId` / `rootAgentId`, or an
  empty / non-`{toolId}` plan, is rejected.
- **Scope checks (S7) → 403 `tool_scope_missing`.** A tool can declare an
  `AgentToolScopeCheck` over the caller's real scopes; the executor never
  fabricates an operator tier for whoever shows up.
- **Server-side target metadata (S8).** Kill switches match on
  family/provider/region, which come from server-side `toolTargets` keyed by
  tool — never from the request body — so an armed family switch cannot be
  evaded by omitting a field. Tools without metadata get `'default'`.
- **Run-ownership (S9) → 409 `run_id_conflict`.** A `runId` already persisted by
  another member is never overwritable.
- **Operator switches read every request.** `resolveOperatorKillSwitches()`
  reads the admin agentic-operations store on every request (the arm → next
  boundary ≤ 5 s budget); client-supplied switches and budgets are merged _in
  addition_ — a request can tighten governance but never loosen it.

The admin operator surface for these controls is a set of **API routes**, not a
rich React dashboard: `apps/oshun/admin/src/app/api/admin/agentic-operations/`
exposes `kill-switches`, `snapshot`, `gold-sets/promote`, and
`champion-challengers/rollout`. The only agent _page_ in the product is
`apps/oshun/tenant-admin/src/app/agents/page.tsx`. The "operator dashboard for
replay and audit" is a data/query layer (`dashboard/dashboard-query.ts`,
`runs/replay.ts`, `runs/streaming-progress.ts`), not a shipped admin dashboard
page — see
[Output Gallery, Lineage, Branch, and Replay](./output-gallery-lineage.md).

## Run statuses, the cost ledger, and run controls

A note on the `agent.terminate` outcome vocabulary: the implemented terminal set
(`TERMINAL_STATUSES`) is `completed`, `failed`, `cancelled`, `killed`. There is
no `declined` status; the run controls expose `cancel` (operator-deliberate) and
`kill` (governance-enforced), not a `decline`. Treat any `declined` outcome in
older prose as drift toward `cancelled`.

The full `RUN_STATUSES` enum (`runs/agent-run.ts`) is:

```
queued · starting · running · paused · awaiting_approval ·
awaiting_tool · awaiting_branch_selection · cancelled · killed · failed · completed
```

(Any simplified lifecycle diagram using `pending` / `planning` / `executing` is
aspirational — those tokens are not in the real enum, which has no `planning` or
`executing` state and adds `paused`, `awaiting_tool`, and
`awaiting_branch_selection`.)

The `AgentRunEnvelope` carries `runId`, `rootAgentId`, `parentRunId`,
`pipelineId`, the `plan` (a `RunPlanStep[]` DAG), `toolCalls`,
`intermediateArtifacts`, `evidenceTrail`, the `costLedger`, `decisionRationale`,
`outputs`, and a `provenanceBundleId`. The **`RunCostLedger`** is denominated
for cross-provider comparability and tracks `totalCostUnits`, per-stage and
per-tool cost maps (`perStageCostUnits` / `perToolCostUnits`),
`inputTokenCount`, `outputTokenCount`, `gpuMinutes`, `voiceSeconds`,
`avatarSeconds`, `retrievalCount`, and `externalCallCount`.

**Run controls** (`run-controls.ts`) are pure `(envelope, command) → result`
functions emitting typed audit events: `pause`, `resume`, `cancel`, `kill`,
`chooseBranch`, `fork` (requires a fresh `runId` — re-using the source id is
rejected with `fork-without-fresh-id`), `retryWithChanges` (rewind to a step,
mark it and all later steps pending, apply overrides, restart), and
`operatorStep` (advance exactly one pending step for step-through debugging).
The corresponding audit kinds are `pause` / `resume` / `cancel` / `kill` /
`branch-chosen` / `fork` / `retry-with-changes` / `operator-step`.

> Persistence candor: in V1 the runtime state is **in-memory** — the
> `AgentRegistry` is a `Map`, and the BFF run-lifecycle store is an in-process
> store. `@oshun/agentic-studio` does not import `@oshun/queue`; its runs
> (`dispatcher`, `orchestrator`, `checkpoint`, `replay`) are in-process,
> pure-function logic with no DLQ or SLA monitor _in this library_. The
> durability/priority-class/DLQ story (the shared queue lib lives at
> `libs/shared/queue`) is not wired into agentic-studio yet — see
> [Agent Registry, Job Orchestration, and Multi-Agent Plans](./agentic-registry-jobs-plans.md)
> and §18.

## Multi-agent hand-off protocol

Multi-agent pipelines are valuable but dangerous; the protocol in
`handoff/handoff-protocol.ts` controls budget inheritance, authority delegation,
cycle prevention, and audit completeness so a "Veritas story drafting" agent
that dispatches Sophia retrievers and Isis generators cannot silently spin into
a runaway run.

A parent dispatches via a `HandoffDispatch` carrying the sub-run ids, the
`HandoffBudgetEnvelope` (`costUnits`), the `AuthorityEnvelope`
(`delegableToolIds`, `maxFanoutDegree`, `capabilityModifiers`), the
`approvalRequirement`, the `parentTrace` (ordered ancestor agent ids,
_excluding_ the sub-agent), and an `allowCycle` flag.

`validateDispatch` enforces, in order, with a typed `DispatchDenialReason`:

| Check                 | Denial reason                 | Rule                                                                                                                 |
| --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Cycle                 | `cycle-detected`              | The sub-agent id may not appear in `parentTrace` unless `allowCycle` is set.                                         |
| Depth                 | `depth-cap-exceeded`          | `parentTrace.length >= cap`; `DEFAULT_DEPTH_CAP = 5`, overridable per pipeline.                                      |
| Authority — tools     | `authority-widening-rejected` | Every `delegableToolId` must be in the parent's set (a subset, never a superset).                                    |
| Authority — fanout    | `authority-widening-rejected` | `maxFanoutDegree` may not exceed the parent's.                                                                       |
| Authority — modifiers | `authority-widening-rejected` | `capabilityModifiers` must be _narrower_ (numbers `<=`, arrays subset, strings equal, objects recursively narrower). |
| Budget — positivity   | `budget-non-positive`         | The sub-budget must be finite and `> 0`.                                                                             |
| Budget — envelope     | `budget-exceeds-parent`       | The sub-budget may not exceed the parent's remaining budget.                                                         |

`deductDispatchBudget` validates and then subtracts the sub-budget from the
parent's remaining envelope. On terminal state, `reconcileTerminalBudget`
returns the **unused budget to the parent** (`returnedToParent`) when there is
no overshoot, and emits an `overshootEvent` of kind `subagent_budget_exceeded`
(and `shouldKillSubagentForBudget` returns `true`) when the sub-agent overran.

**Approval requirements** (`APPROVAL_REQUIREMENTS`) are `none`, `operator`,
`teacher`, `guardian`, and `user-confirm-before-proceed`.
`initialSubagentRunStatus` puts a sub-agent into `running` only when the
requirement is `none`; otherwise it enters `awaiting_approval`.
`approvalRolePermitted` maps each requirement to the roles that can satisfy it
(e.g., `operator` accepts `platform-operator` / `tenant-operator`;
`user-confirm-before-proceed` accepts `customer` / `creator`).

**Audit and attribution.** `recordDispatchAudit` and `recordTerminalAudit` emit
a `HandoffAuditEvent` of kind `dispatch` (target, sub-run, granted budget,
authority) and `terminal` (terminal state, budget consumed, produced artifact
ids). `deriveAttributionChain` stamps every artifact with its full
`[...parentTrace, producingAgentId]` provenance chain — reader-facing surfaces
can collapse it to "agent assisted", but the bundle preserves the path.

## Cost-quality modes and their concrete thresholds

`modes/modes.ts` defines five customer/creator-selectable `COST_QUALITY_MODES`,
each with declared, value-tested thresholds (not qualitative hand-waving):

| Mode                | maxCostUnits | p95 latency (ms) | min cite density /1k words | min eval score | multi-agent | human checkpoints | external side effects |
| ------------------- | ------------ | ---------------- | -------------------------- | -------------- | ----------- | ----------------- | --------------------- |
| `fast`              | 50           | 1 500            | 0                          | 0.6            | no          | 0                 | yes                   |
| `balanced`          | 500          | 10 000           | 2                          | 0.7            | no          | 0                 | yes                   |
| `deep`              | 5 000        | 120 000          | 4                          | 0.8            | yes         | 1                 | yes                   |
| `exhaustive`        | 50 000       | 1 800 000        | 6                          | 0.85           | yes         | 3                 | yes                   |
| `rehearsal-dry-run` | 1 000        | 60 000           | 0                          | 0              | yes         | 0                 | **no**                |

`checkModeCompliance` verifies an actual run against its mode and returns typed
violations: `cost-exceeded`, `latency-exceeded`, `citation-density-below-floor`,
`evaluation-score-below-floor`, and (for rehearsals)
`rehearsal-side-effects-detected`. `applyOverride` is **tighten-only**: a
`tenant-operator` or `customer` may narrow the mode within the domain policy's
`allowedModes`, but a `cost-raise-rejected` is returned if the requested mode's
cost cap exceeds the current one. The `rehearsal-dry-run` profile's
`hasExternalSideEffects: false` is what lets a pipeline rehearse against
fixtures with no real publication, generation, or notification.

## Tool isolation, SSRF guards, and secret scoping

`capabilities/tool-isolation.ts` backs `web.fetch`'s "SSRF guards" with real
network reasoning rather than a comment:

- **`isPrivateOrLoopbackHost`** rejects loopback (`localhost`, `127.0.0.0/8`,
  `::1`, `0.0.0.0`, `::`), link-local (`169.254.0.0/16`, `fe80:`), IPv6
  unique-local (`fc00::/7`), and the RFC-1918 private ranges via integer CIDR
  bounds: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. IPv4-mapped IPv6
  (`::ffff:…`) is unwrapped and re-checked.
- **`matchHostPattern`** supports exact hosts and `*.example.com` subdomain
  wildcards (normalizing case and bracketed IPv6).
- **`checkEgressRequest`** layers the checks: invalid URL → `invalid-url`; SSRF
  host → the matching SSRF reason; not in the allowlist →
  `host-not-in-allowlist`; wrong scheme → `scheme-not-allowed`; wrong method →
  `method-not-allowed`. The `EgressDenialReason` set is the typed vocabulary.
- **`checkSecretAccess`** scopes secrets to permitted agents and tools within a
  tenant, returning `cross-tenant`, `agent-not-permitted`, or
  `tool-not-permitted`.

### Adversarial security-test catalog

`capabilities/adversarial-tests.ts` ships the concrete probe taxonomy
(`ADVERSARIAL_CATEGORIES`): `prompt-injection`, `tool-call-exfiltration`,
`scope-escalation`, `sandbox-escape`, `persona-bypass`, `policy-bypass`, and
`source-fabrication`. Each `AdversarialFixture` carries an `expectedVerdict`
(`refuse | sanitize | comply-and-disclose`) and a `severity`;
`evaluateFixtureResult` grades an observed result as
`safe | suspected | failure` (an observed `leak` is always a `failure`), and
`buildSuiteReport` rolls up per-category counts with a worst-severity into an
`overallVerdict` of `pass | review | block` — the release-gate signal.

## Champion-challenger rollout (statistical readiness)

`feedback/champion-challenger.ts` is genuinely statistical, not a toy.
`evaluateReadiness` requires `MIN_SAMPLES = 100`, then runs a **two-proportion
z-test** on task-success rates, with the two-tailed p-value computed from an
**Abramowitz & Stegun 26.2.17** normal survival-function approximation
(`normalSf`). It returns a `ReadinessReport` (`zScore`, `pValue`, latency and
cost deltas) and a verdict of `not-enough-samples`, `inconclusive`,
`challenger-worse`, or `challenger-ready` (the last two requiring
`pValue <= P_THRESHOLD = 0.05`). Rollouts run in `shadow` (challenger never
returned to the user) or `canary` (`routeInvocation` routes a `canaryFraction`
of traffic). `recordTransition` gates the `ROLLOUT_TRANSITIONS` —
`promote-challenger-to-champion` (refused `readiness-required` unless the
verdict is `challenger-ready`), `freeze-rollout`, and
`rollback-to-prior-champion` — and requires a rationale on every transition.

## Cross-domain pipeline pattern reference

V1 ships a small set of named cross-domain autonomous pipelines. The
implementation note in the source docs points at `libs/oshun/agent-pipelines/`,
but that package (`@oshun/agent-pipelines` v0.1.0) is only a **four-file
re-export shim** whose `src/index.ts` re-exports `grants/resolver` and
`pipelines/index` straight from `@oshun/agentic-studio`. The real pipeline
**specs** live in `libs/oshun/agentic-studio/src/pipelines/v1-pipelines.ts`, and
the `PipelineRegistry` class lives in `pipelines/pipeline-registry.ts`.

`V1_PIPELINES` contains **seven** specs, all with `lifecycleState: 'live'`. A
reader should note that **Veritas has two distinct pipelines** — the source docs
list them in different sections and never reconcile that, but the code is right:
`veritas.weekly_briefing_pack` (creator-tier) and `veritas.story_drafting`
(platform-operator-tier) are separate specs (the source file flags the overlap
in a comment).

| `pipelineId`                   | Invocation tier (`PIPELINE_TIERS`) | Budget cap | Hand-off depth | Mandatory approval gate(s)                    | Provenance |
| ------------------------------ | ---------------------------------- | ---------- | -------------- | --------------------------------------------- | ---------- |
| `veritas.weekly_briefing_pack` | creator                            | 5 000      | 3              | `editor.review`                               | standard   |
| `veritas.story_drafting`       | platform-operator                  | 12 000     | 4              | `editorial.inbox.handoff`                     | extended   |
| `tara.seasonal_program`        | platform-operator                  | 10 000     | 2              | `tara.lineage.confirm`, `editor.review`       | extended   |
| `nisaba.edition_study_guide`   | customer                           | 3 000      | 2              | `scholar.review`                              | standard   |
| `nyx.event_explainer_set`      | tenant-operator                    | 2 500      | 2              | `nyx.desk.review`                             | standard   |
| `arete.weekly_review_draft`    | customer                           | 500        | 2              | `user.confirm.before.publish`                 | minimal    |
| `metis.course_from_byom`       | customer                           | 20 000     | 4              | `themis.prescreen`, `tenant.teacher.approval` | extended   |

Each `PipelineSpec` declares its `participatingAgentIds`, the `plan` (a stage
DAG), `expectedHandoffDepth`, `totalBudgetCap`, `mandatoryApprovalGateIds`,
`expectedOutputSchemaNames`, `provenanceShape`, an `evaluationFixtureSetId`, and
a `lifecycleState`. Some concrete stage flows:

- **`veritas.story_drafting`** (platform-operator only): source-enumeration →
  ingestion-verification → claim-extraction → fact-check → contradiction-check →
  counterclaim-generation → lilith-tone-review → editorial-inbox-handoff.
- **`metis.course_from_byom`** (customer, under tenant policy): byom-safety-scan
  → ingestion → objective-extraction → kg-anchor → prerequisite-chain →
  item-bank → calibration → themis-prescreen → tenant-teacher-approval (see
  [Metis — Education and Tutoring](./domain-metis.md)).
- **`tara.seasonal_program`** leads with lineage-rights-confirmation and a
  Lilith strict tone check; **`arete.weekly_review_draft`** runs only against
  the user's own consented data and ends with user-confirm-before-publish (see
  [Arete — Goals, Habits, and Reflection](./domain-arete.md)).

### Pipeline scheduling, customization, and observability

The "tenant-scoped pipeline scheduling" and "pipeline observability" lines have
real backing symbols:

- **Scheduling** — `PipelineSchedule` and
  `nextFireFor(schedule, nowUnixSeconds)` (`pipelines/observability.ts`) compute
  the next fire time for a cadence.
- **Tenant customization** — `TenantPipelineCustomization` and
  `effectivePipelineForTenant({ base, customization })`
  (`pipelines/pipeline-registry.ts`) resolve a tenant's effective pipeline from
  the base spec plus its overrides; `PIPELINE_TIERS` enumerates the invocation
  tiers.
- **Observability** — `buildPipelineObservabilityReport` produces SLA-per-stage,
  evidence-completeness, and human-touch-point measures from
  `PipelineStageObservation`s; `detectPipelineDeviation` flags runs whose shape
  drifts from the declared pipeline and routes them to operator review.

## The autonomous creative orchestrator (`@oshun/creative-orchestrator`)

Distinct from `agentic-studio` (which is the governance/orchestration brain),
`libs/oshun/creative-orchestrator` (`@oshun/creative-orchestrator` v0.0.1,
depends on `@oshun/ai`) is the **brief → produced-content** engine. It is fully
real (58 passing tests) and built on `@oshun/ai/agent-loop`'s
`runStructuredOutput` and `runReflexion`, yet is undocumented elsewhere in the
V1 feature docs. Its exported surface:

- **`decomposeBrief`** (with `CREATIVE_PLAN_SCHEMA`) — turns a brief into a
  schema-validated, acyclic `CreativePlan` DAG (validated by
  `validateDagStructure` / `detectCycle` / `topologicalOrder`).
- **`routePlan` / `CreativeOrchestrator` / `orchestrateBrief`** — governed plan
  → domain-generator dispatch through a `GeneratorRegistry`.
- **`reviseArtifact`** — a bounded generate → critique → revise (Reflexion) loop
  around every artifact, with critics `createMetricCritic`,
  `createLlmJudgeCritic`, and `createContentEvalCritic` (a non-provider-gated
  default judge).
- **`BudgetGovernanceGate` / `ALLOW_ALL_GATE`** — the governance seam over
  dispatch.
- **`createYemayaAgentGenerator` / `createMetisNarrator`** — domain generator
  adapters.

Like the rest of the autonomous-content story, the model and provider boundaries
are injectable and **fail-loud** — the orchestrator never fabricates a plan or
an artifact; with no provider wired, it refuses rather than inventing output.

## Tests

The protocol and catalog behaviors above are value-tested (271 tests, 16 files
in `@oshun/agentic-studio`):

- **Tool catalog** — every V1 entry round-trips through the contract; semver
  dependency resolution against registered tools.
- **Grant scope** — per-scope revocation propagation across in-flight runs and
  sub-agents within the documented latency budget (the `withinFiveSecondSlo`
  flag).
- **Multi-agent budget inheritance** — parent plus sub-budget sum within the
  declared envelope; unused returns; overshoots emit `subagent_budget_exceeded`.
- **Authority envelope** — widening rejected at dispatch; narrowing honored;
  capability modifiers propagated narrower.
- **Cycle prevention** — re-entering an ancestor `agentId` blocked unless
  `allowCycle` is set at the root; depth cap honored.
- **Hand-off audit** — dispatch / terminal / attribution records complete.
- **Cross-domain pipeline fixtures** — each named pipeline produces the expected
  output shape on its declared fixture set; approval gates pause correctly.
- **Kill-switch propagation** — platform-wide tool kill propagates to all
  in-flight runs (≤ 5 s budget); tenant-scoped kill is confined to that tenant.
- **Runtime gate** — `admitToolCall` / `dispatchGuardedToolCall` /
  `runGuardedToolPlan` execute a tool only when admitted; a kill switch or
  exhausted budget stops the plan mid-flight.

See §18 in [../TODOS.md](../TODOS.md) for the agentic backlog, and the
`agentic-pipeline-customer-invocation` end-to-end walkthrough (classified
"deep", with a creds-bound tool DAG) for the live-provider boundary.

## Related

- [Agent Registry, Job Orchestration, and Multi-Agent Plans](./agentic-registry-jobs-plans.md)
- [Agent Invocation, Budgets, Memory, and Feedback Loops](./agentic-invocation-budgets-grants.md)
- [Sophia Grounding](./sophia-grounding.md)
- [Isis Generation Control](./isis-generation-control.md)
- [Output Gallery, Lineage, Branch, and Replay](./output-gallery-lineage.md)
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md)
- [Architecture, Platform Foundations, and Security](./platform-foundations-and-security.md)
- [../features.md](../features.md)
