@sema-agent/core
Version:
Stateless, task-oriented AI agent core
254 lines (253 loc) • 17.6 kB
TypeScript
/**
* PrepareTask's POLICY-CHAIN phase, verbatim from the driver: the caller policy
* and its name audit (advisory once per deployment; the two hard refusals), the session / ancestor / frozen-projection /
* skill-scope deny-narrowing layers and their fold, the resume-edit base policy, the hook record with its timeout seat
* and observational half, the same-instance arbitration machinery (the caller slot's first-decision record), the
* inherited-unavailable marker channel, the grant record, the sandbox-admission mode's arming and its three
* sideband maps, the source-identity and risk-axes factories, the two permission-rule lanes and the rule-offer
* factory, the approved-edit re-check, the inherited-constraint wrappers (the same-instance adapter and the true
* wrapper), the rewrite-emitter index and the instrumented chain, the integrity guards, the fold, and the final-form
* re-check that is the effective policy. The advised-names dedupe pair only this section reads moved with it.
* The interface is the dependency list the segment had implicitly.
*
* ASYNC PHASE: the stretch awaits the session-policy store (the task's own rules and each ancestor's live re-read)
* exactly where the segment did; the closures it builds await at tool-call time. The phase boundary adds one
* continuation between its last await and the driver's next read — the accepted class under {@link RunInternals}
* `@contract prepare.deps-read-stable`.
*
* NOT CHANGED HERE: the ask-origin vocabulary. This phase is a mint site for `AskOrigin` words (through the inherited
* arms' `judgeInheritedClassifier` / `inheritedAskCarry`), and it carries them verbatim — the closed set, its fence and
* every spelling are exactly the driver's.
*/
import type { AgentTool } from "../../internal/harness.js";
import type { ExecutionEnv } from "../../internal/harness.js";
import { type AskClass } from "../ask-class.js";
import type { AutoModeDecider } from "../auto-mode.js";
import { type Hooks } from "../hooks.js";
import type { LockedPreflight } from "../locked-config.js";
import type { SessionPermissionRules } from "../session-policy-store.js";
import { type OnAsk, type ToolPolicy } from "../tool-policy.js";
import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect } from "../types.js";
import type { CwdRef } from "../../tools/fs/fs-shared.js";
import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
import type { SkillGrantLane, SkillGrantTable } from "../skill-grant.js";
import { type AllowEntryLane, type AllowEntryTable } from "../allow-entry.js";
import type { InheritedGate, IrreversibilityTier, PrepareResume, RunInternals, StopForDenialLimit } from "./contracts.js";
import { type Arbitration } from "../ask-origin.js";
import { type ModeGateSeats, type PermissionModeController } from "../mode-policy.js";
import { type InheritedAskGrant } from "./inherited-ask-grants.js";
import { createRuleOffersOf, type PermissionRuleLanes } from "./permission-rule-lanes.js";
import type { ToolCallLike } from "../tool-registry.js";
export interface PreparePolicyChainInput {
/** borrowed-readonly — the REBOUND spec. Read: `sessionId` / `principal` (the session-rule read and the lanes' identity),
* `hooks` (spec over deps), `durableApproval` / `checkpointStore` (the mandate-park facility test and the sandbox arming's park prerequisites), `taskId` (the late-ask observer's detail).
* Never mutated. */
spec: Pick<TaskSpec, "sessionId" | "principal" | "hooks" | "durableApproval" | "taskId" | "checkpointStore">;
/** borrowed-readonly — the deployment seats: `sessionPolicyStore` (the rule reads), `hooks` (the fallback record),
* `permissionRuleStore` / `localOwnerRules` (the lanes and the offer factory), `onError` / `onNotice` (the advisories,
* the refusal disclosures, the late-ask observer), `tracer` (the admission and rule-store frames), `checkpointStore` (the park facility). Receiver preserved;
* the advisory dedupe is KEYED on this object's identity. */
deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore" | "a2a">;
/** borrowed-readonly — the trusted spawn-side channel: `parentTaskId` (the from-subagent bit), `explicitAgentName` /
* `agentName` (the source agent name) — the ask source identity's local, trusted facts. */
internals: Pick<RunInternals, "parentTaskId" | "explicitAgentName" | "agentName"> | undefined;
/** borrowed-readonly — the acquired session id (every operator line's attribution; the ask source's task id). */
sessionId: string;
/** borrowed-readonly — the run's invocation id (the late-ask observer's detail). */
runId: string;
/** borrowed-readonly — the per-task identity (the trace frames' taskId; the rule-store disclosure). */
hostTaskId: string;
/** borrowed-readonly — the FINAL task root (path rules resolve against it; the lanes' and the offers' root). */
taskRootFinal: string;
/** borrowed-readonly — the leg's canonical write roots beyond the task root (`additionalDirectories` + advertised writable
* dirs): the `acceptEdits` mode gate allows writes inside the root and these, and asks outside them all. */
additionalRootsCanonical: readonly string[];
/** borrowed-readonly — the run's resolved env (path rules resolve in it; the sandbox declaration is read off it). */
executionEnv: ExecutionEnv;
/** borrowed-readonly — the frozen preflight: `toolPolicy` (the caller slot) and `basePolicyForResumeEdit` (the lock-aware
* resume-edit override). */
lockedPreflight: Pick<LockedPreflight, "toolPolicy" | "basePolicyForResumeEdit">;
/** borrowed-readonly — the run's ONE tool roster, read only here (the name audit's universe: names + aliases). */
tools: ReadonlyArray<AgentTool>;
/** borrowed-readonly — the harness list (placeholders keep their deferred names), the audit universe's second half. */
harnessTools: ReadonlyArray<AgentTool>;
/** borrowed-readonly — the effect collection, read by the narrowing layers (path-confinable write tools). */
toolEffects: ReadonlyMap<string, ToolEffect>;
/** borrowed-readonly — the egress mark set (the risk axes, the boundary predicate, the offer factory). */
egressTools: ReadonlySet<string>;
/** borrowed-readonly — the folded irreversibility tiers (the risk axes, the offer factory). */
irreversibilityTier: IrreversibilityTier;
/** borrowed-readonly — the explicit negative declarations per tool (the risk axes' "engine judged it false"). */
axisExplicitNegatives: ReadonlyMap<string, {
irreversible?: false;
egress?: false;
}>;
/** borrowed-readonly — the ENGINE HAND roster: every tool whose effect dispatches through the task's execution env,
* membership minted at the mount sites (the sandbox boundary predicate's one containment fact). */
envHandToolNames: ReadonlySet<string>;
/** borrowed-readonly — the shell-seat provenance set (the offer factory's `shellGated` mandate term, read per call). */
shellGatedTools: ReadonlySet<string>;
/** borrowed-readonly — who marked each name's seat (the offer factory's `shellGated` term reads it as the gate does). */
toolMarkSources: ReadonlyMap<string, ReadonlySet<import("../ask-origin.js").ToolMarkSource>>;
/** borrowed-readonly — the tracked-cwd seat, read per call by the lanes and the offer factory (never written here). */
handsCwdRef: CwdRef | undefined;
/** borrowed-readonly — the compiled read-deny judge, or undefined (the offer factory's directory negative control). */
readDenyMatcher: ReadDenyMatcher | undefined;
/** borrowed-readonly — the run's one notice for a person's rule store that could not be read (`ruleStoreUnreadableNotice`). */
announceRuleStoreSkipped: (at: "prepare" | "call") => void;
/** borrowed-readonly — whether the question tool mounted (the lanes' question-tool exclusion). */
questionToolMounted: boolean;
/** borrowed-readonly — the resolved per-principal entitlements; only `forceDurableGate` (the mandate-park facility and
* the sandbox arming's prerequisite). */
runtimeCaps: Pick<RuntimeCaps, "forceDurableGate"> | undefined;
/** borrowed-readonly — the ancestors' session-rule snapshots (each re-read live-preferring, floor kept).
* Never written: an entry is some other leg's or some host's object — see `ancestorSessionRulesRef`. */
inheritedAncestorRules: InheritedGate["ancestorRules"];
/** borrowed-mutable — THIS leg's record of what each ancestor layer now ENFORCES, keyed by the entry's
* POSITION (see the seat's own declaration in `prepare-inherited-gate.ts` for why position and not
* identity). Writer: this phase, through each ancestor layer's `onEnforced`. */
ancestorSessionRulesRef: {
current: Map<number, {
sessionId: string;
principal?: string;
rev: number;
gen?: number;
rules: SessionPermissionRules;
}>;
};
/** borrowed-readonly — the ancestors' caller-policy constraints, ancestors first (one wrapper each). */
inheritedParentConstraints: NonNullable<InheritedGate["parentConstraints"]>[number][] | undefined;
/** borrowed-readonly — the resumed leg's SEED half; only `constraintChain` (the frozen deny projections). */
seedInheritedGate: Pick<NonNullable<PrepareResume["seed"]["inheritedGate"]>, "constraintChain"> | undefined;
/** borrowed-readonly — the run's skill grant table; this phase binds its lane to the call facts the person's rule lane
* reads (root, session, live cwd, the execution environment's home). Not written here. */
skillGrants: SkillGrantTable;
/** borrowed-readonly — the request's and the deployment's allow-entry tables (prepare seeded them); this phase binds
* their lanes to the same call facts. Not written here. */
allowedToolTables: {
readonly request: AllowEntryTable;
readonly deployment: AllowEntryTable;
};
/** borrowed-readonly — the FROZEN approver seat (the pass-through test's approver identity). */
frozenOnAsk: OnAsk | undefined;
/** borrowed-readonly — the identity of this task's hook-env source (the pass-through test's environment term). */
hookEnvSource: unknown;
/** borrowed-readonly — the own-hook crash reporter (the timeout seat's bad-value disclosure). */
notifyOwnHookCrash: (err: unknown) => void;
/** borrowed-readonly — LATE-BOUND: this task's own classifier instance (the pass-through test's decider term), read at chain
* assembly (the arming runs after this phase, and again on a steer into `auto`). */
autoModeDecider: () => AutoModeDecider | undefined;
/** borrowed-readonly — the leg's live permission-mode seat: the mode gate policy layer and the inherited stations' arbitration
* read `current` per call. */
permissionMode: PermissionModeController;
/** borrowed-readonly — the task's scratchpad directory (the mode write gate's one exemption), or undefined. */
scratchpadDir: string | undefined;
/** borrowed-readonly — the leg's live roster seat; the mode write gate reads `current` per call. */ toolRosterDeltas: NonNullable<ModeGateSeats["roster"]>;
/** borrowed-readonly — the run's ONE typed stop for a denial-limit bound (the fallback settlements call it). */
stopForDenialLimit: StopForDenialLimit;
/** borrowed-readonly — the effective delegation facts; only `isDelegatedChild` (the ask source identity). */
delegation: {
isDelegatedChild: boolean;
};
/** borrowed-readonly — the run's abort controller; only `signal` (the default call signal of every closure here). */
abortController: Pick<AbortController, "signal">;
/** borrowed-readonly — the injectable wall-clock (the inherited arms' human-wait timing). */
now: () => number;
/** borrowed-mutable — the own session-rules snapshot seat (the inherited-gate phase mints it; `inheritedGateForChildren`
* reads it late). Writer: THIS phase, once, when the store returns this task's own rules. */
ownSessionRulesRef: {
current?: {
sessionId: string;
principal?: string;
rev: number;
gen?: number;
rules: SessionPermissionRules;
};
};
}
export interface PreparePolicyChainResult {
/** owned — the resolved hook record (`spec.hooks ?? deps.hooks`), or undefined. */
hooks: Hooks | undefined;
/** owned — the per-leg hook timeout, resolved and validated ONCE (`Prepared.hookTimeoutMs`). */
hookTimeoutMs: number;
/** owned — whether the deployment declared its screening face observational. */
preToolUseObservational: boolean;
/** owned — the screening face this task's OWN gate runs in phase 1 (the observational wrapper when declared). */
ownGatePreToolUse: Hooks["preToolUse"] | undefined;
/** owned — the resume-edit base policy (`Prepared.basePolicyForResumeEdit`), or undefined. */
basePolicyForResumeEdit: ToolPolicy | undefined;
/** owned — the deny-narrowing fold (`Prepared.denyNarrowingPolicy`), or undefined when no layer exists. */
denyNarrowingPolicy: ToolPolicy | undefined;
/** owned — the skill grant lane (`Prepared.skillGrants`; the gate's skill member and deny rows read it). */
skillGrantLane: SkillGrantLane;
/** owned — the request's and the deployment's allow-entry lanes (the gate's two entry-list members). */
allowEntryLanes: {
readonly request: AllowEntryLane;
readonly deployment: AllowEntryLane;
};
/** owned — the caller-facing policy layers (the wiring manifest reads their COUNT). */
policyLayers: ToolPolicy[];
/** owned — THE effective policy the gate station adjudicates with (the fold plus its final-form re-check), or undefined. */
effectivePolicy: ToolPolicy | undefined;
/** borrowed-mutable — the inherited-unavailable marker channel. Writers: the inherited arms here (mark); the gate station
* (the lifecycle-end sweep) and the park lane's consumers (delete on consumption). */
inheritedUnavailableAsks: Set<string>;
/** owned — the inherited stations' MODE ARBITRATIONS by tool call (a release or a refusal made inside the fold,
* where an ancestor's ask is arbitrated before its frozen approver). Writers: the three inherited stations here; the gate's
* exit reads it onto the disposition and the gate station deletes it at the call's end. */
ancestorArbitrations: Map<string, Exclude<Arbitration, {
kind: "ask";
}>>;
/** borrowed-mutable — the grant record. Writers: the inherited arms here (record a clean allow at a live approver);
* the gate station (consume on reuse, sweep at settlement). */
inheritedAskGrants: Map<string, InheritedAskGrant>;
/** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
askSourceIdentity: () => {
principal?: string;
sourceTaskId?: string;
fromSubagent?: true;
sourceAgentName?: string;
isDelegatedChild?: true;
};
/** owned — the engine-judged risk axes for an ask, from the same resolved sources the safety tightens use. */
riskAxesOf: (call: Pick<ToolCallLike, "toolName" | "face">) => {
riskAxes?: {
irreversible?: boolean;
egress?: boolean;
};
};
/** owned — the ONE rule-offer factory for all five ask-mint sites. */
ruleOffersOf: ReturnType<typeof createRuleOffersOf>;
/** owned — the personal permission-rule lane, or undefined. */
permissionRuleLane: PermissionRuleLanes["personal"];
/** owned — the org permission-rule lane (`Prepared.permissionRuleOrg`), or undefined. */
permissionRuleOrgLane: PermissionRuleLanes["org"];
/** owned — whether the sandbox-admission mode armed (declared isolation with its park prerequisites). */
sandboxAdmissionArmed: boolean;
/** owned — the NOT-CONTAINED classification: a call the declared sandbox does not contain. */
sandboxBoundaryCapable: (call: Pick<ToolCallLike, "toolName" | "face">) => boolean;
/** owned — the admission disclosure's trace emitter (the gate station emits it once the fold settles). */
emitSandboxAdmitted: (info: {
toolCallId: string;
toolName: string;
askClasses: ReadonlyArray<{
cls: AskClass;
layer: string;
}>;
boundary: "sandbox_internal";
}) => void;
/** borrowed-mutable — the per-call fold ask-class record. Writers: the instrumented chain here (record); the gate station
* (read for strict dominance, then sweep). */
foldAskClasses: Map<string, Array<{
cls: AskClass;
layer: string;
}>>;
/** borrowed-mutable — the ancestor arms' deferred sandbox admissions. Writers: the inherited arms here (record); the gate
* station (emit once the fold settles, then sweep). */
ancestorSandboxAdmissions: Map<string, Array<{
cls: AskClass;
layer: string;
}>>;
}
/** The M17 phase body — prepareTask's policy-chain stretch, verbatim (see the module header). */
export declare function preparePolicyChain(input: PreparePolicyChainInput): Promise<PreparePolicyChainResult>;