import { FileError, StubExecutionEnv, type ExecutionEnv, type Result, type ToolPolicy } from "@sema-agent/core"; import { type DurableAskOptions } from "./approval.js"; import type { ServiceConfig } from "./config.js"; import type { applyRuntimeGovernance } from "./runtime-governance.js"; /** `applyRuntimeGovernance` 的 governance 实参 —— 从折叠属主的签名位**推导**,不复制形状 * (改一处签名,本口跟着变;两份手写形状正是漂移的成因)。 */ export type DeploymentGovernanceInputs = Parameters[1]; /** 本口读的部署配置切面(`ServiceConfig` 结构满足;窄接口让消费腿与测试不必铸整只 config)。 */ export interface DeploymentGovernanceConfigView { readonly autonomy?: ServiceConfig["autonomy"]; readonly commandPolicy?: ServiceConfig["commandPolicy"]; readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"]; readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"]; /** * 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂 * **誊本门**(`/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见 * {@link DeploymentGovernanceRoots});下面这段历史注保留,记的是 S-125 件⑦ 把两根混作一根的由来。 * * S-125 件⑦(core 7.4.0 [ref] HRD-PRM-10)—— 当时把本键原样交给 `createSensitivePathPolicy` 的 `dataRoot`。 * * 为什么必须**显式**传而不是吃 core 的缺省:core 的缺省是 `$AGENT_DATA_DIR ?? ~/.ai-agent`,而本仓的 * 数据根解析链多一节最高优先级的覆盖(`config.ts`:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR * → ~/.ai-agent`)。只设 `LOCAL_DATA_ROOT=/data/sema` 的部署,数据真的躺在 `/data/sema`,而 core 的缺省 * 会去猜 `~/.ai-agent` ⇒ 守卫把真数据根**当普通路径**判(它的祖先段一旦命中守卫集里的某一条,整棵子树 * 连同模型被**指示**要写的记忆库一起被硬拒),同时又把一个不存在的目录当成豁免区。两个方向都错,且都静默。 * ⇒ 本键 = 「这道门与 transcript 门说的是同一个目录」的单一属主,值与 `ServiceConfig.localDataRoot` * **同一个**解析结果(不在本口重算,重算就是第二个写者)。 * * 缺席(窄接口的老调用方 / 测试夹具)⇒ **不铸 `dataRoot` 键**,core 走它自己的缺省 —— 与本口既有的 * `rootPath` 条件展开同姿势:缺席是「本调用方没有这项知识」,不是「数据根是 undefined」。 */ readonly localDataRoot?: ServiceConfig["localDataRoot"]; } /** * S-133 —— 守卫集 `dataRoot` 的**显式供给**(与 `DeploymentGovernanceConfigView.localDataRoot` 是两件事): * `engineDataRoot` = 引擎写记忆的根 = boot 已铸的 `memoryEngine.root`(boot/stores.ts 是唯一写者;推导函数 * `memoryEngineRootFor` 只在那里与 `memoryEngineBackendFor` 里调)。**调用腿读那只活对象,不重推导**——重推导 * 就是第二个写者(7.59.0 重扫 codex 轮抓到 HTTP 腿的 fallback 与 stores 不同源,正是这一形)。参数非可选: * 一条腿忘了 = 编译红。`undefined` = 引擎未接(或窄接口的测试夹具),那时不铸 `dataRoot` 键,core 走自己的缺省。 */ export interface DeploymentGovernanceRoots { readonly engineDataRoot: string | undefined; } /** 审批基线读的配置切面(同上,窄接口)。 */ export interface ApprovalBaselineConfigView { readonly approvalRequire: ServiceConfig["approvalRequire"]; readonly approvalDeny: ServiceConfig["approvalDeny"]; readonly approvalAutoBudget: ServiceConfig["approvalAutoBudget"]; readonly approvalNeverAuto: ServiceConfig["approvalNeverAuto"]; } /** 守卫集裁决的 env/cwd 基准。**lane 分形归调用腿**(host 真 fs / 沙箱 deferred 代理 / run-local * workspace)—— 那是消费腿的属主知识,本口不重复它,只按 cwd 的在场性决定要不要补词法臂。 */ export interface PathAdjudication { /** 守卫策略 canonicalize 每一个写目标用的 env。 */ readonly env: ExecutionEnv; /** 相对路径基准(core 的 `rootPath`)。缺席 ⇒ 相对形在真身那层判不了,补词法臂(见下)。 */ readonly cwd?: string; /** 🔴 S-177:**真任务根**(权限规则模式的基)。与上面那个 `cwd` 分开:后者在 host 腿的无 session-cwd * 形上是一个永不创建的哨兵目录 —— 对写门 fail-safe,对规则 fail-open。理由逐字见 * `task-settings.ts` 的 `FsWriteGateWiring.taskRoot`。缺席 ⇒ 需要基的规则拼法被 core 整表拒(fail-loud)。 */ readonly taskRoot?: string; /** 🔴 S-177 / codex r2 + S-213④:**执行环境的 home**(`~/…` 形规则的基)。在**本部署的执行车道 * 真知道自己的 home 时**在场 —— 判据的唯一属主是 `execution-lane-caps.ts` 的 `executionLaneHomeDir` * (adapter 的 `ExecutionEnv.homeDir` 座读的是同一个函数)。绝不回落引擎进程自己的 home:远端车道上 * 那是另一个用户的目录,拿它去匹配 = 一条收紧规则静默守错地方。逐字见 * `task-settings.ts` 的 `FsWriteGateWiring.taskHome`。 */ readonly taskHome?: string; } /** [ref]([ref] 案二):durable 部署上的 AskUserQuestion 门的活体面探针(QuestionCoordinator 的切面)。 */ export interface LiveQuestionFace { hasLiveContext(): boolean; } /** durable 轴的审批基线入参。缺席 ⇒ adjudicated allow-all 基线(见 {@link createApprovalBaselinePolicy})。 */ export interface DurableApprovalSeat { /** 活体问答面;`undefined` ⇒ 恒 durable park 的原形门。 */ readonly question: LiveQuestionFace | undefined; /** 每会话「本会话不再询问」探针(canonical toolName 键空间)。 */ readonly exempt?: DurableAskOptions["exempt"]; /** 豁免短路时的审计钩子。 */ readonly onExempted?: DurableAskOptions["onExempted"]; } /** * [ref]([ref] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形 * `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿: * 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到 * RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、 * durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留([ref] 后无活流腿放行执行 * 也不会产出空答:coordinator 无 ALS ctx ⇒ 冻结 `{kind:"unavailable"}`(src/question.ts),永不悬挂; * park 仍是把问题送到人面前的唯一那条腿,正当性不变、只是反事实前提换了)。 * 工具名取 `approval-content-kind.ts` 的单一属主(它 re-export core 根导出的 `ASK_USER_QUESTION_TOOL_NAME`); * server.ts 的 pre-CAS 守卫读同一枚(S-213①:此处旧注称该常量「core 未根导出」= 失实,曾据此手抄 13 处)。 * ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的 * 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里 * 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker) * 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整 * 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。 */ export declare function createDurableQuestionGate(live: LiveQuestionFace | undefined): ToolPolicy; /** * 沙箱 lane 上**相对形**写目标的守卫补层用 env(codex 交叉复审 round1 finding 1,红先复现)。 * * 缺口:沙箱 lane 的守卫策略拿不到 `rootPath`(沙箱 cwd 不是 server 能猜的,[ref] 裁定 1),而 * `DeferredSandboxPathEnv.absolutePath` 对相对形一律报错。core 的 `canonicalizeTarget` 在 * **absolutePath 失败**这一支不置 `unresolvedSymlink`,于是守卫策略走的是 * 「判不了就弃权」的 `allow`(dist 亲读)。写门在场时这条腿被门的 `ask` 兜住;而 * `bypassPermissions` / settings 缺席这几形**根本没有门**,于是 `Write(file_path: ".env")` 一路放行—— * 而 core 的结构化写工具会把相对形按 engine 跟踪的 cwd 解析后真写下去(fs-write.js `resolveKey`)。 * * 补法:**同一只**守卫策略工厂再铸一个实例,只把「路径→canonical key」这一步换成 * 纯词法基准(本 env)。判定与提取(哪个参数是写目标、NotebookEdit 的 notebook_path 优先、段匹配) * 全部仍是 core 的,server 侧零复刻——复刻 core 的裁决逻辑正是「同源谎」那一类错误。 * * 三条不可动的边界: * · **绝对形一律弃权**(absolutePath 报错 ⇒ canon 失败且非 unresolvedSymlink ⇒ core 判 allow): * 绝对形归真身裁决那一层,[ref]「真身胜过名字」的裁定(域内良性软链名叫 `.ssh` 只 ask)不受影响。 * · **只会 deny,不会放行**:相对形自身拼写里出现的段,解析成绝对路径后仍在,所以词法命中即真命中; * 反过来一条名叫 `.env` 而真身良性的相对软链会被误 deny —— 方向是 fail-closed,与守卫集语义同向。 * · **覆盖面(2026-08-08 按 core 5.19.0 [ref] 校正;旧文见下方「历史」段)**:本层只在 * `ToolCallRequest.cwd` **缺席**那一形上说话。core 5.19.0 起每条路径解析型守卫按 `req.cwd ?? rootPath` * 解析写目标(dist `core/sensitive-path-policy.js`),而 `canonicalizeTarget` 拿到 baseCwd 后会先把 * 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `canonicalizeTarget`,7.17.0 起该分支是 * `baseCwd && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled` * —— [ref] 把「算不算绝对 / 怎么拼」双双改成**按树的家族**判,旧注引的 `isAbsolutePathForm` 那一行已不是 * 这里的实现行;**结论不变**,本层仍是「有戳时自动让位」)—— 本层的 `absolutePath` 对绝对形一律报错弃权 ⇒ **有戳时本层自动让位**,由真身那一层 * (沙箱 `DeferredSandboxPathEnv` / host `NodeExecutionEnv`)按活 cwd 裁决,cwd 里的守卫段现在真看得见。 * ⇒ 本层今天的射程 = 「引擎没盖戳」的调用:相对形**自身拼写**里带守卫段的那一类(`Write(".env")`、 * `Write("cfg/.ssh/id_rsa")`),仍由本层 fail-closed 兜住。**不删臂**:让位与冗余不是一回事—— * 删掉它等于把「缺戳即无守卫」写死,而缺戳形在契约上是 core 明确保留的回落语义(直接调用形)。 * 两处特征化钉现在各带两臂(带戳 deny / 缺戳 allow):test/task-settings.test.ts 与 * test/run-local.test.ts(后者是真引擎端到端,已翻成 🔴 正控)。 * * 📜 **历史(留档,别当现状读)**:2026-08-08 之前本层的覆盖面到「cwd 里的守卫段看不见」为止—— * 本层把相对形挂在 `/` 上,而工具挂在 engine 活 cwd 上,`cd .git` 后 `Write("config")` 真写 * `/.git/config` 而本层只看得到 `/config` ⇒ 弃权。[ref] 刀3 的口径更正查明这条残余面 * **不是沙箱 lane 局部的**:host 腿虽供了 `rootPath`,那也是装配期的静态值,一样追不上被 Bash `cd` * 就地改写的 `cwdRef.current`(run-local 端到端真复现:`cd .git/hooks` 后 `Write("pre-commit")` 真落盘)。 * 当年判定属主是引擎那条缝(不是任何一条消费腿——在消费腿里自己拿静态 cwd 追 `cd`,是拿会漂的复制品 * 追引擎的真值,本文件反复点名的病),并把两条钉写成「引擎缝落地后一起翻面」。**该缝即 core backlog * [ref],已在 5.19.0 到货**,两条钉按上述翻面完毕。另一条备选收口(守卫集开启即把沙箱 lane 相对写 * 一律 deny,有真受损方且无对应旋钮)因此作废,无需部署方拍板。 */ /** * 沙箱 lane 两层守卫的**显式**分工(core 7.17.1 起必须显式,不能再靠「让 `absolutePath` 失败」隐式弃权): * `createSensitivePathPolicy` 对 canon 失败自 7.17.1 一律 `deny` 并点名 cause(它闭掉的正是本仓 [ref]② 上板的 * 自拒形放行洞)。旧形里两层都拿「判不了 ⇒ core 判 allow」当让位:真身层对**缺戳相对形**报错(沙箱 cwd 不是 * 门能猜的,[ref] 裁定 1)、词法层对**绝对形**报错 —— 新引擎上两处都翻成硬拒,沙箱 lane 的每一次写都被拒(提货批 * 14 格红)。让位与「判不了」是两件事:让位是**本层的**决定,要由本层自己说出来。分工(与两层类注一致,一字未改): * · 真身层(`DeferredSandboxPathEnv`)判 **绝对形 / 带 `req.cwd` 戳的相对形**(core 按戳解析进真身); * · 词法层(`RelativeTargetLexicalEnv`)判 **缺戳相对形**(自身拼写里的守卫段,fail-closed); * 两层射程互补、无重叠、无缝隙;各自射程外**显式 allow**(= 让位给另一层),canon 失败留给真的判不了的形 * (UNC/NUL/越根)—— 那正是 7.17.1 要 deny 的。提取写目标用 core 自家 `protectivePathTargetOf` + `pathTargetValue` * (与 core 守卫策略同一只提取器,零复刻);「算不算绝对」用 core `isAbsoluteForFamily(undefined, …)`(沙箱 lane * 无 root/cwd 声明 ⇒ 家族 undefined ⇒ 只认 `/`-rooted,与引擎同一律)。 */ type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped"; /** 只在 `form` 那一类写目标上说话,其余显式 allow(让位);非写调用与提不出目标的调用原样交给 policy(它自己判 allow)。 */ export declare function scopedToWriteTargetForm(policy: ToolPolicy, form: WriteTargetForm): ToolPolicy; export declare class RelativeTargetLexicalEnv extends StubExecutionEnv { /** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(fail-closed 守底:`relativeTargetsOnly` 已把这些形挡在外面, * 这里不会再被绝对形命中;若有人绕过包装直接用本 env,报错在 7.17.1 上读作 deny,方向仍是保守)。 */ absolutePath(path: string): Promise>; /** 恒「不存在」⇒ core 的 `canonicalizeNewPath` 逐级回退,最终把词法归一形当 canonical key 交给段匹配。 * 这里绝不能报错:报错会被 core 读成 `unresolvedSymlink` 而对**每一个**相对目标 deny(含普通文件)。 */ exists(_path: string, _abortSignal?: AbortSignal): Promise>; } /** * boot 期的守卫集**可编译性**门([ref] 收口①,随 [ref] 件一搬进本口 —— 三条消费腿同得)。 * * 守卫集的编译发生在**每个请求**上。core 的 `compilePatterns` 对「一个路径段都没有」的模式(`"/"`、 * `"//"`)THROW,那条 throw 会变成**每一个任务一条 500**,且运维从错误里看不出是自己的 env 写错了。 * 消费腿在装配期先编译一次:非法旋钮值当场炸在启动上(与 config.ts 的 env fail-loud 同族),指名键与 * core 的原因。env 只是编译期的占位(compilePatterns 不碰它),真裁决用的是每请求按 lane 铸的那一个。 */ export declare function assertGuardPatternsUsable(config: Pick): void; /** 一条 boot 期运维告警的**载荷**(纯数据 ⇒ `build*`;打给谁、用哪只 logger 归消费腿)。 */ export interface OperatorWarning { readonly event: string; readonly fields: Record; } /** {@link buildOnlySensitiveBaselineWarning} 读的座位量 —— 「这个部署到底有没有一道**真**门」。 */ export interface GateSeatView { /** durable 审批门(checkpoint suspend/resume)是否在这条腿上真装。 */ readonly durableEnabled: boolean; /** 单用户 turnkey 的 auto-accept 基线是否适用(= 零门意图的既定姿势,不是 misconfig)。 */ readonly singleUserAutoAcceptBaseline: boolean; } /** * UNGATED 信号的**补偿**([ref] 件一收编 / 件三三腿同得)。 * * 审批基线铺开之后 core 的 `hasEffectAwareGate` 恒真,于是它那条 "write-capable hand tools are present * but UNGATED" 的 onError 不再触发(判据是 `policyLayers.length > 0`,prepare-task dist 亲读)。那条信号 * 此前是「这个部署一个门都没接」这个 misconfig 的**唯一**提示,而守卫集只挡那二十来个路径段、其余写 * 目标照旧无裁决 —— 信号不能因为我们铺了基线就静默消失,所以由我们自己按同一判据说一次。 * * 判据(与信号消失的条件逐字互补):守卫集在场 ∧ 两条产**真**门的腿都不在场(durable 门关 ∧ 单用户 * auto-accept 基线不适用)。三个量都是部署常量 ⇒ 消费腿在 boot 期说一次,不是每任务一次。 * * ⚠️ 单一属主([ref] 件三):HTTP 腿与 run-local 腿共用本判据与文案。两处各写一份 = 一处改了另一处 * 没改,而两份都长得像对的 —— 那正是本文件存在的理由。 */ export declare function buildOnlySensitiveBaselineWarning(config: Pick, seat: GateSeatView): OperatorWarning | undefined; /** * `applyRuntimeGovernance` 的 governance 实参预铸([ref] 件一)。 * * 键存在性 profile 逐字保持消费腿原样:`autonomy`/`commandPolicy` 恒在场(值可为 `undefined`), * `manualModeShellGate`/`sensitivePathPolicy` 按在场性条件展开 —— `applyRuntimeGovernance` 的 * `!== undefined` 判据对两者等价,但 profile 是折叠面的可观测字节,搬家不许顺手改。 */ export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication, roots: DeploymentGovernanceRoots): DeploymentGovernanceInputs; /** * 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座([ref] 件一)。**恒非 * `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。 * * 两形: * · `durable` 在场 ⇒ durable 轴 = AskUserQuestion 判决门 + F4 高危写审批门(deny/neverAuto/预算/ * 会话豁免全在 `createDurableAskPolicy` 里)。gated `ask` 由 core 变成 durable checkpoint suspend。 * · `durable` 缺席 ⇒ **adjudicated allow-all**(`createAllowDenyPolicy({})`):一条**在场的**、 * effect-aware 的策略。CC 的信任模型是 auto-accept,但门**机制**必须在场(core 原则「机制留、默认 * 可更宽」)—— 满足 core 的 `hasEffectAwareGate`,恢复可观测性与 hook/tighten 点,而运维照旧用 * `AUTONOMY`/`commandPolicy` 收紧不可逆操作(由 `applyRuntimeGovernance` tighten-only 叠上)。 * * ⚠️ 「零门意图才铺 allow-all」这条准入判据**不在本口**:它是消费腿的部署形判断(单用户/多租户、 * 有无 checkpoint 店),属主是 `hasOperatorGateIntent`/`assertGateIntentServiceable`(src/approval.ts)。 * 本口只按调用方给的形铸策略,绝不替它判「这个部署该不该有门」。 */ export declare function createApprovalBaselinePolicy(config: ApprovalBaselineConfigView, durable?: DurableApprovalSeat): ToolPolicy; export {}; //# sourceMappingURL=deployment-governance.d.ts.map