/** * S-138 —— **写保护表**(core `RunnerDeps.writeProtectedPaths` 座)的姿态推导点(形制照 * `memory-posture.ts` / `sql-engine-posture.ts`:一次推导,两个读面)。 * * ## 病(B-023 F2 的根) * * core 的写保护表是**字面名表**(CC `DANGEROUS_FILES` / `DANGEROUS_DIRECTORIES` / * `DANGEROUS_DIRECTORY_PATHS` 三形对位):一次落在表行上的可定路径写(Write/Edit/NotebookEdit)会把 * 幸存的 `allow` 降级成 `ask`。座**缺席 = 缺省表在岗**(不是「没装」)。可是本仓此前没有任何读面能 * 回答「这台部署的写保护表在不在、有几行」—— 邻仓因此只能用「`writeProtectedPaths` 键名 grep 零命中」 * 去推断「表未装配」,推错了(座缺席正是缺省表在岗的姿势),而**读面缺席让这个错误无法被证否**。 * * ## 药 * * 一次推导,两个读面: * · operator 面(`GET /v1/diagnostics/wiring` 的 `writeProtection` 段)—— **逐行**表 + 来源 + 被丢的缺省行名; * · 租户面(`GET /v1/capabilities` 的 `writeProtection` 位)—— `{armed, rows, replaced}` 三项,**不含行内容**。 * * ## 三条诚实纪律 * * ① **表由 core 解析,本仓零重算**:`rows` 逐字来自 `compileWriteProtection().rows`(core 自己的 * 「disclosure face」),连行的**身份折叠**(大小写 + ı/ſ 两个易混字)都借 core 的 * `resolveWriteProtectedTable` 去判(见 {@link defaultRowsMissingFrom})—— 本仓不铸第二套判定语义, * 也**不复制缺省 51 行**(单一属主在 core;上游改表之日起本仓不会成为第二份真源)。 * ② **两项刻意不合成一个布尔**(`permissionModeAuto` 同判据):`armed`(表非空)与 `replaced`(整表 * 替换座被写)回答的是两件事 —— 一个「引擎会不会对这类路径弹卡」,一个「运维有没有动过那张表」。 * 合成之后,「缺省表在岗」与「运维自定义了一张同样非空的表」得到同一个 true,而壳/运维对这两种 * 要做的事不同。 * ③ **行内容只上 operator 面**:缺省 51 行是 CC 公开表(零增量信息),但**部署自定义行**可能含内部 * 路径名(`.corp-deploy-key` 之类)⇒ 逐行披露给租户面等于告诉想绕过的人「哪些名字不受保护」。 * * ## 与 `SENSITIVE_WRITE_PATTERNS` 的关系(**并列,不合流**) * * 本仓另有一套**模式型**(正则/glob)写 deny 集(`createSensitivePathPolicy`,旋钮 * `SENSITIVE_WRITE_PATTERNS`,经 deployment-governance 无条件施加)。两套按 core 的 d.ts 是并列机制、 * 互不覆盖,所以两个读面**分开报**:合成一个 `writeProtected: true` 会让壳答不出「这条路径是被哪一套 * 拦的」,而两套的解法完全不同(一套改模式,一套改名表)。 */ import { type WriteProtectedRow } from "@sema-agent/core"; import type { ServiceConfig } from "./config-types.js"; /** 表的来源(闭集;`switch` 穷尽 —— 新增词 = 编译红,[ref] 词表纪律)。 */ export declare const WRITE_PROTECTION_SOURCES: readonly ["default", "extra", "replace", "off"]; export type WriteProtectionSource = (typeof WRITE_PROTECTION_SOURCES)[number]; /** operator 面(`GET /v1/diagnostics/wiring` 的 `writeProtection` 段)。 */ export interface WriteProtectionPosture { /** 生效表的**逐行**内容,逐字来自 core `compileWriteProtection().rows`(空 = 显式无表)。 */ readonly rows: readonly WriteProtectedRow[]; /** `default` = 两根旋钮都没写(引擎缺省表在岗);`extra` = 缺省表 + 增量;`replace` = 整表替换; * `off` = 整表替换成空表(合法但响亮的姿势)。 */ readonly source: WriteProtectionSource; /** 整表替换**丢掉**的缺省行名(替换族才在场;`extra`/`default` 结构上丢不掉任何行)。 */ readonly droppedDefaultRows?: readonly string[]; } /** 租户面窄投影(`GET /v1/capabilities` 的 `writeProtection` 位)。**不含行内容**(见顶注纪律③)。 */ export interface WriteProtectionCapability { /** 生效表非空 = 引擎真会对表行弹卡。 */ readonly armed: boolean; /** 生效表的**行数**(内容不上租户面)。 */ readonly rows: number; /** 整表替换座被写(缺省表不再原样在岗)。与 `armed` 刻意不合成(顶注纪律②)。 */ readonly replaced: boolean; } /** 本模块读的 config 面(取型而非重述 —— 键名/形永不与真配置漂)。 */ export type WriteProtectionConfigView = Pick; /** * boot 期算一次的姿态(纯函数、零 I/O)。**同一只解析器**:`rows` 是 core 编译判子时用的那份表,不是 * 本仓照着 config 又算了一遍的复述 —— 「诊断页说 51 行、引擎按 2 行判」在结构上不可能。 * * `compileWriteProtection` 对**空表**返回 `undefined`(core 契约:没有表就不挂 tighten)⇒ 本函数把它 * 读成 `rows: []`,那是 `off` 姿势的诚实形,不是「读不出来」。 */ export declare function buildWriteProtectionPosture(config: WriteProtectionConfigView): WriteProtectionPosture; /** * operator 面 → 租户面的窄投影。 * * `null` = **这个进程说不出来**(不是 composition root 装起来的测试夹具形)—— 与 `sql` / `memoryPosture` * 两位同姿势:不铸一个看起来像答案的空壳(那会让壳把夹具形读成「本部署无表」)。 */ export declare function projectWriteProtectionCapability(posture: WriteProtectionPosture | undefined): WriteProtectionCapability | null; //# sourceMappingURL=write-protection.d.ts.map