/** * Runtime governance compiler — the "second baton" (center §10, core 1.114.0): turn the * operator's declarative `runtime.autonomy` + `runtime.commandPolicy` (`@sema-agent/settings-schema`, * carried to the service via sema-registry `EffectiveConfig.runtime`) into TIGHTEN-ONLY overrides on the * per-task `TaskSpec`. Pure so it is unit-testable in isolation (the spec is otherwise assembled inside * main.ts's `resolveSpec`). * * 🪤 TWO TRAPS this module exists to NOT fall into (see core * `tool-policy.ts` / `tighten-task-spec.ts` JSDoc): * * TRAP #1 — `TaskSpec.toolPolicy` OVERRIDES (not merges) `RunnerDeps.toolPolicy` (the runner does * `spec.toolPolicy ?? deps.toolPolicy`). A bare `spec.toolPolicy = createCoarseCommandNamePolicy(…)` would * SILENTLY DISCARD the deployment's approval/durable baseline. We compose via {@link tightenTaskSpec}, whose * toolPolicy rule is `combinePolicies(base, override)` (deny-wins) — the command gate can only ADD denies/asks * on top of the baseline, never clear it. * * TRAP #2 — `autonomy` must EXPAND INTO the three core primitives (`handsReadOnly` / `shellGate` / a * `toolPolicy`), NOT become a parallel enum on the spec (design-research §10: a parallel `autonomy` field + * `handsReadOnly:true` would conflict). The expansion is tighten-only and routed through `tightenTaskSpec`, so * `auto` (the loosest mode) can never LOOSEN a stricter base — `tightenTaskSpec` THROWS instead. * * ⚠️ `commandPolicy` is a COARSE name-level filter, NOT a sandbox. `sh -c …` / `$(…)` / pipes / `env X=…` / * path-prefixed commands all bypass the argv[0] match (they route to `ask`, fail-closed, but are not blocked by * name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`. */ import { type TaskSpec, type ToolPolicy } from "@sema-agent/core"; import { GovernanceConfig } from "@sema-agent/settings-schema"; import { type GovernanceAskMarks } from "./governance-ask-marks.js"; /** 运维 autonomy 四档的**闭集**(同上)。`config.ts` 的 `parseAutonomy` 也从这里读 —— 四档词表全仓一份。 */ export declare const AUTONOMY_MODES: ["read-only", "ask", "plan", "auto"]; /** A per-command governance rule (registry `runtime.commandPolicy[]`) —— **型也从契约派生**: * 契约给 `command` 加一条 refine / 给 `decision` 加一个判词,本仓的型当场跟上(或在下面的 * `RANK` / 穷尽 switch 上编译红),而不是靠人记得把手写联合改一遍。 */ export type CommandRule = NonNullable[number]; /** True when `name` names a DELEGATION tool — the subagent tool(CC-187 canonical `"Agent"`,core * `DEFAULT_SUBAGENT_TOOL_NAME`)。5.0.0 [ref]:折叠面退役,RAW 比对 core 单源常量——旧名("Task") * 在 core roster 层响亮 miss,不再需要归一;单源常量本身就防了当年 1.202 改名时裸字符串失配的 * fail-open(那个教训的解=引用常量,不是折叠)。 */ export declare function isDelegationToolName(name: string): boolean; /** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter — * not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation * tool at all. * * 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly` * (`dist/agents/subagent.js` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume * resume 腿同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以 * 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松): * 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将 * 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。 * * Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */ export declare function stripDelegationTools(tools: readonly T[]): T[]; /** The operator autonomy mode (registry `governance.autonomy`) —— 契约派生(见 {@link AUTONOMY_MODES})。 */ export type Autonomy = NonNullable; /** 导出给 `test/coarse-shell-tools-mirror.test.ts` 的同源钉用(见那里的顶注)。 */ export declare const COARSE_SHELL_TOOLS_MIRROR: readonly string[]; /** Validate `commandPolicy` rules — the shape (array of {command, decision}), the `command` (a bare argv[0] name, * see {@link VALID_COMMAND_NAME}), AND the `decision` enum. Returns the list of human-readable errors (empty = * OK). Called at config-apply (config-center/facade.ts `applyRuntimeHot`) so a malformed rule that BYPASSED the * registry schema (a hand-edited config.d / a non-conformant publish) is rejected LOUDLY rather than * silently mis-compiling — review MEDIUM: an out-of-enum `decision` (e.g. "DENY") would otherwise fall through * {@link compileCommandPolicy}'s `decision==="deny"?…:"ask"` to ASK, silently WEAKENING an intended deny. */ export declare function validateCommandRules(rules: CommandRule[]): string[]; /** * Compile `runtime.commandPolicy` (per-command 3-way rules) into a single ToolPolicy, or `undefined` when there * is nothing to govern. The registry model (per-command `allow`/`ask`/`deny`) and core's helper model * (`allow[]` + `deny[]` + `defaultAction`) don't map 1:1, so the compilation picks a mode by the rules present: * * - **Allowlist mode** — triggered when ANY rule resolves to `allow`. Only `allow` commands pass; `deny` wins; * EVERY other command — `ask`-decision AND unlisted — falls to `defaultAction:"ask"`. * 🔴 OPERATOR FOOTGUN (documented contract): adding even one `allow` rule flips the whole policy to a strict * allowlist — previously-passing unlisted commands now ASK. * - **Blocklist mode** — no `allow` rules. Only `deny` commands are blocked; the rest pass THROUGH to the * deployment baseline. Per-command `ask` is carried by {@link createCommandAskListPolicy} (the coarse helper * can't express it). Un-parseable bypass commands fail toward `ask` in both the deny gate and the ask-list. * * Duplicate rules for the SAME command collapse to the STRICTEST decision (`deny > ask > allow`) BEFORE the mode * split, so e.g. a command listed both `allow` and `ask` resolves to `ask` — honoring `combinePolicies`' * strictest-wins (without this, the `allow` rule would shadow the `ask` for that command in allowlist mode). * * Either way the result only ever ADDS asks/denies — composed onto the baseline via `combinePolicies` * (deny-wins) by {@link applyRuntimeGovernance}, so it is tighten-only. (Assumes commands are pre-validated by * {@link validateCommandRules}; an un-matchable command here is simply inert, never a parse error.) * * 🔴 **S-496:词表外的 `decision` 折 `deny`,不折 `ask`。** {@link validateCommandRules} 的头注写下的那条 * 后果(「域外的 `decision` 穿过本函数的三元链落到 ASK,静默把一条 intended deny 弱化掉」)此前只靠 * **调用方记得先校验**挡着 —— 而本函数在 `resolve-spec` 里是逐请求现调的,`config.commandPolicy` 的来路 * (center hot apply / 本地 `config.d/governance.json` / LKG / 测试直塞)不止一条。现在闭集在本函数**内部** * 收口:`VALID_DECISIONS` 之外的值一律折最严的 `deny`(fail-closed,[ref] 的运行期 miss 臂,下方自陈), * 闭集本身则由契约 zod 派生 ⇒ 契约加一个判词,`RANK` 与桶分配的穷尽 switch 双双**编译红**。 */ export declare function compileCommandPolicy(rules: CommandRule[] | undefined): ToolPolicy | undefined; /** * Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2): * - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the * §6 verifier read-only boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan * for approval" intent is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands. * - `ask` → `shellGate: "always"` (every `Bash` command tightens to an `irreversible_ask` durable suspend — * the fail-closed default for an unattended deployment with no parsed classifier). * - `auto` / `undefined` → `{}` (no extra tightening; still subject to the deployment baseline + commandPolicy). * * 🔴 **为什么只读那两档同时说 `writeFace:"roots"`**([ref] 随批;不是补丁,是把这一档的表态说全): * core 7.18.0 的 `resolveContainmentFace` 第 1 行把「只读(verifier)挂载 × 任务层 `writeFace:"open"`」 * 判成**真实矛盾**并**抛** `config.write_face_readonly_conflict`(理由逐字:那种挂载根本不带写面, * 声明一个开放写面 = 为一次调用没有的工具表态)。而 `permissionMode:"bypassPermissions"` 自 [ref] 起 * 会在阶段④铸出 `writeFace:"open"` —— 于是「`AUTONOMY=read-only` 的部署 × 一个 bypass 请求」这一格 * 不修就是**整条 run 当场配置错误失败**。 * * 修在这里而不是在写点加一条「若 handsReadOnly 则别写」的判据:两个字段**在这一档里是同一件事** * (「这条 run 没有写面」),而 `tightenTaskSpec` 本来就把 `writeFace` 当 tighten-only 安全字段收 * (`base:"open"` × `override:"roots"` ⇒ `"roots"`,反向 throw)—— 用既有的合并表表达它,规则数不变; * 在写点加判据则是让阶段④去读一个它不拥有的治理事实,那才是第二个判官。 * 与 `ask → shellGate:"always"` 同姿势:这一档**自己就是**一次「围栏在」的表态,所以无表态的请求上 * 它照样把键写出来(不是「只在有 open 时才压」——那又是一条判据)。 */ export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial; /** * [ref]:**部署级**判据 —— 本部署的治理层自己是否把 shellGate 抬到 `"always"`。 * * 用途 = 持久门(park 行)的出身归因。活卡腿判「这只 ask 是治理层产的」靠 ALS 标记表(见本文件 * `createGovernanceAskMarkingPolicy` / `createGovernanceShellGateMarkPolicy`),而 park 腿天然跨副本、 * 跨重启,进程内的表在那里结构上够不着;行上唯一的取证格是 core 写的 * `gate.riskDescriptor.shellGateDoctrine`。 * * 🔴 **为什么单看行上那一格不够**:有效档 `"always"` **不止治理层一个产地** —— `resolve-spec` 的 SUP 路由 * 姿态(`supPostureOverrides`)在 governance **之后**叠,同样产 `"always"`(本文件 `manualModeShellGate` * 合成段的注释里已如实记着这条时序)。只看行 ⇒ 一条 SUP 路由出来的门会被谎报成「运维治理层强制」, * 而那恰是这个信号要回答的那个问题。所以归因取**合取**:行上是 `always` **∧** 本部署的治理层本来就 * 要求 `always`(后者成立时,治理层就是一个真成因,SUP 是否也抬过不改变这句话的真假)。 * * 判据逐字对着两个产地(与 `applyRuntimeGovernance` 里 `overrides.shellGate` 的取值同源): * `autonomyOverrides(autonomy).shellGate`(`AUTONOMY=ask`)与 `MANUAL_MODE_SHELL_GATE` 旋钮,取「有一个 * 是 always」。`commandPolicy` 不入判据:它产的是 `toolPolicy` 的 ask,不抬 shellGate 档,与本格无关。 * * ⚠️ **已知残留(codex 交叉复审 2026-08-11 逮到,如实登记而不是掩盖)**:本判据读的是**当下**的 * config,而 `config.autonomy` 是**热改**的(`config-center/apply-effective.ts` 就地写 `config.autonomy`)。 * 于是失真是**双向**的,不是我原先写的「只往缺席方向」: * · 缺席向(常见):mint 之后运维把 `AUTONOMY=ask` 撤了 ⇒ 老 park 行归不出治理出身,少标一个; * · **在场向**(窄):一条**纯 SUP 路由**产的门(`ROUTER_ENABLED` 开 ∧ 路由判 supervisor ∧ 当时治理层 * 并不要求 always),之后运维把 autonomy 热改成 `ask`,那条老行会被标成治理出身 —— 五个前提要**同时** * 成立,且该行仍未决。 * 根治要**在 mint 那一刻把出身写进行里**(core 的 `CheckpointGate` 属主面,不是本仓能单方面做的); * 在那之前这一位的定位是**分诊提示**,不是裁决输入 —— 它不参与任何门/CAS/resume 判定,消费端也只拿它 * 渲染徽标。登记在此,不加特征化测试(把已知残留钉成契约是另一种病)。 */ export declare function governanceMandatesShellGateAlways(governance: { autonomy?: Autonomy; manualModeShellGate?: "always" | "classify"; }): boolean; /** spec 序里的秩(0 起,越大越严)。入参收 `string`(fail-loud 边界:schema `trustRank` 同款姿势)——词表外 ⇒ * RangeError,绝不 undefined/NaN 让比较静默成 false。导出只为对表钉(test/safety-merge-spec-consumption.test.ts, * [ref] 件4)——运行时消费仍走上表(键类型闭集)。 */ export declare function shellGateRank(word: string): number; /** * Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base * `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy` * override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to * `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any * override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently * weakening the gate. Returns `base` untouched when there is nothing to apply. * * [ref]([ref] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家 * (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮 * 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp) * ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane * fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成: * autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。 * * [ref]([ref] issue #29,**邻仓 test AI 的 46 格真机矩阵**实证——不是本仓的钉,别到本仓文件里数 46; * 本仓的类级门是 test/operator-knob-client-posture-matrix.test.ts 的 旋钮×表态×lane 矩阵): * `SENSITIVE_WRITE_PATTERNS` 是 [ref] 那次搬家漏下的兄弟旋钮——同一个病灶原样复发在 sensitive-path * DENY 腿上(唯一合成点在 deriveSettingsPolicy 的 fsWriteGate 闭包,bypassPermissions/键缺席/settings * 整缺席三形整条腿不建)。同方修:调用方(resolve-spec)把 operator 守卫集预铸成 `sensitivePathPolicy` * 从这里**无条件施加**——它只在守卫段上产 deny(core 语义 deny/"safety"),对其余目标返回的是 * `action:"allow"`(**不是**「无意见」这种第三态:core 的 ToolPolicy 没有弃权值。它在 * `combinePolicies` 的 deny/ask 优先折叠里不构成一票,所以在**折叠语境**下等价于弃权;若日后有人把 * 这条策略单独当成唯一 policy 用,那个 allow 就是真放行——别照抄这句话去别的语境)。于是对 bypass * 只挡守卫段、不给普通目标加 ask 门([ref] 表第四行「bypass=不加门非开门」保持逐字)。策略在这里 * 只组合不构造:裁决 env 的 lane 分形(host 真 fs / 沙箱 deferred 代理)是 resolve-spec 的属主知识, * governance 层不重复它。 */ export declare function applyRuntimeGovernance(base: TaskSpec, governance: { autonomy?: Autonomy; commandPolicy?: CommandRule[]; manualModeShellGate?: "always" | "classify"; sensitivePathPolicy?: ToolPolicy; /** [ref]/[ref] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见 * `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */ askMarks?: GovernanceAskMarks; }): TaskSpec; //# sourceMappingURL=runtime-governance.d.ts.map