@sema-agent/core
Version:
Stateless, task-oriented AI agent core
195 lines (194 loc) • 15.5 kB
TypeScript
/**
* The two envelopes that say what a run MAY do: `WorkflowGovernanceBaseline` (the floor a deployment
* puts under every workflow step) and `RuntimeCaps` (the per-agent capability projection). Neither
* names the other; they share a module because "the host's declared ceiling" is one concern, and both
* are read off `RunnerDeps`. Layer 0 vocabulary; `types.ts` re-exports both, so no consumer's import
* changes.
*/
import type { TaskSpec } from "./task-spec.js";
/**
* design/98 §E.2 / §2.5 (S8b) — the deployment-trusted governance that EVERY sub-agent an LLM-authored
* workflow script spawns INHERITS (tighten-only). The child spec is BUILT from this baseline + a strict
* WHITELIST of safe work-fields the script may set — `{ ...base, ...pick(scriptSpec, WHITELIST_KEYS) }` then
* `tightenTaskSpec(base, child)` (default-deny / complete-by-construction, NOT a blacklist
* strip — a missed control-plane field would otherwise leak). It is captured from `RunnerDeps` (trusted,
* never LLM-influenced), NEVER from the LLM-influenced initiating task spec.
*
* Why an EXPLICIT field (arbitration): `RunnerDeps` has no `tools`/`mcp`/`skills`/`principal`
* (those are TaskSpec-only), so the capability a workflow child gets cannot be auto-derived from deps —
* the deployment must DECLARE it here. Unset ⇒ self-orchestration is FAIL-CLOSED (the `run_workflow` tool is
* not mounted, same hard deploy gate as a missing sandbox): a script-spawned agent must never run without a
* governed baseline.
*/
export interface WorkflowGovernanceBaseline {
/**
* The deployment's trusted control-plane the child inherits — toolPolicy / onAsk / onQuestion / hooks /
* principal / tools / mcp / skills / lspManager / checkpointStore / getApiKeyAndHeaders / promptProvider /
* handsReadOnly / shellGate / limits / maxCostUsd / maxTokens, etc. Snapshotted into each child spec FIRST,
* so every governance field comes from here; only whitelisted work-fields overlay it.
*/
base: Partial<TaskSpec>;
/**
* OPTIONAL baseline OVERLAY for a child the script spawns WITH `isolation: "worktree"` (design/97 CORE-6 ×
* governance). A worktree-isolated agent works in its OWN detached git worktree, so a deployment whose `base`
* clamps writes (e.g. `handsReadOnly: true` — the right default for children sharing one working tree) can
* grant isolated children write access WITHOUT loosening the shared-tree baseline — typically
* `worktreeBase: { handsReadOnly: false }` (lift only the write clamp; the worktree provides the file
* containment instead of the spec clamp).
*
* **Overlay semantics (shallow merge)**: when — and only when — the spawn requested `isolation: "worktree"`,
* the effective baseline is `{ ...base, ...worktreeBase }`. Field-level at the TOP level only: a field
* present here overrides the same-named `base` field WHOLESALE (a nested object such as `limits` or
* `toolPolicy` is replaced entirely, never deep-merged); an absent field inherits from `base` — so
* toolPolicy / shellGate / limits / maxCostUsd keep governing isolated children unless deliberately
* overridden. The same whitelist/allowlist/clamp construction applies on top. **Absent ⇒ `base` governs
* every child, isolated or not (zero behavior change).**
*
* **What the containment actually is (honest scope)**: the worktree is COOPERATIVE file isolation — it is
* the cwd domain the file tools resolve against, not a jail. Bash can `cd` out of it (same posture as the
* upstream worktree-isolation feature this mirrors; a worktree was never a shell sandbox). Relaxing writes
* here is therefore safe only under the deployment's trust in the workflow-script author lane it already
* accepted. Residuals: an isolation request that cannot be honored FAILS the spawn (fail-closed, see
* {@link ExecutionEnvFactoryContext.isolation}) with the observable degrade shapes rejected outright, but a
* factory that mints a worktree DEEP INSIDE the shared tree is within-contract — verifying that the minted
* dir is a genuine fresh worktree is the factory's MUST-throw contract, not an engine receipt.
*/
worktreeBase?: Partial<TaskSpec>;
/**
* The allowed model NAMES an LLM-authored script may pick (design/98 §2.5 新洞2). A script gives a
* `modelName` string (never a `Model` object — that carries `baseUrl`/`headers` = exfil); the engine
* resolves it against this list to a deploy-configured `Model` (the script never sees the object).
* **FAIL-CLOSED**: `undefined`/empty ⇒ the script CANNOT pick a model — a spec that names `modelName`
* THROWS `WorkflowModelNotAllowedError` and the child is never spawned. The refusal is loud, not a
* silent downgrade: the script asked for a model it may not have, so the agent call FAILS rather than
* quietly running on the workflow's default role (a script that wants the default must simply OMIT
* the field). The same throw covers a name that is off the list, and a listed name the `models`
* catalog does not hold — so the legal set is the INTERSECTION of this list and the catalog keys
* (note the catalog is the Runner's TIER-EXPANDED one, so on a deployment with `RunnerDeps.tiers`
* the tier words and CC aliases are catalog keys here too, still gated by this list).
* NEVER fail-open to the whole `models` catalog for an LLM-authored workflow.
*/
workflowModelAllowlist?: string[];
}
/**
* design/99 §K — the per-principal runtime ENTITLEMENTS the engine enforces server-side, resolved by a
* deployment via {@link RunnerDeps.runtimeCapsResolver} from center's `GET /api/config/effective?principal=`.
* Every field is TIGHTEN-ONLY (a cap only ever DENIES or FORCES) — but the family carries THREE
* polarities, and "absent" does not read the same on each:
* · DENY-shaped, `false` denies / absent is no restriction: {@link allowWorkflows}, {@link allowFork},
* {@link autoMode} (the per-principal deny bit of a user-enabled mode), {@link allowMemoryOptOut}
* (the one member whose resolver-FAULT degrade is allow, on the privacy axis);
* · GRANT-shaped, `=== true` opts in / absent is OFF: {@link allowObservers};
* · FORCE-shaped, `true` mandates / absent is the caller's opt-in default: {@link forceDurableGate}
* (binds only where a checkpoint store is wired; announced `config.durable_gate_unavailable` where none is).
* These seven are the caps the ENGINE enforces — {@link bypassPermissions} is the seventh, the deny bit of the
* `bypassPermissions` mode; `allowUltracode` stays a shell-UX concern (see {@link RunnerDeps.runtimeCapsResolver}).
*/
/**
* #692 C-d — WHICH plane's ratchet denied auto mode, when the resolver knows: the three settings sources
* CC 2.1.250's `permissions.disableAutoMode` can come from, as the resolver folds them into the one deny bit
* ({@link RuntimeCaps.autoMode} `=== false`). `org` = a managed / organization policy plane; `local` = the
* machine's local settings; `settings` = the user's / project's settings file. Read ONLY beside the deny bit
* (a source with no denial is a contradiction the value screen refuses as a resolver fault); projected onto
* the leg's manifest read face (`WiringManifest.autoMode.deniedSource`) so a shell can say "your organization
* turned auto mode off" vs "your settings did" instead of one word for both.
*/
export declare const AUTO_MODE_DENY_SOURCES: readonly ["org", "local", "settings"];
export type AutoModeDenySource = (typeof AUTO_MODE_DENY_SOURCES)[number];
/** Membership test for {@link AutoModeDenySource}. */
export declare function isAutoModeDenySource(v: unknown): v is AutoModeDenySource;
export interface RuntimeCaps {
/** `false` DENIES workflow self-orchestration for this principal server-side — even on a deployment that is
* capable ({@link RunnerDeps.workflowScriptRunner} + governance). The third stage of the workflows gate.
* `undefined`/`true` = no per-principal restriction (the deployment capability still governs). */
allowWorkflows?: boolean;
/** `false` DENIES fork (`Agent(subagent_type:"fork")` — design/136: the only fork face since the standalone
* tool was retired; the deny is the honest `fork.disabled` result, and since 1.256 it binds the Agent-fork
* route too, closing the 1.254 governance circumvention) for this principal server-side — even on a fork-capable
* (durable-store) deployment. Fork is a compute-amplification primitive (a principal spawns
* context-inheriting children, each spending on its own quota), so a multi-tenant operator gates it
* per-principal the SAME way as {@link allowWorkflows}. `undefined`/`true` = no per-principal restriction
* (the task `enableFork` + durable-store capability still govern). Tighten-only: it can DENY a capable
* deployment, never GRANT beyond it. */
allowFork?: boolean;
/**
* Observer agents (CC 2.1.206 parity, docs/CC206-OBSERVER-ANCHORS-2026-07-11.md §6): honor
* {@link AgentDefinition.observer} auto-spawn declarations. **POLARITY NOTE — unlike
* `allowWorkflows`/`allowFork` (`undefined` = unrestricted), this one is `=== true` EXPLICIT
* opt-in, default OFF**: CC's gate pair is a default-UNSET experimental env var
* (`CLAUDE_CODE_EXPERIMENTAL_OBSERVER_AGENTS`) ANDed with a default-true statsig gate — the
* composite ships dark. `undefined`/`false` ⇒ declarations resolve to unobserved (no warn: the
* gate, not a config mistake).
*/
allowObservers?: boolean;
/** `true` FORCES this principal's run onto the durable-approval path: the engine synthesizes a
* `durableApproval{scope: principal}` so a policy `ask` suspends to the wire (a per-tool CC-faithful gate)
* even when the caller did not opt in — center's fleet-wide "interactive runs gate" mandate. A
* caller-supplied `TaskSpec.durableApproval` always wins (it may carry a tighter scope/ttl).
* The mandate binds only where a {@link RunnerDeps.checkpointStore} is wired — the park facility it
* forces the run onto. On a store-less deployment it has no facility: the leg announces
* `config.durable_gate_unavailable` once (before its first ask), every ask resolves on the live chain
* (a live approver / question face in-stream, or the fail-closed deny with none), no durable record
* is written, and a live question face is consulted rather than vetoed (a mandate that cannot park
* must not demote a reachable person into the model answering for them). */
forceDurableGate?: boolean;
/**
* The per-principal auto-mode DENY bit (CC 2.1.250 polarity — the mirror of the settings-plane
* `permissions.disableAutoMode` ratchet: an organization may only take auto mode AWAY; it is the
* USER who turns it on, by requesting the mode at {@link TaskSpec.permissionMode}).
*
* **POLARITY: `=== false` DENIES; ABSENT IS NOT A DENIAL.** This is the family's deny-shaped
* member (like {@link allowWorkflows} / {@link allowFork}), NOT a grant-shaped one like
* {@link allowObservers}: a deployment with no entitlement source at all resolves `undefined` here
* and still arms on intent + capability. `true` is accepted and means the same as absent (no
* per-principal restriction) — it is not a grant, and it cannot arm a task whose intent seat is
* unset. The arming itself: intent ∧ {@link RunnerDeps.autoMode} present ∧ this bit not `false`
* (allow → run; block → deny `source:"classifier"`; classifier failure → FAIL-CLOSED back to the
* original ask chain). The trust gate is unchanged: classifier RULES enter only through the
* deployment assembly face, never a repo-controlled plane (the CC 2.1.207 three-source invariant).
* A denial resolves to a no-op (asks flow the original chain; no warn — the gate, not a mistake).
*/
autoMode?: boolean;
/**
* #692 C-d (additive) — WHICH source's ratchet set {@link autoMode} to `false`, when the resolver folded
* several ({@link AutoModeDenySource}). Optional: a resolver that does not track sources omits it and the
* manifest reads `denied` with no source. Present with `autoMode` not `false`, or spelled outside the set,
* is a resolver FAULT (announced through `onError`, the deny bit coined fail-closed, `resolver_fault` on the
* manifest) — a source named for a denial that did not happen is a marshalling error, never a fact.
*/
autoModeDenySource?: AutoModeDenySource;
/**
* The per-principal `bypassPermissions` DENY bit — the engine seat of the settings-plane
* `permissions.disableBypassPermissionsMode: "disable"` ratchet. **`=== false` DENIES; ABSENT IS NOT A DENIAL**
* (the family's deny shape, like {@link autoMode}). A task that requests `bypassPermissions` on a leg whose
* caps carry `false` runs in `default` instead, announced once (`config.permission_mode_unavailable`,
* `by: "operator_deny"`) and reported on `wiring_manifest.permissionMode` — a mode declaration is folded, never
* a 4xx. A steer into `bypassPermissions` on such a leg folds the same way.
*/
bypassPermissions?: boolean;
/**
* design/383 §3.1 — may this principal declare a session memory-capture OPT-OUT
* (`TaskSpec.memory.capture: "off"` / the Runner flip verb)? `false` = the deployment REQUIRES
* capture for this principal (compliance / knowledge retention) — the declaration is refused
* terminal (`memory.capture_optout_denied`). `undefined` / `true` = no per-principal restriction.
*
* ⚠️ POLARITY REVERSAL — this member is the family's one deliberate exception to the
* fail-closed resolver degrade (`{allowWorkflows:false, allowFork:false}`): on the PRIVACY axis
* "deny" means "capture the session of a user who explicitly said no", which is the IRREVERSIBLE
* side (a wrongly-captured session has been read, may be folded into products, may have synced —
* deleting later does not unhappen it), while a wrongly-honored opt-out costs one uncaptured
* session (recoverable). Under the default/`"open"` posture a resolver FAULT therefore reads as
* ALLOW (+ a named `onError` disclosure), and only an explicit `false` denies; a deployment for
* which the compliance side is the heavier irreversibility declares `memoryCapturePolicy:
* "governed"`, which flips the fault arm to refuse-to-run (see {@link RunnerDeps.memoryCapturePolicy}).
*
* 🔒 FAULT-TRANSIT CONTRACT (server F2, written here because the resolver's habits run the other
* way): a resolver failure for THIS member must surface as the member being ABSENT (`undefined`)
* or as a THROW — both read as "fault". The resolver MUST NOT coin a stand-in explicit value on
* failure (the `{allowWorkflows:false}` degrade habit): an explicit `false` IS the denied
* verdict, and encoding an outage as one silently swaps the fault arm for the verdict arm — under
* `"open"` that is the exact polarity inversion this member exists to prevent. A non-boolean
* value here is treated as a fault (never truthiness-read) and disclosed.
*/
allowMemoryOptOut?: boolean;
}