UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

195 lines (194 loc) • 15.5 kB
/** * 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; }