@sema-agent/core
Version:
Stateless, task-oriented AI agent core
162 lines (161 loc) • 9.7 kB
TypeScript
/**
* design/125 — **SpecContract**:spec→Verifier 闭环的机器可读任务契约(make-real 两周实战纪律的
* 一等原语化)。本模块 = 纯 core 零件:契约类型 + 三值 oracle 执行器 + frozenPaths 校验/快照/恢复。
* 装配(policy 组合、stop 把关、outcome 发射)在 `orchestration/run-spec.ts`。
*
* 🔴 **信任声明(fable-M3)**:SpecContract 是 **CODE-tier** 输入 —— `oracle[].cmd` 会被亲跑(execFile,
* 任意命令)。它只能来自部署作者/可信代码路径;LLM 生成的 contract 直接喂 runSpec = 不受信输入获得
* 命令执行权(越红线),禁止。contract 里的 `spec` **文本**才是 DATA-tier(runSpec 恒围栏置入 objective)。
* 命令可信 ≠ 它跑的代码可信:门跑的测试会加载模型改过的代码,故门子进程的环境缺省经 `scrubSecretEnv`
* (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"` 时交给它。
*
* 🔴 **oracle 三值(fable-M6)**:每门结果 ∈ {green, red, error} —— 判据不满足 = red;基础设施故障
* (ENOENT/timeout/maxBuffer/parse 解析不出)= **error 非 red**。假红防线:error 绝不冒充"曾经红过"
* (design/73 红线② 的机器化 —— `oracleHadRedRun` 只由真 red 观察置位)。
*/
import type { EngineNotice } from "./engine-notice.js";
/** Declarative green criteria for one oracle gate. Absent ⇒ `exitCode === 0`. All declared criteria AND. */
export interface OracleGreenSpec {
/** Expected exit code. Default 0 when no `parse` is declared; with `parse`, exit code is only
* checked when explicitly set here (test runners exit non-zero on failures the counts already judge). */
exitCode?: number;
/** Regex (source string) that MUST match the combined stdout+stderr. */
mustMatch?: string;
/** Regex (source string) that MUST NOT match the combined stdout+stderr. */
mustNotMatch?: string;
/**
* Parsing criteria (fable-M5/B1 一体解): extract pass/fail counts from the output. Each regex's FIRST
* capture group is the integer count (e.g. `"ℹ pass (\\d+)"`). Either regex failing to match = the gate
* is **error** (解析不出 ≠ red — a runner that printed nothing proves nothing).
*/
parse?: {
passRe: string;
failRe: string;
};
/**
* Lower bound on the parsed pass count (requires `parse`). `"baseline"` = the pass+fail TOTAL this gate
* observed at preflight — the anti-「测试消失式走捷径」 gate (the model leaves tests untouched but makes
* them undiscovered; harness :114 实战门, fable-B1). Numeric default when absent: 1 (a zero-test green
* is vacuous).
*/
minPass?: number | "baseline";
}
/** One mechanical adjudicator: a command run via execFile (NO shell) + declarative green criteria. */
export interface OracleGateSpec {
/** Stable gate name (baseline capture and per-gate reporting key on it). */
name: string;
/** argv — `cmd[0]` is the executable, the rest are arguments. execFile, never a shell. */
cmd: string[];
/** Working directory, containment-checked against the runSpec root (same rule as frozenPaths). */
cwd?: string;
/** Per-gate timeout. Default 60s. */
timeoutMs?: number;
green?: OracleGreenSpec;
/**
* How much of the engine's environment the gate's command inherits — the tool shell's `inheritEnv`, read the same
* way. Absent / `"scrub"`: the environment without its credential-shaped variables (`scrubSecretEnv`); the command
* runs code the model wrote, so the credentials are not handed to it by default. `"all"`: the whole environment.
* A list: exactly the named variables that are set (a strict allow list — `PATH` too, when the command is looked
* up on it). Names only, never values: the contract is serializable data. A value of any other shape makes the
* gate `error`.
*/
inheritEnv?: "all" | "scrub" | string[];
}
/** design/125 D1 — the machine-readable task contract (CODE-tier input; see module doc). */
export interface SpecContract {
/** Task spec text (or a file reference resolved against the runSpec root). runSpec owns the objective
* framing; the spec text itself is DATA-tier and is ALWAYS fenced before injection. */
spec: string | {
path: string;
};
/**
* Read-only specification surface. **Validation (fable-M2)**: absolute paths and `..` segments are
* rejected; paths resolve against the runSpec root with a containment check; policy matching uses the
* RESOLVED path. Exact paths, no globs (v1 收窄).
*/
frozenPaths: string[];
/** Mechanical adjudicators (serializable, checkpointable, service-boundary-safe, feed-recordable). */
oracle: OracleGateSpec[];
/**
* 双向验证: preflight runs the full oracle and REQUIRES red (proves the spec has substance), capturing
* baseline counts for `minPass:"baseline"` (fable-B1: the red gate and the baseline are two faces of the
* same preflight). Default **true** (the discipline is the default; opting out must be explicit).
* `{ exempt }` documents WHY this contract legitimately starts green (e.g. pure-refactor spec).
*/
requireRedStart?: boolean | {
exempt: string;
};
/** design/73 aggregation key. Default: `spec:<sha256(specText) first 16 hex>`. */
taskSignature?: string;
}
/** Three-value gate verdict — `error` = the adjudicator itself failed to run/parse (NEVER counts as red). */
export type OracleVerdict = "green" | "red" | "error";
export interface OracleGateResult {
name: string;
verdict: OracleVerdict;
/** Process exit code (null when the process did not run/complete — spawn error or timeout kill). */
exitCode: number | null;
/** Parsed counts when `green.parse` was declared and both regexes matched. */
pass?: number;
fail?: number;
/** Which criterion failed / what the infrastructure error was (first-party classification string). */
reason?: string;
/** Tail of the combined stdout+stderr (UNTRUSTED — the model can influence it via the code under test;
* fence with `delimitUntrusted` before any model-facing use). */
outputTail: string;
}
/** Per-gate preflight counts backing `minPass:"baseline"` (keyed by gate name). */
export type OracleBaseline = Record<string, {
pass: number;
fail: number;
}>;
export interface OracleRunReport {
/** Worst-across-gates: any `error` ⇒ error (fail-loud dominates); else any `red` ⇒ red; else green. */
verdict: OracleVerdict;
gates: OracleGateResult[];
}
/**
* Run every gate of the contract's oracle (sequentially — gates commonly share a working tree) and fold
* the three-value verdicts: any `error` ⇒ `error` (fail-loud dominates; an unfalsifiable adjudication must
* never pass silently), else any `red` ⇒ `red`, else `green`.
*
* `cwdRoot` is the runSpec root: each gate's `cwd` resolves against it with the same containment rule as
* frozenPaths (a contract cannot point its adjudicator outside the workcopy).
*/
export declare function runOracle(gates: OracleGateSpec[], cwdRoot: string, opts?: {
baseline?: OracleBaseline;
signal?: AbortSignal;
/** Receives the once-per-process `config.secret_env_scrubbed` notice when a gate's scrub withholds names (the
* tool shell's seat and latch); absent ⇒ the console line. */
onNotice?: (notice: EngineNotice) => void;
}): Promise<OracleRunReport>;
/** Extract the per-gate baseline counts from a (preflight) report — only gates whose parse succeeded. */
export declare function captureBaseline(report: OracleRunReport): OracleBaseline;
/** A coded error (`config.*` / `spec.*` convention — same shape as goal.ts's reserved-name fail-fast). */
export declare function specError(code: string, message: string): Error & {
code: string;
};
/**
* Validate and resolve `frozenPaths` against the runSpec root (fable-M2): absolute paths and `..`
* segments are rejected outright (`spec.frozen_path_invalid`); the resolved path must stay inside the
* root. Returns resolved absolute paths (the ONLY form policy matching and snapshots use).
*/
export declare function resolveFrozenPaths(rootDir: string, frozenPaths: string[]): string[];
/**
* Byte-level snapshot of the frozen surface (fable-M1: a hash can only DETECT tampering — restoring needs
* the CONTENT, and the snapshot is the restore source; no external seed directory dependency). Missing
* file ⇒ `spec.frozen_missing` (a frozen path that doesn't exist can't be a specification).
*/
export declare function snapshotFrozenPaths(resolvedPaths: string[]): Map<string, Buffer>;
/**
* Restore any frozen file whose on-disk bytes differ from the snapshot (belt for `echo > test/x`-style
* bash writes the Write/Edit deny policy cannot see — fable-MIN2). Returns the number of files restored.
*
* Symlink-safe (实现审 B2): a tampered entry that is no longer a regular file (the model swapped
* it for a symlink pointing outside the root) is REMOVED first — the snapshot bytes are then written to
* a fresh regular file, never THROUGH a link. When `rootDir` is given, the parent directory's physical
* path is containment-checked too (a symlinked PARENT dir would also carry the write outside); an entry
* whose parent escapes is skipped (counted) — the terminal oracle then adjudicates the missing spec
* files honestly (fail-safe direction) instead of runSpec writing outside its root.
*/
export declare function restoreFrozenPaths(snapshot: Map<string, Buffer>, rootDir?: string): number;