@sema-agent/server 7.55.0 → 7.57.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 (129) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +14 -7
  3. package/README.zh-CN.md +10 -6
  4. package/USAGE.md +207 -59
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +100 -1
  7. package/dist/approval-ask-audit-store.js +103 -2
  8. package/dist/approval-card.d.ts +148 -13
  9. package/dist/approval-card.js +82 -13
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval.d.ts +31 -0
  13. package/dist/approval.js +18 -0
  14. package/dist/auto-mode-face.d.ts +111 -0
  15. package/dist/auto-mode-face.js +99 -0
  16. package/dist/bench/s1/arms.js +5 -5
  17. package/dist/bench/s1/live-deps.js +11 -11
  18. package/dist/bench/s1/run-firm.js +3 -0
  19. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  20. package/dist/bench/s1/runner-ctx.js +7 -0
  21. package/dist/boot/config-center.js +23 -1
  22. package/dist/boot/coordinators.js +13 -1
  23. package/dist/boot/leader.d.ts +21 -0
  24. package/dist/boot/leader.js +6 -0
  25. package/dist/boot/parked-revive-gate.d.ts +7 -2
  26. package/dist/boot/parked-revive-gate.js +12 -1
  27. package/dist/boot/resolve-spec.d.ts +3 -0
  28. package/dist/boot/resolve-spec.js +3 -1
  29. package/dist/boot/runner-deps.d.ts +12 -0
  30. package/dist/boot/runner-deps.js +28 -3
  31. package/dist/boot/runtime-caps.d.ts +19 -8
  32. package/dist/boot/runtime-caps.js +50 -20
  33. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  34. package/dist/boot/session-shell-gate-registry.js +21 -0
  35. package/dist/config-catalog.js +14 -5
  36. package/dist/config-center/types.d.ts +2 -1
  37. package/dist/config-provider.js +1 -0
  38. package/dist/config-types.d.ts +41 -10
  39. package/dist/config.d.ts +21 -0
  40. package/dist/config.js +40 -5
  41. package/dist/http/route-ctx.d.ts +90 -3
  42. package/dist/http/routes/a2a-serve.js +5 -3
  43. package/dist/http/routes/approvals-assistant.js +21 -8
  44. package/dist/http/routes/capabilities.js +21 -4
  45. package/dist/http/routes/leader.js +2 -2
  46. package/dist/http/routes/memory-origin.d.ts +2 -2
  47. package/dist/http/routes/rules.js +3 -2
  48. package/dist/http/routes/runs.d.ts +0 -14
  49. package/dist/http/routes/runs.js +20 -8
  50. package/dist/http/routes/tasks.js +34 -8
  51. package/dist/http/routes/workflows.js +20 -5
  52. package/dist/http/server.d.ts +11 -1
  53. package/dist/http/server.js +220 -34
  54. package/dist/http/wire-types.d.ts +9 -4
  55. package/dist/leader/diffout.js +2 -1
  56. package/dist/leader/endpoint.js +49 -15
  57. package/dist/leader/fanout.js +6 -5
  58. package/dist/leader/leader.js +17 -14
  59. package/dist/leader/merge.js +17 -12
  60. package/dist/leader/planner.js +3 -2
  61. package/dist/leader/repair-oracle.js +6 -4
  62. package/dist/leader/repair-wire.js +5 -3
  63. package/dist/leader/wire.d.ts +80 -1
  64. package/dist/leader/wire.js +55 -26
  65. package/dist/main.js +44 -8
  66. package/dist/observability/err-text.d.ts +6 -0
  67. package/dist/observability/err-text.js +10 -0
  68. package/dist/observability/fail-open.d.ts +40 -0
  69. package/dist/observability/fail-open.js +19 -0
  70. package/dist/observability/metrics.js +1 -1
  71. package/dist/observability/run-terminal-log.d.ts +120 -0
  72. package/dist/observability/run-terminal-log.js +360 -0
  73. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  74. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  75. package/dist/permission-rule-vocab.d.ts +40 -0
  76. package/dist/permission-rule-vocab.js +17 -0
  77. package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
  78. package/dist/plugins/checkpoint-store-sql.js +23 -4
  79. package/dist/plugins/file-run-store.d.ts +3 -34
  80. package/dist/plugins/file-run-store.js +21 -0
  81. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  82. package/dist/plugins/local-checkpoint-store.js +5 -0
  83. package/dist/plugins/local-session-store.d.ts +9 -9
  84. package/dist/plugins/local-session-store.js +5 -3
  85. package/dist/plugins/memory-run-store.d.ts +3 -15
  86. package/dist/plugins/memory-run-store.js +21 -0
  87. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  88. package/dist/plugins/permission-rule-store-file.js +25 -11
  89. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  90. package/dist/plugins/permission-rule-store-sql.js +39 -23
  91. package/dist/plugins/pg-session-storage.js +17 -13
  92. package/dist/plugins/remote-scratchpad.js +3 -2
  93. package/dist/plugins/run-store-sql.d.ts +3 -42
  94. package/dist/plugins/run-store-sql.js +36 -5
  95. package/dist/plugins/store-backend.d.ts +1 -1
  96. package/dist/plugins/store-contracts.d.ts +70 -1
  97. package/dist/plugins/tidb-session-store.js +18 -14
  98. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  99. package/dist/plugins/workflow-run-store-sql.js +10 -2
  100. package/dist/rules-consent.d.ts +5 -4
  101. package/dist/rules-consent.js +25 -46
  102. package/dist/run-cancel-context.d.ts +13 -0
  103. package/dist/run-cancel-context.js +15 -0
  104. package/dist/run-local.js +2 -2
  105. package/dist/runs.d.ts +4 -1
  106. package/dist/runs.js +41 -10
  107. package/dist/runtime-caps-resolver.d.ts +133 -17
  108. package/dist/runtime-caps-resolver.js +44 -4
  109. package/dist/security.d.ts +7 -0
  110. package/dist/task-settings.d.ts +57 -3
  111. package/dist/task-settings.js +78 -5
  112. package/dist/task-workflow.d.ts +32 -2
  113. package/dist/task-workflow.js +8 -3
  114. package/dist/tool-approval.d.ts +38 -63
  115. package/dist/tool-approval.js +102 -74
  116. package/dist/trace/core-keyset-guard.d.ts +2 -2
  117. package/dist/trace/ledger-sink.d.ts +13 -4
  118. package/dist/trace/ledger-sink.js +14 -6
  119. package/dist/trace/project.d.ts +13 -0
  120. package/dist/trace/project.js +10 -1
  121. package/dist/trace/redact.d.ts +22 -11
  122. package/dist/trace/redact.js +655 -20
  123. package/dist/trace/sema-provenance.d.ts +26 -0
  124. package/dist/trace/sema-provenance.js +11 -0
  125. package/dist/turn-activity.d.ts +32 -2
  126. package/dist/turn-activity.js +29 -4
  127. package/package.json +3 -3
  128. package/skills/find-skills.md +1 -1
  129. package/skills/loop.md +1 -1
@@ -145,16 +145,36 @@ export interface A2aServeConfig {
145
145
  * the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv —
146
146
  * operator free-text never reaches the runner. */
147
147
  /** [ref] 车3([ref] 流内审批协议)的配置面。总开关默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后;
148
- * 树上已生效,发布线落在 7.4.0 之后的下一个发布);四个从属旋钮只在开关为真时生效
148
+ * 树上已生效,发布线落在 7.4.0 之后的下一个发布);从属旋钮**除 `windowMs` 外**只在开关为真时生效
149
+ * ([ref]:`STREAM_ASK_WINDOW_MS` 是活卡窗长,与协议上不上场解耦 —— 逐条辖域见各字段域注)
149
150
  * (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
150
151
  export interface StreamApprovalConfig {
151
152
  /** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。
152
153
  * 翻真要等回决端点(车4)与对账收敛器(车5)到位——在此之前是个不完整协议(有卡无正规回决口、
153
154
  * 有 PARKING 无收敛器、跨副本回决无唤醒路径)。 */
154
155
  enabled: boolean;
155
- /** 开关为真时作为 `ToolApprovalCoordinator` 的 `ttlMs`([ref] §3.3 的「配置窗默认」)。
156
- * `STREAM_ASK_WINDOW_MS`,默认 **300000**(同裁与 sync 腿既有 5min 活卡窗对齐)。`0` = 运维显式关窗 ⇒ 恒 park(§3.3 窗=0 语义)
157
- * 开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)。 */
156
+ /** `ToolApprovalCoordinator` 的 `ttlMs` —— **一只活卡等人多久**([ref] §3.3 的「配置窗默认」)。
157
+ * `STREAM_ASK_WINDOW_MS`,默认 **300000**(同裁与 sync 腿既有 5min 活卡窗对齐)。
158
+ *
159
+ * 🔴 **辖域([ref] 起):无条件** —— 本值在**每一条**呈活卡的腿上生效,与流内协议上不上场无关
160
+ * (`DB_BACKEND=local` / 无 park 设施 / `STREAM_APPROVAL_ENABLED=false` 的部署同样按它计窗)。
161
+ * 旧文「开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)」是**已修的病**,不是
162
+ * 设计:那让一根部署级旋钮的生效与否挂在装配在场性上,运维配了它、没有任何一行说它没生效
163
+ * (clay 令:部署级旋钮禁挂在场性派生腿)。缺省值与那个常量同为 300000 ⇒ **没配过这根 env 的部署
164
+ * 字节不变**;协议上不上场改变的是**持久层与协议帧**(ask 行 / 重放 / 迟到赎回),不是窗长。
165
+ *
166
+ * `0` = 运维显式关窗 ⇒ **不发任何审批卡**,每只受门 ask 在铸造时刻直接走 `unattendedAskOutcome()`
167
+ * (§3.3 窗=0 语义;[ref] 起两条车道同义 —— 此前 `0` 在协议不上场的腿上被静默读成「5min 活卡」)。
168
+ * 该形有 park 设施 ⇒ 全落 durable park(经 `POST /v1/approvals/:sessionId/decide` 兑现);无 park 设施
169
+ * 或 `UNATTENDED_APPROVAL_POLICY=deny` ⇒ core fail-closed 拒 = **受门工具全关**,故 boot 期有一行
170
+ * `stream_ask_window_zero` 点名(`boot/coordinators.ts`)。
171
+ * **坏值拒启两侧**([ref]):负值(`-1` 此前静默落成「立即到期」并白发一张没人赢得了的卡)与
172
+ * **超过 Node 定时器上界 `2147483647`(2^31−1 ms ≈ 24.8 天)的值**(codex r1-[high]:`setTimeout` 把更大的
173
+ * delay 静默折成 1ms ⇒ 想调长反而立即到期;跨旋钮门只管 `窗 + adhoc宽限 < orphanTtl`,而 orphanTtl 域上界
174
+ * 90 天,所以 30 天的窗此前合法通过)—— 两侧都点名拒启,不夹取。
175
+ *
176
+ * 窗到期的**去向**不由本键决定(`UNATTENDED_APPROVAL_POLICY` + park 设施在场性决定),本键只决定
177
+ * 「等多久」。悬挂 ask(零活流铸造,[ref] 案A)走的是 `unreachedTtlMs`,不是本键。 */
158
178
  windowMs: number;
159
179
  /** 开流重放(§5)每次最多投几张未决卡——**读面**的帽,超出只投最新的并记一次 warn,开流不失败。
160
180
  * `STREAM_APPROVAL_REPLAY_MAX`,默认 **50**。 */
@@ -512,10 +532,12 @@ export interface ServiceConfigFlat {
512
532
  * `open` 的故障极性**相反**,一个被静默折成缺省的拼写错误会让合规部署在故障窗里放行留存必需用户的 opt-out。
513
533
  * core 侧 `prepareConfigDoors` 另有同判门(`config.memory_capture_policy`),两道同向不冲突。
514
534
  *
515
- * 🔴 **`governed` 的半配置拒启**:该姿态要求本部署有 per-principal verdict 源。本批唯一源 = SQL 后端上的
516
- * `memory_optout_grant` 表(S-2;center `EntitlementRuntimeCaps.allowMemoryOptOut` 是 settings-schema
517
- * 的独立小件,到货前不算源)。governed 而无源 ⇒ 每个 opt-out 声明都被 core 拒且没人知道为什么 ⇒
535
+ * 🔴 **`governed` 的半配置拒启**:该姿态要求本部署有 per-principal verdict 源。源**两条**(析取,[ref] 起):
536
+ * SQL 后端上的 `memory_optout_grant` 表(S-2),或非 dry-run 的 config center `EntitlementRuntimeCaps.allowMemoryOptOut`
537
+ * (settings-schema 1.4.0 到货)。两源皆无 ⇒ 每个 opt-out 声明都被 core 拒且没人知道为什么 ⇒
518
538
  * `boot/runtime-caps.ts` 的 `assertMemoryCapturePolicyWirable` 在启动期拒启(与 consolidation 阀门 D1 同姿态)。
539
+ * ⚠️ [ref]:center 那一臂只证「源在场」,不证 center 会**发**这个可选键 —— center-only 的部署起动后仍可能次次拒,
540
+ * 两处 warn 把它响亮化(`memory_capture_governed_center_only_source` / `memory_capture_governed_verdict_absent`)。
519
541
  *
520
542
  * ⚠️ **部署席 ONLY**:TaskSpec 无同名键;不冻进 checkpoint(resume 腿跟当前部署配置走,core 成文)。
521
543
  * 运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.capturePolicy`(`null`=本部署未设)。 */
@@ -1270,6 +1292,13 @@ export interface ServiceConfigFlat {
1270
1292
  * other value (incl. `"off"`) is treated as unset — this knob has no "off" value to opt BACK OUT with once a
1271
1293
  * session's baseline already tightened elsewhere (tightenTaskSpec would reject that as a loosen anyway). */
1272
1294
  manualModeShellGate?: "always" | "classify";
1295
+ /** [ref] ③(黑板 [ref]/[ref],core [ref] 合修)—— CC `permissions.disableAutoMode` 的本地镜像
1296
+ * (`PERMISSIONS_DISABLE_AUTO_MODE`,布尔词表旋钮,opt-in 缺省 false):本部署对**每个** principal 把
1297
+ * `RuntimeCaps.autoMode` 折成 `false` = org deny 位(core [ref] 三态谓词 `runtimeCaps?.autoMode !== false`
1298
+ * 的拒臂)。**tighten-only**:center 授予 `true` 翻不回;未设 = 不动 caps(无 center 的部署 caps 仍缺席 ⇒
1299
+ * 「缺席=不阻」⇒ 壳发 `permissionMode:"auto"` 即武装)。词表外的值 boot 拒启(安全轴旋钮不许静默回默认)。
1300
+ * 合成点 = `boot/runtime-caps.ts` 最外层(applyLocalAutoModeDeny);读面 = `/v1/capabilities.permissionModeAuto`。 */
1301
+ permissionsDisableAutoMode: boolean;
1273
1302
  /** [ref]④ defer face (EXPERIMENTAL, default OFF): mark the default scenario's LONG-TAIL first-party tools
1274
1303
  * (today = SelectEnvironment only) `defer:true` (core tool-disclosure: the model sees a placeholder until it
1275
1304
  * ToolSearch-loads the schema). Assessment says the win is marginal for us (spec.tools total ≈4.2k tokens, of
@@ -1400,8 +1429,10 @@ export interface ServiceConfigFlat {
1400
1429
  * `DEFAULT_APPROVAL_TTL_MS` 对齐——「开协议」不再顺带把窗砍短)。⚠️ 版本坐标:翻转**已在树上生效**,
1401
1430
  * 发布线上落在 7.4.0 之后的下一个发布,所以一台自报 7.4.0 的**已发布** worker 仍是 OFF,别只按
1402
1431
  * `/health` 的版本号推默认。显式 `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到
1403
- * 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、窗 = 既有
1404
- * `DEFAULT_APPROVAL_TTL_MS`(5min)、`askStore` 不注入协调器。
1432
+ * 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、`askStore` 不注入
1433
+ * 协调器。⚠️ **窗不在这份还原清单里了([ref])**:`STREAM_ASK_WINDOW_MS` 自此在关协议的部署上照样
1434
+ * 生效(缺省 300000 == `DEFAULT_APPROVAL_TTL_MS` ⇒ 没配过它的部署仍然字节不变),理由见
1435
+ * `StreamApprovalConfig.windowMs` 的辖域段。
1405
1436
  *
1406
1437
  * **旋钮为什么存在(2026-08-08 翻转前的成文前提,已被上面的裁定取代,留作沿革)**:那一版默认 false +
1407
1438
  * 60s 窗,三问记的是那个世界——
@@ -1597,7 +1628,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
1597
1628
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
1598
1629
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
1599
1630
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1600
- export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
1631
+ export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate" | "permissionsDisableAutoMode">;
1601
1632
  /** 组:memory(记忆面 + TOC 同步腿)。 */
1602
1633
  export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "memoryConsolidationDriver" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1603
1634
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
package/dist/config.d.ts CHANGED
@@ -169,6 +169,27 @@ export declare const hostExecSpoolEnabled: () => boolean;
169
169
  * a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
170
170
  * Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
171
171
  export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
172
+ /**
173
+ * [ref] 件⑤ —— 思考档两键的**闭集词表**与拒启判据(`MODEL_DEFAULT_THINKING` / `MODEL_REASONING_EFFORT_LEVELS`)。
174
+ *
175
+ * 改前两键都走 `isThinkingLevel(...)` **过滤**:拼错的词被**静默丢掉**,而两根旋钮的失败方向恰好都是
176
+ * 「把运维的意图反向执行」——
177
+ * · `MODEL_DEFAULT_THINKING=hight` ⇒ `defaultThinking` 不铸键 ⇒ 全部任务回落到「无模型级默认档」,
178
+ * 运维以为整台机器抬到了 high;
179
+ * · `MODEL_REASONING_EFFORT_LEVELS=med,high` ⇒ 只剩 `["high"]`;写成 `med` 一个词则整键落空、
180
+ * `compat` 不铸 ⇒ 回落 core 的保守梯(`minimal|low|medium|high`),`xhigh`/`max` 被 clamp 下来。
181
+ * 运维配这根旋钮的**全部用途**就是告诉引擎「本端点吃得下更高档」,静默落空 = 高档永远上不去。
182
+ * 两条都是 `MODEL_DEGRADE_ON` / `TASK_TIMEOUT_SEC` 那条族律的同形([ref] A 档:坏值拒启,不静默折默认),
183
+ * 所以改成 boot 响亮拒 —— 与 `MODEL_DEGRADE_ON` 的 `parseDegradeOn` 逐字同款(点名坏词 + 列合法词)。
184
+ *
185
+ * 词表用 `Record<ThinkingLevel, true>` 铸 = 与 llm-core 锁步:上游加一档而这里不补 ⇒ **编译红**
186
+ * (`MODEL_DEGRADE_ON` 的 `Set<DegradeReason>` 同款纪律);目录行的 `enumValues` 由本表派生,
187
+ * 「文档写五档、代码收六档」那种分叉在类型层就不可能发生。
188
+ */
189
+ type ModelThinkingLevel = NonNullable<Model["defaultThinking"]>;
190
+ /** 六档闭集(有序:低→高,取自上表的书写序),目录行 `enumValues` 与两条拒因文案的单一真源。
191
+ * (`CATALOG_RESTART_SLICES = Object.keys(RESTART_SLICE_SET)` 同款派生式。) */
192
+ export declare const THINKING_LEVELS: readonly ModelThinkingLevel[];
172
193
  /**
173
194
  * 一条工具名**永不匹配任何现役工具**的三种拼法,及其修正指引。审批/权限名单是 server 侧 ToolPolicy 的
174
195
  * RAW 逐字比对(canonicalToolName 折叠面已随 core 5.0.0 [ref] 退役),所以写错的名字不会报错、只会静默
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
5
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
6
6
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
@@ -521,7 +521,8 @@ function noticeAutoCompactAboveWindow(slot, envName, m) {
521
521
  model: m.id,
522
522
  value: String(act),
523
523
  contextWindow: String(window),
524
- note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window, so a value above the physical window means autocompaction effectively never fires and the destructive context guard truncates history instead (the opposite of what this knob is for). The value is applied as configured (not clamped, not rejected); lower it below the window — CC's own shape is window-33000.`,
524
+ clampedTo: String(window),
525
+ note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window and CLAMPS it to the physical window (resolveTriggerWindow), so this knob is a NO-OP here: the trigger geometry falls back to ${window} and core mints one config.autocompact_window_clamped notice per prepare. server neither clamps nor rejects the value (clamping is core's, at the read site); lower it below the window to make the knob do something — CC's own shape is window-33000.`,
525
526
  },
526
527
  });
527
528
  }
@@ -637,8 +638,8 @@ function parseModelDomain() {
637
638
  return "gateway";
638
639
  })();
639
640
  const extraBody = buildExtraBody();
640
- const thinkingDefault = (() => { const v = process.env.MODEL_DEFAULT_THINKING; return v && isThinkingLevel(v) && v !== "off" ? v : undefined; })();
641
- const effortLevels = (process.env.MODEL_REASONING_EFFORT_LEVELS ?? "").split(",").map((s) => s.trim()).filter((s) => isThinkingLevel(s) && s !== "off");
641
+ const thinkingDefault = parseDefaultThinking(process.env.MODEL_DEFAULT_THINKING);
642
+ const effortLevels = parseReasoningEffortLevels(csv("MODEL_REASONING_EFFORT_LEVELS"));
642
643
  const model = {
643
644
  id: modelId,
644
645
  name: modelId,
@@ -777,6 +778,32 @@ function parseModelDomain() {
777
778
  : undefined,
778
779
  };
779
780
  }
781
+ const THINKING_LEVEL_TABLE = { minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
782
+ export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_TABLE);
783
+ const isThinkingTier = (w) => THINKING_LEVELS.includes(w);
784
+ function parseDefaultThinking(raw) {
785
+ const v = raw?.trim();
786
+ if (v === undefined || v === "")
787
+ return undefined;
788
+ if (v === "off")
789
+ return undefined;
790
+ if (!isThinkingTier(v)) {
791
+ throw new Error(`env MODEL_DEFAULT_THINKING="${v}" is not a thinking tier — valid: ${THINKING_LEVELS.join(" | ")} | off ` +
792
+ `("off" = no model-level default). A typo used to be dropped SILENTLY, leaving the deployment on no default at all (#210 A 档).`);
793
+ }
794
+ return v;
795
+ }
796
+ function parseReasoningEffortLevels(words) {
797
+ if (words.length === 0)
798
+ return [];
799
+ const bad = words.filter((w) => !isThinkingTier(w));
800
+ if (bad.length > 0) {
801
+ throw new Error(`env MODEL_REASONING_EFFORT_LEVELS has unknown tier(s): ${bad.join(", ")} (valid: ${THINKING_LEVELS.join(" | ")})` +
802
+ `${bad.includes("off") ? ' — "off" belongs to MODEL_DEFAULT_THINKING (no model-level default), it is not an effort tier an endpoint can accept' : ""}. ` +
803
+ `Bad words used to be dropped SILENTLY, narrowing the declared ladder without a word (#210 A 档).`);
804
+ }
805
+ return words.filter(isThinkingTier);
806
+ }
780
807
  const DEGRADE_ON_WORDS = new Set(["breaker_open", "rate_limit", "budget", "server_error", "last_resort"]);
781
808
  function parseDegradeOn(words) {
782
809
  if (words.length === 0)
@@ -853,6 +880,7 @@ export function formatUnmatchableToolNames(source, bad) {
853
880
  return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
854
881
  bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
855
882
  }
883
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
856
884
  function streamApprovalConfig(gates) {
857
885
  const cfg = {
858
886
  enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
@@ -866,6 +894,12 @@ function streamApprovalConfig(gates) {
866
894
  orphanTtlMs: numEnvBounded("STREAM_APPROVAL_ORPHAN_TTL_MS", String(7 * 24 * 60 * 60 * 1000), 60_000, 90 * 24 * 60 * 60 * 1000),
867
895
  unreachedTtlMs: numEnvBounded("UNREACHED_ASK_TTL_MS", String(60 * 60 * 1000), 60_000, 24 * 60 * 60 * 1000),
868
896
  };
897
+ if (cfg.windowMs < 0) {
898
+ throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} must not be negative — 0 means "close the window" (every gated ask goes straight to the unattended outcome and NO approval card is emitted), any positive value is the live-card window in ms. A negative value used to emit a card nobody could ever win.`);
899
+ }
900
+ if (cfg.windowMs > MAX_TIMER_DELAY_MS) {
901
+ throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} exceeds the Node timer ceiling ${MAX_TIMER_DELAY_MS} (2^31-1 ms ≈ 24.8 days) — setTimeout SILENTLY collapses a larger delay to 1ms, so the window you meant to lengthen would expire immediately and every gated ask would take the unattended route on the next tick. Pick a window at or below the ceiling (and remember STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS must stay strictly below STREAM_APPROVAL_ORPHAN_TTL_MS).`);
902
+ }
869
903
  if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
870
904
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
871
905
  }
@@ -945,6 +979,7 @@ function parseApprovalDomain(ctx) {
945
979
  `The words are matched EXACTLY (lowercase, surrounding whitespace trimmed): "OFF"/"Classify" land here too, deliberately — a closed word table with no case-folding is what keeps "it looked accepted" from meaning "it did nothing". ` +
946
980
  `Refusing to boot rather than treating a misspelled value as "unset": that silently ran the deployment with NO shell gate while the operator believed one was configured`);
947
981
  })(process.env.MANUAL_MODE_SHELL_GATE),
982
+ permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
948
983
  mcpElicitation: {
949
984
  enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
950
985
  throttle: {
@@ -1757,7 +1792,7 @@ const APPROVAL_GROUP_KEYS = [
1757
1792
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1758
1793
  "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1759
1794
  "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
1760
- "sensitiveWritePatterns", "manualModeShellGate",
1795
+ "sensitiveWritePatterns", "manualModeShellGate", "permissionsDisableAutoMode",
1761
1796
  ];
1762
1797
  const MEMORY_GROUP_KEYS = [
1763
1798
  "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
@@ -47,6 +47,24 @@ export interface RouteHelpers {
47
47
  rateLimited(req: IncomingMessage, res: ServerResponse): boolean;
48
48
  quotaExceeded(req: IncomingMessage, res: ServerResponse): boolean;
49
49
  leaseDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
50
+ /**
51
+ * 🔴 **[ref]:按**付费**方(不是请求方)判的成本配额准入 —— {@link quotaExceeded} 的孪生。**
52
+ *
53
+ * `quotaExceeded` 键在 `gatedPrincipal(req)` = 谁点的按钮;本门键在**这笔钱记谁的账**。两者在自助
54
+ * 场景恒相等,在「显式 operator 代别人动手」的场景不等 —— 那正是配额上限被绕过的那一支。
55
+ * 参数 `owner` 由调用点从**已属主校验过的那一行**取(run 行的 `owner` / checkpoint 的 scope),
56
+ * 本门不自己推身份(推得出来就说明调用点还有第二个身份源,那才是病)。
57
+ * `undefined`(匿名/开放行)⇒ 无付费方 ⇒ 不 gated,与 `quotaExceeded` 的成文语义同判。
58
+ * 拒 = 429 + `Retry-After`,码/文案/extras 与 `quotaExceeded` 逐字同一套。
59
+ * 位序纪律:**必须排在本路由的 owner 门之后**(反枚举 —— 否则 429 会变成「这行存在且它属主超顶了」
60
+ * 的谕示);且排在真正驱动模型之前。
61
+ */
62
+ ownerQuotaDenied(res: ServerResponse, owner: string | undefined): boolean;
63
+ /** 🔴 **[ref]:{@link ownerQuotaDenied} 的 lease 孪生 —— 按**付费**方判的 fleet-lease 准入。**
64
+ * `leaseDenied` 键在请求方;本门键在付费方(`driveResumeLeg` 的 `fleetLease.admit(卡属主)` 早就是这个
65
+ * 语义,本位把它交给路由域)。两门在自助场景恒同答,在「operator 代别人动手」时不同 —— 那正是
66
+ * 「拿自己的额度去烧别人的账」的那一支。同样必须排在 owner 门之后、驱动之前。 */
67
+ ownerLeaseDenied(res: ServerResponse, owner: string | undefined): Promise<boolean>;
50
68
  /** [ref]-T1:治理窗 pre-admission(异步 202 车道的唯一 429 出路;key 公式与 core 单源)。 */
51
69
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
52
70
  safeDecode(seg: string): string | null;
@@ -79,6 +97,17 @@ export interface RouteRequestState {
79
97
  streamDetached: boolean;
80
98
  /** 凭证派生的调用方系统身份(`source`)。handle() 的鉴权门是**唯一**赋值点,域模块只读。 */
81
99
  source: string | null;
100
+ /**
101
+ * [ref] 件2(cli [ref] sema-bug4 (c)):本请求驱动的那条 run 的**终局**(`completed` / `failed` /
102
+ * `suspended` / …)与结构化失败码。`handle()` 的 `request` 日志行读它 —— 让「端点 200 ≠ 模型成功」
103
+ * 这条歧义消失(取证:模型 connect timeout 的那一刻,日志里唯一的行是 `request status:200`)。
104
+ *
105
+ * 赋值点在 `routes/tasks.ts`(`/v1/tasks` 与 `/v1/tasks/stream` 两腿的终局到手处);拿不到 run 结果
106
+ * (受理即拒 / 幂等重放 / 非任务端点)⇒ **键缺席**,绝不写 null —— 缺席 = 「这条请求没驱动 run」,
107
+ * 与「run 结果不明」是两句话,而 `null` 会把两者压成一个形。
108
+ */
109
+ runStatus?: string;
110
+ runErrorCode?: string;
82
111
  }
83
112
  /** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
84
113
  * 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
@@ -141,6 +170,52 @@ export interface DriveResumeArgs {
141
170
  * **唯一不设此位的**是内部 D-D SLA deny-sweep:它没有在等响应的人,提前受理对它零收益,而且那条
142
171
  * 循环靠 `await` 串行给每 tick 的重活限流(见其调用处的登记注)。 */
143
172
  acceptEarly?: boolean;
173
+ /**
174
+ * 🔴 **[ref](codex 交叉复审 r2-[medium] 验真后修):这条腿是**调用方发起**的吗?**
175
+ *
176
+ * `true` = 有人在 HTTP 上点了按钮(resume / plan_review / wake / decide / 赎回席)⇒ 吃按**付费方**
177
+ * (卡属主)判的成本配额准入(`ownerCostQuotaDenied`)。缺席/`false` = **部署自己的结算腿**
178
+ * (内部 D-D SLA deny-sweep)⇒ **不吃**那道门。
179
+ *
180
+ * 为什么系统腿必须豁免(亲核后改的判据,首版曾一并拦下 sweep):sweep 的活是把过了 SLA 的卡**结算掉**
181
+ * (graceful deny → 行收敛 → 释放 `task_active` claim)。拦住它并不能省钱,只会让超顶租户的卡**挂着**
182
+ * —— 而挂多久的兜底**比首版注释里写的差得多**:`checkpoint-store-sql.ts` 的 `reapExpired` 在
183
+ * **deadline 分支**上逐字排除了 `APPROVAL_GATE_KINDS`(`gate_kind NOT IN (…) OR tool_name='AskUserQuestion'`),
184
+ * 所以一张过期的 human / irreversible_ask 卡根本不走那条腿;真正兜住它的只有绝对 backstop
185
+ * `terminal_at_ms`(`TERMINAL_BACKSTOP_MS` 默认 **30 天**)。于是「拦 sweep」= 把一个由
186
+ * `APPROVAL_TIMEOUT_SEC` 界定的 SLA 换成「等成本窗滚过(`COST_QUOTA_WINDOW_SEC` 默认 24h)、最坏等 30 天」,
187
+ * 并且整段时间攥着会话 claim。**方向也自洽**:deny 是**停下**这条 run 的动作,为了省钱而拒绝执行「停」
188
+ * 与 preempt 路由早就成文的那句同一条(「否则超顶租户连自己的花费都停不下来」)。
189
+ *
190
+ * 判别子取 `req` 的在场与否 —— 这在 `resumeCheckpoint` 里**已经是**「系统腿 vs 调用方腿」的既有判据
191
+ * (绑定强制、parked 分流两处都写着 `req !== undefined` 与「内部 D-D SLA deny-sweep(req 缺席)豁免」),
192
+ * 本位只是把同一条判据显式化成一个有名字的位,而不是让下游去猜。
193
+ * ⚠️ **射程覆盖两道门**:紧邻的 fleet-lease 准入(`ownerFleetLeaseDenied`)读**同一位** —— [ref] 件⑤②
194
+ * (codex r3-[high])把它一并改吃了 `callerInitiated`,理由与三问逐字同 cost 半场。
195
+ * 🔴 本句是 [ref] R4 改写的(2026-09-03,7.57.0 发车前合并重扫 wf_1abaaa9a):此处原文写着「紧邻的
196
+ * fleet-lease 准入**没有**这条豁免(对 sweep 一视同仁),本批不动它,分歧已登记为遗留」—— 那是**同一批
197
+ * 件⑤② 落地之前**的话,件⑤② 修好后这条 JSDoc 没跟着改,于是注释与代码正相反(亲读
198
+ * `server.ts` 的 `ownerFleetLeaseDenied` 消费点:两处都是 `args.callerInitiated === true` /
199
+ * `req !== undefined`)。两道门在这一位上**无分歧**,[ref] 上也没有这一项。
200
+ */
201
+ callerInitiated?: boolean;
202
+ /**
203
+ * 🔴 **[ref] codex 对抗复审 r1-F3 [medium](验真后修):本次请求的属主 lease 准入**已经做过了**。**
204
+ *
205
+ * `resumeCheckpoint` 在 parked 子代赎回分流**之前**先判一次属主 lease([ref] R3:那条腿提前 `return`,
206
+ * 结构上够不到 `driveResumeLeg` 的同一道门)。分流**未命中**时请求落回 legacy 腿,于是同一条请求、
207
+ * 同一个付费方会向 center 协商**两次**。cost 半场可以「过两次门」(本地同步、纯读);lease 半场**不行**:
208
+ * `FleetLeaseManager.admit` 的五条 fail-open 路径(center 不可达 / 5xx / 超时 / 401-403 / 畸形响应)
209
+ * 一律**不写缓存态**(亲读 `fleet-lease.ts` 的 `issueOrRenewInner`),而 single-flight 只合并**并发**调用 ——
210
+ * 两道门是串行的 ⇒ 半开 center 下一个请求连吃两次 `LEASE_FETCH_TIMEOUT_MS`,持续故障时整条 legacy
211
+ * resume 车道把控制面负载翻倍。
212
+ *
213
+ * 所以由上游把「已受理」这个事实传下来,`driveResumeLeg` 据此跳过重复协商。**安全性**:两处的键是
214
+ * **同一个表达式**(`decodeCheckpointScope(cp.scope)`,上游门与下游 `auth.principal` 同源),射程条件也
215
+ * 同一条(`req !== undefined` ⇔ `callerInitiated`)⇒ 跳过的是一次**逐字相同**的判定,不是一道门。
216
+ * 缺席/`false` ⇒ 照常自己判(其余三条驱动腿与 sweep 一字不变)。
217
+ */
218
+ ownerLeaseAdmitted?: boolean;
144
219
  }
145
220
  /** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
146
221
  * run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
@@ -198,10 +273,16 @@ export interface RouteLegs {
198
273
  status: number;
199
274
  body: object;
200
275
  }>;
201
- /** assistant 抢占腿的复位(gate-kind 守卫 + 驱动)。 */
276
+ /** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
202
277
  resumePreempted(sessionId: string, req: IncomingMessage | undefined,
203
278
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/resume` 腿传 `true`。 */
204
- acceptEarly?: boolean): Promise<{
279
+ acceptEarly?: boolean,
280
+ /** [ref]②([ref]):调用方已验身份的裁定 —— 腿内在被真正载入的 checkpoint 行上重跑属主判
281
+ * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
282
+ decider?: {
283
+ principal?: string;
284
+ explicitOperator: boolean;
285
+ }): Promise<{
205
286
  status: number;
206
287
  body: object;
207
288
  }>;
@@ -221,7 +302,13 @@ export interface RouteLegs {
221
302
  principalPresent?: boolean;
222
303
  },
223
304
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
224
- acceptEarly?: boolean): Promise<{
305
+ acceptEarly?: boolean,
306
+ /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
307
+ * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
308
+ decider?: {
309
+ principal?: string;
310
+ explicitOperator: boolean;
311
+ }): Promise<{
225
312
  status: number;
226
313
  body: object;
227
314
  }>;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
2
3
  import { A2A_TASK_STATES } from "@sema-agent/core";
3
- import { isParkedRunStatus } from "../../plugins/store-contracts.js";
4
+ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
4
5
  import { HttpError } from "../../security.js";
5
6
  import { scopedIdempotencyKey } from "../idempotency.js";
6
7
  import { sendJson, sendError } from "../send.js";
@@ -80,10 +81,11 @@ function statusTextForRun(run, state) {
80
81
  return "this task is suspended awaiting an approval decision on the serving deployment; it resumes once that decision is made";
81
82
  if (run.result?.result)
82
83
  return run.result.result;
84
+ const memo = (seat, raw) => (isTerminalRunStatus(run.status) ? memoRedactedRead(`a2a.${seat}`, raw, (v) => redactErrorMessage(v) ?? "") : (redactErrorMessage(raw) ?? ""));
83
85
  if (run.error)
84
- return run.error;
86
+ return memo("error", run.error);
85
87
  if (run.result?.errorMessage)
86
- return run.result.errorMessage;
88
+ return memo("errorMessage", run.result.errorMessage);
87
89
  return undefined;
88
90
  }
89
91
  function taskFromRun(run, stale) {
@@ -107,6 +107,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
107
107
  : (principal ?? "__none__");
108
108
  sendJson(res, 200, {
109
109
  pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
110
+ serverNowMs: Date.now(),
110
111
  ...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
111
112
  ...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
112
113
  });
@@ -299,7 +300,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
299
300
  sendError(res, 404, "not_found.run", "task not found");
300
301
  return;
301
302
  }
302
- const out = await resumePreempted(run.sessionId, req, true);
303
+ const out = await resumePreempted(run.sessionId, req, true, { principal, explicitOperator });
303
304
  sendResumeOutcome(res, out, deps.logger);
304
305
  return;
305
306
  }
@@ -365,7 +366,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
365
366
  sendError(res, 404, "not_found.run", "task not found");
366
367
  return;
367
368
  }
368
- const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true);
369
+ const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true, { principal: deciderPrincipal, explicitOperator });
369
370
  sendResumeOutcome(res, out, deps.logger);
370
371
  return;
371
372
  }
@@ -520,9 +521,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
520
521
  const APPROVALS_STREAM_POLL_MS = 3000;
521
522
  function redactPendingDisclosures(row) {
522
523
  const rd = row.riskDescriptor;
523
- if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
524
+ if (rd === null || rd === undefined)
524
525
  return row;
525
- return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
526
+ const shadowedRule = typeof rd.shadowedRule === "string" ? redactSecrets(rd.shadowedRule) : undefined;
527
+ const summary = typeof rd.summary === "string" ? redactSecrets(rd.summary) : undefined;
528
+ if (shadowedRule === undefined && summary === undefined)
529
+ return row;
530
+ return {
531
+ ...row,
532
+ riskDescriptor: {
533
+ ...rd,
534
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
535
+ ...(summary !== undefined ? { summary } : {}),
536
+ },
537
+ };
526
538
  }
527
539
  function projectPendingForWire(row, governance) {
528
540
  const redacted = redactPendingDisclosures(row);
@@ -553,19 +565,20 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
553
565
  }
554
566
  const cur = new Map(pending.map((p) => [keyOf(p), p]));
555
567
  const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
568
+ const pendingFrame = (k) => JSON.stringify({ type: "pending", ...cur.get(k), serverNowMs: Date.now() });
556
569
  if (first) {
557
- for (const frame of curFrames.values())
558
- res.write(`event: pending\ndata: ${frame}\n\n`);
570
+ for (const k of curFrames.keys())
571
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
559
572
  res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
560
573
  first = false;
561
574
  }
562
575
  else {
563
576
  for (const [k, frame] of curFrames)
564
577
  if (prevFrames.get(k) !== frame)
565
- res.write(`event: pending\ndata: ${frame}\n\n`);
578
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
566
579
  for (const [k, p] of prev)
567
580
  if (!cur.has(k))
568
- res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
581
+ res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: Date.now() })}\n\n`);
569
582
  }
570
583
  prev = cur;
571
584
  prevFrames = curFrames;
@@ -9,8 +9,10 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
9
9
  import { sendJson, sendError } from "../send.js";
10
10
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
11
11
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
12
- import { workflowModelAllowlistFor } from "../../task-workflow.js";
12
+ import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
13
13
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
14
+ import { judgeAutoModeArming } from "../../auto-mode-face.js";
15
+ import { coercePermissionModeStrict } from "../../task-settings.js";
14
16
  import { gatedPrincipal } from "../principal-gate.js";
15
17
  function routePairingOf(deps, name, m) {
16
18
  if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
@@ -43,6 +45,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
43
45
  return;
44
46
  }
45
47
  if (req.method === "GET" && url === "/v1/capabilities") {
48
+ const workflowsEngineCan = deps.workflowsCapable ?? Boolean(deps.workflowRunStore);
49
+ const workflowsDenial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
50
+ const requestedModeRaw = new URL(req.url ?? "", "http://x").searchParams.get("permissionMode");
51
+ const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
52
+ if (requestedModeRaw !== null && requestedMode === undefined) {
53
+ sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
54
+ return;
55
+ }
56
+ const entitlementSource = centerEntitlementSourceWired(deps.config);
57
+ const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), entitlementSource, localDeny: deps.config.permissionsDisableAutoMode });
58
+ if (autoModeFault !== undefined)
59
+ deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
46
60
  sendJson(res, 200, {
47
61
  ...deps.capabilities,
48
62
  asyncRuns: Boolean(deps.runStore),
@@ -92,8 +106,10 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
92
106
  modeShellGateTranslation: true,
93
107
  permissionModeAuto: {
94
108
  accepted: true,
95
- classifierSeat: true,
96
- entitlementSource: centerEntitlementSourceWired(deps.config),
109
+ classifierSeat: deps.autoModeFace?.seatMounted === true,
110
+ entitlementSource,
111
+ intentArming: deps.autoModeFace?.engineArmsOnIntent === true,
112
+ ...autoMode,
97
113
  },
98
114
  readFace: deps.config.readFace ?? null,
99
115
  appendSystemPrompt: true,
@@ -141,8 +157,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
141
157
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
142
158
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
143
159
  sendUserFileLedger: Boolean(deps.sendFileLedger),
144
- workflows: deps.workflowsCapable ?? Boolean(deps.workflowRunStore),
160
+ workflows: workflowsEngineCan && workflowsDenial === null,
145
161
  workflowsList: Boolean(deps.workflowRunStore),
162
+ workflowsGate: { engineCan: workflowsEngineCan, denial: workflowsDenial },
146
163
  workflowModels: (() => {
147
164
  const allow = deps.workflowModelAllowlist
148
165
  ?? workflowModelAllowlistFor({
@@ -7,7 +7,7 @@ export async function handleLeader(req, res, url, ctx) {
7
7
  }
8
8
  async function handleLeaderBody(req, res, url, ctx, miss) {
9
9
  const { deps } = ctx;
10
- const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
10
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
11
11
  if (deps.leaderEndpoint) {
12
12
  const isLeaderPost = req.method === "POST" && url === "/v1/leader";
13
13
  const isLeaderGet = req.method === "GET" && /^\/v1\/leader\/[^/]+$/.test(url);
@@ -18,7 +18,7 @@ async function handleLeaderBody(req, res, url, ctx, miss) {
18
18
  }
19
19
  const requester = gatedPrincipal(req, deps.config) ?? null;
20
20
  if (isLeaderPost) {
21
- if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
21
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
22
22
  return;
23
23
  let body;
24
24
  try {
@@ -99,7 +99,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
99
99
  detail?: string | undefined;
100
100
  eventId: string;
101
101
  at: number;
102
- to: "failed" | "done";
102
+ to: "done" | "failed";
103
103
  requestId: string;
104
104
  }[];
105
105
  custodyBytes: number;
@@ -107,7 +107,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
107
107
  requestId: string;
108
108
  reason: string;
109
109
  at: number;
110
- status: "pending" | "failed" | "done";
110
+ status: "pending" | "done" | "failed";
111
111
  originUnknownKeys?: string[] | undefined;
112
112
  clearanceId: string;
113
113
  entryId: string;
@@ -6,6 +6,7 @@ import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, seri
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
8
  import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
9
10
  const logger = createLogger();
10
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
11
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -45,8 +46,8 @@ const LocalImportRowSchema = z
45
46
  .object({
46
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
47
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
48
- tool: z.literal("Bash"),
49
- match: z.enum(["exact", "prefix"]),
49
+ tool: z.enum(PERSISTED_RULE_TOOLS),
50
+ match: z.enum(PERSISTED_RULE_MATCHES),
50
51
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
51
52
  adds: z.array(LocalImportAddSchema).max(64),
52
53
  })
@@ -1,17 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— 异步 run 面与它的全部动词。
3
- *
4
- * 路由:`POST /v1/runs`(异步提交)· `GET /v1/runs/:id[/events]`(轮询 / 可恢复 SSE)· 运行期动词
5
- * `cancel` / `steer` / `compact` / `detach` · 子代理面 `subagents/:target/{steer,resume,output,stream}` ·
6
- * 通用任务句柄 `tasks/:handle/{output,stop}` · 三条 HITL 应答口 `elicitations|questions|tool-approvals/:id/respond`。
7
- *
8
- * 两段搬运:提交+run 级动词一段,子代理/任务句柄/HITL 应答一段——它们在 `handle()` 里原本被 SVC-5 的
9
- * workflow steer 隔开,拆分保持各自位置(本文件里的先后只是同域归并)。`POST /v1/runs` 与 tasks 域共用
10
- * `ctx.legs.prepareSpec`(同一条读体/校验/建 spec 的前段)。
11
- *
12
- * 可变状态全部来自跨域 registry(`idemCache` 幂等、`inflightRuns`/`preemptableRuns` 中止句柄、
13
- * `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
14
- */
15
1
  import type { IncomingMessage, ServerResponse } from "node:http";
16
2
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
17
3
  import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";