#!/usr/bin/env node import { type Brain, type OnAsk, type TaskSpec } from "@sema-agent/core"; import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js"; import { type ServiceConfig } from "./config.js"; import { type Logger } from "./observability/logger.js"; /** Parsed CLI invocation. */ export interface RunLocalArgs { /** The task objective (the first positional argument). */ objective: string; /** `--root ` — the local config/data root (overrides CONFIG_LOCAL_DIR / AGENT_DATA_DIR). */ root?: string; /** `--json` — print the full TaskResult JSON instead of just the final assistant text. */ json: boolean; /** `--scenario ` — pick a capability bundle (default: config.defaultScenario). */ scenario?: string; /** `--session ` — REUSE a session across invocations (durable multi-turn). Default: a fresh id (the * session + its events still persist to disk, just not continued). */ session?: string; /** `--workspace ` — the FIXED dir the agent's tool calls operate in (CC-like). Default: the cwd. The * agent's file edits PERSIST there (never deleted) — this is the user's own directory, not a sandbox. */ workspace?: string; /** `--user ` — the local memory/identity scope (so long-term memory accumulates per user). Default "local". */ user?: string; } /** Injectable side-effects so {@link runLocal} is deterministic + testable (no real process I/O). */ export interface RunLocalDeps { /** Override the Brain (a mock in tests). When absent, the real `createBrain(config, { fetchImpl })` is used. */ brain?: Brain; /** Forwarded into `createBrain` when no `brain` is given — lets a test fake the gateway wire. */ fetchImpl?: typeof fetch; /** Structured logger (defaults to the real one at config.logLevel). */ logger?: Logger; /** stdout sink for the human/JSON result (defaults to console.log). */ print?: (line: string) => void; /** stderr sink for the "you still need to set …" guidance (defaults to console.error). */ printErr?: (line: string) => void; /** [ref] 件三: override the TTY detection behind the {@link createLocalApprover} seat * (defaults to `process.stdin.isTTY === true`) — lets a test drive BOTH arms deterministically. */ isTty?: boolean; /** [ref] 件三: the TTY arm's prompt (defaults to a readline `y/N` on stdin/stderr); * `signal` = core `OnAsk` 的取消(EOF/abort 决断臂见 {@link promptYesNoOnTty})。 */ askYesNo?: (prompt: string, signal?: AbortSignal) => Promise; } /** Parse argv (everything AFTER `node run-local.js`). Throws a clear Error on a missing objective. */ export declare function parseArgs(argv: string[]): RunLocalArgs; /** A bad-usage error → print to stderr + exit 2 (distinct from a task failure exit 1). */ export declare class UsageError extends Error { } /** * The model GATEWAY pre-flight (mirrors the doctor message): the engine cannot run a real * task without (a) a gateway base URL that ends in `/v1` and (b) the gateway/model API key resolvable from * env. We surface a "you still need to set: …" list rather than letting the first brain call crash with a * cryptic auth/connect error. Returns a list of missing items (empty = OK). * * NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips * this check in that case — the check is about a REAL run, not the assembly. */ export declare function missingGatewayRequirements(config: ServiceConfig): string[]; /** 本腿读的部署配置切面(`ServiceConfig` 结构满足;两个构造口各自的切面之和)。 */ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView { } /** * [ref] 件三:run-local 腿的**部署治理链装配**([ref])。 * * 与另外两条腿(HTTP 的 `resolveSpec`、durable park 的赎回腿)装的是**同一条**链,经**同一个**构造口: * 审批基线作 base 的 `toolPolicy` 座,再经**折叠属主** `applyRuntimeGovernance`(→ core `tightenTaskSpec`) * 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不 * 自己合成。 * * · **审批基线 = adjudicated allow-all**:一次性 CLI 没有 durable 审批设施(没有 `/decide` 腿可赎回), * 所以 `createApprovalBaselinePolicy` 走 durable 缺席那一臂。它是一条**在场的** effect-aware 策略 * (满足 core 的 `hasEffectAwareGate`),运维照旧用 `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。 * · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上 * (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标 * 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。 * ✅ **旧残余面已收口(core 5.19.0 [ref],2026-08-08 校正)**:`rootPath` 是装配期的**静态**值,而 core * 的写工具解析相对形用的是被 Bash `cd` 就地改写的活 cwd —— 于是 `cd .git/hooks` 之后 * `Write("pre-commit")` 落进守卫段却判不出来(两条 host 腿同形,本腿与 resolve-spec)。5.19.0 起守卫 * 按 `ToolCallRequest.cwd`(引擎每次调用从同一只 tracked cwd 盖戳)解析写目标,本腿零改动即得保护; * **不对称是 core 故意的**:目标跟活 cwd 走,守卫自己配置的目录仍锚静态根,`cd` 搬不动栅栏。全文与 * 证据见 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 顶注,端到端正控钉见 * test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。 * · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。 */ /** @param engineDataRoot 本腿已铸的引擎记忆根(`memoryEngineBackendFor(config, root)?.root`,S-133 单一写者);`undefined` = 引擎未接。 */ export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string, engineDataRoot: string | undefined): TaskSpec; /** {@link createLocalApprover} 的 io 面(全部可注入,所以两条臂都测得动)。 */ export interface LocalApproverIo { /** 这次调用是不是接在一个人面前(生产里 = `process.stdin.isTTY`)。 */ readonly isTty: boolean; /** TTY 臂的问人器(默认 {@link promptYesNoOnTty} 的 readline y/N;`signal`=core 取消透传)。 */ readonly askYesNo: (prompt: string, signal?: AbortSignal) => Promise; /** 非 TTY 臂的告知面(stderr)。 */ readonly printErr: (line: string) => void; } /** * [ref] 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。 * * 🔴 为什么这个座位是接治理链的**同窗必做项**(成因,留档):彼时 run-local 的主 runner 带着 * checkpointStore(file backend),于是 `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进 * `irreversibleTools` ⇒ park lane effective ⇒ **每一次 shell 调用都 durable park 成一张永远没人 resume * 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里没有任何赎回腿,那不是「等人来批」,是卡死。 * core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk` 早退(prepare-task dist 亲读),ask 于是落回 * 本回调 —— 把 park 换成「问人 / 当场拒」的正当接法。 * * 🔴 **本席不是那颗雷的唯一防线,也从来不该是**(B-060 批 codex 交叉复审 [medium] 的教训):它只挡 * **审批**那条车道;core 的**平台/资源停驻**(`suspendForPlatformLimit`)由「店在不在」一位武装, * 压根不经过 onAsk。所以自 B-060 起本腿的结构性防线是**整条腿不接 checkpointStore**(装配点在 * `createSharedRunnerDeps` 调用处,理由全文在那里),本席退回它本来的职责:**给 TTY 前的人一次真的 * 问答**。两道并存不是冗余 —— 一道管「谁来答」,一道管「答不了时会不会留下无人兑付的行」。 * * ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层 * 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。 * * 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道 * `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处 * 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。 */ export declare function createLocalApprover(io: LocalApproverIo): OnAsk; /** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。 * streams 可注入=EOF/abort 两臂测得动(默认 process.stdin/stderr)。 * * 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时 * **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁 * 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false: * · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted); * · readline `close`(输入流 end/close 时 readline 自关); * · `question` 自身 reject(abort throw / 流错误)。 */ export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: { input: NodeJS.ReadableStream; output: NodeJS.WritableStream; }): Promise; /** * Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit * code (0 = completed, 1 = the task did not complete / a fatal error, 2 = bad usage / unmet env). Pure of * `process.exit` so a test can assert the code; `main()` applies it. */ export declare function runLocal(argv: string[], deps?: RunLocalDeps): Promise; //# sourceMappingURL=run-local.d.ts.map