UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

114 lines (113 loc) • 7.75 kB
import type { OracleIsolationVerdict } from "./oracle-isolation.js"; import type { SkillSpec } from "./types.js"; /** * The terminal of {@link decideAutoPromote}. * - `"auto_promote"` — the ONLY exit that skips human review. Reachable only when all four prior guards * (tripwire / irreversible / statistics / concurrence) clear AND all three gates are present+passing. * - `"candidate_only"` — surface the artifact as a candidate; the SOLE shortfall is the oracle isolation class * being downgraded to `in_process_probe_only` (oracle-isolation.ts:34) with everything else passing. * - `"needs_human"` — fail-closed default: escalate to a human (any guard tripped, any other gate failing). */ export type AutoPromoteTerminal = "auto_promote" | "candidate_only" | "needs_human"; /** * Gate-2 handle (裁决净增③): the verdict the promoted artifact's OWN promotable write landed with, held in one place * so the decision reads one verdict and never a loose boolean. (It used to carry the artifact's lineage id as the scope * key of the retired per-skill narrowing; with that gone the lineage keys nothing the fold reads.) */ export interface PromotedRecord { /** * Gate-2 chokepoint result: the promotable write landed WITH a positive utility verdict (memory.ts:240), * or it was absent/ungated. Only `"present_positive"` is gate-passing; `"absent_or_ungated"` ⇒ needs_human. */ gate2Verdict: "present_positive" | "absent_or_ungated"; } /** * Inputs to {@link decideAutoPromote}. Every field is a PRESENCE/boolean/set the profile has already computed — * the core fold inspects no numeric threshold, no stats shape, no taxonomy. The caller (a profile-promotion job) * has already RE-asserted Gate-1 (`assertOracleIsolation`), run the exec-gate, read the appended Gate-2 verdict, * and armed the Gate-4 seam BEFORE building this struct; this function only READS those outputs. */ export interface AutoPromoteInputs { /** Gate-1 verdict (oracle-isolation.ts:37); the caller re-asserted it before calling this function. */ oracle: OracleIsolationVerdict; /** Gate-2 lineage-bound single handle (净增③). */ record: PromotedRecord; /** Gate-4: the irreversibility tighten is in place (or the artifact touches no irreversible tool). */ gate4Armed: boolean; /** * §6 permanent human door (净增①): the artifact touches at least one irreversible TYPED tool. When true the * decision is ALWAYS `needs_human`, even if everything else would pass — because G4 is pure in-process * gate-PRESENCE (2-of-3 asymmetry) and `gate4Armed` alone ("the gate is wired") is not trustworthy for an * irreversible action. */ touchesIrreversibleTool: boolean; /** * The profile's statistics RAN for this run (it produced a verdict). Core requires PRESENCE only — the * statistical THRESHOLDS (train/val/test split, canary, min-samples, cost-per-truly-correct) are profile. */ statisticsPresent: boolean; /** * The KIND labels of the independent mechanical signals that concur. Core enforces `size >= 2` DISTINCT * kinds (净增②) — feeding the SAME kind twice still yields size 1 (it's a Set keyed by kind). WHICH kinds * count, and whether they are truly de-correlated, are profile concerns (§7 honest residual). */ concurringSignals: ReadonlySet<string>; /** Tripwire result — ESCALATE-ONLY (§5 ③); when `escalate` it always wins and is never cleared. */ tripwire: TripwireResult; } /** * The fail-closed PRESENCE AND-chain (design/79 §1 §2). Any missing/failing gate ⇒ NEVER `auto_promote`; it * degrades to `needs_human` (or, for the single oracle-isolation-class shortfall, `candidate_only`). This * function computes NO numeric threshold — only PRESENCE/boolean/set-size checks. * * **Guard order (load-bearing):** * 1. `tripwire.escalate` → needs_human (tripwire always wins, never clears — §5 ③) * 2. `touchesIrreversibleTool` → needs_human (§6 permanent human door — 净增①; even if all else passes) * 3. `!statisticsPresent` → needs_human (statistics must have RAN — core-required; thresholds are profile) * 4. `concurringSignals.size < 2` → needs_human (≥2 DISTINCT KIND mechanical signals concur — 净增②) * 5. the three-gate present-passing check; if not all pass, the SOLE-shortfall arm: * `in_process_probe_only` oracle (and every other gate passing) → `candidate_only`; otherwise needs_human. * 6. all three gates present+passing (with guards clear) → `auto_promote` (the only auto exit). */ export declare function decideAutoPromote(i: AutoPromoteInputs): AutoPromoteTerminal; /** * The result of {@link deriveTripwire}: an ESCALATE-ONLY advisory. `escalate` can only ever be set to `true` * by a scan; a clean scan contributes nothing and NEVER clears another scan's escalation. `reasons` carries * the matched tokens / drifted symbols for observability. */ export interface TripwireResult { escalate: boolean; reasons: string[]; } /** * The CORE-FROZEN denylist floor (design/79 §3 净增④ / P0#4): a safety invariant the profile MAY NOT remove. * `deriveTripwire`'s `profileTokens` may only UNION (add) to this — never subtract — resolving the tension * between "the token set can rot (new exfil primitives appear)" and "constitution-minimal". These are RAW-BYTE * substrings scanned against the artifact's raw bytes INCLUDING its markdown bytes (extractSymbols skips * markdown, so the byte scan is what covers prose/fenced-code that names an egress/exec primitive). */ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>; /** * Build a tripwire verdict over a promoted artifact's text. **ESCALATE-ONLY** (design/79 §3,守 design/76 * §5.1 r3): it can ONLY ever raise `escalate:true`; a clean artifact yields `{escalate:false, reasons:[]}` and * NEVER clears anything — it contains nothing itself: the real protection is the out-of-process boundary and the * permission gate at call time, not this advisory. Two mechanical scans, both escalate-only: * * 1. **symbol-mention drift** via {@link extractSymbols} (repo-map.ts). extractSymbols is a v1 heuristic: * column-0 / source-only, it skips indented lines and is structurally INCAPABLE of being a containment * proof (it can't enumerate everything the artifact can call). So it is used ONLY to ESCALATE — if a * surfaced top-level symbol names a tool/capability that is NOT in `declared.allowedTools`, escalate. * 2. **raw-byte denylist** over the artifact's RAW bytes INCLUDING markdown bytes — {@link FROZEN_DENYLIST_FLOOR} * UNIONed with `profileTokens` (add-only). A substring hit → escalate, with the matched token in `reasons`. * * **Fail-closed:** if EITHER scan THROWS on real content, return `{escalate:true, ...}` — a scan failure must * NOT silently become "no escalation". (This is disjoint from the composition-layer "tripwire DOWN ⇒ zero * contribution, never block" case, which is the caller's concern, not a throw inside the scan.) * * @param artifactText the artifact's raw text (markdown body included). * @param declared the artifact's declared tool list (the `SkillSpec` member the loader copies) — its `allowedTools` * entries, read by the list reader as a rule for the tool each names (one it reads as nothing names none), are the * symbol-drift baseline. * @param profileTokens optional ADD-ONLY extra denylist tokens (UNIONed onto the frozen floor; cannot remove). */ export declare function deriveTripwire(artifactText: string, declared: Pick<SkillSpec, "allowedTools">, profileTokens?: ReadonlySet<string>): TripwireResult;