@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
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { StubExecutionEnv } from "@sema-agent/core";
3
- import { EXECUTION_LANE_WORDS as DECLARABLE_LANE_WORDS, EXECUTION_LANES, isBlankCoordinate } from "@sema-agent/settings-schema/execution-lanes";
3
+ import { EXECUTION_LANE_WORDS as DECLARABLE_LANE_WORDS, EXECUTION_LANES, isBlankCoordinate, retiredLaneWordRefusal } from "@sema-agent/settings-schema/execution-lanes";
4
4
  import { DECLARE_HOME_REMEDY, LANE_HOME_UNDECLARED_EVENT, LANE_HOME_UNMEASURED_EVENT, buildHomeAbsentNote, buildHomeUnmeasuredNote } from "./lane-home-probe.js";
5
5
  const NO = "unsupported";
6
6
  const YES = "supported";
@@ -97,27 +97,61 @@ export function executionLaneCaps(lane) {
97
97
  };
98
98
  }
99
99
  }
100
+ export const SANDBOX_READ_FLOOR_ENV = "SANDBOX_READ_FLOOR";
101
+ export const SANDBOX_READ_FLOOR_WORDS = ["keep", "off"];
102
+ export const SANDBOX_READ_FLOOR_DEFAULT = "keep";
103
+ export function readFloorStateOf(knob, served) {
104
+ switch (knob) {
105
+ case "off":
106
+ return "off";
107
+ case "keep":
108
+ return served ? "kept" : "unserved";
109
+ }
110
+ }
111
+ export function sandboxReadFloorState(lane, knob) {
112
+ const k = knob ?? SANDBOX_READ_FLOOR_DEFAULT;
113
+ switch (lane) {
114
+ case "e2b":
115
+ return readFloorStateOf(k, false);
116
+ case "k8s":
117
+ return readFloorStateOf(k, false);
118
+ case "local-docker":
119
+ return readFloorStateOf(k, false);
120
+ }
121
+ }
100
122
  const IN_PROCESS_BLOCK = new StubExecutionEnv().capabilities;
101
123
  const HOST_BLOCK = Object.freeze({ isolation: false, suspendable: false, sharesProcessTable: true, canonicalPathAuthoritative: true, hostLocalPaths: true, externalContentTarget: false, io: true, keepsReadFloor: false });
102
- const E2B_BLOCK = Object.freeze({ isolation: true, suspendable: true, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false });
103
- const K8S_BLOCK = Object.freeze({ isolation: true, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false });
104
- const K8S_SNAPSHOT_BLOCK = Object.freeze({ ...K8S_BLOCK, suspendable: true });
105
- const LOCAL_DOCKER_BLOCK = Object.freeze({ isolation: true, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false });
124
+ const E2B_SHAPE = Object.freeze({ isolation: true, suspendable: true, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true });
125
+ const K8S_SHAPE = Object.freeze({ isolation: true, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true });
126
+ const K8S_SNAPSHOT_SHAPE = Object.freeze({ ...K8S_SHAPE, suspendable: true });
127
+ const LOCAL_DOCKER_SHAPE = Object.freeze({ isolation: true, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true });
128
+ function sandboxBlock(shape, state) {
129
+ return Object.freeze({ ...shape, keepsReadFloor: state === "kept" });
130
+ }
131
+ function sandboxShape(lane, snapshotStore) {
132
+ switch (lane) {
133
+ case "e2b":
134
+ return E2B_SHAPE;
135
+ case "k8s":
136
+ return snapshotStore ? K8S_SNAPSHOT_SHAPE : K8S_SHAPE;
137
+ case "local-docker":
138
+ return LOCAL_DOCKER_SHAPE;
139
+ }
140
+ }
106
141
  const SSH_BLOCK = Object.freeze({ isolation: false, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false });
107
142
  const ADB_BLOCK = Object.freeze({ isolation: false, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false });
108
143
  const DEVICE_BLOCK = Object.freeze({ isolation: false, suspendable: false, sharesProcessTable: false, canonicalPathAuthoritative: true, hostLocalPaths: false, externalContentTarget: true, io: true, keepsReadFloor: false });
109
- function laneBlock(lane, snapshotStore) {
144
+ const NO_SUB_KNOBS = Object.freeze({ snapshotStore: false, sandboxReadFloor: undefined });
145
+ function laneBlock(lane, sub) {
110
146
  switch (lane) {
111
147
  case "in-process":
112
148
  return IN_PROCESS_BLOCK;
113
149
  case "host":
114
150
  return HOST_BLOCK;
115
151
  case "e2b":
116
- return E2B_BLOCK;
117
152
  case "k8s":
118
- return snapshotStore ? K8S_SNAPSHOT_BLOCK : K8S_BLOCK;
119
153
  case "local-docker":
120
- return LOCAL_DOCKER_BLOCK;
154
+ return sandboxBlock(sandboxShape(lane, sub.snapshotStore), sandboxReadFloorState(lane, sub.sandboxReadFloor));
121
155
  case "ssh":
122
156
  return SSH_BLOCK;
123
157
  case "adb":
@@ -130,7 +164,24 @@ export function capabilitiesFor(deployment) {
130
164
  const lane = executionLaneOf(deployment.remoteExec?.provider);
131
165
  if (lane === undefined)
132
166
  return undefined;
133
- return laneBlock(lane, lane === "k8s" && Boolean(deployment.remoteExec?.s3Snapshot));
167
+ return laneBlock(lane, { snapshotStore: lane === "k8s" && Boolean(deployment.remoteExec?.s3Snapshot), sandboxReadFloor: deployment.sandboxReadFloor });
168
+ }
169
+ export function laneSandboxReadFloorState(deployment) {
170
+ const lane = executionLaneOf(deployment.remoteExec?.provider);
171
+ if (lane === undefined)
172
+ return undefined;
173
+ switch (lane) {
174
+ case "e2b":
175
+ case "k8s":
176
+ case "local-docker":
177
+ return sandboxReadFloorState(lane, deployment.sandboxReadFloor);
178
+ case "in-process":
179
+ case "host":
180
+ case "ssh":
181
+ case "adb":
182
+ case "device":
183
+ return undefined;
184
+ }
134
185
  }
135
186
  export const ALL_EXECUTION_LANES = Object.freeze(["in-process", ...DECLARABLE_LANE_WORDS]);
136
187
  const EXECUTION_LANE_SET = new Set(ALL_EXECUTION_LANES);
@@ -166,6 +217,32 @@ export function isBlankRequiredEnvValue(value) {
166
217
  export function laneMissingEnv(lane, env) {
167
218
  return laneRequiredEnv(lane).filter((name) => isBlankRequiredEnvValue(env[name]));
168
219
  }
220
+ export function readDeclaredLaneTable(raw) {
221
+ const segments = raw.split(",");
222
+ const lanes = [];
223
+ for (const [i, segment] of segments.entries()) {
224
+ if (segment === "") {
225
+ return { refusal: `REMOTE_EXEC="${raw}" has an empty entry at position ${i + 1} — it is an ordered, comma-separated list of execution lanes (the first is the default) with no blank entries; valid lanes: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}.` };
226
+ }
227
+ const lane = REMOTE_EXEC_PROVIDER_WORDS.find((w) => w === segment);
228
+ const retired = lane === undefined ? retiredLaneWordRefusal(segment) : undefined;
229
+ if (retired !== undefined)
230
+ return { refusal: `REMOTE_EXEC="${raw}": ${retired}.` };
231
+ if (lane === undefined) {
232
+ return {
233
+ refusal: `REMOTE_EXEC="${raw}"${segments.length > 1 ? ` lists "${segment}", which` : ""} is not an execution lane — valid: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}. ` +
234
+ `An unrecognized value used to fall through to the host/in-process lane silently; refusing to start instead (#157).`,
235
+ };
236
+ }
237
+ if (lanes.includes(lane))
238
+ return { refusal: `REMOTE_EXEC="${raw}" lists "${lane}" twice — each execution lane appears once (the first one is the default).` };
239
+ lanes.push(lane);
240
+ }
241
+ const [first, ...rest] = lanes;
242
+ if (first === undefined)
243
+ throw new Error(`readDeclaredLaneTable: "${raw}" split into no segments`);
244
+ return { lanes: [first, ...rest] };
245
+ }
169
246
  export function executionLaneRequiredRefusal(deployment) {
170
247
  if (!deployment.requireExecutionLane || executionLaneOf(deployment.remoteExec?.provider) !== "in-process")
171
248
  return undefined;
@@ -189,7 +266,7 @@ export function deployedExecutionLanes(config) {
189
266
  return [executionLaneOf(config.remoteExec?.provider)];
190
267
  }
191
268
  export function handsOnWorkerFilePlane(lane) {
192
- const block = laneBlock(lane, false);
269
+ const block = laneBlock(lane, NO_SUB_KNOBS);
193
270
  return block.io && block.hostLocalPaths;
194
271
  }
195
272
  export function laneBindsReads(lane) {
@@ -236,10 +313,10 @@ export function laneReadFaceDefault(lane) {
236
313
  }
237
314
  export const LANE_OPEN_READ_FACE_CAUTION = "a deployment that mounts host paths or shared volumes into the sandbox (hostPath, bind mount) exposes those host files to reads on an open face — pin READ_FACE=roots or add read-deny rows (READ_DENY_PATTERNS) for them";
238
315
  export function laneIsolated(lane) {
239
- return laneBlock(lane, false).isolation;
316
+ return laneBlock(lane, NO_SUB_KNOBS).isolation;
240
317
  }
241
318
  export function laneWiresExecutionEnv(lane) {
242
- return laneBlock(lane, false).io;
319
+ return laneBlock(lane, NO_SUB_KNOBS).io;
243
320
  }
244
321
  export const PLANNED_LANE_DIVERGENCES = Object.freeze([]);
245
322
  export const E2B_SANDBOX_HOME = "/home/user";
@@ -8,6 +8,7 @@
8
8
  * `execution` 位(`plugins/local-session-store.ts`)。处处 `execution` 限定,与同行 `placement_*`(委派分区)不同义;执行格只在 `placement_kind IS NULL` 的行上 ——
9
9
  * 委派分区行读作「不在」。
10
10
  * · **读只有一只**:会话店的执行席 {@link ExecutionPlacementSeat.read}(一次行读:住客 + 格;行不在 / 委派分区行 ⇒ `undefined`;读故障抛,不吞成 `absent`)。
11
+ * 跨行的聚合也只有一只:设备店的绑定数 `countDeviceBindings`(writer(device, D) 的格数 = {@link boundDeviceOf} 的聚合;只交数,不交根 id)。
11
12
  * · **写只按住客谓词** `(root, incarnation, owner) ∧ placement_kind IS NULL`:住客换了(删后重建,哪怕同一属主;local M10 窗内的认领)= 这一格不在 ⇒ 落空;
12
13
  * 0 行一律按住客复读分类({@link settledPlaceOutcome} / {@link settledMirrorOutcome} / {@link settledDeviceBindOutcome}),**永不对现住客重试**。
13
14
  * · **没有的转换**:writer ↔ mirror、writer(L) → writer(L')(device 车道的换绑 writer(device, D) → writer(device, D') 除外,它有自己的语句与门)。
@@ -248,6 +249,8 @@ export declare class InMemoryExecutionPlacementSeat implements ExecutionPlacemen
248
249
  read(root: string): Promise<ExecutionPlacementReading | undefined>;
249
250
  place(root: string, occupant: SessionFace, cell: ExecutionWriterCell): Promise<PlaceOutcome>;
250
251
  markMirror(root: string, occupant: SessionFace): Promise<MarkMirrorOutcome>;
252
+ /** 绑定数(SQL 孪生 `countDeviceBindingsOn` 的对位,同一只判官):{@link boundDeviceOf} 读得出设备的格数。 */
253
+ countDeviceBindings(): number;
251
254
  /** device 首绑(SQL `placeDeviceStatement` / `bindDeviceStatement` 的对位):absent 或 writer(device, ∅) ⇒ writer(device, D)、rev 0。 */
252
255
  bindDevice(root: string, occupant: SessionFace, device: {
253
256
  readonly id: string;
@@ -175,6 +175,13 @@ export class InMemoryExecutionPlacementSeat {
175
175
  this.rows.set(root, { occupant, cell: { role: "mirror" } });
176
176
  return { outcome: "mirrored" };
177
177
  }
178
+ countDeviceBindings() {
179
+ let bindings = 0;
180
+ for (const row of this.rows.values())
181
+ if (boundDeviceOf(row.cell) !== undefined)
182
+ bindings += 1;
183
+ return bindings;
184
+ }
178
185
  bindDevice(root, occupant, device, placedAt) {
179
186
  const settled = settledDeviceBindOutcome(root, this.rows.get(root), occupant, device.id);
180
187
  if (settled !== WRITABLE)
@@ -30,6 +30,7 @@
30
30
  * 纯类型 + 纯函数叶(值 import 只有 scope 读法叶与 run 行判词叶)。
31
31
  */
32
32
  import type { DrainReceipt, DrainRequest, EngineDraining } from "@sema-agent/settings-schema/fleet";
33
+ import type { EngineLeaseFacts } from "../boot/engine-lease.js";
33
34
  /**
34
35
  * 引擎自铸的动作号 —— **一张表**(排空事实与回执里不来自请求体的全部拼法;请求体的 `actionId` 由调用方自取):
35
36
  * · {@link ENGINE_DRAIN_CAUSES}:没有请求承载的排空成因(停止信号 / SIGHUP 升级 / 附着租约自退);
@@ -89,6 +90,8 @@ export interface DrainState {
89
90
  lastActivityAt?: () => number;
90
91
  attachedStreams?: () => number;
91
92
  lastAttachEndedAt?: () => number;
93
+ /** [ref]:附着租约窗的读口(`/health.engineLease`)。收尾链在**装了租约腿**时才装它,读口现读租约腿的把手 —— 腿还在观察才有读数,未装配的盒子里根本没有这只口。 */
94
+ engineLease?: () => EngineLeaseFacts | undefined;
92
95
  }
93
96
  /** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
94
97
  export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
@@ -65,6 +65,7 @@
65
65
  * `docs/DEPLOY-PREREQS.md` 的「托管 / 多租部署」段把它写成部署前置,并列出忘设它的后果。
66
66
  */
67
67
  import { type ServiceConfigFlat } from "./config-types.js";
68
+ import { type PermissionMode } from "@sema-agent/core";
68
69
  /** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。
69
70
  * 🔴 恰一词是裁定([ref] ①),不是暂时的:加回一条推断臂 = 翻案,先要新裁定。 */
70
71
  export declare const HOSTED_EVIDENCE_WORDS: readonly ["require-principal"];
@@ -123,6 +124,13 @@ export type TaskHostPostureScope = HostedPostureInput & Readonly<Pick<ServiceCon
123
124
  * 两条腿同射程、不同处置,射程只有一处答案。
124
125
  */
125
126
  export declare function taskWriteFaceRefusal(c: TaskWriteFaceInput): string | undefined;
127
+ /**
128
+ * **腿② 的词形入口** —— 一个模式词在这台部署上会不会开出它掀不开的写围栏:词的模式列开写面(引擎模式表本尊,
129
+ * `modePolicyOf(word).writeFace === "open"`,今天只有 `bypassPermissions`)∧ {@link taskWriteFaceRefusal} 答拒 ⇒ 拒因;否则 `undefined`。
130
+ * 读者两个,同是受理判官的托管一步(`permission-mode-admission.ts` 顶注「两口的调用形」):在飞 run 的切档口经判官本体;提交口与续跑重放存储体经装配期那一判
131
+ * (`boot/resolve-spec.ts`,问盖过章的词)。哪天 core 让第二个模式也开写面,两处当场跟上,不是再手抄一个模式词。
132
+ */
133
+ export declare function permissionModeWriteFaceRefusal(word: PermissionMode | undefined, c: TaskWriteFaceInput): string | undefined;
126
134
  /**
127
135
  * **腿④** —— 这条请求要让 host shell 继承整份用户环境(`inheritEnv:"all"`),这台部署给不给?返回拒因;`undefined` = 放行。
128
136
  * 调用方只在值为 `"all"` 时问(`"scrub"` 就是缺省,任何部署都受理)。
@@ -1,5 +1,6 @@
1
1
  import { TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
2
2
  import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
3
+ import { modePolicyOf } from "@sema-agent/core";
3
4
  export const HOSTED_EVIDENCE_WORDS = ["require-principal"];
4
5
  export function buildHostedPosture(c) {
5
6
  const evidence = [];
@@ -41,6 +42,9 @@ export function taskWriteFaceRefusal(c) {
41
42
  `Refused because this deployment is HOSTED/multi-tenant (${hostedBecause(facts)}), so the task layer may only TIGHTEN the write face, never open it. ` +
42
43
  `Set TASK_WRITE_FACE_ESCALATION=allow if the DEPLOYMENT permits the task layer to open it, or omit permissionMode:"bypassPermissions".`);
43
44
  }
45
+ export function permissionModeWriteFaceRefusal(word, c) {
46
+ return word !== undefined && modePolicyOf(word).writeFace === "open" ? taskWriteFaceRefusal(c) : undefined;
47
+ }
44
48
  export function taskInheritEnvRefusal(c) {
45
49
  if (DEPLOYMENT_SHAPE_PREDICATES.taskHostPostureDeclarations.holds(c))
46
50
  return undefined;
@@ -2,11 +2,11 @@ import { APPROVAL_REACH, IMAGE_ADMISSION, isApprovalReach, isPermissionMode, isT
2
2
  import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { echoModelRef, modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError, sessionIdShapeIssue } from "../security.js";
5
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, hooksKeyIssue, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
5
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, hooksKeyIssue, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
6
  import { tightenOnlySeatIssue } from "../tighten-only-seats.js";
7
7
  import { approvalFrameFamiliesIssue } from "../tool-approval.js";
8
8
  import { buildEntryModeClampNotice } from "../entry-mode-clamp-notice.js";
9
- import { autonomyModeFloor } from "../runtime-governance.js";
9
+ import { stampSubmissionEntryMode } from "../permission-mode-admission.js";
10
10
  import { taskBodyKeyIssue } from "../request-key-closure.js";
11
11
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, fileHistoryRestoreKeysInBody, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
12
12
  import * as facts from "../capability-facts.js";
@@ -738,7 +738,7 @@ export function createAdmission(ctx) {
738
738
  return null;
739
739
  }
740
740
  }
741
- const { body: admitted, entry: entryMode } = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
741
+ const { body: admitted, entry: entryMode } = stampSubmissionEntryMode(body, deps.config);
742
742
  const lease = deps.peerDesk.lease("fresh");
743
743
  try {
744
744
  const auth = await deps.authorize({ req, body: admitted });
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * ## 律
5
5
  * 契约(settings-schema `SAFETY_MERGE_SPEC.permissions` 的成员阶梯)按**层的信任档**准入每个成员:`allow` 放宽免问面(`loosen`),要 `global`;`deny` / `ask` 只收紧,
6
- * 文件层最低档 `local` 就收。wire 上没有层 —— 提交体是合并后的扁平结果,层只在合并端(cli 本地)存在;自报一个层 / 作者位不可验(引擎侧已拒立这种位)。所以本服务
7
- * 的「层」= **这次请求验过的凭据**:档只由部署形 + 身份来源 + 登录令牌的 scope 角色推,调用方说什么都不改档。
6
+ * 文件层最低档 `local` 就收。准入不读层:提交体是合并后的结果,层只在合并端(cli 本地)存在;deny / ask 条目可以带出处层(`{ rule, layer }`),但那个层只选这条规则的
7
+ * `/` 读在哪个根(`permission-rule-list.ts` 的 `layerRuleRoot`),自报的层不可验、**不进本判**。所以本服务准入的「层」= **这次请求验过的凭据**:档只由部署形 + 身份来源
8
+ * + 登录令牌的 scope 角色推,调用方说什么(含条目上的层)都不改档。
8
9
  *
9
10
  * · 单用户 turnkey(`requirePrincipal !== true`,本仓 posture 判别式同一只)⇒ `global`:一台机一位超管;
10
11
  * · 多租 · 部署的 service 凭据 ⇒ `global`:部署自己的凭据是运维档(契约给「启动方自己的行为」—— `SETTINGS_SOURCE_CONTRACT.flagSettings` —— 的档),持它的前端为自己转发的内容负责;
@@ -1,6 +1,7 @@
1
1
  import { domainWriteRole, isAtLeast, isRole, memberAdmission, membersOf, roleAtLeast, USER_ROLES, } from "@sema-agent/settings-schema";
2
2
  import { entryProblem, entryProblemsFacts } from "../entry-problems.js";
3
3
  import { ssoVerifiedRole } from "../security.js";
4
+ import { ruleTextOf } from "../permission-rule-list.js";
4
5
  import { verifiedIdentitySource } from "./principal-gate.js";
5
6
  export function callerTrust(req, config) {
6
7
  if (config.requirePrincipal !== true)
@@ -34,7 +35,7 @@ function effectiveRuleTables(t) {
34
35
  for (const member of membersOf("permissions")) {
35
36
  const entries = t.permissions[member];
36
37
  if (entries !== undefined)
37
- out.push({ member, field: `settings.permissions.${member}`, entries, source: "settings" });
38
+ out.push({ member, field: `settings.permissions.${member}`, entries: entries.map(ruleTextOf), source: "settings" });
38
39
  }
39
40
  }
40
41
  if (t.skills !== undefined && t.userInvokedSkills !== undefined) {
@@ -138,6 +138,10 @@ export function createDispatch(ctx) {
138
138
  ...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
139
139
  ...healthDrainingKeys(signal),
140
140
  ...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
141
+ ...(() => {
142
+ const lease = deps.drainState?.engineLease?.();
143
+ return lease ? { engineLease: { lingerMs: lease.lingerMs, tickMs: lease.tickMs } } : {};
144
+ })(),
141
145
  ...(deps.storePosture?.degradedReason !== undefined ? { durable: false, storeDegraded: true } : {}),
142
146
  ...healthReadinessKeys(signal.verdict),
143
147
  model: deps.config.model.id,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `POST /v1/tasks/stream`(活流腿:新提交、同键重放、在飞合流的跟随方)的**唯一**帧写口。
3
+ *
4
+ * 律(一条):这条流上的每一帧都是 `[id: <eventId>\n]event: <type>\ndata: <json>\n\n`,事件名 = 载荷的 `type`。
5
+ * 心跳是同一只写口写的 `event: heartbeat` + `data: {}`。与 durable 尾(`http/sse-log.ts`)、sdk 的 `toSseResponse` 中继同形。
6
+ *
7
+ * `id:` 行(契约 §0.6):载荷带非空 string `eventId` 才发,值逐字 = `eventId`;没有就不发,EventSource 沿用上一游标 ——
8
+ * 绝不合成占位 id(那会把不可锚的位置伪装成可锚)。`id:` 行是**每帧唯一**的续流游标,所以载荷的 `eventId` 必须是**本帧
9
+ * 自己的**身份(server 自铸帧的唯一铸点:`trace/model-segment.ts` 的 `reasoningSegmentEventId`)。三道门各管一段:
10
+ * {@link LiveFrame} 管「铸点说得出这两个键的型」;本函数的运行期判据管「真的是」(通体 `Record<string, unknown>` 的
11
+ * 构造器返回值进得来 —— `null` 读 `.length` 会把整条流摔掉、数组会被强转成一枚编出来的游标);`test/live-sse-cursor.test.ts`
12
+ * 的规则式全流判据管「同一条流内不重复」。
13
+ */
14
+ import type { ServerResponse } from "node:http";
15
+ /** 活流帧的载荷型:`type` 是事件名(必填,型说得出口);`eventId` 在场即**本帧自己的**续流游标。其余键由各自铸点白名单。 */
16
+ export type LiveFrame = {
17
+ readonly type: string;
18
+ readonly eventId?: string;
19
+ } & Record<string, unknown>;
20
+ /** 写一帧。`type` 不是非空串 = 铸点违约(型门之外漏进来的通体记录),响亮抛 —— 写一行编出来的事件名是撒谎,不写事件名就回到两只形。 */
21
+ export declare function writeLiveFrame(res: ServerResponse, payload: LiveFrame): void;
22
+ /** 心跳:真帧,不是 SSE 注释(逐帧解析的 BFF 会丢注释行,下游看到的是一段零帧的窗)。没有监听心跳的 EventSource 客户端忽略它。 */
23
+ export declare function writeLiveHeartbeat(res: ServerResponse): void;
24
+ //# sourceMappingURL=live-frame.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { liveToolEndForBacklog } from "../trace/tool-output-media.js";
2
+ function frameBytes(event, data, anchor) {
3
+ const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
4
+ return `${idLine}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
5
+ }
6
+ export function writeLiveFrame(res, payload) {
7
+ const name = payload.type;
8
+ if (typeof name !== "string" || name.length === 0)
9
+ throw new TypeError(`live SSE frame without a string type: ${JSON.stringify(name)}`);
10
+ const frame = name === "tool_end" ? liveToolEndForBacklog(payload, res.writableLength) : payload;
11
+ res.write(frameBytes(name, frame, payload.eventId));
12
+ }
13
+ export function writeLiveHeartbeat(res) {
14
+ res.write(frameBytes("heartbeat", {}, undefined));
15
+ }
16
+ //# sourceMappingURL=live-frame.js.map
@@ -7,9 +7,10 @@ import { headReadOf } from "../session-incarnation.js";
7
7
  import { createSessionWorkspaceRecorder, legSessionWitnessOf, sessionWorkspaceStoreOf } from "../session-workspace.js";
8
8
  const DELIVERY_NOT_RECORDED = "workflow park decide: this delivery no longer holds the host session (its claim was released) — the decision was not recorded and this delivery was not started";
9
9
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
10
- import { graftParkToolCallId, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
10
+ import { graftParkToolCallId, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort, voidHeldParkCard } from "../runs.js";
11
11
  import { atHostBoundary, failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
12
- import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../run-cancel-context.js";
12
+ import { voidVerifierPauseCard } from "../verifier-pause-card.js";
13
+ import { buildCancelledLedgerRow, CARD_SETTLED_BY_CANCEL_MESSAGE, createRunAbort, mintCancelledResult, withCardSettledByCancelMessage } from "../run-cancel-context.js";
13
14
  import { isTerminalRunStatus } from "../plugins/store-contracts.js";
14
15
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
15
16
  import { recordResumeResourceWindow } from "../resource-window.js";
@@ -679,6 +680,7 @@ export function createResumeLegs(ctx) {
679
680
  fleetPub?.onTerminal(status, residuals);
680
681
  };
681
682
  let result;
683
+ let heldPark;
682
684
  let reopenConfirmedPark = false;
683
685
  const confirmCheckpointReopened = async () => {
684
686
  const store = deps.checkpointStore;
@@ -695,28 +697,32 @@ export function createResumeLegs(ctx) {
695
697
  const settleCardForCancel = async () => {
696
698
  const cs2 = deps.checkpointStore;
697
699
  if (!cs2)
698
- return;
700
+ return false;
699
701
  try {
700
702
  const live = await cs2.get(token);
701
- if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
702
- deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
703
- }
703
+ if (live?.status !== "pending")
704
+ return false;
705
+ if (await cs2.expire(token, live.scope))
706
+ return true;
707
+ deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
708
+ return false;
704
709
  }
705
710
  catch (cancelSettleErr) {
706
711
  deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
707
712
  taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
708
713
  });
714
+ return false;
709
715
  }
710
716
  };
711
717
  const reopenedAfterLeg = async (leg, cancelledAtDone) => {
712
718
  if (freshRunClaimed || !legMayHaveReopenedCheckpoint(leg))
713
- return false;
719
+ return { reopened: false, leg };
714
720
  const pending = cancelledAtDone ? false : await confirmCheckpointReopened();
715
721
  if (cancelledAtDone || cancelCtrl.cause() !== undefined) {
716
- await settleCardForCancel();
717
- return false;
722
+ const expired = await settleCardForCancel();
723
+ return { reopened: false, leg: expired ? withCardSettledByCancelMessage(leg) : leg };
718
724
  }
719
- return pending;
725
+ return { reopened: pending, leg };
720
726
  };
721
727
  let resumeHeartbeat;
722
728
  let resumeManifestLease = 0;
@@ -850,15 +856,18 @@ export function createResumeLegs(ctx) {
850
856
  resumeLegLive = true;
851
857
  const vrEngine = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
852
858
  const vrCause = cancelCtrl.cause();
859
+ await voidVerifierPauseCard(vrEngine, deps.checkpointStore);
853
860
  const vrJudged = atHostBoundary(vrEngine, sessionId);
854
- const vr = vrCause !== undefined && taskId !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: vrJudged, cause: vrCause }) : vrJudged;
861
+ if (vrJudged.terminal.kind === "paused")
862
+ heldPark = vrJudged;
863
+ const vrMinted = vrCause !== undefined && taskId !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: vrJudged, cause: vrCause }) : vrJudged;
855
864
  await commitResumeCtx();
865
+ const { reopened: vrReopened, leg: vr } = await reopenedAfterLeg(vrMinted, vrCause !== undefined);
856
866
  let safe = sealResultForExit(vr);
857
867
  const vrPaused = vr.terminal.kind === "paused" ? vr.terminal : undefined;
858
868
  const vrParkWord = vrPaused !== undefined ? outwardPlane(vrPaused).status : undefined;
859
869
  const reParkEvent = vrParkWord !== undefined && isParkLedgerEventType(vrParkWord) ? vrParkWord : undefined;
860
870
  const reSuspended = reParkEvent !== undefined;
861
- const vrReopened = await reopenedAfterLeg(vr, vrCause !== undefined);
862
871
  if (vrReopened)
863
872
  reopenConfirmedPark = true;
864
873
  if (taskId && deps.runStore) {
@@ -916,7 +925,7 @@ export function createResumeLegs(ctx) {
916
925
  const captureUserMessageAnchor = deps.resumeAnchorStore && taskId
917
926
  ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
918
927
  : undefined;
919
- const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
928
+ const anchor = new TurnAnchorCapture(captureTurnAnchor, "resume", captureUserMessageAnchor);
920
929
  const flush = async () => {
921
930
  if (reasoning) {
922
931
  await append("reasoning", modelTextEventData({ text: reasoning, eventId: reasoningEventId }));
@@ -997,15 +1006,18 @@ export function createResumeLegs(ctx) {
997
1006
  for await (const ev of stream)
998
1007
  await resumeWorkspace.observe(ev);
999
1008
  const rNoRow = await stream.result();
1000
- if (await reopenedAfterLeg(rNoRow, cancelCtrl.cause() !== undefined))
1009
+ const noRowLeg = await reopenedAfterLeg(rNoRow, cancelCtrl.cause() !== undefined);
1010
+ if (noRowLeg.reopened)
1001
1011
  reopenConfirmedPark = true;
1002
- return sealResultForExit(rNoRow);
1012
+ return sealResultForExit(noRowLeg.leg);
1003
1013
  }
1004
1014
  onAccepted?.(acceptedResumingReceipt(taskId, sessionId, buildSettlementEvidence({ at: "engine_accepted", commitment: args.commitment })));
1005
1015
  let causeAtDone;
1006
1016
  for await (const ev of stream) {
1007
1017
  if (ev.type === "done")
1008
1018
  causeAtDone = cancelCtrl.cause();
1019
+ if (ev.type === "done" && ev.result.terminal.kind === "paused")
1020
+ heldPark = ev.result;
1009
1021
  recordTurnActivity(taskId);
1010
1022
  fleetPub?.onEvent(ev);
1011
1023
  await deps.userInputLedger?.observeLegEvent(stream, ev);
@@ -1125,6 +1137,8 @@ export function createResumeLegs(ctx) {
1125
1137
  await flush();
1126
1138
  const rEngine = await stream.result();
1127
1139
  const r = causeAtDone !== undefined ? mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", engineResult: rEngine, cause: causeAtDone }) : rEngine;
1140
+ if (r.terminal.kind === "paused")
1141
+ heldPark = r;
1128
1142
  const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
1129
1143
  if (rPark === "suspended") {
1130
1144
  await append("suspended", parkEventData(await graftParkToolCallId(r, deps.checkpointStore)));
@@ -1136,7 +1150,8 @@ export function createResumeLegs(ctx) {
1136
1150
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1137
1151
  return r;
1138
1152
  }
1139
- if (await reopenedAfterLeg(r, causeAtDone !== undefined)) {
1153
+ const settledLeg = await reopenedAfterLeg(r, causeAtDone !== undefined);
1154
+ if (settledLeg.reopened) {
1140
1155
  reopenConfirmedPark = true;
1141
1156
  try {
1142
1157
  await append(reparkStatus, parkEventData(r, failureCodeOf(r.terminal) ?? null));
@@ -1146,7 +1161,7 @@ export function createResumeLegs(ctx) {
1146
1161
  }
1147
1162
  return r;
1148
1163
  }
1149
- const safe = await attachModelUsage(sealResultForExit(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
1164
+ const safe = await attachModelUsage(sealResultForExit(settledLeg.leg), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
1150
1165
  await append("done", { result: safe });
1151
1166
  if (taskConfig.suggestNextPrompts && safe.terminal.kind === "completed") {
1152
1167
  const suggestions = await stream.suggestions().catch(() => []);
@@ -1184,14 +1199,14 @@ export function createResumeLegs(ctx) {
1184
1199
  if (e instanceof CheckpointError) {
1185
1200
  const disposition = resumeRefusalDisposition(e);
1186
1201
  if (disposition === "cancelled") {
1187
- await settleCardForCancel();
1202
+ const abortedText = (await settleCardForCancel()) ? CARD_SETTLED_BY_CANCEL_MESSAGE : e.message;
1188
1203
  if (claimedRow && taskId && deps.runStore) {
1189
1204
  const handleCause = cancelCtrl.cause();
1190
1205
  if (handleCause === undefined)
1191
1206
  recordFailOpen("server.runs.abort-cause-missing", `task=${taskId} arm=resume-aborted`);
1192
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message, cause: handleCause ?? "cancel_verb" });
1207
+ const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: abortedText, cause: handleCause ?? "cancel_verb" });
1193
1208
  await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-resume-aborted ${errorText(err)}`));
1194
- await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
1209
+ await deps.runStore.setTerminal(taskId, "failed", c, abortedText).catch(() => undefined);
1195
1210
  }
1196
1211
  settleFleet("failed");
1197
1212
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
@@ -1219,6 +1234,7 @@ export function createResumeLegs(ctx) {
1219
1234
  const thrownCause = cancelCtrl.cause();
1220
1235
  if (thrownCause !== undefined) {
1221
1236
  if (taskId && deps.runStore) {
1237
+ await voidHeldParkCard(heldPark, sessionId, deps.checkpointStore, "resume");
1222
1238
  const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", cause: thrownCause });
1223
1239
  await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-thrown-cancel ${errorText(err)}`));
1224
1240
  await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
@@ -1227,6 +1243,7 @@ export function createResumeLegs(ctx) {
1227
1243
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1228
1244
  }
1229
1245
  if (claimedRow && taskId && deps.runStore) {
1246
+ await voidHeldParkCard(heldPark, sessionId, deps.checkpointStore, "resume");
1230
1247
  await deps.runStore.setTerminal(taskId, "failed", null, errorText(e)).catch(() => undefined);
1231
1248
  }
1232
1249
  settleFleet("failed");
@@ -29,6 +29,7 @@ import { handleNotifyWake, NOTIFY_WAKE_ROUTES } from "./routes/notify-wake.js";
29
29
  import { handleApprovalsAssistant, APPROVALS_ASSISTANT_ROUTES } from "./routes/approvals-assistant.js";
30
30
  import { handleRuns, handleRunVerbs, RUNS_ROUTES, RUN_VERBS_ROUTES } from "./routes/runs.js";
31
31
  import { handleRunMemoryCapture, RUN_MEMORY_CAPTURE_ROUTES } from "./routes/run-memory-capture.js";
32
+ import { handlePermissionMode, RUN_PERMISSION_MODE_ROUTES } from "./routes/run-permission-mode.js";
32
33
  import { handleMcpReconnect, MCP_RECONNECT_ROUTES } from "./routes/mcp-reconnect.js";
33
34
  import { handleBackground, BACKGROUND_ROUTES } from "./routes/background.js";
34
35
  import { handleSideQuery, SIDE_QUERY_ROUTES } from "./routes/side-query.js";
@@ -51,6 +52,7 @@ export const ROUTES = [
51
52
  row({ handler: handleWorkflowAgentSteer, decls: WORKFLOW_AGENT_STEER_ROUTES, position: "post-credential", label: "workflow-agent-steer" }),
52
53
  row({ handler: handleRunVerbs, decls: RUN_VERBS_ROUTES, position: "post-credential", label: "run-verbs" }),
53
54
  row({ handler: handleRunMemoryCapture, decls: RUN_MEMORY_CAPTURE_ROUTES, position: "post-credential", label: "run-memory-capture" }),
55
+ row({ handler: handlePermissionMode, decls: RUN_PERMISSION_MODE_ROUTES, position: "post-credential", label: "permission-mode" }),
54
56
  row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
55
57
  row({ handler: handleBackground, decls: BACKGROUND_ROUTES, position: "post-credential", label: "background" }),
56
58
  row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader" }),
@@ -408,6 +408,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
408
408
  ...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
409
409
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
410
410
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
411
+ ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
411
412
  ...skillCardFacts(s),
412
413
  ...(s.boundGrantAbsence !== undefined ? { boundGrantAbsence: s.boundGrantAbsence } : {}),
413
414
  objective,
@@ -24,7 +24,7 @@ import { projectStoreCapability } from "../../store-posture.js";
24
24
  import { projectWebSearchCapability } from "../../plugins/web-search.js";
25
25
  import { projectExecutionLaneCapability } from "../../capabilities/execution-lane.js";
26
26
  import { judgeSandboxGitPlane, sandboxGitPlanePremisesOf } from "../../sandbox-git-plane.js";
27
- import { MCP_ASSERTION_HEADER_SENT } from "../../mcp-assertion-header.js";
27
+ import { MCP_ASSERTION_HEADER_SENT, MCP_REQUEST_TRANSPORT_KINDS } from "../../mcp-assertion-header.js";
28
28
  import { gateCompositeMembers } from "../../capabilities/composite-members.js";
29
29
  import { projectApprovalsStreamLiveCapability } from "../../capabilities/approvals-push.js";
30
30
  import { workflowModelAllowlistFor } from "../../task-workflow.js";
@@ -96,7 +96,7 @@ export async function handleCapabilities(req, res, match, ctx) {
96
96
  maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4,
97
97
  wsPath: DEVICE_WS_PATH,
98
98
  management: facts.deviceManagement(deps).available,
99
- ...(deps.config.remoteExec?.provider === "device" && deps.config.remoteExec.relayUrl !== undefined ? { relayUrl: deps.config.remoteExec.relayUrl } : {}),
99
+ ...(deps.config.remoteExec?.provider === "device" && deps.config.remoteExec.relay.kind === "url" ? { relayUrl: deps.config.remoteExec.relay.url } : {}),
100
100
  }
101
101
  : false,
102
102
  sessionEvents: facts.sessionEventStream(deps).available,
@@ -133,7 +133,7 @@ export async function handleCapabilities(req, res, match, ctx) {
133
133
  oneShot: true,
134
134
  policy: true,
135
135
  usage: Boolean(deps.costQuota),
136
- permissionModeWrite: false,
136
+ permissionModeWrite: facts.permissionModeWrite(deps).available,
137
137
  modelSelection: true,
138
138
  effortSelection: true,
139
139
  toolOutput: true,
@@ -143,7 +143,7 @@ export async function handleCapabilities(req, res, match, ctx) {
143
143
  subagentSteer: facts.subagentSteer(deps).available,
144
144
  subagentResume: facts.subagentSteer(deps).available,
145
145
  taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: DEPLOYMENT_SHAPE_PREDICATES.callerCwd.holds(deps.config), hooks: taskHooksHonored(deps.config), pluginHooks: taskHooksHonored(deps.config), managedHookGroups: taskHooksHonored(deps.config), tightenOnlySeats: true, approvalFrameFamilies: true },
146
- settings: { permissions: { allowGrant: true, allowLayer: true, callerTrust: callerTrust(req, deps.config).trust } },
146
+ settings: { permissions: { allowGrant: true, allowLayer: true, layers: true, callerTrust: callerTrust(req, deps.config).trust } },
147
147
  permissionModeAuto: {
148
148
  accepted: true,
149
149
  classifierSeat: deps.autoModeFace?.seatMounted === true,
@@ -181,6 +181,7 @@ export async function handleCapabilities(req, res, match, ctx) {
181
181
  mcp: true,
182
182
  mcpProbe: true,
183
183
  mcpAssertionHeaderSent: MCP_ASSERTION_HEADER_SENT,
184
+ mcpTransportKinds: [...MCP_REQUEST_TRANSPORT_KINDS],
184
185
  mcpElicitation: facts.elicitation(deps).available,
185
186
  askUserQuestion: facts.askUserQuestion(deps).available,
186
187
  toolApproval: facts.toolApproval(deps).available,