@sema-agent/server 7.140.0 → 7.141.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 (200) hide show
  1. package/MIGRATION.md +290 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +75 -13
  5. package/dist/approval-batch-census.d.ts +27 -0
  6. package/dist/approval-batch-census.js +27 -0
  7. package/dist/approval-card.d.ts +31 -16
  8. package/dist/approval-card.js +6 -3
  9. package/dist/approval.d.ts +15 -0
  10. package/dist/approval.js +10 -0
  11. package/dist/audit.d.ts +4 -2
  12. package/dist/audit.js +2 -2
  13. package/dist/boot/device-lane.d.ts +11 -0
  14. package/dist/boot/device-lane.js +16 -0
  15. package/dist/boot/engine-lease.d.ts +24 -3
  16. package/dist/boot/engine-lease.js +9 -2
  17. package/dist/boot/execution-env.js +8 -4
  18. package/dist/boot/memory-boundary.d.ts +2 -2
  19. package/dist/boot/memory-consolidation.d.ts +1 -1
  20. package/dist/boot/memory-consolidation.js +1 -1
  21. package/dist/boot/reapers.d.ts +2 -2
  22. package/dist/boot/resolve-spec.js +7 -4
  23. package/dist/boot/session-faces.d.ts +1 -1
  24. package/dist/boot/session-faces.js +4 -3
  25. package/dist/boot/shutdown.js +5 -0
  26. package/dist/boot/stage-05-execution-env.js +3 -1
  27. package/dist/boot/stage-06-runners.d.ts +0 -2
  28. package/dist/boot/stage-06-runners.js +1 -1
  29. package/dist/boot/stage-07-capability-layer.d.ts +0 -1
  30. package/dist/boot/stage-07-capability-layer.js +8 -10
  31. package/dist/boot/stage-08-reapers.d.ts +0 -1
  32. package/dist/boot/stage-09-leader.d.ts +0 -1
  33. package/dist/boot/stage-10-http-server.d.ts +0 -1
  34. package/dist/boot/stage-10-http-server.js +1 -0
  35. package/dist/boot/stores.js +10 -4
  36. package/dist/boot/web-admin.js +1 -0
  37. package/dist/capabilities/scenarios.d.ts +1 -1
  38. package/dist/capability-facts.d.ts +9 -0
  39. package/dist/capability-facts.js +9 -0
  40. package/dist/card-void.d.ts +38 -0
  41. package/dist/card-void.js +23 -0
  42. package/dist/config-catalog.js +11 -8
  43. package/dist/config-provider.d.ts +13 -13
  44. package/dist/config-provider.js +19 -33
  45. package/dist/config-types.d.ts +26 -8
  46. package/dist/config.js +55 -46
  47. package/dist/delegated-session-owner.d.ts +21 -19
  48. package/dist/delegated-session-owner.js +1 -1
  49. package/dist/device-store.d.ts +7 -0
  50. package/dist/device-store.js +3 -0
  51. package/dist/execution-config-root.d.ts +22 -0
  52. package/dist/execution-config-root.js +35 -0
  53. package/dist/execution-lane-caps.d.ts +49 -2
  54. package/dist/execution-lane-caps.js +90 -13
  55. package/dist/execution-placement.d.ts +3 -0
  56. package/dist/execution-placement.js +7 -0
  57. package/dist/fleet/drain-entry.d.ts +3 -0
  58. package/dist/hosted-posture.d.ts +8 -0
  59. package/dist/hosted-posture.js +4 -0
  60. package/dist/http/admission.js +3 -3
  61. package/dist/http/caller-trust.d.ts +3 -2
  62. package/dist/http/caller-trust.js +2 -1
  63. package/dist/http/dispatch.js +4 -0
  64. package/dist/http/live-frame.d.ts +24 -0
  65. package/dist/http/live-frame.js +16 -0
  66. package/dist/http/resume-legs.js +37 -20
  67. package/dist/http/route-table.js +2 -0
  68. package/dist/http/routes/approvals-assistant.js +1 -0
  69. package/dist/http/routes/capabilities.js +5 -4
  70. package/dist/http/routes/diagnostics.d.ts +0 -19
  71. package/dist/http/routes/diagnostics.js +16 -2
  72. package/dist/http/routes/memory-bundle.d.ts +2 -2
  73. package/dist/http/routes/memory-bundle.js +1 -1
  74. package/dist/http/routes/memory-compliance.js +1 -1
  75. package/dist/http/routes/memory-policy.js +2 -2
  76. package/dist/http/routes/run-permission-mode.d.ts +75 -0
  77. package/dist/http/routes/run-permission-mode.js +170 -0
  78. package/dist/http/routes/session-sync.d.ts +3 -6
  79. package/dist/http/routes/session-sync.js +7 -2
  80. package/dist/http/routes/sessions.js +5 -4
  81. package/dist/http/routes/tasks.js +63 -66
  82. package/dist/http/run-read-face.js +3 -3
  83. package/dist/http/send.d.ts +1 -1
  84. package/dist/http/send.js +2 -2
  85. package/dist/http/server.d.ts +9 -2
  86. package/dist/http/sse-log.d.ts +3 -8
  87. package/dist/http/stored-body-replay.d.ts +3 -3
  88. package/dist/http/stored-body-replay.js +3 -1
  89. package/dist/instruction-source-notice.d.ts +7 -0
  90. package/dist/instruction-source-notice.js +6 -6
  91. package/dist/mcp-assertion-header.d.ts +4 -0
  92. package/dist/mcp-assertion-header.js +5 -0
  93. package/dist/memory-posture.d.ts +1 -1
  94. package/dist/memory-scope.d.ts +4 -4
  95. package/dist/memory-scope.js +1 -1
  96. package/dist/memory-sync-client.d.ts +14 -2
  97. package/dist/memory-sync-client.js +80 -18
  98. package/dist/observability/fail-open.d.ts +30 -2
  99. package/dist/observability/fail-open.js +30 -2
  100. package/dist/observability/metrics.js +0 -1
  101. package/dist/observability/run-terminal.d.ts +6 -2
  102. package/dist/observability/run-terminal.js +25 -26
  103. package/dist/observability/secret-env-scrub.d.ts +20 -1
  104. package/dist/observability/secret-env-scrub.js +14 -0
  105. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
  106. package/dist/orchestration/workflow-completion-inbox.js +13 -5
  107. package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
  108. package/dist/orchestration/workflow-notify-journal.js +2 -1
  109. package/dist/permission-mode-admission.d.ts +68 -0
  110. package/dist/permission-mode-admission.js +23 -0
  111. package/dist/permission-rule-list.d.ts +55 -6
  112. package/dist/permission-rule-list.js +52 -5
  113. package/dist/plugins/caching-session-store.d.ts +6 -0
  114. package/dist/plugins/caching-session-store.js +3 -0
  115. package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
  116. package/dist/plugins/checkpoint-store-sql.js +74 -3
  117. package/dist/plugins/device-store-sql.d.ts +5 -2
  118. package/dist/plugins/device-store-sql.js +4 -1
  119. package/dist/plugins/execution-placement-sql.d.ts +11 -1
  120. package/dist/plugins/execution-placement-sql.js +12 -1
  121. package/dist/plugins/file-run-store.d.ts +2 -2
  122. package/dist/plugins/file-run-store.js +1 -1
  123. package/dist/plugins/fork-routing-session-store.d.ts +10 -32
  124. package/dist/plugins/fork-routing-session-store.js +2 -61
  125. package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
  126. package/dist/plugins/k8s-exec-protocol.js +6 -1
  127. package/dist/plugins/local-checkpoint-store.d.ts +10 -1
  128. package/dist/plugins/local-checkpoint-store.js +22 -0
  129. package/dist/plugins/local-session-store.d.ts +227 -35
  130. package/dist/plugins/local-session-store.js +518 -37
  131. package/dist/plugins/local-session-window.d.ts +17 -3
  132. package/dist/plugins/local-session-window.js +7 -13
  133. package/dist/plugins/memory-run-store.d.ts +2 -2
  134. package/dist/plugins/memory-run-store.js +1 -1
  135. package/dist/plugins/one-time-migrations.d.ts +42 -2
  136. package/dist/plugins/one-time-migrations.js +65 -3
  137. package/dist/plugins/pg-pool.js +3 -2
  138. package/dist/plugins/pg-session-storage.d.ts +2 -0
  139. package/dist/plugins/pg-session-storage.js +3 -2
  140. package/dist/plugins/remote-env-e2b.d.ts +24 -3
  141. package/dist/plugins/remote-env-e2b.js +131 -18
  142. package/dist/plugins/remote-env-k8s.d.ts +20 -1
  143. package/dist/plugins/remote-env-k8s.js +148 -25
  144. package/dist/plugins/remote-env-local-docker.d.ts +25 -11
  145. package/dist/plugins/remote-env-local-docker.js +87 -21
  146. package/dist/plugins/run-store-sql.d.ts +3 -3
  147. package/dist/plugins/run-store-sql.js +4 -3
  148. package/dist/plugins/sandbox-read-floor.d.ts +273 -0
  149. package/dist/plugins/sandbox-read-floor.js +608 -0
  150. package/dist/plugins/session-ownership.d.ts +2 -1
  151. package/dist/plugins/session-placement.d.ts +17 -4
  152. package/dist/plugins/session-placement.js +2 -1
  153. package/dist/plugins/session-store.d.ts +18 -20
  154. package/dist/plugins/session-store.js +4 -12
  155. package/dist/plugins/store-backend.d.ts +31 -0
  156. package/dist/plugins/store-backend.js +21 -4
  157. package/dist/plugins/store-contracts.d.ts +9 -2
  158. package/dist/plugins/tidb-pool.js +2 -1
  159. package/dist/plugins/tidb-session-store.d.ts +4 -1
  160. package/dist/plugins/tidb-session-store.js +3 -2
  161. package/dist/run-cancel-context.d.ts +34 -0
  162. package/dist/run-cancel-context.js +7 -0
  163. package/dist/runs.d.ts +39 -12
  164. package/dist/runs.js +68 -53
  165. package/dist/security.d.ts +2 -2
  166. package/dist/server-secret-env.d.ts +2 -1
  167. package/dist/server-secret-env.js +2 -4
  168. package/dist/session-error-wire.d.ts +22 -4
  169. package/dist/session-error-wire.js +16 -0
  170. package/dist/session-incarnation.d.ts +9 -3
  171. package/dist/session-sync-kernel.d.ts +42 -1
  172. package/dist/session-sync-kernel.js +17 -0
  173. package/dist/session-sync.js +16 -4
  174. package/dist/sql-engine-coordinates.d.ts +49 -0
  175. package/dist/sql-engine-coordinates.js +27 -0
  176. package/dist/standing-answer-grant.d.ts +31 -2
  177. package/dist/standing-answer-grant.js +14 -2
  178. package/dist/store-posture.d.ts +1 -1
  179. package/dist/task-a2a.d.ts +1 -1
  180. package/dist/task-mcp.d.ts +1 -1
  181. package/dist/task-settings.d.ts +52 -25
  182. package/dist/task-settings.js +91 -28
  183. package/dist/terminal.d.ts +77 -20
  184. package/dist/terminal.js +66 -20
  185. package/dist/trace/core-keyset-guard.d.ts +79 -2
  186. package/dist/trace/exit-seat.d.ts +7 -0
  187. package/dist/trace/exit-seat.js +1 -0
  188. package/dist/trace/model-segment.d.ts +2 -2
  189. package/dist/trace/redact.js +323 -90
  190. package/dist/trace/self-minted-wire.d.ts +168 -2
  191. package/dist/trace/self-minted-wire.js +63 -0
  192. package/dist/verifier-pause-card.d.ts +8 -0
  193. package/dist/verifier-pause-card.js +19 -0
  194. package/dist/web-admin-client/apply-effective.d.ts +5 -7
  195. package/dist/web-admin-client/apply-effective.js +2 -2
  196. package/dist/web-admin-client/candidate-gate.d.ts +1 -1
  197. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  198. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
  199. package/package.json +2 -2
  200. package/wire-changes/server@7.141.0.json +367 -0
@@ -73,25 +73,6 @@ export interface StaticWiringFacts {
73
73
  /** store backend 的种类;`undefined` = env-only worker(无 backend)。 */
74
74
  backendKind: "mysql" | "pg" | "local" | undefined;
75
75
  }
76
- /**
77
- * 三项对表(设计稿件二)。判据全是**两个独立谓词对同一件事的答案**,矛盾即装配谎言 —— 装配谎言不是
78
- * 「配置不理想」,是「服务端对自己接了什么线的两套说法互斥」,继续启动只会把矛盾带进运行期的每一条腿。
79
- *
80
- * ① park 设施:core 从 `spec.checkpointStore ?? deps.checkpointStore` 判 `parkLane.capable`,我方从
81
- * `backend.checkpoint ∧ DURABLE_APPROVAL` 判 `parkFacility` 并据此决定流内审批协议上不上场。两者
82
- * 不等 ⇒ 协议按「窗到期能 park」上场而引擎侧根本不 park(结局是 core fail-closed deny),或反之
83
- * 白白关掉一个可用的协议。`capable` 真而 `effective` 不真同样致命:能力在、这条部署形上却不会真的
84
- * park,窗到期依旧无降级目的地。
85
- * ② 会话店持久性:manifest 声明 `declared_durable` 而后端是 local / 根本没有后端 ⇒ 「重启存活」的声明
86
- * 没有介质兑现。这条与 memory+durable 两条既有拒启同族(声明与介质必须对得上)。
87
- * ③ interaction posture 声明 `interactive` 却没有任何 question 投递面 ⇒ 只 **warn**:core 在 prepare 时
88
- * 会按腿拒,boot 先一步把话说成人话。不拒启,因为部署形本身可能只是暂时没接活体面,而 core 侧已有
89
- * 兜底门 —— 拒启会把一个可运行的部署挡在门外。
90
- * ④ park 车道的持久性读数不可信时 ⇒ **warn**(理由与这条判据为什么不是拒启,见其行内注)。
91
- *
92
- * 判决分两级是有判据的:**拒启**留给「两个谓词对同一件事互斥」(其中一个必错,继续跑就是带着谎言服务);
93
- * **警告**留给「读数不可信/声明不完整」(信号本身还不足以断定谁错,据此拒启会挡住正常部署)。
94
- */
95
76
  export declare function buildStaticWiringAudit(facts: StaticWiringFacts): StaticWiringAudit;
96
77
  /**
97
78
  * boot 消费口:`refusals` 非空即抛(拒启,与 memory+durable 两条既有拒启同族的 fail-loud 形),
@@ -43,6 +43,20 @@ export function buildServerWiringGates(input) {
43
43
  approvalGrant: [...input.approvalGrant],
44
44
  };
45
45
  }
46
+ function durableSessionMedium(kind) {
47
+ switch (kind) {
48
+ case "mysql":
49
+ case "pg":
50
+ case "local":
51
+ return true;
52
+ case undefined:
53
+ return false;
54
+ default: {
55
+ const unknownKind = kind;
56
+ throw new Error(`durableSessionMedium: no ruling for store backend kind ${JSON.stringify(unknownKind)}`);
57
+ }
58
+ }
59
+ }
46
60
  export function buildStaticWiringAudit(facts) {
47
61
  const refusals = [];
48
62
  const warnings = [];
@@ -56,9 +70,9 @@ export function buildStaticWiringAudit(facts) {
56
70
  refusals.push(`park facility contradiction: this server assembled its approval gates with parkFacility=true, but the engine's static wiring resolves parkLane.effective=${JSON.stringify(park.effective)} (reasons: ${park.reasons.join(", ") || "none"}). ` +
57
71
  `A window expiry would then have no degradation target and adjudicate as a fail-closed deny — refusing to start.`);
58
72
  }
59
- if (facts.manifest.session.store === "declared_durable" && facts.backendKind !== "mysql" && facts.backendKind !== "pg") {
73
+ if (facts.manifest.session.store === "declared_durable" && !durableSessionMedium(facts.backendKind)) {
60
74
  refusals.push(`session store durability contradiction: the engine's static wiring reports session.store="declared_durable" while the store backend is ${facts.backendKind === undefined ? "absent (env-only worker)" : `"${facts.backendKind}"`}. ` +
61
- `A durable declaration with no durable medium is a claim nothing can honor — refusing to start. Wire DB_BACKEND=mysql|pg, or fix the store's durability declaration.`);
75
+ `A durable declaration with no durable medium is a claim nothing can honor — refusing to start. Wire DB_BACKEND=mysql|pg|local, or fix the store's durability declaration.`);
62
76
  }
63
77
  if (facts.parkFacility && facts.manifest.parkLane.checkpointDurability !== "declared_durable") {
64
78
  warnings.push(`park facility is enabled but the checkpoint store does not declare durable storage ` +
@@ -20,12 +20,12 @@
20
20
  * 不该从「这个部署有没有记忆引擎」上读出部署形态。
21
21
  *
22
22
  * ── 能力面的诚实形(与 GET 面**方向相反**,这不是笔误)──────────────────────────────────────
23
- * `GET /v1/memory/export` 要的是 **DB 记忆平面**(pg|tidb),file 形态 501。本面要的是 core 的
23
+ * `GET /v1/memory/export` 要的是 **DB 记忆平面**(pg|mysql),file 形态 501。本面要的是 core 的
24
24
  * **bundle 复合面**(`exportSnapshotOf` / `importBundleCommit`),而它今天只在 core 自带的 **File**
25
25
  * 后端上实装:本仓的两只 SQL 记忆孪生(plugins/memory-engine-{pg,tidb}.ts)没有实现这两个可选面。
26
26
  * 因此**两个成因同落 501** `capability.memory_engine_required`(都是「换部署形态才行」,消费端分支相同):
27
27
  * · 记忆引擎整个没接线(`MEMORY_ENGINE=off` / 多租 file 底座等暗态);
28
- * · 引擎接了但后端没有 bundle 复合面(pg/tidb 孪生)—— 这一支在**挂载期**就判掉了
28
+ * · 引擎接了但后端没有 bundle 复合面(pg/mysql 孪生)—— 这一支在**挂载期**就判掉了
29
29
  * (`createMemoryBundleFaces` 返回 undefined ⇒ 两口整个不挂),能力位 `memoryBundle` 因此与「路由真能用」
30
30
  * 严格同真。🔴 首版把它当运行期事实(照常挂载、让 core 每次抛 409/422),被 codex 交叉复审驳倒并改此形:
31
31
  * 面在不在是一次**属性检查**、boot 期判得出、且是**永久**缺席 ⇒ 照常挂着就是一个恒假的 yes,
@@ -83,7 +83,7 @@ export async function handleMemoryBundle(req, res, match, ctx) {
83
83
  if (!fact.available) {
84
84
  switch (fact.why) {
85
85
  case "memory_bundle_faces_absent":
86
- sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/tidb memory backends do not");
86
+ sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/mysql memory backends do not");
87
87
  return;
88
88
  case "operator_principals_empty":
89
89
  sendOperatorPrincipalsRequired(res);
@@ -39,7 +39,7 @@ export async function handleMemoryCompliance(req, res, match, ctx) {
39
39
  if (!fact.available) {
40
40
  switch (fact.why) {
41
41
  case "memory_compliance_faces_absent":
42
- sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
42
+ sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
43
43
  return;
44
44
  case "operator_principals_empty":
45
45
  sendOperatorPrincipalsRequired(res);
@@ -74,7 +74,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
74
74
  return;
75
75
  }
76
76
  if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
77
- sendError(res, 501, "capability.memory_store_required", "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
77
+ sendError(res, 501, "capability.memory_store_required", "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|mysql)");
78
78
  return;
79
79
  }
80
80
  const q = new URLSearchParams(match.search);
@@ -136,7 +136,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
136
136
  return;
137
137
  }
138
138
  if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
139
- sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
139
+ sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|mysql)");
140
140
  return;
141
141
  }
142
142
  const scope = safeDecode(match.params.scope);
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 在飞 run 的**权限模式切档口** —— 引擎动词 `TaskStream.setPermissionMode(word)` 的 wire 面:
3
+ *
4
+ * `POST /v1/runs/:id/permission-mode` body `{ mode }`(`mode` ∈ 引擎 `PERMISSION_MODES` 闭集)
5
+ * → 200 `{ taskId, sessionId, status: "running", permissionMode, admission?, persisted, persistMiss? }`
6
+ *
7
+ * 语义 = 引擎的切档:改写这条腿的请求词,部署的折叠(`RuntimeCaps` 拒位 / 分类器席缺席)由引擎重判,**下一次**工具调用按新档仲裁;
8
+ * 进 `auto` 当场武装分类器,武装失败 ⇒ 引擎回滚(被替换的词照旧)并拒。`permissionMode` 是引擎回执原样(`{ requested?, effective, foldedBy? }`,
9
+ * 与 `wiring_manifest.permissionMode` 同形):每次都回,动没动都回 —— 壳渲染引擎说的,不渲染它自己发的。
10
+ *
11
+ * ── 一条规则:切档口与提交口是同一只受理判官(`permission-mode-admission.ts`)───────────────────────────────
12
+ * 交给引擎的不是调用方的原词,是受理判官对它的答:本任务 settings 的 kill-switch 折叠(读这条 run 的**存储体**)→ 运维 `AUTONOMY` 入口地板钳
13
+ * → 托管写围栏拒。提交口在受理时拒过 / 折过的词,切档口同样拒 / 折(两口一张表的钉在 `test/run-permission-mode-admission.test.ts`);
14
+ * 引擎在根腿上没有这三问的拒位,所以它们必须在这里答。折了 ⇒ 回执旁带 `admission: { requested, admitted, foldedBy }`(present-iff:本判官改了词);
15
+ * 拒 ⇒ 422 `request.host_posture_refused`(与提交口同码同句)。
16
+ *
17
+ * ── 回写:准入的词写回存储体(run 模式词的唯一来源)────────────────────────────────────────────────────────
18
+ * 续跑腿(停驻后续跑 / 重启后续跑)按存储体顶层 `permissionMode` 重建 spec;不写回 = 下一腿回到切档前的词。写回走**认领栅栏**
19
+ * (checkpoint 店的 `rewriteCtxWhileClaimed`:只在**接下切档的那条腿**此刻仍持有这条 run 时写,只改既有行)—— 这条 run 终局放掉认领、同会话新 run
20
+ * 已写下它自己的存储体之后,或这条 run 停驻、续跑的新腿(同一个 taskId 回到 running、认领仍在)已写下它续跑用的体之后,迟到的回写零写,
21
+ * 不会拿旧腿的体盖掉新腿的。腿身份 = {@link switchLegHolds}(栅栏里问)。同一条腿上的切档在本进程按活流对象串行(引擎受理序 = 落盘序;活流只在本副本)。
22
+ * 写不进(`persisted:false` + `persistMiss` ∈ {@link PERMISSION_MODE_PERSIST_MISSES})⇒ 本腿的切档照样生效、下一腿读到的是切档前的词:
23
+ * 一行运维 warn + fail-open 计数(`server.run-permission-mode.write-back-unpersisted`),壳据 `persisted` 判要不要在续跑后重切。
24
+ *
25
+ * ── 门(与 steer 同一组)──────────────────────────────────────────────────────────────────────────────
26
+ * 零模型工作(不进 billable);mutating ⇒ `rateLimited`。属主门与 steer / interrupt 同一组放行臂(行经属主存取器 `readOwnedRun` 取,`owner_or_operator`):
27
+ * 显式 operator 可切任一租户的 run(监督动作),普通调用方只切自己的(非属主 404 同串,不给存在性 oracle);`?session=` 的受理期断言同族。非 live 四臂一码 409 `steering.not_running`
28
+ * (停驻 / 终局 / 他副本 / 本副本无活流);坏词 422 `steering.invalid_content`(引擎同码,受理前判)。能力位 `permissionModeWrite`
29
+ * = 事实 `permissionModeWrite`(持久 run 账面 ∧ checkpoint 店),与两道 501 同一只。
30
+ */
31
+ import type { IncomingMessage, ServerResponse } from "node:http";
32
+ import type { PermissionMode, PermissionModeReceipt } from "@sema-agent/core";
33
+ import { type FencedRun } from "../../plugins/store-contracts.js";
34
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
35
+ import { type PermissionModeAdmissionFold } from "../../permission-mode-admission.js";
36
+ /** 回写没落定的成因(闭集;wire `persistMiss`):`claim_released` 接下切档的那条腿这一刻已不持有这条 run(终局 / 停驻 / 已由续跑的新腿接手);
37
+ * `context_absent` 持有但会话没有存储体行;`store_failed` 店写抛(库抖 / 一行装不下 / 封签失败)。 */
38
+ export declare const PERMISSION_MODE_PERSIST_MISSES: readonly ["claim_released", "context_absent", "store_failed"];
39
+ export type PermissionModePersistMiss = (typeof PERMISSION_MODE_PERSIST_MISSES)[number];
40
+ /**
41
+ * **接下切档的那条腿此刻是否仍持有这条 run** —— 回写的腿身份,在认领栅栏里问(`rewriteCtxWhileClaimed` 的 `legHolds`,锁住 / 同步段里那一行的 {@link FencedRun})。
42
+ * 认领与 `running` 只认 taskId,续跑腿让同一个 taskId 停驻之后回到 `running`、认领仍在(ABA);分开两条腿的是:
43
+ * · run 行仍在本副本(`runProvenElsewhere` 的反面 —— 别的副本续跑了它,续跑的 CAS 赢点把行上的副本改成它自己);
44
+ * · 本副本这条 run 此刻登记的活流仍是接下切档的那一只(本副本上续跑的新腿登记的是另一只流对象;腿收场时按对象身份摘掉自己的)。
45
+ */
46
+ export declare function switchLegHolds<S extends object>(fenced: FencedRun, thisInstanceId: string | undefined, liveNow: S | undefined, switched: S): boolean;
47
+ /** 本判官改了词时回执旁的那一格(`admission`)。 */
48
+ interface PermissionModeAdmissionNote {
49
+ readonly requested: PermissionMode;
50
+ readonly admitted: PermissionMode;
51
+ readonly foldedBy: PermissionModeAdmissionFold;
52
+ }
53
+ /** 200 体(键集属主:`trace/self-minted-wire.ts` 的 `permissionModeVerbBodyKey` 按它的键 `satisfies`)。`permissionMode` = 引擎回执原样。 */
54
+ export interface PermissionModeVerbReply {
55
+ readonly taskId: string;
56
+ readonly sessionId: string;
57
+ readonly status: "running";
58
+ readonly permissionMode: PermissionModeReceipt;
59
+ /** present-iff 受理判官改了词。 */
60
+ readonly admission?: PermissionModeAdmissionNote;
61
+ readonly persisted: boolean;
62
+ /** present-iff `persisted: false`。 */
63
+ readonly persistMiss?: PermissionModePersistMiss;
64
+ }
65
+ export declare function handlePermissionMode(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RunPermissionModeRouteId>, ctx: RouteCtx): Promise<void>;
66
+ export declare const RUN_PERMISSION_MODE_ROUTES: readonly [{
67
+ readonly id: "run-permission-mode";
68
+ readonly pattern: RegExp;
69
+ readonly label: "/v1/runs/:id/permission-mode";
70
+ readonly methods: readonly ["POST"];
71
+ }];
72
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
73
+ export type RunPermissionModeRouteId = RouteIdsOf<typeof RUN_PERMISSION_MODE_ROUTES>;
74
+ export {};
75
+ //# sourceMappingURL=run-permission-mode.d.ts.map
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+ import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
3
+ import { sendJson, sendError } from "../send.js";
4
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { readOwnedRun } from "../owner-access.js";
6
+ import { runNotFoundMessage } from "../route-ctx.js";
7
+ import { assertNever } from "../route-ctx.js";
8
+ import * as facts from "../../capability-facts.js";
9
+ import { PERMISSION_MODE_WORDS, coercePermissionModeStrict, parseTaskSettings } from "../../task-settings.js";
10
+ import { admitPermissionModeWord } from "../../permission-mode-admission.js";
11
+ import { readResumeCtx, resumeCtxCorruptAnswer, resumeCtxUnsealableAnswer } from "../resume-ctx-read.js";
12
+ import { sealCheckpointCtx, withPermissionModeRewritten } from "../checkpoint-ctx.js";
13
+ import { serviceVersion } from "../../service-version.js";
14
+ import { recordFailOpen } from "../../observability/fail-open.js";
15
+ export const PERMISSION_MODE_PERSIST_MISSES = ["claim_released", "context_absent", "store_failed"];
16
+ const BodySchema = z.object({ mode: z.string() }).strict();
17
+ const switchChains = new WeakMap();
18
+ function serialOnStream(stream, work) {
19
+ const prior = switchChains.get(stream) ?? Promise.resolve();
20
+ const job = prior.then(work, work);
21
+ switchChains.set(stream, job.catch(() => undefined));
22
+ return job;
23
+ }
24
+ export function switchLegHolds(fenced, thisInstanceId, liveNow, switched) {
25
+ return !runProvenElsewhere(fenced, thisInstanceId) && liveNow === switched;
26
+ }
27
+ export async function handlePermissionMode(req, res, match, ctx) {
28
+ const { deps } = ctx;
29
+ switch (match.decl.id) {
30
+ case "run-permission-mode": {
31
+ if (ctx.helpers.rateLimited(req, res))
32
+ return;
33
+ const seat = facts.permissionModeWrite(deps);
34
+ if (!seat.available) {
35
+ switch (seat.why) {
36
+ case "run_store_absent":
37
+ sendError(res, 501, "capability.run_store_required", "changing a running run's permission mode requires a durable run store (DB_BACKEND=mysql|pg|local)");
38
+ return;
39
+ case "checkpoint_store_absent":
40
+ sendError(res, 501, "capability.checkpoint_store_required", "changing a running run's permission mode requires the checkpoint store (DURABLE_APPROVAL): the run's stored request is where its permission mode lives and where the task's own settings are read");
41
+ return;
42
+ default:
43
+ return assertNever(seat.why);
44
+ }
45
+ }
46
+ const { runStore, checkpointStore } = seat;
47
+ const principal = gatedPrincipal(req, deps.config);
48
+ if (deps.config.requirePrincipal && principal === undefined) {
49
+ sendPrincipalRefusal(res, req, deps.config);
50
+ return;
51
+ }
52
+ const taskId = match.params.taskId;
53
+ const parsed = BodySchema.safeParse((await ctx.helpers.readJson(req)) ?? {});
54
+ if (!parsed.success) {
55
+ sendError(res, 400, "request.body_shape", `body must be { mode: ${PERMISSION_MODE_WORDS} } — the permission-mode verb takes no other keys`);
56
+ return;
57
+ }
58
+ const word = coercePermissionModeStrict(parsed.data.mode);
59
+ if (word === undefined) {
60
+ sendError(res, 422, "steering.invalid_content", `mode must be one of ${PERMISSION_MODE_WORDS} (got ${JSON.stringify(parsed.data.mode.slice(0, 64))})`);
61
+ return;
62
+ }
63
+ const run = await readOwnedRun(deps.config, runStore, principal, taskId, "owner_or_operator");
64
+ if (!run) {
65
+ sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
66
+ return;
67
+ }
68
+ if (!ctx.helpers.runSessionAcceptOk(req, res, run, "run.permission_mode"))
69
+ return;
70
+ const live = ctx.registry.steerableRuns.get(taskId);
71
+ if (!live) {
72
+ if (isParkedRunStatus(run.status)) {
73
+ sendError(res, 409, "steering.not_running", "run is waiting on a pending decision — the permission-mode verb changes a running run; when it resumes, its mode is rebuilt from the run's stored request (a plan review's approval names the mode it continues in)");
74
+ return;
75
+ }
76
+ if (isTerminalRunStatus(run.status)) {
77
+ sendError(res, 409, "steering.not_running", "run already ended — the permission-mode verb changes a running run; submit the next turn of this session with the permissionMode you want");
78
+ return;
79
+ }
80
+ if (runProvenElsewhere(run, deps.instanceId)) {
81
+ sendError(res, 409, "steering.not_running", "run is live on another replica — the permission-mode verb must reach the replica holding the stream; resend it there");
82
+ return;
83
+ }
84
+ sendError(res, 409, "steering.not_running", "no live steerable face for this run on this replica — a verify or cascade submission exposes none, so the permission-mode verb has nothing to switch");
85
+ return;
86
+ }
87
+ const sessionId = run.sessionId;
88
+ const out = await serialOnStream(live, async () => {
89
+ const read = await readResumeCtx(checkpointStore.getCtx(sessionId), { sessionId, opener: deps.checkpointCtxSealing });
90
+ if (read.kind === "corrupt" || read.kind === "unsealable") {
91
+ const { status, body: { error, errorCode, ...facts } } = read.kind === "corrupt" ? resumeCtxCorruptAnswer(read.where) : resumeCtxUnsealableAnswer(read);
92
+ sendError(res, status, errorCode, error, facts);
93
+ return { kind: "answered" };
94
+ }
95
+ if (read.kind === "absent") {
96
+ sendError(res, 409, "conflict.resume_context_unavailable", "this run's stored request is missing — its permission mode cannot be changed (the task's settings are read from it and the new mode is written back to it)");
97
+ return { kind: "answered" };
98
+ }
99
+ const stored = read.ctx;
100
+ const admission = admitPermissionModeWord(word, { deployment: deps.config, settings: parseTaskSettings(stored.body.settings).settings });
101
+ if (admission.kind === "refused") {
102
+ sendError(res, admission.status, admission.code, admission.message);
103
+ return { kind: "answered" };
104
+ }
105
+ const admitted = admission.admitted ?? word;
106
+ let receipt;
107
+ try {
108
+ receipt = await live.setPermissionMode(admitted);
109
+ }
110
+ catch (e) {
111
+ const code = e.code;
112
+ if (code === "steering.invalid_content") {
113
+ sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : String(e));
114
+ return { kind: "answered" };
115
+ }
116
+ if (code === "steering.not_running") {
117
+ sendError(res, 409, "steering.not_running", "run just finished — the permission-mode verb needs a running run; submit the next turn of this session with the permissionMode you want");
118
+ return { kind: "answered" };
119
+ }
120
+ throw e;
121
+ }
122
+ let persistMiss;
123
+ try {
124
+ const next = sealCheckpointCtx({ ...withPermissionModeRewritten(stored, { ...stored.body, permissionMode: admitted }), writerVersion: serviceVersion() }, deps.checkpointCtxSealing, sessionId);
125
+ const rewrite = await checkpointStore.rewriteCtxWhileClaimed(sessionId, taskId, next, (fenced) => switchLegHolds(fenced, deps.instanceId, ctx.registry.steerableRuns.get(taskId), live));
126
+ persistMiss = rewrite === "written" ? undefined : rewrite;
127
+ }
128
+ catch (e) {
129
+ persistMiss = "store_failed";
130
+ deps.logger?.warn?.("run_permission_mode_write_back_failed", { taskId, sessionId, err: e instanceof Error ? e.message : String(e) });
131
+ }
132
+ if (persistMiss !== undefined) {
133
+ recordFailOpen("server.run-permission-mode.write-back-unpersisted", `miss=${persistMiss} mode=${admitted}`);
134
+ deps.logger?.warn?.("run_permission_mode_unpersisted", { taskId, sessionId, mode: admitted, miss: persistMiss });
135
+ }
136
+ return {
137
+ kind: "switched",
138
+ receipt,
139
+ ...(admission.foldedBy !== undefined ? { admission: { requested: word, admitted, foldedBy: admission.foldedBy } } : {}),
140
+ ...(persistMiss !== undefined ? { persistMiss } : {}),
141
+ };
142
+ });
143
+ switch (out.kind) {
144
+ case "answered":
145
+ return;
146
+ case "switched": {
147
+ const reply = {
148
+ taskId,
149
+ sessionId,
150
+ status: "running",
151
+ permissionMode: out.receipt,
152
+ ...(out.admission !== undefined ? { admission: out.admission } : {}),
153
+ persisted: out.persistMiss === undefined,
154
+ ...(out.persistMiss !== undefined ? { persistMiss: out.persistMiss } : {}),
155
+ };
156
+ sendJson(res, 200, reply);
157
+ return;
158
+ }
159
+ default:
160
+ return assertNever(out);
161
+ }
162
+ }
163
+ default:
164
+ return assertNever(match.decl.id);
165
+ }
166
+ }
167
+ export const RUN_PERMISSION_MODE_ROUTES = [
168
+ { id: "run-permission-mode", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/permission-mode$/, label: "/v1/runs/:id/permission-mode", methods: ["POST"] },
169
+ ];
170
+ //# sourceMappingURL=run-permission-mode.js.map
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { IncomingMessage, ServerResponse } from "node:http";
11
11
  import { type SessionRulesRecord, type FileHistoryExport } from "@sema-agent/core";
12
- import { type StagingHandle } from "../../session-sync-kernel.js";
12
+ import { type ImportAnchor, type StagingHandle } from "../../session-sync-kernel.js";
13
13
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
14
14
  import { type SessionRowPatch } from "../../session-row.js";
15
15
  export type PendingStaging = {
@@ -21,11 +21,8 @@ export type PendingStaging = {
21
21
  /** [ref]: the session's file-history envelope (or null — no history travelled). Replaces the E19 per-key manifests. */
22
22
  fileHistory: FileHistoryExport | null;
23
23
  policy: SessionRulesRecord[];
24
- anchors: Array<{
25
- eventId: string;
26
- entryId: string;
27
- owner: string | null;
28
- }>;
24
+ /** 已在 Phase A 入口判过的锚(`readImportAnchors`:每条 `entryId` 都在 `declaredEntryIds` 里)—— 提交后的重放腿只迭代它,不再判;owner 不带(重钉到 `owner`)。 */
25
+ anchors: ImportAnchor[];
29
26
  /** 会话行的两位可写位(Phase A 体里带了哪几键就重放哪几键;老客户端不带 ⇒ `{}`,目的端两位不动)。提交后经会话店唯一写口落。 */
30
27
  row: SessionRowPatch;
31
28
  /** 交叉复查 A-3(HIGH):Phase A's DECLARED entry-id set — Phase B verifies the streamed NDJSON matches it
@@ -4,7 +4,7 @@ import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { FileHistoryUnreadableError, blobGetterOf, exportSessionManifest, exportedFileHistory, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
6
6
  import * as facts from "../../capability-facts.js";
7
- import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
7
+ import { classifySyncRelationshipByIds, readImportAnchors, SyncConflictError } from "../../session-sync-kernel.js";
8
8
  import { isUuidShape, legacyUnownedSessionOk, sessionOwnerCellOf } from "../../security.js";
9
9
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, logRequestError, msg } from "../send.js";
10
10
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
@@ -307,6 +307,11 @@ export async function handleSessionSync(req, res, match, ctx) {
307
307
  sendError(res, 400, "request.body_shape", "malformed import metadata");
308
308
  return;
309
309
  }
310
+ const anchorsRead = readImportAnchors(body.anchors, body.entryIds);
311
+ if ("problem" in anchorsRead) {
312
+ sendError(res, 400, "request.field_invalid", anchorsRead.problem);
313
+ return;
314
+ }
310
315
  const policyRead = readSessionRulesRecords(body.policy);
311
316
  if (policyRead.problem !== undefined) {
312
317
  if (policyRead.problem.kind === "rules") {
@@ -350,7 +355,7 @@ export async function handleSessionSync(req, res, match, ctx) {
350
355
  const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
351
356
  const entryIds = body.entryIds;
352
357
  const policy = policyRead.records;
353
- const anchors = body.anchors;
358
+ const anchors = anchorsRead.anchors;
354
359
  const dstIds = (await sessionStorage.listEntryIds?.(sessionId)) ?? null;
355
360
  const relA = classifySyncRelationshipByIds(entryIds, dstIds);
356
361
  if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
@@ -526,14 +526,14 @@ export async function handleSessions(req, res, match, ctx) {
526
526
  }
527
527
  const auditFact = facts.sessionAuditView(deps);
528
528
  if (!auditFact.available) {
529
- sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
529
+ sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
530
530
  return;
531
531
  }
532
532
  if ((await readOwnedSessionView(auditFact.sessionAudit, sessionId)) === undefined)
533
533
  return;
534
534
  const faces = deps.memoryCompliance;
535
535
  if (faces === undefined) {
536
- sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
536
+ sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
537
537
  return;
538
538
  }
539
539
  const raw = (await readJson(req));
@@ -585,14 +585,14 @@ export async function handleSessions(req, res, match, ctx) {
585
585
  }
586
586
  const auditFact = facts.sessionAuditView(deps);
587
587
  if (!auditFact.available) {
588
- sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
588
+ sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
589
589
  return;
590
590
  }
591
591
  if ((await readOwnedSessionView(auditFact.sessionAudit, sessionId)) === undefined)
592
592
  return;
593
593
  const face = deps.sessionMemoryStatus;
594
594
  if (face === undefined) {
595
- sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
595
+ sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
596
596
  return;
597
597
  }
598
598
  const query = new URLSearchParams(match.search);
@@ -619,6 +619,7 @@ export async function handleSessions(req, res, match, ctx) {
619
619
  sendJson(res, 200, {
620
620
  sessionId,
621
621
  autoConsolidationArmed: deps.memoryAutoConsolidationArmed,
622
+ ...(deps.memorySyncLeg !== undefined ? { memorySync: { parked: deps.memorySyncLeg.parked().map((p) => ({ scope: p.scope, wayOut: p.wayOut })) } } : {}),
622
623
  ...(s.captureOptedOut !== undefined ? { captureOptedOut: s.captureOptedOut } : {}),
623
624
  ...(s.committedCount !== undefined ? { committedCount: s.committedCount } : {}),
624
625
  ...(s.foldedCount !== undefined ? { foldedCount: s.foldedCount } : {}),