UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

91 lines (90 loc) • 7.24 kB
import type { ExecutionEnv } from "../internal/harness-types.js"; import type { PermissionResult, ToolPolicy } from "./tool-policy.js"; import { type PathGuardRow } from "./path-guard-rows.js"; /** One write domain SOURCE as an allow row of the one table: it reaches every location at or below one of its admitted keys. * The write gate's three sources, and a session rule's `allowDirs` (the confinement that admits a write only inside them). */ export declare function domainRow(source: "advertised" | "configured" | "accepted" | "session", dirs: readonly string[]): PathGuardRow; /** design/388 B22 — the identity triple the exemption probe hands the store beside the name. */ export interface ToolCallIdentity { capabilityId: string; contractId: string; shapeDigest: string; } /** Options for {@link createFsWriteGatePolicy}. */ export interface FsWriteGatePolicyOptions { /** The task's real execution env (the one the hands run against) — canonicalization must see the * actual filesystem (a remote/E2B task resolves in its container). */ env: ExecutionEnv; /** The FALLBACK base a relative path resolves against (omit = paths resolve as given). The * call's TARGET prefers the live tracked cwd when the engine stamps one ({@link ToolCallRequest.cwd}); * the `acceptDirs`/`exemptDirs` entries below always resolve against THIS value. */ rootPath?: string; /** Writes whose canonical target lands inside one of these dirs are ALLOWED (the accept-session * domain, e.g. the cwd subtree after "allow all edits during this session"). */ acceptDirs?: readonly string[]; /** Writes whose canonical target lands inside one of these dirs are ALLOWED and never asked * (auto-approved working areas: the scratchpad dir, plan files). Checked before `acceptDirs` * (same verdict — the split is intent/observability, exemptions are structural not session-won). * An entry that RESOLVES onto `rootPath` — or above it — grants nothing: a domain that holds the * root is not a working area but the whole workspace, and a symlinked scratchpad is all it takes. * Same rule, same predicate as the prompt's advertised writable directories; say "every write here * is pre-approved" with `defaultWrite: "allow"` instead. `acceptDirs` is NOT judged by it (a person * may grant the whole root for a session — see the module header). */ exemptDirs?: readonly string[]; /** Verdict for a covered write OUTSIDE every domain: `"ask"` (the manual-mode default) routes it * to the ask-resolution chain (live onAsk / durable park / headless deny); `"allow"` is the * acceptEdits-mode shape (domain checks still run so a sensitive-path deny composes unchanged). */ defaultWrite: "ask" | "allow"; /** The ask the default verdict mints in place of the gate's ordinary write question — a permission mode's own * sentence over the resolved `path`, with the gate's domain note (a configured domain that was not honored) to * append when there is one. Read only where `defaultWrite` is `"ask"`. */ defaultAsk?: (path: string, note: string | undefined) => PermissionResult; /** * Session-exemption lookup ("allow all for this session" grants recorded by the deployment's approval * surface). Consulted ONCE per covered write, after the domain checks and immediately BEFORE the gate * would produce its `ask`/default verdict — a `true` return short-circuits to `allow` (with a message * naming the exemption source for the audit trail) so an already-granted write does not re-ask/park. * * · `toolName` is the CANONICAL tool name (same normalization this gate matches on), so an exemption * recorded under either alias of a tool matches both — key your store with the same normalization. * · `canonicalPath` is the CANONICALIZED real target (the exact key the gate's own symlink-safe * containment judged) — a symlink alias of an exempted file therefore still hits the exemption, * and an exemption can never be minted for a raw alias that resolves elsewhere. * · FAIL-CLOSED: a throwing/rejecting lookup counts as "not exempt" (the write asks as before) — * an exemption-store outage must never widen the gate. * · Cannot override a DENY: this factory never denies; a sensitive-path deny composed alongside via * `combinePolicies` outranks this `allow` under the deny > ask > allow fold, and the two fail-closed * asks for an UNRESOLVABLE target fire before any canonical key exists, so they are never exempted. */ isExempt?: (toolName: string, canonicalPath: string, identity?: ToolCallIdentity) => boolean | Promise<boolean>; /** * design/380 O13 — the deployment HARD-CLOSE knob for guarded writes. Armed (`true`), a covered * write whose env does NOT provide `writeFileGuarded` is REFUSED (deny, typed message) — never the * plain-writeFile fallback: for a deployment that exists to close the adjudicate→write TOCTOU * window, a silent advisory downgrade would open the hole right beside its patch. The version-skew * arm is the same refusal (an older adapter without the method IS an env without the capability). * Default off = every verdict ACTION exactly as before (the capability's ABSENCE is still disclosed * on the ask face either way — see the advisory sentence at the ask sites). The knob lives on the * deployment's gate options, not in core-global state: core stays lane-agnostic (no device concept * here); the deployment that runs guarded-capable envs arms it. * * WHAT ARMED DOES AND DOES NOT CLOSE (honesty boundary): armed guarantees every covered write lands * through an ATOMIC precondition-verified backend step, closing the tool's own canonicalize→write * window. It does NOT bind THIS GATE's adjudicated canonical key across an approval wait: the tool * re-canonicalizes at execute time, so a symlink swapped between this gate's verdict and the tool's * execution is re-resolved (then atomically verified against the NEW resolution). Closing that * remaining window needs the adjudicated key carried through a trusted approval receipt into tool * execution — a separate seat, deliberately not smuggled into this knob. */ requireGuardedWrite?: boolean; } /** The coded refusal a gate wired with a root-covering exempt domain throws at CONSTRUCTION — the * `config.*` prepare-refusal family (one exported spelling per member, thrown so the task never * starts), so a deployment can compare against this constant instead of matching message text. */ export declare const WRITE_EXEMPT_DIR_COVERS_ROOT_CODE = "config.write_exempt_dir_covers_root"; /** * Build the file-write approval gate policy. Mount via `combinePolicies` alongside the deployment's * other layers (sensitive-path deny, session rules, caller policy) — see the module header for the * layering contract. Idempotent + side-effect free per {@link combinePolicies}'s requirement. */ export declare function createFsWriteGatePolicy(opts: FsWriteGatePolicyOptions): ToolPolicy;