@sema-agent/server 7.98.0 → 7.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +19 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +86 -60
- package/deploy/sema-up/lib/envf.sh +110 -42
- package/deploy/sema-up/sema-up.sh +76 -34
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +3 -0
- package/dist/approval-card.d.ts +74 -1
- package/dist/approval-card.js +41 -8
- package/dist/approval.d.ts +44 -20
- package/dist/approval.js +14 -6
- package/dist/boot/config-center.d.ts +16 -8
- package/dist/boot/config-center.js +206 -103
- package/dist/boot/execution-env.d.ts +1 -2
- package/dist/boot/execution-env.js +2 -2
- package/dist/boot/leader.js +12 -9
- package/dist/boot/memory-consolidation.d.ts +38 -0
- package/dist/boot/memory-consolidation.js +35 -0
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/resolve-spec.d.ts +0 -1
- package/dist/boot/resolve-spec.js +4 -3
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +3 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +6 -6
- package/dist/boot/stage-08-reapers.d.ts +3 -4
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/capabilities/center-plugins.d.ts +14 -2
- package/dist/capabilities/center-plugins.js +13 -5
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/capabilities/hands-lane.js +2 -1
- package/dist/capabilities/scenarios.js +18 -5
- package/dist/capabilities/tool-defer.d.ts +2 -2
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.d.ts +2 -2
- package/dist/config-catalog.js +15 -15
- package/dist/config-center/apply-effective.d.ts +34 -39
- package/dist/config-center/apply-effective.js +100 -64
- package/dist/config-center/apply-ledger.d.ts +35 -16
- package/dist/config-center/apply-ledger.js +25 -18
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/facade.d.ts +11 -5
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/hot-keys-registry.d.ts +11 -4
- package/dist/config-center/hot-keys-registry.js +11 -7
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/restart-signal.d.ts +21 -14
- package/dist/config-center/restart-signal.js +16 -22
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-center/types.d.ts +3 -3
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +0 -8
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +30 -11
- package/dist/config.js +11 -13
- package/dist/cross-session-settings.js +1 -1
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/host-lsp-manager.d.ts +29 -0
- package/dist/host-lsp-manager.js +14 -0
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/http/active-run-conflict.d.ts +2 -12
- package/dist/http/active-run-conflict.js +1 -6
- package/dist/http/admission.js +8 -0
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +14 -10
- package/dist/http/route-ctx.d.ts +2 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/memory-compliance.js +5 -0
- package/dist/http/routes/memory-origin.js +5 -0
- package/dist/http/routes/sessions.js +6 -0
- package/dist/http/routes/tasks.js +7 -6
- package/dist/http/server.d.ts +6 -5
- package/dist/http/wire-types.d.ts +6 -3
- package/dist/leader/wire.d.ts +20 -12
- package/dist/leader/wire.js +7 -6
- package/dist/memory-layer-preflight.d.ts +46 -0
- package/dist/memory-layer-preflight.js +121 -0
- package/dist/memory-operator-faces.js +17 -1
- package/dist/observability/corrupt-read-seat.d.ts +21 -4
- package/dist/observability/corrupt-read-seat.js +31 -0
- package/dist/observability/fail-open.d.ts +8 -4
- package/dist/observability/fail-open.js +8 -4
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +3 -26
- package/dist/observability/secret-env-scrub.js +3 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
- package/dist/orchestration/workflow-completion-inbox.js +8 -3
- package/dist/plugins/approval-ask-store-file.d.ts +20 -2
- package/dist/plugins/approval-ask-store-file.js +21 -3
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
- package/dist/plugins/approval-ask-store-memory.js +39 -13
- package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
- package/dist/plugins/approval-ask-store-sql.js +27 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
- package/dist/plugins/checkpoint-store-sql.js +19 -2
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/mailbox-store-sql.d.ts +7 -5
- package/dist/plugins/mailbox-store-sql.js +6 -4
- package/dist/plugins/permission-rule-store-file.js +3 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/plugins/sql-json-column.d.ts +16 -3
- package/dist/plugins/sql-json-column.js +15 -4
- package/dist/plugins/web-search.d.ts +42 -22
- package/dist/plugins/web-search.js +141 -49
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.js +6 -7
- package/dist/runs.js +6 -5
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/runtime-governance.d.ts +50 -3
- package/dist/runtime-governance.js +5 -0
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +43 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/task-settings.d.ts +1 -1
- package/dist/tool-approval.d.ts +144 -9
- package/dist/tool-approval.js +74 -18
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +22 -5
- package/dist/trace/projection-drop.d.ts +2 -0
- package/dist/trace/projection-drop.js +6 -0
- package/dist/trace/sema-provenance.d.ts +4 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/dist/trace/task-notification-facets.d.ts +65 -0
- package/dist/trace/task-notification-facets.js +31 -0
- package/dist/trace/wire-projection-faces.d.ts +3 -3
- package/dist/trace/wire-projection-faces.js +3 -3
- package/package.json +5 -4
|
@@ -30,7 +30,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
30
30
|
},
|
|
31
31
|
"server.trace.projection-input-malformed": {
|
|
32
32
|
cls: "F",
|
|
33
|
-
note: "7.78.0(core 7.18.0 #741/#786/#789/#790,`trace/project.ts`
|
|
33
|
+
note: "7.78.0(core 7.18.0 #741/#786/#789/#790,`trace/project.ts` 的五张新投影脸共用一条规则;之后逐版入族 —— NP-5 / 7.100.0 起含 `trace/task-notification-facets.ts` 的 S-616 委派终态 facet / 重供两键与 `compacted.summarizedCrossSessionMessages`,助手住叶模块 `trace/projection-drop.ts`,全表见普查表第 73 行):`tool_progress` / `tool_disclosure` 两帧的必需位读不出 ⇒ **整帧不发**;`wiring_manifest.hooks[]` / `wiring_manifest.lsp` / `context_usage.sections[]` 三段的容器形或成员形读不出 ⇒ 那一行/那一段被丢、全丢光即键缺席。规则一句话:**在场但读不出 ⇒ 计一次并丢弃;缺席从不计数**(缺席是这几张脸上的一等状态,契约把它定义成一个正面事实 —— `hooks` 缺席 = 「本条腿没有自己的钩子记录」、`sections` 缺席 = 「这条腿没装配出段 IR」、`lsp` 缺席 = 「本部署没接 LSP」)。放行的最坏后果=这条腿的消费端少一张观测/渲染面(壳退回上一份已知状态或通用卡片),**不影响任何裁决**——门在引擎里早已判完。反方向(半张脸照发)严格更坏:消费端会把读不出的行当成那个正面事实。计数是**唯一**能把「坏世代 / 第三方 producer 一直在发坏载荷」与那个正面事实分开的位。",
|
|
34
34
|
},
|
|
35
35
|
"server.task-mcp.source-label-dropped": {
|
|
36
36
|
cls: "F",
|
|
@@ -112,6 +112,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
112
112
|
cls: "F",
|
|
113
113
|
note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor 的行之后、`shadowedRule` 无可脱敏、读面派生的 `governanceForced` 徽章不亮 —— 三者都是**展示/分诊**,不是执法判据(门早已 park,真按 `shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,不经本读面),且 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 cell 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),连同其余所有行一起消失。故 F 类;tag 与候选那格**分开计**,并计会让「哪一格在坏」在遥测里读不出来。",
|
|
114
114
|
},
|
|
115
|
+
"server.approvals.park-row-blob-unreadable": {
|
|
116
|
+
cls: "F",
|
|
117
|
+
note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` 而读)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的两位 park 行事实缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
|
|
118
|
+
},
|
|
115
119
|
"server.sql-json.checkpoint.pending-steer-queue-lossy": {
|
|
116
120
|
cls: "F",
|
|
117
121
|
note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `get()`,core 5.14.0 #147 的耐久转向队列):一条挂起 run 的 `checkpoint.pending_steer_queue` 这一格**没能完整读出来**(不是合法 JSON 文本 / 不是数组 / 某一条目的四个必备键不合形)⇒ 读不出的那部分**丢掉**,run 照常 resume。放行的最坏后果 = 一条已经 park 的操作员转向指令**没有**在这次 resume 上注入(人以为说过的话没送到);不放行的代价是这条挂起的 run **永远不可 resume** —— resume 是它唯一的出路,为一格读不出的转向把整条 run 变成砖是错误的失败方向(写侧刻意相反:`setPendingSteer` 往一份读不全的队列上追加会把读不出的条目永久抹掉,那里 fail-loud)。故 F 类。留痕从一条 `logger.info` 升成登记 tag 的理由:日志行在**遥测轴**上是零信号,「这台机器的转向队列一直读不出来」与「这台机器本来就没人 steer」在指标上完全同形。计数非零 = 去按日志里的 token 指纹查那一行。",
|
|
@@ -218,7 +222,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
218
222
|
},
|
|
219
223
|
"server.approval.ambiguous-cas.expire": {
|
|
220
224
|
cls: "P-DEBT",
|
|
221
|
-
note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290)→ S-69 收窄(S-25-R1 终局,core design/384 §4.3 七律 / §4.4 键形对表,[5934]):流内 ask 的**窗到期臂**打原子终局 claim(`claimTerminal(expire)`,S-69 前 `expireAsk` CAS)被 reject(抛错 / 超墙钟,什么也证明不了 —— 律 3 K2)之后,有界退避**重发同一 claim**(6 格 ⇒ 至多 7 把,在飞帽 2 按持久 askId 记、撞帽拍不计 attempt、让路探针读同帽同界、帽满或同 askId 尚有在飞操作时探针不计 rung、真相广播同 askId 全部登记(本登记已 done 亦广播)—— codex S-69 R2-1/R3-1/R3-2/R4-1/R5-1/R6-1/R6-2/R7-1;末格睡后仍再发 —— codex R1-F1 同律;总墙钟硬上界 TERMINAL_LADDER_WALL_MS ≈10.15s —— R2-2 修正算术 + R3-3 硬封顶)**全部 reject** ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(A-075.116②:其兜底覆盖本形经确认后本 tag 降 F)。**不计**的形(S-69 起命中条件收窄到「店对整条阶梯持续不可用」):claim 输 ⇒ 败方答案**携真相**(DECIDED ⇒ 按人的决议结算,零补读;PARKING/PARKED ⇒ park 路由;absent = 行不在/错 scope ⇒ K1 零退避投影),迟到回来的 claim 同样自足(迟到赢 ⇒ 副作用补做,迟到输 ⇒ 按真决议结算 —— 此前迟到回调只认 won 的那条残余窗即 S-25-R1,本批闭合);编辑在飞标记在场 ⇒ 有界让路(不重打 claim)、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);阶梯里曾拿到真相而标记已放开 ⇒ 按那份真相结算。detail 带 askId 与 claim 把数。",
|
|
225
|
+
note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290)→ S-69 收窄(S-25-R1 终局,core design/384 §4.3 七律 / §4.4 键形对表,[5934]):流内 ask 的**窗到期臂**打原子终局 claim(`claimTerminal(expire)`,S-69 前 `expireAsk` CAS;NP-2 / 7.100.0 起 `deny` 政策下意图是 `refuse` = 当场落 DECIDED(deny)+`settled_by=policy`,同一架阶梯、同一个 tag)被 reject(抛错 / 超墙钟,什么也证明不了 —— 律 3 K2)之后,有界退避**重发同一 claim**(6 格 ⇒ 至多 7 把,在飞帽 2 按持久 askId 记、撞帽拍不计 attempt、让路探针读同帽同界、帽满或同 askId 尚有在飞操作时探针不计 rung、真相广播同 askId 全部登记(本登记已 done 亦广播)—— codex S-69 R2-1/R3-1/R3-2/R4-1/R5-1/R6-1/R6-2/R7-1;末格睡后仍再发 —— codex R1-F1 同律;总墙钟硬上界 TERMINAL_LADDER_WALL_MS ≈10.15s —— R2-2 修正算术 + R3-3 硬封顶)**全部 reject** ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(A-075.116②:其兜底覆盖本形经确认后本 tag 降 F)。**不计**的形(S-69 起命中条件收窄到「店对整条阶梯持续不可用」):claim 输 ⇒ 败方答案**携真相**(DECIDED ⇒ 按人的决议结算,零补读;PARKING/PARKED ⇒ park 路由;absent = 行不在/错 scope ⇒ K1 零退避投影),迟到回来的 claim 同样自足(迟到赢 ⇒ 副作用补做,迟到输 ⇒ 按真决议结算 —— 此前迟到回调只认 won 的那条残余窗即 S-25-R1,本批闭合);编辑在飞标记在场 ⇒ 有界让路(不重打 claim)、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);阶梯里曾拿到真相而标记已放开 ⇒ 按那份真相结算。detail 带 askId 与 claim 把数。",
|
|
222
226
|
},
|
|
223
227
|
"server.approval.ambiguous-cas.cancel": {
|
|
224
228
|
cls: "P-DEBT",
|
|
@@ -226,7 +230,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
226
230
|
},
|
|
227
231
|
"server.approval.ambiguous-cas.unreachable": {
|
|
228
232
|
cls: "P-DEBT",
|
|
229
|
-
note: "S-25 同族第三员 → S-69 收窄:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `claimTerminal(expire)`(S-69 前 `expireAsk` CAS)被 reject 之后,有界重发至多 7 把 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;S-25 起行读出 DECIDED ⇒ 按人的决议结算;S-69 起真相随败方答案回来、迟到答案自足,只有店对整条阶梯持续 reject 才计数。",
|
|
233
|
+
note: "S-25 同族第三员 → S-69 收窄:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `claimTerminal(expire)`(S-69 前 `expireAsk` CAS;NP-2 起 `deny` 政策下是 `refuse`,同窗到期员)被 reject 之后,有界重发至多 7 把 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;S-25 起行读出 DECIDED ⇒ 按人的决议结算;S-69 起真相随败方答案回来、迟到答案自足,只有店对整条阶梯持续 reject 才计数。",
|
|
230
234
|
},
|
|
231
235
|
"server.approval-ask-audit.append-failed": {
|
|
232
236
|
cls: "F",
|
|
@@ -244,7 +248,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
244
248
|
"server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
|
|
245
249
|
"server.approval-card.closed-word-out-of-set": {
|
|
246
250
|
cls: "F",
|
|
247
|
-
note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` 的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40)。",
|
|
251
|
+
note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
|
|
248
252
|
},
|
|
249
253
|
"server.pg-pool.idle-client-error": {
|
|
250
254
|
cls: "F",
|
|
@@ -1,34 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* [ref]([ref])—— **密钥形 env 剥除的留痕面**(core D1 `scrubSecretEnv` 的可观测半场)。
|
|
3
|
-
*
|
|
4
|
-
* ## 它解决的问题
|
|
5
|
-
* `scrubSecretEnv` 是本仓两条 shell/子进程腿的**安全默认**:模型可驱动的 shell(`inheritEnv:"scrub"`)与
|
|
6
|
-
* git 子进程(钩子/凭证助手)拿到的 env 里,`*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`… 一律先丢掉。
|
|
7
|
-
* 两处都以**单参形**调用,于是「这一次到底丢了哪些 key、按哪条规则(suffix-rule / exact-name)、置信度多少」
|
|
8
|
-
* **零留痕** —— 部署方既证明不了保护生效过,也发现不了它悄悄不生效了(词表漂移 / 一个新的密钥命名法从
|
|
9
|
-
* 规则的缝里穿过去 / 某条腿被改成 `inheritEnv:"all"`)。core 早就把收集口开好了(`findings?:
|
|
10
|
-
* SecretEnvFinding[]`,d.ts 原话:the VALUE is never carried — this record is meant to be loggable),
|
|
11
|
-
* 缺的只是本仓这半场。本模块就是那半场,**不是**一个新的执法面:它一个字节也不改变剥除行为。
|
|
12
|
-
*
|
|
13
|
-
* ## 形(照 `fail-open.ts` 的仓内先例)
|
|
14
|
-
* 1. **计数(逐次)** —— `secret_env_scrubbed_total{site,kind,confidence}`;
|
|
15
|
-
* 2. **结构化披露(每 site×key 一次)** —— 首次在某条腿上丢掉某个 key 时一条 `info`,之后同 key 只走计数。
|
|
16
|
-
* 不是「每 site 一次」:一个**新**的密钥形 key 后来才出现(部署加了一个凭证)时,那才是最该看见的一行。
|
|
17
|
-
* 值永不进任何一件(core 的 finding 本身就不携带值)。
|
|
18
|
-
*
|
|
19
|
-
* ## 为什么是进程级自由函数而不是注入
|
|
20
|
-
* 与 `recordFailOpen` 同因:两个调用点都在**没有 logger/metrics 句柄**的深处
|
|
21
|
-
* (`plugins/remote-env-host.ts` 的 `mergeEnv` 全文零观测通道;`project-memory.ts` 的 `git()` 是个自由函数)。
|
|
22
|
-
* 装配层用 {@link installSecretEnvScrubRecorder} 把句柄接上,装配之前命中的次数不会丢(计数照常累加,
|
|
23
|
-
* install 时一次性补进 metrics),披露行则在下一次命中时补发(未成功披露的 key 不进 `disclosed` 集)。
|
|
24
|
-
*/
|
|
25
|
-
import { type SecretEnvFinding } from "@sema-agent/core";
|
|
1
|
+
import type { ServerSecretEnvFinding } from "../server-secret-env.js";
|
|
26
2
|
import type { Logger } from "./logger.js";
|
|
27
3
|
import type { Metrics } from "./metrics.js";
|
|
28
4
|
/** 登记过的剥除现场。闭集:未登记的 site 传不进 {@link recordSecretEnvScrub}(编译期拒,同 FAIL_OPEN_TAGS 纪律)。 */
|
|
29
5
|
export declare const SECRET_ENV_SCRUB_SITES: {
|
|
30
6
|
readonly "server.remote-env-host.inherit-scrub": "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令";
|
|
31
7
|
readonly "server.project-memory.git-subprocess": "project-memory 读仓叙事时的 git 子进程 env —— 仓内的钩子/凭证助手会以本进程身份运行";
|
|
8
|
+
readonly "server.lsp.spawn-env": "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓";
|
|
32
9
|
};
|
|
33
10
|
/** 词表键推导的闭集类型。 */
|
|
34
11
|
export type SecretEnvScrubSite = keyof typeof SECRET_ENV_SCRUB_SITES;
|
|
@@ -40,7 +17,7 @@ interface Sink {
|
|
|
40
17
|
* 记一次剥除。**总不抛** —— 观测本身绝不能变成故障(同 `recordFailOpen` / reapers 的判据)。
|
|
41
18
|
* `findings` 为空(这一次没丢任何 key)时什么也不做:那不是事件,是常态。
|
|
42
19
|
*/
|
|
43
|
-
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly
|
|
20
|
+
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly ServerSecretEnvFinding[]): void;
|
|
44
21
|
/** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。装配前累计的次数在此补进 metrics。 */
|
|
45
22
|
export declare function installSecretEnvScrubRecorder(deps: Sink): void;
|
|
46
23
|
/** 已留驻的「现场×键」条数(测试/诊断读面)。恒 ≤ {@link DISCLOSED_CAP}。 */
|
|
@@ -2,6 +2,7 @@ import { summarizeRedactions } from "@sema-agent/core";
|
|
|
2
2
|
export const SECRET_ENV_SCRUB_SITES = {
|
|
3
3
|
"server.remote-env-host.inherit-scrub": "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令",
|
|
4
4
|
"server.project-memory.git-subprocess": "project-memory 读仓叙事时的 git 子进程 env —— 仓内的钩子/凭证助手会以本进程身份运行",
|
|
5
|
+
"server.lsp.spawn-env": "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓",
|
|
5
6
|
};
|
|
6
7
|
const SCRUB_COUNTER = "secret_env_scrubbed_total";
|
|
7
8
|
const DISCLOSED_CAP = 512;
|
|
@@ -62,7 +63,7 @@ export function recordSecretEnvScrub(site, findings) {
|
|
|
62
63
|
}
|
|
63
64
|
export function installSecretEnvScrubRecorder(deps) {
|
|
64
65
|
sink = { ...deps };
|
|
65
|
-
deps.metrics?.counter(SCRUB_COUNTER, "secret
|
|
66
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv shape rule + server config-catalog secret names)");
|
|
66
67
|
drainToMetrics();
|
|
67
68
|
}
|
|
68
69
|
export function secretEnvScrubDisclosedSize() {
|
|
@@ -76,6 +77,6 @@ export function resetSecretEnvScrubRecorderForTest(deps = {}) {
|
|
|
76
77
|
pushed = new Map();
|
|
77
78
|
disclosed = new Set();
|
|
78
79
|
sink = { ...deps };
|
|
79
|
-
deps.metrics?.counter(SCRUB_COUNTER, "secret
|
|
80
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv shape rule + server config-catalog secret names)");
|
|
80
81
|
}
|
|
81
82
|
//# sourceMappingURL=secret-env-scrub.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SemaProvenanceInput } from "../trace/sema-provenance.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type SystemInjectionPriority, type TaskNotificationPayload } from "@sema-agent/core";
|
|
3
3
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
4
4
|
/** One pending completion, scoped to the session that must be told about it. `summary` is ALREADY
|
|
5
5
|
* redacted + length-bounded by core's notifier seam (see {@link WorkflowCompletionPayload}). */
|
|
@@ -76,7 +76,7 @@ export declare const SERVED_FENCE_MS: number;
|
|
|
76
76
|
* 三路复审修1(MED):status 保留 RAW,不再归一 completed/failed —— 归一让同 id 无 seq 的两条 external
|
|
77
77
|
* 通知(先 `event` 后 `cancelled`)折叠成同一把 `…:failed` 键,inbox map 里第二条 REPLACE 第一条,drain
|
|
78
78
|
* 只吐一条(entry 的存储 status 列仍归一,那是账本的双终态列,不动;RAW status 本就随 payload extras
|
|
79
|
-
* 透传)
|
|
79
|
+
* 透传)。这与流侧去重键(core `taskNotificationDedupKey`,见 {@link taskNotificationStreamKeyOf})的 RAW 语义对齐(stream 侧早已保 RAW)。
|
|
80
80
|
*
|
|
81
81
|
* 编码=防碰撞元组:域/taskId/status 走 JSON 数组(调用方自由串自造不了分隔符);`#seq` 尾折在 JSON 之外
|
|
82
82
|
* ——JSON 段恒以 `"]` 结尾,`#digits` 尾无歧义,于是 {@link foldKeyFamily} 的剥尾语义 AND SQL 双生子
|
|
@@ -92,33 +92,30 @@ export declare function taskNotificationFoldKey(n: {
|
|
|
92
92
|
status: string;
|
|
93
93
|
seq?: number;
|
|
94
94
|
}): string;
|
|
95
|
-
/**
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
status: string;
|
|
113
|
-
seq?: number;
|
|
114
|
-
}): string;
|
|
95
|
+
/** 一帧里组成流侧去重键的那几个字段 —— 投影后的 wire 帧(drain 出帧 / stream 事件的 `notification`)与 core 载荷同形读。
|
|
96
|
+
* 型取 core 载荷的那五位(`Partial`:帧上任一位都可能缺,缺了就不成键,见 {@link taskNotificationStreamKeyOf})。 */
|
|
97
|
+
export type TaskNotificationKeyFields = Partial<Pick<TaskNotificationPayload, "task_type" | "task_id" | "status" | "seq" | "attempt">>;
|
|
98
|
+
/**
|
|
99
|
+
* **唯一**从一帧上读键字段的地方(S-616 按类修):此前七个调用点各自手抄 `{ task_type, task_id, status, seq }` 四字段投影,
|
|
100
|
+
* core 给帧身份加一个分量(本版的 `attempt`)时,每一处手抄都得有人记得补 —— 漏一处就是那一腿的重供帧被吞。现在调用点
|
|
101
|
+
* 把整只帧交过来,键字段只在这里读。
|
|
102
|
+
*
|
|
103
|
+
* 🔴 **键 = core `taskNotificationDedupKey` 的输出**(NP-6,7.100.0 合并复审 [ref] 交叉表 #8,opus A3;[ref]):本仓流侧键此前
|
|
104
|
+
* 是 core 那只注入去重键的手抄孪生(`域:id:status[#seq]`),S-616 又逐字抄了它的 `:a<attempt>` 尾 —— core 改一次键规则,
|
|
105
|
+
* 这里就得有人记得跟。键只活在进程内(每腿 {@link NotifiedKeys},不落盘、不上 wire),换成 core 的输出零迁移面。
|
|
106
|
+
* core 那只键同样按车道分域(external 车道 `external:<id>`,其余裸 id —— codex F2 的「external 帧不许与内部帧撞键」照旧成立)、
|
|
107
|
+
* status 保 RAW、`seq` 折入、`attempt` 缺席 / 1 时字节不变。调用点手里已有 core 载荷的(observer 臂)直接调 core 函数。
|
|
108
|
+
* 成不了键 ⇒ `undefined`(该帧不进去重账,照写 —— at-least-once,壳按键去重兜底):`task_id` 缺席 / 空串(与旧形
|
|
109
|
+
* `f.task_id &&` 同判)、`task_type` / `status` 缺席(core 4.0.0 起两者在载荷上必填,缺了只能是坏帧 / 前 4.0 的存量 inbox 行)。
|
|
110
|
+
*/
|
|
111
|
+
export declare function taskNotificationStreamKeyOf(n: TaskNotificationKeyFields | undefined): string | undefined;
|
|
115
112
|
/** Per-leg notified-key ledger cap (codex 1.189 前双审 F3):一条超长腿(海量互异 bg 通知)不许把键集
|
|
116
113
|
* 撑到无界——超出删最老插入(Map 迭代序=插入序)。远高于任何真实 per-leg 通知量;被逐键的老通知
|
|
117
114
|
* 最坏情形=同键晚到帧多写一次(at-least-once,shell 按键 dedup 兜底),绝不吞新帧。 */
|
|
118
115
|
export declare const NOTIFIED_KEYS_MAX = 4096;
|
|
119
116
|
/**
|
|
120
117
|
* Per-leg delivered-notification ledger(codex 1.189 前双审 F1 HIGH 的修位)。
|
|
121
|
-
* 键 = {@link
|
|
118
|
+
* 键 = {@link taskNotificationStreamKeyOf} / core `taskNotificationDedupKey`(同一只);值 = 「认领这把键的那次写是否真落盘」的 promise(true=成功)。
|
|
122
119
|
* 为什么是 Map<string, Promise<boolean>> 而不是 Set:bg/resume 腿的 observer 臂是 fire-and-forget append
|
|
123
120
|
* (`void append(...)`)——旧 Set 下 append 失败(SQL blip)时键已被占而盘上无帧,core pend-drain 把同
|
|
124
121
|
* payload 重放到 stream 臂时被键吞 = 通知永丢。现在 stream 臂对已见键 `await` 认领方的真实结果,false 则
|
|
@@ -268,6 +265,17 @@ export declare function taskNotificationInboxEntry(sessionId: string, owner: str
|
|
|
268
265
|
body: string;
|
|
269
266
|
};
|
|
270
267
|
_sema_provenance?: SemaProvenanceInput;
|
|
268
|
+
/** S-616(core 7.30.0 [ref] S1 / #1034 / #1036):委派终态帧三枚 facet + 重供两键 —— 判据与 live 腿同一只
|
|
269
|
+
* (`trace/task-notification-facets.ts`,入参按 `unknown` 收、判过形才成 typed)。 */
|
|
270
|
+
spawnName?: string;
|
|
271
|
+
handback?: string;
|
|
272
|
+
compaction?: {
|
|
273
|
+
passes: number;
|
|
274
|
+
breakerOpen: boolean;
|
|
275
|
+
kind?: string;
|
|
276
|
+
};
|
|
277
|
+
resupplied?: boolean;
|
|
278
|
+
attempt?: number;
|
|
271
279
|
}, enqueuedAt: number,
|
|
272
280
|
/** lead-b (dual-ID audit): the PARENT run's durable taskId (the uuid the shell's footer rows
|
|
273
281
|
* key on) — rides the drained frame so a consumer can bind the a*-keyed child notification to its uuid-keyed
|
|
@@ -3,6 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { redactSecrets } from "../trace/redact.js";
|
|
4
4
|
import { injectionTierOrAbsent } from "../trace/injection-tier.js";
|
|
5
5
|
import { semaProvenanceOrAbsent } from "../trace/sema-provenance.js";
|
|
6
|
+
import { resupplyFields, terminalFrameFacetFields } from "../trace/task-notification-facets.js";
|
|
7
|
+
import { taskNotificationDedupKey } from "@sema-agent/core";
|
|
6
8
|
export const MAX_PENDING_PER_SESSION = 100;
|
|
7
9
|
export const COMPACT_EVERY = 512;
|
|
8
10
|
export const PURGE_FENCE_MS = 10 * 60 * 1000;
|
|
@@ -11,9 +13,10 @@ export function taskNotificationFoldKey(n) {
|
|
|
11
13
|
const domain = n.task_type === "external" ? "external" : "task";
|
|
12
14
|
return `${JSON.stringify([domain, n.task_id, n.status])}${n.seq !== undefined ? `#${n.seq}` : ""}`;
|
|
13
15
|
}
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export function taskNotificationStreamKeyOf(n) {
|
|
17
|
+
if (n === undefined || typeof n.task_id !== "string" || n.task_id.length === 0 || n.task_type === undefined || n.status === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
return taskNotificationDedupKey({ task_type: n.task_type, task_id: n.task_id, status: n.status, seq: n.seq, attempt: n.attempt });
|
|
17
20
|
}
|
|
18
21
|
export const NOTIFIED_KEYS_MAX = 4096;
|
|
19
22
|
export class NotifiedKeys {
|
|
@@ -322,6 +325,8 @@ export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, pare
|
|
|
322
325
|
const p = semaProvenanceOrAbsent(n._sema_provenance);
|
|
323
326
|
return p !== undefined ? { _sema_provenance: p } : {};
|
|
324
327
|
})(),
|
|
328
|
+
...terminalFrameFacetFields(n),
|
|
329
|
+
...resupplyFields(n),
|
|
325
330
|
};
|
|
326
331
|
return {
|
|
327
332
|
sessionId,
|
|
@@ -21,7 +21,7 @@ export declare const ASK_JOURNAL_COMPACT_GROWTH = 2;
|
|
|
21
21
|
* 账本的**动词闭集**([ref] 形)。运行期的词表判定与编译期的穷尽 `switch` 读同一份:
|
|
22
22
|
* 往这里加一个词,{@link FileApprovalAskStore.replayOp} 的 switch 立刻编译红(漏一口不可能发布)。
|
|
23
23
|
*/
|
|
24
|
-
declare const JOURNAL_OPS: readonly ["ensureAsk", "transitionAsk", "decideAsk", "claimTerminal", "expireAsk", "bindBatch", "abortBatch", "deferReconcile", "resolveProvisional", "deleteByTask"];
|
|
24
|
+
declare const JOURNAL_OPS: readonly ["ensureAsk", "transitionAsk", "decideAsk", "claimTerminal", "claimTerminalRefuse", "expireAsk", "bindBatch", "abortBatch", "deferReconcile", "resolveProvisional", "deleteByTask"];
|
|
25
25
|
type JournalOpWord = (typeof JOURNAL_OPS)[number];
|
|
26
26
|
/** 运行期词表(上面的数组)与类型面动词维({@link JournalCall})的**双向**对表 —— 任一边加词而另一边
|
|
27
27
|
* 没跟上,这个别名解析成 `never`,{@link JOURNAL_OP_TABLE_GATE} 的赋值当场编译红。 */
|
|
@@ -29,6 +29,18 @@ type JournalOpTableIsExhaustive = (JournalCall["op"] extends JournalOpWord ? (Jo
|
|
|
29
29
|
/** 上面那条对表的**执法位**(占位常量形,`core-keyset-guard.ts` 的占位元组同精神):运行期无用,
|
|
30
30
|
* 存在的唯一理由是让「词表与类型面分家」在编译期就红,而不是等到某条记录重放不出来。 */
|
|
31
31
|
export declare const JOURNAL_OP_TABLE_GATE: JournalOpTableIsExhaustive;
|
|
32
|
+
/**
|
|
33
|
+
* 老动词 `claimTerminal` 下**只许**出现的两个意图(NP-9,7.100.0 合并复审 gpt-6 验修腿 [medium])。
|
|
34
|
+
*
|
|
35
|
+
* 🔴 **为什么终局 claim 的新意图必须记成新动词**:已发布的读者(7.99.0 及以前)的内核按 `intent === "expire" ? … : <cancel 臂>`
|
|
36
|
+
* 分叉 —— 它不认识的意图**不会被拒,而是被当成 cancel 重放**(行 VOID)。所以一个新意图若记在老动词下,回滚到老版本时
|
|
37
|
+
* 那条结算会**静默**改判(真读者复现:`refuse` ⇒ `VOID/null/null`,零拒绝记录)。老读者对**闭集外动词**是拒启并点名出路
|
|
38
|
+
* (「a DOWNGRADE: a newer server wrote it」)⇒ 新意图记成新动词 = 降级时响亮拒启,而不是静默误读。
|
|
39
|
+
* 规则:`AskTerminalClaimIntent` 每加一个意图,就在 {@link JournalCall} 里加一个动词、在 {@link FileApprovalAskStore.claimTerminal}
|
|
40
|
+
* 的穷尽 switch 里给它一臂(漏了 ⇒ 编译红);本型永远只有这两个词。本版读者对老动词下的其它意图同样**拒启**(不按某一臂重放)。
|
|
41
|
+
*/
|
|
42
|
+
declare const JOURNAL_V1_CLAIM_INTENTS: readonly ["expire", "cancel"];
|
|
43
|
+
type JournalV1ClaimIntent = (typeof JOURNAL_V1_CLAIM_INTENTS)[number];
|
|
32
44
|
/** 一次写调用的实参(动词维判别式)——盘上记的就是它,重放时原样喂回内核。 */
|
|
33
45
|
type JournalCall = {
|
|
34
46
|
op: "ensureAsk";
|
|
@@ -48,7 +60,13 @@ type JournalCall = {
|
|
|
48
60
|
op: "claimTerminal";
|
|
49
61
|
askId: string;
|
|
50
62
|
batchId: string;
|
|
51
|
-
intent:
|
|
63
|
+
intent: JournalV1ClaimIntent;
|
|
64
|
+
}
|
|
65
|
+
/** NP-2 的部署政策拒(`claimTerminal(…, "refuse")`)—— 新动词而不是老动词带新意图,理由见 {@link JOURNAL_V1_CLAIM_INTENTS}。 */
|
|
66
|
+
| {
|
|
67
|
+
op: "claimTerminalRefuse";
|
|
68
|
+
askId: string;
|
|
69
|
+
batchId: string;
|
|
52
70
|
} | {
|
|
53
71
|
op: "expireAsk";
|
|
54
72
|
askId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, copyFileSync, openSync, readSync, statSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { AppendLog, atomicWriteFile, ensureDir, readJsonlRecords } from "@sema-agent/core";
|
|
4
|
+
import { createSkippedRecordSink } from "../observability/corrupt-read-seat.js";
|
|
4
5
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
6
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
6
7
|
import { InMemoryApprovalAskStore, encodeDecideUpdatedInput, encodeStoreJsonPayload } from "./approval-ask-store-memory.js";
|
|
@@ -13,6 +14,7 @@ const JOURNAL_OPS = [
|
|
|
13
14
|
"transitionAsk",
|
|
14
15
|
"decideAsk",
|
|
15
16
|
"claimTerminal",
|
|
17
|
+
"claimTerminalRefuse",
|
|
16
18
|
"expireAsk",
|
|
17
19
|
"bindBatch",
|
|
18
20
|
"abortBatch",
|
|
@@ -22,6 +24,8 @@ const JOURNAL_OPS = [
|
|
|
22
24
|
];
|
|
23
25
|
const JOURNAL_OP_WORDS = new Set(JOURNAL_OPS);
|
|
24
26
|
export const JOURNAL_OP_TABLE_GATE = true;
|
|
27
|
+
const JOURNAL_V1_CLAIM_INTENTS = ["expire", "cancel"];
|
|
28
|
+
const JOURNAL_V1_CLAIM_INTENT_WORDS = new Set(JOURNAL_V1_CLAIM_INTENTS);
|
|
25
29
|
function endsAtRecordBoundary(path) {
|
|
26
30
|
const size = statSync(path).size;
|
|
27
31
|
if (size === 0)
|
|
@@ -54,10 +58,10 @@ export class FileApprovalAskStore {
|
|
|
54
58
|
this.path = join(dir, APPROVAL_ASK_JOURNAL_FILE);
|
|
55
59
|
this.core = new InMemoryApprovalAskStore({ now: () => this.clockRead() });
|
|
56
60
|
const tornTail = existsSync(this.path) && !endsAtRecordBoundary(this.path);
|
|
57
|
-
const records = readJsonlRecords(this.path, (info) => {
|
|
61
|
+
const records = readJsonlRecords(this.path, createSkippedRecordSink((info) => {
|
|
58
62
|
this.corruptLineCount += 1;
|
|
59
63
|
recordFailOpen("server.approval-ask-journal.corrupt-record-skipped", `path=${info.path} reason=${info.reason}`);
|
|
60
|
-
});
|
|
64
|
+
}));
|
|
61
65
|
for (const rec of records)
|
|
62
66
|
this.replayRecord(rec);
|
|
63
67
|
this.records = records.length;
|
|
@@ -156,6 +160,8 @@ export class FileApprovalAskStore {
|
|
|
156
160
|
refuse("verb outside this build's closed set (a DOWNGRADE: a newer server wrote it)");
|
|
157
161
|
if (typeof probe.t !== "number" || !Number.isFinite(probe.t))
|
|
158
162
|
refuse("call record without a clock reading");
|
|
163
|
+
if (probe.op === "claimTerminal" && !JOURNAL_V1_CLAIM_INTENT_WORDS.has(String(raw.intent)))
|
|
164
|
+
refuse("claimTerminal intent outside this build's journal set (a newer server wrote it)");
|
|
159
165
|
const rec = raw;
|
|
160
166
|
this.pinnedNow = rec.t;
|
|
161
167
|
try {
|
|
@@ -180,6 +186,8 @@ export class FileApprovalAskStore {
|
|
|
180
186
|
return this.core.decideAsk(rec.askId, rec.batchId, rec.decision);
|
|
181
187
|
case "claimTerminal":
|
|
182
188
|
return this.core.claimTerminal(rec.askId, rec.batchId, rec.intent);
|
|
189
|
+
case "claimTerminalRefuse":
|
|
190
|
+
return this.core.claimTerminal(rec.askId, rec.batchId, "refuse");
|
|
183
191
|
case "expireAsk":
|
|
184
192
|
return this.core.expireAsk(rec.askId, rec.batchId);
|
|
185
193
|
case "bindBatch":
|
|
@@ -209,7 +217,17 @@ export class FileApprovalAskStore {
|
|
|
209
217
|
return this.write({ op: "decideAsk", askId, batchId, decision: journalled }, (c) => this.core.decideAsk(c.askId, c.batchId, c.decision));
|
|
210
218
|
}
|
|
211
219
|
async claimTerminal(askId, batchId, intent) {
|
|
212
|
-
|
|
220
|
+
switch (intent) {
|
|
221
|
+
case "expire":
|
|
222
|
+
case "cancel":
|
|
223
|
+
return this.write({ op: "claimTerminal", askId, batchId, intent }, (c) => this.core.claimTerminal(c.askId, c.batchId, c.intent));
|
|
224
|
+
case "refuse":
|
|
225
|
+
return this.write({ op: "claimTerminalRefuse", askId, batchId }, (c) => this.core.claimTerminal(c.askId, c.batchId, "refuse"));
|
|
226
|
+
default: {
|
|
227
|
+
const unreachable = intent;
|
|
228
|
+
throw new Error(`FileApprovalAskStore.claimTerminal: unknown intent ${String(unreachable)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
213
231
|
}
|
|
214
232
|
async expireAsk(askId, batchId) {
|
|
215
233
|
return this.write({ op: "expireAsk", askId, batchId }, (c) => this.core.expireAsk(c.askId, c.batchId));
|
|
@@ -82,6 +82,8 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
|
|
|
82
82
|
* 既有 verb(expire ⇒ `this.expireAsk`;cancel ⇒ `this.transitionAsk(STREAM_PENDING→VOID)`)——律 2
|
|
83
83
|
* 「一台机器」在本 twin 的落笔处是**字面同一个函数做写**(零第二套谓词);委托也让测试对 verb 的
|
|
84
84
|
* 实例级故障注入(`store.expireAsk = throwing` 等)对 claim 恒有效,注入面不因原语换名而漂移。
|
|
85
|
+
* `refuse`(NP-2)没有可委托的既有 verb(`decideAsk` 要批 OPEN —— 人答臂的语义),就地写;对它的故障注入走
|
|
86
|
+
* `store.claimTerminal` 本身。
|
|
85
87
|
* 原子性:方法体的判定与读回之间唯一的 await 是对**同步体** verb 的调用(本店头注的单线程天然原子
|
|
86
88
|
* 论证);await 后读到的行 ≥ CAS 线性化点,而非 pending 态无回边 ⇒ 败方读数自足(与 SQL 侧同一条
|
|
87
89
|
* 单调性论证)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
2
|
-
import { assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
2
|
+
import { assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
3
3
|
function applyPatch(row, patch) {
|
|
4
4
|
if (patch.decision !== undefined)
|
|
5
5
|
row.decision = patch.decision;
|
|
@@ -153,18 +153,44 @@ export class InMemoryApprovalAskStore {
|
|
|
153
153
|
return { ok: true, row: { ...ask } };
|
|
154
154
|
}
|
|
155
155
|
async claimTerminal(askId, batchId, intent) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
switch (intent) {
|
|
157
|
+
case "expire": {
|
|
158
|
+
const res = await this.expireAsk(askId, batchId);
|
|
159
|
+
if (res.won)
|
|
160
|
+
return { claimed: true, voidedSiblings: res.voidedSiblings };
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "cancel": {
|
|
164
|
+
const scoped = this.asks.get(askId);
|
|
165
|
+
if (scoped !== undefined && scoped.batchId !== batchId)
|
|
166
|
+
return { claimed: false, current: { state: "absent" } };
|
|
167
|
+
const won = await this.transitionAsk(askId, "STREAM_PENDING", "VOID", { updatedAtMs: this.now() });
|
|
168
|
+
if (won)
|
|
169
|
+
return { claimed: true, voidedSiblings: [] };
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "refuse": {
|
|
173
|
+
assertDecideSettledBy(POLICY_REFUSAL_DECISION);
|
|
174
|
+
const to = claimTargetState(intent);
|
|
175
|
+
if (!canAskTransition("STREAM_PENDING", to))
|
|
176
|
+
throw new Error(`InMemoryApprovalAskStore.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
|
|
177
|
+
const ask = this.asks.get(askId);
|
|
178
|
+
if (ask !== undefined && ask.state === "STREAM_PENDING" && ask.batchId === batchId) {
|
|
179
|
+
const now = this.now();
|
|
180
|
+
ask.state = to;
|
|
181
|
+
ask.decision = POLICY_REFUSAL_DECISION.decision;
|
|
182
|
+
ask.settledBy = POLICY_REFUSAL_DECISION.settledBy;
|
|
183
|
+
ask.decidedAtMs = now;
|
|
184
|
+
ask.rev += 1;
|
|
185
|
+
ask.updatedAtMs = now;
|
|
186
|
+
return { claimed: true, voidedSiblings: [] };
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
default: {
|
|
191
|
+
const unreachable = intent;
|
|
192
|
+
throw new Error(`InMemoryApprovalAskStore.claimTerminal: unknown intent ${String(unreachable)}`);
|
|
193
|
+
}
|
|
168
194
|
}
|
|
169
195
|
const cur = this.asks.get(askId);
|
|
170
196
|
return { claimed: false, current: projectAskTerminalClaimTruth(cur !== undefined && cur.batchId === batchId ? { ...cur } : null) };
|
|
@@ -302,13 +302,32 @@ export interface ExpireResult {
|
|
|
302
302
|
}
|
|
303
303
|
/**
|
|
304
304
|
* [ref](DEBTS [ref]-R1 终局;core [ref] §4.4「@server 对表键形」,黑板 [ref] 认领):终局 claim 的
|
|
305
|
-
* 意图。ask
|
|
306
|
-
*
|
|
305
|
+
* 意图。ask 行的本地终局 claim 有三员:
|
|
306
|
+
* · `expire` —— 窗到期 / emit 全灭在 **park 政策**下:STREAM_PENDING→PARKING + 批转投递面 + 兄弟撤卡,与
|
|
307
|
+
* `expireAsk` 同一台 CAS 机器(批是 park 路由的单位);
|
|
308
|
+
* · `cancel` —— 取消臂:STREAM_PENDING→VOID;
|
|
309
|
+
* · `refuse` —— 同两臂在 **deny 政策**下(NP-2,7.100.0 合并复审 [ref] 交叉表 #4):STREAM_PENDING→DECIDED,
|
|
310
|
+
* 决议 = {@link POLICY_REFUSAL_DECISION}(deny + 结算来源 `policy`),**按 ask 作用域** —— 不碰批、不连坐兄弟
|
|
311
|
+
* (deny 政策下没有 park 路由,批不是任何东西的单位;存量整腿批行因此同判)。存在理由:部署政策拒交给 core 的
|
|
312
|
+
* 是 `{allow:false, settledBy:"policy"}`,行若停 PARKING,收敛后成 DENIED 且无来源 ⇒ 同 ask 重入回放裸 `false`
|
|
313
|
+
* (core 契约里那是「一个人拒了」)。行落的来源与交出去的来源是同一个词,回放由 `outcomeOfDecidedRow` 读回同一个对象。
|
|
307
314
|
* **人答臂(decide)不入本枚举**:`decideAsk` 带决议载荷与回决幂等键,其失败臂本就携行读数(384 §4.4
|
|
308
|
-
* 表「decideAsk(人答臂)」格的「视现有 CAS 形」裁定)
|
|
309
|
-
* WHERE 谓词 {@link pendingClaimWhere}
|
|
315
|
+
* 表「decideAsk(人答臂)」格的「视现有 CAS 形」裁定)——终局 verb 的「一台机器义务」(384 §4.3-2)由共享
|
|
316
|
+
* WHERE 谓词 {@link pendingClaimWhere} 兑现,不是把不同 verb 塞进一个函数。
|
|
310
317
|
*/
|
|
311
|
-
export type AskTerminalClaimIntent = "expire" | "cancel";
|
|
318
|
+
export type AskTerminalClaimIntent = "expire" | "cancel" | "refuse";
|
|
319
|
+
/**
|
|
320
|
+
* `refuse` 意图落行的决议(NP-2)—— **部署政策拒**:deny + 结算来源 `policy`。来源词的型取 core 的 `HostSettledBy`
|
|
321
|
+
* (core 改词 / 退词 ⇒ 这里 tsc 红);协调器交给 core 的那一只(`tool-approval.ts` 的 `UnattendedPolicyRefusal`)读的是
|
|
322
|
+
* **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link assertDecideSettledBy}
|
|
323
|
+
* (settled_by 的唯一形门:长度 ≤ 列宽、只与 deny 同行)。
|
|
324
|
+
*/
|
|
325
|
+
export declare const POLICY_REFUSAL_DECISION: {
|
|
326
|
+
readonly decision: "deny";
|
|
327
|
+
readonly settledBy: Extract<HostSettledBy, "policy">;
|
|
328
|
+
};
|
|
329
|
+
/** 终局 claim 意图 → 本次 CAS 的目标态(穷举 switch:意图闭集加一个词 ⇒ 这里编译期先红)。 */
|
|
330
|
+
export declare function claimTargetState(intent: AskTerminalClaimIntent): AskState;
|
|
312
331
|
/**
|
|
313
332
|
* 败方读数:行上终局相关列的一致投影(status 词表 = 行自己的 `AskState`,零新值域 —— 384 §4.2 的
|
|
314
333
|
* 「reuse the row's own vocabulary」)。
|
|
@@ -52,6 +52,21 @@ async function withReadSignal(op, signal, where) {
|
|
|
52
52
|
signal.removeEventListener("abort", onAbort);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
export const POLICY_REFUSAL_DECISION = { decision: "deny", settledBy: "policy" };
|
|
56
|
+
export function claimTargetState(intent) {
|
|
57
|
+
switch (intent) {
|
|
58
|
+
case "expire":
|
|
59
|
+
return "PARKING";
|
|
60
|
+
case "cancel":
|
|
61
|
+
return "VOID";
|
|
62
|
+
case "refuse":
|
|
63
|
+
return "DECIDED";
|
|
64
|
+
default: {
|
|
65
|
+
const unreachable = intent;
|
|
66
|
+
throw new Error(`approval-ask-store: unknown terminal claim intent ${String(unreachable)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
export function projectAskTerminalClaimTruth(row) {
|
|
56
71
|
if (row === null || row.state === "STREAM_PENDING")
|
|
57
72
|
return { state: "absent" };
|
|
@@ -484,10 +499,12 @@ export class SqlApprovalAskStore {
|
|
|
484
499
|
return { ok: true, row };
|
|
485
500
|
}
|
|
486
501
|
async claimTerminal(askId, batchId, intent) {
|
|
487
|
-
const to = intent
|
|
502
|
+
const to = claimTargetState(intent);
|
|
488
503
|
if (!canAskTransition("STREAM_PENDING", to)) {
|
|
489
504
|
throw new Error(`approval-ask-store.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
|
|
490
505
|
}
|
|
506
|
+
if (intent === "refuse")
|
|
507
|
+
assertDecideSettledBy(POLICY_REFUSAL_DECISION);
|
|
491
508
|
const now = Date.now();
|
|
492
509
|
const dialect = this.db.dialect;
|
|
493
510
|
const conn = await this.db.connect();
|
|
@@ -500,13 +517,20 @@ export class SqlApprovalAskStore {
|
|
|
500
517
|
return { claimed: false, current: { state: "absent" } };
|
|
501
518
|
}
|
|
502
519
|
}
|
|
503
|
-
const askRes =
|
|
520
|
+
const askRes = intent === "refuse"
|
|
521
|
+
? await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, decision = ${ph(dialect, 2)}, settled_by = ${ph(dialect, 3)}, decided_at_ms = ${ph(dialect, 4)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 5)} WHERE ${pendingClaimWhere(dialect, 6)}`, [to, POLICY_REFUSAL_DECISION.decision, POLICY_REFUSAL_DECISION.settledBy, now, now, askId, batchId])
|
|
522
|
+
: await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [
|
|
523
|
+
to,
|
|
524
|
+
now,
|
|
525
|
+
askId,
|
|
526
|
+
batchId,
|
|
527
|
+
]);
|
|
504
528
|
if (askRes.affected !== 1) {
|
|
505
529
|
const sel = await conn.query(`SELECT ${askCols(dialect)} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ${ph(dialect, 1)} AND batch_id = ${ph(dialect, 2)} FOR UPDATE`, [askId, batchId]);
|
|
506
530
|
await conn.rollback();
|
|
507
531
|
return { claimed: false, current: projectAskTerminalClaimTruth(sel.rows[0] ? mapAskRow(sel.rows[0]) : null) };
|
|
508
532
|
}
|
|
509
|
-
if (intent
|
|
533
|
+
if (intent !== "expire") {
|
|
510
534
|
await conn.commit();
|
|
511
535
|
return { claimed: true, voidedSiblings: [] };
|
|
512
536
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
3
|
import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
4
|
-
import { type RuleOfferProjection } from "../approval-card.js";
|
|
4
|
+
import { type ParkRowFacts, type RuleOfferProjection } from "../approval-card.js";
|
|
5
5
|
import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
|
|
6
6
|
export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
|
|
7
7
|
/**
|
|
@@ -30,8 +30,12 @@ export declare const TERMINAL_GRACE_MS = 3600000;
|
|
|
30
30
|
* resume-read-path log we emit a stable, non-reversible fingerprint instead, enough to correlate get↔resolve.
|
|
31
31
|
*/
|
|
32
32
|
export declare function tokenFingerprint(token: string): string;
|
|
33
|
-
/** Non-secret view of a pending checkpoint for the operator approval queue — deliberately NO token.
|
|
34
|
-
|
|
33
|
+
/** Non-secret view of a pending checkpoint for the operator approval queue — deliberately NO token.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 S-700:行**顶层**另带 {@link ParkRowFacts}(`requiresRealApproval` / `ruleOffersAbsence`,真才带、缺席 ≠ false)——
|
|
36
|
+
* 型与值都只有一个来源:`approval-card.ts` 的 {@link parkRowFacts}(两只店各读自己手里的那只寄存行 `pendingAction`,
|
|
37
|
+
* 交给它、原样展开)。继承而不是在这里逐键再写一遍:core 给这一臂再加一位同类事实时,改的只有那张 Pick 表。 */
|
|
38
|
+
export interface PendingCheckpoint extends ParkRowFacts {
|
|
35
39
|
sessionId: string;
|
|
36
40
|
scope: string;
|
|
37
41
|
toolName: string | null;
|
|
@@ -109,6 +113,8 @@ export interface PendingCheckpoint {
|
|
|
109
113
|
* (`PendingAction.hasBidiControls`,core [ref]),`put()` 在铸行
|
|
110
114
|
* 这一刻反范式成 `checkpoint.has_bidi_controls` 列(同行 `tool_input` / `bound_input_hash` / `gate_kind` /
|
|
111
115
|
* `risk_descriptor` / `rule_suggestions` 五列同一条「铸行时一格、读口零 blob」先例),server 不重算。
|
|
116
|
+
* (S-700 起本读口**确实**逐行读 blob —— 只为 {@link ParkRowFacts} 那几位,且是按「本仓不发 ALTER TABLE、
|
|
117
|
+
* 加列 = 删库丢在飞 park」裁出来的代价,见 `listPending` 那一段;本位与五列的「铸行时一格」形不变。)
|
|
112
118
|
*
|
|
113
119
|
* 🔴 `true` **或缺席,恒不写 `false`/`null`**(core d.ts 的 OMIT 契约逐字点名 BFF:「never null, never
|
|
114
120
|
* present-but-undefined, never false」):缺席 = **没检出**(干净 / core 的有界扫描没够着 / 本列诞生前
|