@sema-agent/server 7.5.0 → 7.7.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.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +100 -2
  5. package/dist/approval-reconciler.d.ts +1 -1
  6. package/dist/approval-reconciler.js +6 -5
  7. package/dist/boot/budget-tracing.js +1 -1
  8. package/dist/boot/config-center.js +17 -3
  9. package/dist/boot/execution-env.js +1 -1
  10. package/dist/boot/parked-revive-gate.d.ts +78 -0
  11. package/dist/boot/parked-revive-gate.js +114 -0
  12. package/dist/boot/resolve-spec.d.ts +8 -20
  13. package/dist/boot/resolve-spec.js +40 -125
  14. package/dist/budget.d.ts +1 -1
  15. package/dist/budget.js +1 -1
  16. package/dist/capabilities/hands-lane.d.ts +91 -0
  17. package/dist/capabilities/hands-lane.js +104 -0
  18. package/dist/capabilities/repo-tools.d.ts +1 -1
  19. package/dist/capabilities/repo-tools.js +8 -2
  20. package/dist/capabilities/scenarios.d.ts +49 -2
  21. package/dist/capabilities/scenarios.js +84 -16
  22. package/dist/config-provider.d.ts +1 -0
  23. package/dist/config-provider.js +23 -3
  24. package/dist/config-types.d.ts +5 -3
  25. package/dist/config.js +5 -4
  26. package/dist/deployment-governance.d.ts +168 -0
  27. package/dist/deployment-governance.js +206 -0
  28. package/dist/fleet/fleet-bus.d.ts +11 -1
  29. package/dist/fleet/fleet-bus.js +43 -0
  30. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  31. package/dist/fleet/fleet-terminal-window.js +319 -0
  32. package/dist/hooks/hook-runner.js +11 -0
  33. package/dist/http/route-ctx.d.ts +7 -1
  34. package/dist/http/routes/approvals-assistant.js +6 -7
  35. package/dist/http/routes/fleet.js +164 -14
  36. package/dist/http/routes/runs.js +7 -2
  37. package/dist/http/routes/tasks.js +5 -4
  38. package/dist/http/routes/trace-usage.js +3 -4
  39. package/dist/http/send.d.ts +23 -0
  40. package/dist/http/send.js +23 -0
  41. package/dist/http/server.d.ts +10 -0
  42. package/dist/http/server.js +28 -12
  43. package/dist/http/sse-log.js +3 -4
  44. package/dist/leader/diffout.d.ts +10 -0
  45. package/dist/leader/diffout.js +14 -2
  46. package/dist/leader/diffup.js +3 -2
  47. package/dist/leader/planner.js +7 -0
  48. package/dist/main.js +79 -32
  49. package/dist/observability/fail-open.d.ts +4 -0
  50. package/dist/observability/fail-open.js +4 -0
  51. package/dist/observability/tool-trace.d.ts +13 -0
  52. package/dist/observability/tool-trace.js +14 -0
  53. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  54. package/dist/orchestration/workflow-notify-journal.js +14 -34
  55. package/dist/plugins/approval-ask-store-sql.d.ts +33 -0
  56. package/dist/plugins/approval-ask-store-sql.js +66 -33
  57. package/dist/plugins/background-agent-store-sql.js +16 -16
  58. package/dist/plugins/breaker-state-sql.js +2 -2
  59. package/dist/plugins/checkpoint-store-sql.d.ts +5 -2
  60. package/dist/plugins/checkpoint-store-sql.js +5 -2
  61. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  62. package/dist/plugins/image-bake-store-sql.js +27 -27
  63. package/dist/plugins/image-index-sql.js +15 -15
  64. package/dist/plugins/mailbox-store-sql.js +3 -3
  65. package/dist/plugins/memory-engine-pg.js +9 -9
  66. package/dist/plugins/memory-engine-tidb.js +7 -7
  67. package/dist/plugins/memory-sync-store-pg.js +13 -13
  68. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  69. package/dist/plugins/outcome-ledger-sql.js +7 -7
  70. package/dist/plugins/pg-cost-quota.js +3 -3
  71. package/dist/plugins/pg-pool.js +84 -75
  72. package/dist/plugins/pg-rate-limiter.js +3 -3
  73. package/dist/plugins/pg-session-storage.d.ts +1 -1
  74. package/dist/plugins/pg-session-storage.js +12 -13
  75. package/dist/plugins/remote-env-host.js +3 -1
  76. package/dist/plugins/remote-env-local-docker.js +6 -3
  77. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  78. package/dist/plugins/roster-store-sql.js +8 -8
  79. package/dist/plugins/store-contracts.d.ts +19 -0
  80. package/dist/plugins/store-contracts.js +42 -0
  81. package/dist/plugins/task-attachment-store.js +5 -5
  82. package/dist/plugins/task-list-store-sql.js +1 -1
  83. package/dist/plugins/tidb-cost-quota.js +1 -1
  84. package/dist/plugins/tidb-pool.js +83 -60
  85. package/dist/plugins/tidb-rate-limiter.js +1 -1
  86. package/dist/plugins/tidb-session-store.js +2 -5
  87. package/dist/plugins/tool-result-store-sql.js +2 -2
  88. package/dist/plugins/usage-window-store-sql.js +13 -13
  89. package/dist/plugins/write-behind-counter.d.ts +10 -2
  90. package/dist/plugins/write-behind-counter.js +13 -3
  91. package/dist/resource-suspend.d.ts +3 -1
  92. package/dist/resource-suspend.js +3 -1
  93. package/dist/run-local.d.ts +73 -1
  94. package/dist/run-local.js +160 -9
  95. package/dist/runs.d.ts +3 -1
  96. package/dist/runs.js +3 -1
  97. package/dist/security.d.ts +12 -0
  98. package/dist/security.js +12 -0
  99. package/dist/session-sync-kernel.d.ts +13 -0
  100. package/dist/session-sync-kernel.js +13 -0
  101. package/dist/trace/core-keyset-guard.d.ts +1 -1
  102. package/dist/trace/project.d.ts +10 -1
  103. package/dist/trace/project.js +31 -0
  104. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { type Brain } from "@sema-agent/core";
2
+ import { type Brain, type OnAsk, type TaskSpec } from "@sema-agent/core";
3
+ import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
3
4
  import { type ServiceConfig } from "./config.js";
4
5
  import { type Logger } from "./observability/logger.js";
5
6
  /** Parsed CLI invocation. */
@@ -33,6 +34,12 @@ export interface RunLocalDeps {
33
34
  print?: (line: string) => void;
34
35
  /** stderr sink for the "you still need to set …" guidance (defaults to console.error). */
35
36
  printErr?: (line: string) => void;
37
+ /** design/181 件三: override the TTY detection behind the {@link createLocalApprover} seat
38
+ * (defaults to `process.stdin.isTTY === true`) — lets a test drive BOTH arms deterministically. */
39
+ isTty?: boolean;
40
+ /** design/181 件三: the TTY arm's prompt (defaults to a readline `y/N` on stdin/stderr);
41
+ * `signal` = core `OnAsk` 的取消(EOF/abort 决断臂见 {@link promptYesNoOnTty})。 */
42
+ askYesNo?: (prompt: string, signal?: AbortSignal) => Promise<boolean>;
36
43
  }
37
44
  /** Parse argv (everything AFTER `node run-local.js`). Throws a clear Error on a missing objective. */
38
45
  export declare function parseArgs(argv: string[]): RunLocalArgs;
@@ -49,6 +56,71 @@ export declare class UsageError extends Error {
49
56
  * this check in that case — the check is about a REAL run, not the assembly.
50
57
  */
51
58
  export declare function missingGatewayRequirements(config: ServiceConfig): string[];
59
+ /** 本腿读的部署配置切面(`ServiceConfig` 结构满足;两个构造口各自的切面之和)。 */
60
+ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView {
61
+ }
62
+ /**
63
+ * design/181 件三:run-local 腿的**部署治理链装配**(#180)。
64
+ *
65
+ * 与另外两条腿(HTTP 的 `resolveSpec`、durable park 的赎回腿)装的是**同一条**链,经**同一个**构造口:
66
+ * 审批基线作 base 的 `toolPolicy` 座,再经**折叠属主** `applyRuntimeGovernance`(→ core `tightenTaskSpec`)
67
+ * 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
68
+ * 自己合成。
69
+ *
70
+ * · **审批基线 = adjudicated allow-all**:一次性 CLI 没有 durable 审批设施(没有 `/decide` 腿可赎回),
71
+ * 所以 `createApprovalBaselinePolicy` 走 durable 缺席那一臂。它是一条**在场的** effect-aware 策略
72
+ * (满足 core 的 `hasEffectAwareGate`),运维照旧用 `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
73
+ * · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
74
+ * (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
75
+ * 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
76
+ * ✅ **旧残余面已收口(core 5.19.0 #108,2026-08-08 校正)**:`rootPath` 是装配期的**静态**值,而 core
77
+ * 的写工具解析相对形用的是被 Bash `cd` 就地改写的活 cwd —— 于是 `cd .git/hooks` 之后
78
+ * `Write("pre-commit")` 落进守卫段却判不出来(两条 host 腿同形,本腿与 resolve-spec)。5.19.0 起守卫
79
+ * 按 `ToolCallRequest.cwd`(引擎每次调用从同一只 tracked cwd 盖戳)解析写目标,本腿零改动即得保护;
80
+ * **不对称是 core 故意的**:目标跟活 cwd 走,守卫自己配置的目录仍锚静态根,`cd` 搬不动栅栏。全文与
81
+ * 证据见 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 顶注,端到端正控钉见
82
+ * test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
83
+ * · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
84
+ */
85
+ export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string): TaskSpec;
86
+ /** {@link createLocalApprover} 的 io 面(全部可注入,所以两条臂都测得动)。 */
87
+ export interface LocalApproverIo {
88
+ /** 这次调用是不是接在一个人面前(生产里 = `process.stdin.isTTY`)。 */
89
+ readonly isTty: boolean;
90
+ /** TTY 臂的问人器(默认 {@link promptYesNoOnTty} 的 readline y/N;`signal`=core 取消透传)。 */
91
+ readonly askYesNo: (prompt: string, signal?: AbortSignal) => Promise<boolean>;
92
+ /** 非 TTY 臂的告知面(stderr)。 */
93
+ readonly printErr: (line: string) => void;
94
+ }
95
+ /**
96
+ * design/181 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
97
+ *
98
+ * 🔴 为什么这个座位是接治理链的**同窗必做项**:run-local 有 checkpointStore(file backend),所以
99
+ * `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进 `irreversibleTools` ⇒ park lane effective ⇒ **每一次
100
+ * shell 调用都 durable park 成一张永远没人 resume 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里
101
+ * 没有任何赎回腿,那不是「等人来批」,是卡死。core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk`
102
+ * 早退(prepare-task dist 亲读),ask 于是落回本回调 —— 把 park 换成「问人 / 当场拒」的唯一正当接法。
103
+ *
104
+ * ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
105
+ * 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
106
+ *
107
+ * 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
108
+ * `suspendAsk`**(dist `core/hooks.js:296` 的 `approverUnavailable` 重投),上面那颗雷会原样回来。
109
+ */
110
+ export declare function createLocalApprover(io: LocalApproverIo): OnAsk;
111
+ /** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。
112
+ * streams 可注入=EOF/abort 两臂测得动(默认 process.stdin/stderr)。
113
+ *
114
+ * 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
115
+ * **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
116
+ * 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
117
+ * · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
118
+ * · readline `close`(输入流 end/close 时 readline 自关);
119
+ * · `question` 自身 reject(abort throw / 流错误)。 */
120
+ export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
121
+ input: NodeJS.ReadableStream;
122
+ output: NodeJS.WritableStream;
123
+ }): Promise<boolean>;
52
124
  /**
53
125
  * Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
54
126
  * code (0 = completed, 1 = the task did not complete / a fatal error, 2 = bad usage / unmet env). Pure of
package/dist/run-local.js CHANGED
@@ -13,7 +13,18 @@
13
13
  * applyEffective (config.d/.env drives models/roles/mcp), and the `host` exec lane (run on THIS machine,
14
14
  * no container) — identical building blocks to the server path so behavior matches (no drift). It reuses
15
15
  * main.ts's helpers verbatim; the only thing it skips is the server-only surface (createHttpServer, listen,
16
- * auth/rateLimit/quota/run-store/leader/durable-checkpoint/reapers).
16
+ * auth/rateLimit/quota/run-store/leader/durable-approval-decide/reapers).
17
+ *
18
+ * 🔴 **DEPLOYMENT GOVERNANCE IS NOT IN THAT SKIP LIST** (design/181 件三 / #180). Until 7.4.x this file
19
+ * assembled its `TaskSpec` with the governance chain entirely ABSENT — `loadConfig` + `applyEffective` had
20
+ * already parsed `autonomy` / `commandPolicy` / `sensitiveWritePatterns` into the config, and the spec
21
+ * literal simply never read them (compute-then-discard), so the same operator knob was live on the HTTP leg
22
+ * and structurally non-existent here. It now goes through the SAME single construction port as the other two
23
+ * legs ({@link applyLocalGovernance} → `src/deployment-governance.ts` → `applyRuntimeGovernance` → core's
24
+ * `tightenTaskSpec`). What this leg genuinely does NOT have is the **durable approval** face (a one-shot CLI
25
+ * has no `/v1/approvals/:id/decide` leg to resume from), so the approval baseline is the adjudicated
26
+ * allow-all arm and a gated `ask` is answered by {@link createLocalApprover} — the TTY human, or a
27
+ * fail-closed deny — instead of parking a checkpoint nobody will ever redeem.
17
28
  *
18
29
  * Usage: run-local "<objective>" [--root <dir>] [--json] [--scenario <name>]
19
30
  *
@@ -24,10 +35,14 @@
24
35
  import { homedir } from "node:os";
25
36
  import { join } from "node:path";
26
37
  import { realpathSync } from "node:fs";
38
+ import { createInterface } from "node:readline/promises";
27
39
  import { fileURLToPath } from "node:url";
28
40
  import { loadRemoteExec } from "@sema-agent/registry-core/node";
29
- import { Runner, uuidv7, parseModelMention, FileStorageBackend, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore } from "@sema-agent/core";
41
+ import { Runner, uuidv7, parseModelMention, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore } from "@sema-agent/core";
42
+ import { hasOperatorGateIntent } from "./approval.js";
30
43
  import { createBrain } from "./brain.js";
44
+ import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
45
+ import { applyRuntimeGovernance } from "./runtime-governance.js";
31
46
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
32
47
  import { taskWallClockSec } from "./task-workflow.js";
33
48
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
@@ -41,6 +56,7 @@ import { loadSkills } from "./capabilities/skills.js";
41
56
  import { GiteaClient } from "./capabilities/repo-tools.js";
42
57
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
43
58
  import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
59
+ import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
44
60
  import { HttpError } from "./security.js";
45
61
  import { memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js";
46
62
  import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
@@ -147,6 +163,89 @@ export function missingGatewayRequirements(config) {
147
163
  }
148
164
  return missing;
149
165
  }
166
+ /**
167
+ * design/181 件三:run-local 腿的**部署治理链装配**(#180)。
168
+ *
169
+ * 与另外两条腿(HTTP 的 `resolveSpec`、durable park 的赎回腿)装的是**同一条**链,经**同一个**构造口:
170
+ * 审批基线作 base 的 `toolPolicy` 座,再经**折叠属主** `applyRuntimeGovernance`(→ core `tightenTaskSpec`)
171
+ * 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
172
+ * 自己合成。
173
+ *
174
+ * · **审批基线 = adjudicated allow-all**:一次性 CLI 没有 durable 审批设施(没有 `/decide` 腿可赎回),
175
+ * 所以 `createApprovalBaselinePolicy` 走 durable 缺席那一臂。它是一条**在场的** effect-aware 策略
176
+ * (满足 core 的 `hasEffectAwareGate`),运维照旧用 `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
177
+ * · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
178
+ * (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
179
+ * 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
180
+ * ✅ **旧残余面已收口(core 5.19.0 #108,2026-08-08 校正)**:`rootPath` 是装配期的**静态**值,而 core
181
+ * 的写工具解析相对形用的是被 Bash `cd` 就地改写的活 cwd —— 于是 `cd .git/hooks` 之后
182
+ * `Write("pre-commit")` 落进守卫段却判不出来(两条 host 腿同形,本腿与 resolve-spec)。5.19.0 起守卫
183
+ * 按 `ToolCallRequest.cwd`(引擎每次调用从同一只 tracked cwd 盖戳)解析写目标,本腿零改动即得保护;
184
+ * **不对称是 core 故意的**:目标跟活 cwd 走,守卫自己配置的目录仍锚静态根,`cd` 搬不动栅栏。全文与
185
+ * 证据见 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 顶注,端到端正控钉见
186
+ * test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
187
+ * · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
188
+ */
189
+ export function applyLocalGovernance(base, config, workspaceDir) {
190
+ return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }));
191
+ }
192
+ /**
193
+ * design/181 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
194
+ *
195
+ * 🔴 为什么这个座位是接治理链的**同窗必做项**:run-local 有 checkpointStore(file backend),所以
196
+ * `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进 `irreversibleTools` ⇒ park lane effective ⇒ **每一次
197
+ * shell 调用都 durable park 成一张永远没人 resume 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里
198
+ * 没有任何赎回腿,那不是「等人来批」,是卡死。core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk`
199
+ * 早退(prepare-task dist 亲读),ask 于是落回本回调 —— 把 park 换成「问人 / 当场拒」的唯一正当接法。
200
+ *
201
+ * ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
202
+ * 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
203
+ *
204
+ * 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
205
+ * `suspendAsk`**(dist `core/hooks.js:296` 的 `approverUnavailable` 重投),上面那颗雷会原样回来。
206
+ */
207
+ export function createLocalApprover(io) {
208
+ return async (req, signal) => {
209
+ if (!io.isTty) {
210
+ io.printErr(`denied "${req.toolName}" (${req.message}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
211
+ `The gate comes from this deployment's governance knobs: AUTONOMY, MANUAL_MODE_SHELL_GATE, ` +
212
+ `runtime.commandPolicy (config.d/governance.json). Run it in a terminal to answer interactively, or relax the knob.`);
213
+ return false;
214
+ }
215
+ return io.askYesNo(`approve "${req.toolName}"? ${req.message} [y/N] `, signal);
216
+ };
217
+ }
218
+ /** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。
219
+ * streams 可注入=EOF/abort 两臂测得动(默认 process.stdin/stderr)。
220
+ *
221
+ * 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
222
+ * **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
223
+ * 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
224
+ * · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
225
+ * · readline `close`(输入流 end/close 时 readline 自关);
226
+ * · `question` 自身 reject(abort throw / 流错误)。 */
227
+ export async function promptYesNoOnTty(prompt, signal, streams = { input: process.stdin, output: process.stderr }) {
228
+ if (signal?.aborted)
229
+ return false;
230
+ const rl = createInterface({ input: streams.input, output: streams.output });
231
+ try {
232
+ return await new Promise((resolve) => {
233
+ const settle = (v) => {
234
+ rl.off("close", onClosed);
235
+ signal?.removeEventListener("abort", onAborted);
236
+ resolve(v);
237
+ };
238
+ const onClosed = () => settle(false);
239
+ const onAborted = () => settle(false);
240
+ rl.on("close", onClosed);
241
+ signal?.addEventListener("abort", onAborted, { once: true });
242
+ rl.question(prompt).then((answer) => settle(/^y(es)?$/i.test(answer.trim())), () => settle(false));
243
+ });
244
+ }
245
+ finally {
246
+ rl.close();
247
+ }
248
+ }
150
249
  /**
151
250
  * Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
152
251
  * code (0 = completed, 1 = the task did not complete / a fatal error, 2 = bad usage / unmet env). Pure of
@@ -248,6 +347,28 @@ export async function runLocal(argv, deps = {}) {
248
347
  printErr(`config.d/remote-exec.json is invalid: ${String(err)}`);
249
348
  return 2;
250
349
  }
350
+ // ── design/181 件三 boot 期两件(与 HTTP 腿同一只判据)────────────────────────────────────────
351
+ // ① 守卫集的**可编译性**门:编译发生在每一次装配上,一条「一个路径段都没有」的模式(`"/"`)会 THROW,
352
+ // 运维从那条错误里看不出是自己的 env 写错了。共享 .env 的坏值必须炸在启动上——这条腿按 run-local 的
353
+ // 既定 UX 落地(doctor 文案 + 退 2),而不是把栈抛给用户。
354
+ try {
355
+ assertGuardPatternsUsable(config);
356
+ }
357
+ catch (e) {
358
+ printErr(e instanceof Error ? e.message : String(e));
359
+ printErr(`(local config root: ${root} — fix it in ${join(root, ".env")} or your shell env; \`SENSITIVE_WRITE_PATTERNS=off\` disables the guard set)`);
360
+ return 2;
361
+ }
362
+ // ② UNGATED 补偿:接上审批基线后 core 的 `hasEffectAwareGate` 恒真 ⇒ 它那条 "write-capable hand tools
363
+ // are present but UNGATED" 的信号在这条腿上结构性消失。判据与文案是**同一只**构造口的
364
+ // (buildOnlySensitiveBaselineWarning),两个座位量按本腿的真形取:durable 门恒不在场(一次性 CLI
365
+ // 没有 /decide 赎回腿),单用户 auto-accept 基线的适用性判据与 main.ts 逐字同源。
366
+ const ungatedCompensation = buildOnlySensitiveBaselineWarning(config, {
367
+ durableEnabled: false,
368
+ singleUserAutoAcceptBaseline: config.requirePrincipal !== true && !hasOperatorGateIntent(config),
369
+ });
370
+ if (ungatedCompensation)
371
+ logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
251
372
  // ── Pre-flight: with a REAL brain, validate the gateway + key and print the doctor-style guidance instead
252
373
  // of crashing on the first brain call. With an injected mock brain, no real call happens → skip. ──
253
374
  if (!deps.brain) {
@@ -361,7 +482,21 @@ export async function runLocal(argv, deps = {}) {
361
482
  catch (e) {
362
483
  logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "catalog candidate absent — built-ins serve" });
363
484
  }
364
- const runner = new Runner({
485
+ // design/181 件三:**onAsk 座**(全文理由见 {@link createLocalApprover})。两只 Runner 同装 —— 委派出去
486
+ // 的子任务也在同一台机器上动手,少装一只就是「主任务问人、子任务 park 卡死」。
487
+ const onAsk = createLocalApprover({
488
+ isTty: deps.isTty ?? process.stdin.isTTY === true,
489
+ askYesNo: deps.askYesNo ?? promptYesNoOnTty,
490
+ printErr,
491
+ });
492
+ /** core 的 `RunnerDeps.onError` 座(两只 Runner 共用一只闭包 —— 两处各写一份正是漂移的成因)。 */
493
+ const engineOnError = (err, ctx) => logger.warn("engine_error", {
494
+ phase: ctx.phase,
495
+ sessionId: ctx.sessionId,
496
+ ...(ctx.classification !== undefined ? { classification: ctx.classification } : {}),
497
+ err: String(err),
498
+ });
499
+ const runnerDeps = {
365
500
  brain,
366
501
  models: config.models,
367
502
  roles: config.roles,
@@ -372,6 +507,11 @@ export async function runLocal(argv, deps = {}) {
372
507
  sessionStore,
373
508
  ...(memoryEngine ? { memoryBackend: memoryEngine.backend, memoryEngineDir: memoryEngine.root } : {}),
374
509
  checkpointStore: fileBackend.checkpointStore,
510
+ // design/181 件三:core 的降级/配置类告警此前被 `deps.onError?.()` 的可选链整条吞掉(这条腿从不接这
511
+ // 个座),包括那条 "write-capable hand tools are present but UNGATED"。接上 stderr(logger.warn 走
512
+ // stderr)后其余告警——compaction / prompt-cache / mcp / memory / hook 的降级——也不再无声。
513
+ onError: engineOnError,
514
+ onAsk,
375
515
  // core 1.219 (dogfood: "ReadToolResult(ref) 恒空"): durable tool-result refs on the TOC CLI
376
516
  // lane too — offloaded full text survives a process restart (one file per ref under the data root).
377
517
  toolResultStore: fileBackend.toolResultStore,
@@ -382,14 +522,21 @@ export async function runLocal(argv, deps = {}) {
382
522
  ...(config.projectMemoryEnabled
383
523
  ? { loadProjectMemory: makeLoadProjectMemory({ logger }), probeInstructionSources: makeProbeInstructionSources() }
384
524
  : {}),
385
- });
525
+ };
526
+ const runner = new Runner(runnerDeps);
386
527
  // Sub-task runner (council lenses/team) — mirrors main.ts's subRunner, INCLUDING executionEnvFactory so a delegated
387
528
  // child gets hands (Bash/Read/Edit) on the host lane. Without it the child's handsEnabled is false — the exact
388
529
  // "subagent couldn't run pwd" defect fixed at main.ts:799; this is its run-local twin, caught by the systematic audit.
389
530
  // [1367]① fork server half(main.ts subRunner 同款):ForkRoutingSessionStore——fork/resume 形
390
531
  // (requireExisting)→ host 文件店优先(core 1.350 hostSessionFork 在宿主店 fork,拆店=响亮
391
532
  // resume.session_not_found),普通子任务 → 私有 TTL 店(throwaway 姿势保留)。
392
- const subRunner = new Runner({ brain, models: config.models, roles: config.roles, pricing, promptSource, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), ...(executionEnvFactory ? { executionEnvFactory } : {}), ...(lspManager ? { lspManager } : {}) });
533
+ const subRunnerDeps = { brain, models: config.models, roles: config.roles, pricing, promptSource, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), onError: engineOnError, onAsk, ...(executionEnvFactory ? { executionEnvFactory } : {}), ...(lspManager ? { lspManager } : {}) };
534
+ const subRunner = new Runner(subRunnerDeps);
535
+ // #196:无手孪生一对 —— **mirrors main.ts**(那边是 `handslessRunner` / `handslessSubRunner` 两只,同 deps
536
+ // 摘掉 executionEnvFactory)。run-local 是同形装配点,漏这一对 = 本地 lane 上 scan/council/team 的最终
537
+ // roster 仍被并集进全量可写手(server 修了、CLI 静默留病 —— 这条腿的 subRunner 当年就是这么漏配的)。
538
+ const handslessRunner = new Runner(withoutExecutionEnv(runnerDeps));
539
+ const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
393
540
  // Capability layer (loadSkills + buildScenarios + selectScenario) — assembled exactly like main.ts.
394
541
  const skills = loadSkills(config.skillsDir);
395
542
  const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
@@ -398,7 +545,7 @@ export async function runLocal(argv, deps = {}) {
398
545
  const webSearchCfg = webSearchConfigFromEnv();
399
546
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
400
547
  const scenarioDeps = {
401
- runner, subRunner, model: "default", skills, repoClient, metrics, logger,
548
+ runner, subRunner, handslessSubRunner, model: "default", skills, repoClient, metrics, logger,
402
549
  ...(webSearch ? { webSearch } : {}),
403
550
  brandIdentity: true, // run-local is always a local/TOC deployment → brand the default scenario as Sema.
404
551
  };
@@ -451,7 +598,9 @@ export async function runLocal(argv, deps = {}) {
451
598
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
452
599
  const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "team");
453
600
  const mcp = mcpForScenario(config.mcpServers, scenarioName);
454
- const spec = {
601
+ // design/181 件三:自建的 spec 字面量经**同一条**部署治理链(见 {@link applyLocalGovernance})——
602
+ // 审批基线 + autonomy/commandPolicy/MANUAL_MODE_SHELL_GATE/守卫集,tighten-only,折叠属主仍是 core。
603
+ const spec = applyLocalGovernance({
455
604
  objective: mention.cleanedText || args.objective,
456
605
  sessionId,
457
606
  // design/129: run-local IS the pure TOC lane → session-scoped background children
@@ -471,13 +620,15 @@ export async function runLocal(argv, deps = {}) {
471
620
  ...(mcp ? { mcp } : {}),
472
621
  // core 5.8.0:timeoutSec 键退役 → maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,此处换算一次。
473
622
  ...(timeoutSec !== undefined ? { limits: { maxWalltimeMs: timeoutSec * 1000 } } : {}),
474
- };
623
+ }, config, workspaceDir);
475
624
  logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
476
625
  // ── Run ONE task to completion (the sync /v1/tasks path: plain runTask, no verify/cascade). ──
477
626
  let result;
478
627
  let exitCode = 0;
628
+ // #196:与 HTTP 腿同一条选路 —— 场景声明 hands:"none" 时任务跑无手 Runner(band 不 mount)。
629
+ const taskRunner = pickHandsRunner(cap.hands, { full: runner, handsless: handslessRunner });
479
630
  try {
480
- result = await runner.runTask(spec);
631
+ result = await taskRunner.runTask(spec);
481
632
  }
482
633
  catch (err) {
483
634
  printErr(`task crashed: ${err instanceof Error ? err.message : String(err)}`);
package/dist/runs.d.ts CHANGED
@@ -184,7 +184,9 @@ export declare class TurnAnchorCapture {
184
184
  * Strip the durable-resume capability token from a result before it is persisted to the replayable event
185
185
  * log / run row or returned to the submitter. The token IS the resume credential (token-as-auth, search
186
186
  * [18] Q1b) and must never leave the service. Safe on any result (no-op when absent) — notably core maps a
187
- * NESTED suspend under verify/cascade to status:"failed" + errorCode "suspended.awaiting_approval"(core 5.9.0 W1 改名)while still
187
+ * NESTED suspend under verify/cascade to status:"failed" + errorCode "unexpected.suspended"(inner status
188
+ * "needs_review" ⇒ "unexpected.needs_review";core `agents/suspend-guard.js` `mapNestedSuspend`,由 verify.js
189
+ * 191/201/208 与 cascade.js:125 调用)while still
188
190
  * spreading `checkpointToken`, so that "failed" path (unlike the stream path's explicit suspend handling)
189
191
  * would otherwise leak it into task_event / task_run.result / the sync 200 response. `checkpointGate` is
190
192
  * non-secret and intentionally kept.
package/dist/runs.js CHANGED
@@ -262,7 +262,9 @@ export class TurnAnchorCapture {
262
262
  * Strip the durable-resume capability token from a result before it is persisted to the replayable event
263
263
  * log / run row or returned to the submitter. The token IS the resume credential (token-as-auth, search
264
264
  * [18] Q1b) and must never leave the service. Safe on any result (no-op when absent) — notably core maps a
265
- * NESTED suspend under verify/cascade to status:"failed" + errorCode "suspended.awaiting_approval"(core 5.9.0 W1 改名)while still
265
+ * NESTED suspend under verify/cascade to status:"failed" + errorCode "unexpected.suspended"(inner status
266
+ * "needs_review" ⇒ "unexpected.needs_review";core `agents/suspend-guard.js` `mapNestedSuspend`,由 verify.js
267
+ * 191/201/208 与 cascade.js:125 调用)while still
266
268
  * spreading `checkpointToken`, so that "failed" path (unlike the stream path's explicit suspend handling)
267
269
  * would otherwise leak it into task_event / task_run.result / the sync 200 response. `checkpointGate` is
268
270
  * non-secret and intentionally kept.
@@ -208,6 +208,18 @@ export declare function principalFrom(req: IncomingMessage, config: ServiceConfi
208
208
  /** Header carrying the crypto-verified principal JWT on a direct-door worker (the issuer signs it; the worker only
209
209
  * verifies). The trusted twin of `config.principalHeader` (the latter is client-spoofable on a direct door). */
210
210
  export declare const PRINCIPAL_TOKEN_HEADER = "x-approval-principal-token";
211
+ /**
212
+ * direct-door 决策证明三件套的另外两个头名(JWT 在上面那个常量里)。
213
+ *
214
+ * 为什么也具名(2026-08-08,#176 二轮扫描 finding):这三个头名此前在
215
+ * `http/routes/approvals-assistant.ts` 的两个 `verifyDirectDoorProof` 调用点、以及 `http/server.ts` 的
216
+ * CORS `access-control-allow-headers` 串里**各手抄一份**——而 CORS 那一行同一句里已经在用
217
+ * `${principalHeader}` 插值另一个头名,证明具名写法在该处本就可用。改任一头名时漏改 CORS 串的后果
218
+ * 是**静默的**:浏览器预检把该头挡下,请求到不了 handler ⇒ 表现成「direct-door 上审批面莫名失效」,
219
+ * 而不是任何一处报错。低危(改头名本身即 wire BREAKING、不太可能单边发生),但零成本可消。
220
+ */
221
+ export declare const APPROVAL_MAC_HEADER = "x-approval-mac";
222
+ export declare const APPROVAL_MAC_KID_HEADER = "x-approval-mac-kid";
211
223
  export declare function setSsoPrincipal(req: IncomingMessage, principal: string): void;
212
224
  export declare function ssoVerifiedPrincipal(req: IncomingMessage): string | undefined;
213
225
  export declare function setSsoScope(req: IncomingMessage, scope: string): void;
package/dist/security.js CHANGED
@@ -279,6 +279,18 @@ export function principalFrom(req, config) {
279
279
  /** Header carrying the crypto-verified principal JWT on a direct-door worker (the issuer signs it; the worker only
280
280
  * verifies). The trusted twin of `config.principalHeader` (the latter is client-spoofable on a direct door). */
281
281
  export const PRINCIPAL_TOKEN_HEADER = "x-approval-principal-token";
282
+ /**
283
+ * direct-door 决策证明三件套的另外两个头名(JWT 在上面那个常量里)。
284
+ *
285
+ * 为什么也具名(2026-08-08,#176 二轮扫描 finding):这三个头名此前在
286
+ * `http/routes/approvals-assistant.ts` 的两个 `verifyDirectDoorProof` 调用点、以及 `http/server.ts` 的
287
+ * CORS `access-control-allow-headers` 串里**各手抄一份**——而 CORS 那一行同一句里已经在用
288
+ * `${principalHeader}` 插值另一个头名,证明具名写法在该处本就可用。改任一头名时漏改 CORS 串的后果
289
+ * 是**静默的**:浏览器预检把该头挡下,请求到不了 handler ⇒ 表现成「direct-door 上审批面莫名失效」,
290
+ * 而不是任何一处报错。低危(改头名本身即 wire BREAKING、不太可能单边发生),但零成本可消。
291
+ */
292
+ export const APPROVAL_MAC_HEADER = "x-approval-mac";
293
+ export const APPROVAL_MAC_KID_HEADER = "x-approval-mac-kid";
282
294
  /** The DIRECT-DOOR-SAFE principal — the VERIFIED identity for governance + cost + isolation. On a BFF/gated deployment
283
295
  * the header is already verified upstream (return it); on a direct-door worker the `principalHeader` is client-
284
296
  * spoofable, so identity comes from the crypto-verified `x-approval-principal-token` JWT (the issuer's private key
@@ -66,6 +66,19 @@ export declare function stagingIdFor(realSessionId: string, token: string): stri
66
66
  export declare const STAGING_ID_MARKER = "#stg-";
67
67
  /** Recover the real session id from a staging id (everything before the `#stg-` marker), or null if not a staging id. */
68
68
  export declare function realIdOfStaging(stagingId: string): string | null;
69
+ /**
70
+ * Staging-row reaper grace window — a staging session's rows are GC'able only if it has NO `session_meta` (never
71
+ * committed) AND its OLDEST row is older than this (an in-flight stream keeps appending fresh rows → safe).
72
+ * Mirrors the file-snapshot store's `BLOB_GC_GRACE_MS` discipline (an in-flight import must never be reaped
73
+ * mid-stream).
74
+ *
75
+ * 🔴 住在 kernel 里而不是任一个店里(2026-08-08,#176 二轮扫描 finding):TiDB 与 PG 两个孪生店原先**各
76
+ * 抄一份**同值常量,PG 那份的注释甚至自称「twin of the TiDB store's STAGING_GC_GRACE_MS」——明知孪生却
77
+ * 不共享。单边改值的后果是两个后端对「同一个中断的导入还能不能收」判定分家,而两边都按自己的常量静默
78
+ * 正确工作。这里是两个店**本就都 import** 的那个叶子模块(店契约纪律:适配器不许 import 自己的孪生,
79
+ * 共享常量必须落在中立处),同族先例见 `checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。
80
+ */
81
+ export declare const STAGING_GC_GRACE_MS = 3600000;
69
82
  /**
70
83
  * §7 — the relationship between a SOURCE bundle's entry log and the DESTINATION's CURRENT entry log, the gate that
71
84
  * decides whether an import is a safe sync or a data-losing overwrite. Five exhaustive cases:
@@ -10,6 +10,19 @@ export function realIdOfStaging(stagingId) {
10
10
  const i = stagingId.indexOf(STAGING_ID_MARKER);
11
11
  return i < 0 ? null : stagingId.slice(0, i);
12
12
  }
13
+ /**
14
+ * Staging-row reaper grace window — a staging session's rows are GC'able only if it has NO `session_meta` (never
15
+ * committed) AND its OLDEST row is older than this (an in-flight stream keeps appending fresh rows → safe).
16
+ * Mirrors the file-snapshot store's `BLOB_GC_GRACE_MS` discipline (an in-flight import must never be reaped
17
+ * mid-stream).
18
+ *
19
+ * 🔴 住在 kernel 里而不是任一个店里(2026-08-08,#176 二轮扫描 finding):TiDB 与 PG 两个孪生店原先**各
20
+ * 抄一份**同值常量,PG 那份的注释甚至自称「twin of the TiDB store's STAGING_GC_GRACE_MS」——明知孪生却
21
+ * 不共享。单边改值的后果是两个后端对「同一个中断的导入还能不能收」判定分家,而两边都按自己的常量静默
22
+ * 正确工作。这里是两个店**本就都 import** 的那个叶子模块(店契约纪律:适配器不许 import 自己的孪生,
23
+ * 共享常量必须落在中立处),同族先例见 `checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。
24
+ */
25
+ export const STAGING_GC_GRACE_MS = 3_600_000;
13
26
  /**
14
27
  * §7 — classify how importing `srcEntries` into a destination holding `dstEntries` would relate the two logs. PURE
15
28
  * (no I/O); the §7 fork detector. The comparison is over the ENTRY-ID SETS, not leaf_id:
@@ -27,7 +27,7 @@ type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, Bg
27
27
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
28
28
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
29
29
  type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
30
- type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash";
30
+ type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash" | "ruleSuggestions";
31
31
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
32
32
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest";
33
33
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
@@ -12,7 +12,7 @@
12
12
  * old session-join `toolOutput` hook is gone, there's a real source now); `eventId`/`parentToolCallId` (§E2) ride
13
13
  * each content event, additive / tolerate-absent.
14
14
  */
15
- import type { TaskResult } from "@sema-agent/core";
15
+ import type { ApprovalSettledBy, TaskResult } from "@sema-agent/core";
16
16
  import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
17
17
  import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
18
18
  type AppendFn = (type: string, data: unknown) => Promise<void>;
@@ -90,6 +90,7 @@ export type TraceBlock = {
90
90
  totalChars?: number;
91
91
  structured?: unknown;
92
92
  errorCode?: string;
93
+ settledBy?: ApprovalSettledBy;
93
94
  eventId?: string;
94
95
  parentToolCallId?: string;
95
96
  } | {
@@ -131,6 +132,7 @@ export declare function toolEndEventData(ev: {
131
132
  totalChars?: number;
132
133
  structured?: unknown;
133
134
  errorCode?: string;
135
+ settledBy?: ApprovalSettledBy;
134
136
  eventId?: string;
135
137
  parentToolCallId?: string;
136
138
  }): Record<string, unknown>;
@@ -284,6 +286,13 @@ export declare function workspaceChangedEventData(ev: {
284
286
  * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
285
287
  * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
286
288
  * 侧信道(它不参与 governance 那 4 个布尔的还原)。
289
+ * · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
290
+ * 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
291
+ * 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
292
+ * 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
293
+ * 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
294
+ * ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
295
+ * 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
287
296
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
288
297
  * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
289
298
  */
@@ -1,3 +1,6 @@
1
+ // 值导入(非 `import type`):`settledBy` 的闭集判据必须**复用 core 的谓词**——本仓抄一份三元词表
2
+ // 就是值拷贝漂移源(上游加员时两处静默不同步)。core 5.18.1 #187 导出的单一属主。
3
+ import { isApprovalSettledBy } from "@sema-agent/core";
1
4
  import { redactSecrets, redactDeep } from "./redact.js";
2
5
  /** E8 (shell-host): drain the per-task model-usage accumulator into an append-only `model_usage` DELTA event.
3
6
  * Shared by BOTH durable-write legs (runInBackground + the resume leg) so the persisted shape never drifts; the
@@ -163,6 +166,16 @@ export function toolEndEventData(ev) {
163
166
  // 带 default 臂,禁按两值穷举。verbatim 依据:码是机器短串标识符、非用户内容(不脱敏),边界靠
164
167
  // 显式上限而非词表 —— >128 字符=非法形,整键丢弃(不截断:截断会铸出 core 从未发过的码)。
165
168
  ...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
169
+ // core 5.18.1 #187([3110] 提货批):`settledBy` = 这一次调用的**结算来源**("human"/"timeout"/
170
+ // "aborted"),只挂在经审批结算的那个 tool_end 上。与上面的 errorCode **不同族**,两条差别都承重:
171
+ // ① 它是**闭集**,判据走 core 导出的 `isApprovalSettledBy`(单一属主)—— 不在本仓抄词表,也不拿
172
+ // 长度上限当边界(那是开集的兜底姿势,对闭集等于没门:出格词照样上 wire,下游按闭集写的
173
+ // switch 会撞 default 或更坏地把它当成一次真结算)。上游加员时判据自动跟随。
174
+ // ② **缺席不是语义**:core 契约逐字写「Absence = an older caller — consumers must not infer
175
+ // semantics from a missing key」;姿态臂(headless auto-deny / 整体放行)本就不填。所以缺席
176
+ // 一律不铸键 —— 既不折 "human" 也不折 null,把「不知道」如实表达成「没有这个键」。
177
+ // 值是引擎铸的枚举短串、非用户内容 ⇒ 不脱敏。
178
+ ...(isApprovalSettledBy(ev.settledBy) ? { settledBy: ev.settledBy } : {}),
166
179
  ...identityFields(ev),
167
180
  };
168
181
  }
@@ -363,6 +376,13 @@ export function workspaceChangedEventData(ev) {
363
376
  * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
364
377
  * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
365
378
  * 侧信道(它不参与 governance 那 4 个布尔的还原)。
379
+ * · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
380
+ * 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
381
+ * 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
382
+ * 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
383
+ * 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
384
+ * ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
385
+ * 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
366
386
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
367
387
  * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
368
388
  */
@@ -386,6 +406,7 @@ export function wiringManifestEventData(ev) {
386
406
  const fleetIn = sec(m.fleet) ?? {};
387
407
  const legIn = sec(m.leg) ?? {};
388
408
  const interactionIn = sec(m.interaction) ?? {};
409
+ const rulesIn = sec(m.permissionRules) ?? {};
389
410
  const ask = nonEmpty({
390
411
  ...(str(askIn.form) !== undefined ? { form: askIn.form } : {}),
391
412
  ...(str(askIn.provenance) !== undefined ? { provenance: askIn.provenance } : {}),
@@ -413,6 +434,11 @@ export function wiringManifestEventData(ev) {
413
434
  ...(bool(fleetIn.backgroundAgentStore) !== undefined ? { backgroundAgentStore: fleetIn.backgroundAgentStore } : {}),
414
435
  ...(bool(fleetIn.hostChildEventSink) !== undefined ? { hostChildEventSink: fleetIn.hostChildEventSink } : {}),
415
436
  });
437
+ // core 5.18.0 design/179 段(裁定见头注:租户可见)。逐键挑,与其余段同规:非布尔按缺席处理
438
+ // (不铸假值——一个编出来的 `false` 会被读成「确实没接规则店」,而真相是这段读不出来)。
439
+ const permissionRules = nonEmpty({
440
+ ...(bool(rulesIn.storeWired) !== undefined ? { storeWired: rulesIn.storeWired } : {}),
441
+ });
416
442
  return {
417
443
  ...(num(m.schemaVersion) !== undefined ? { schemaVersion: m.schemaVersion } : {}),
418
444
  ...(str(legIn.kind) !== undefined ? { leg: { kind: legIn.kind } } : {}),
@@ -423,6 +449,7 @@ export function wiringManifestEventData(ev) {
423
449
  ...(parkLane !== undefined ? { parkLane } : {}),
424
450
  ...(str(sessionIn.store) !== undefined ? { session: { store: sessionIn.store } } : {}),
425
451
  ...(fleet !== undefined ? { fleet } : {}),
452
+ ...(permissionRules !== undefined ? { permissionRules } : {}),
426
453
  // governance —— **刻意不在此**(见头注:受众标 operator,无投影不披露)。
427
454
  // configFingerprint —— **也刻意不在此**(codex 复审 2026-08-06 F1,验真后采纳)。剥掉一个段却把
428
455
  // **对它取的哈希**发出去,等于没剥:core 的算法是
@@ -685,6 +712,10 @@ export function toolResultFieldsOf(d) {
685
712
  // core 5.9.0 W3([2535]):机器可判别的 park/死投影码(`gate.parked`/`tool.not_found`)——写侧
686
713
  // toolEndEventData 已带,读腿必须同键 verbatim(absent⇒absent),否则冷回放退回锚 abort 文案字面。
687
714
  ...(typeof d.errorCode === "string" ? { errorCode: d.errorCode } : {}),
715
+ // core 5.18.1 #187:结算来源(闭集)——写侧 toolEndEventData 已带,读腿同键 verbatim(absent⇒absent)。
716
+ // 🔴 读腿的闭集门是**独立的一道**,不是写侧那道的复述:账本行可以来自任何引擎世代、任何第三方
717
+ // producer,写侧的门管不到它们;这里放行 = 把一个 core 从未发过的词直接送上 wire。
718
+ ...(isApprovalSettledBy(d.settledBy) ? { settledBy: d.settledBy } : {}),
688
719
  ...identityFields(d),
689
720
  };
690
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.5.0",
3
+ "version": "7.7.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "^5.17.0",
57
+ "@sema-agent/core": "^5.21.0",
58
58
  "@sema-agent/registry-core": "^0.16.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "^6.8.0",
72
+ "@sema-agent/sdk": "^6.9.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",