@sema-agent/core
Version:
Stateless, task-oriented AI agent core
91 lines (90 loc) • 7.24 kB
TypeScript
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;