@sema-agent/server 7.137.0 → 7.139.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 (328) hide show
  1. package/MIGRATION.md +600 -142
  2. package/README.md +7 -3
  3. package/README.zh-CN.md +6 -3
  4. package/USAGE.md +132 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +23 -16
  9. package/dist/approval-card.js +1 -0
  10. package/dist/audit.js +1 -0
  11. package/dist/background-agent-name.d.ts +5 -0
  12. package/dist/background-agent-name.js +5 -0
  13. package/dist/bench/s1/arms.d.ts +1 -1
  14. package/dist/bench/s1/arms.js +1 -3
  15. package/dist/bench/s1/run-firm.js +2 -2
  16. package/dist/boot/budget-tracing.d.ts +1 -12
  17. package/dist/boot/deployment-mods.d.ts +46 -16
  18. package/dist/boot/deployment-mods.js +82 -6
  19. package/dist/boot/device-lane.d.ts +8 -3
  20. package/dist/boot/device-lane.js +7 -2
  21. package/dist/boot/execution-env.d.ts +4 -2
  22. package/dist/boot/execution-env.js +16 -24
  23. package/dist/boot/execution-lane.d.ts +1 -1
  24. package/dist/boot/governance-seams.d.ts +12 -18
  25. package/dist/boot/governance-seams.js +10 -24
  26. package/dist/boot/leader.d.ts +3 -0
  27. package/dist/boot/leader.js +1 -0
  28. package/dist/boot/memory-consolidation.js +2 -2
  29. package/dist/boot/org-delivery.js +15 -2
  30. package/dist/boot/resolve-spec.d.ts +4 -1
  31. package/dist/boot/resolve-spec.js +29 -19
  32. package/dist/boot/retention-lane.d.ts +14 -8
  33. package/dist/boot/retention-lane.js +2 -8
  34. package/dist/boot/runner-deps.d.ts +10 -4
  35. package/dist/boot/runner-deps.js +2 -0
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-04-stores.d.ts +1 -12
  40. package/dist/boot/stage-05-execution-env.d.ts +3 -12
  41. package/dist/boot/stage-05-execution-env.js +8 -5
  42. package/dist/boot/stage-06-runners.d.ts +3 -12
  43. package/dist/boot/stage-06-runners.js +5 -3
  44. package/dist/boot/stage-07-capability-layer.d.ts +3 -12
  45. package/dist/boot/stage-07-capability-layer.js +2 -2
  46. package/dist/boot/stage-08-reapers.d.ts +3 -12
  47. package/dist/boot/stage-09-leader.d.ts +3 -12
  48. package/dist/boot/stage-09-leader.js +2 -3
  49. package/dist/boot/stage-10-http-server.d.ts +3 -12
  50. package/dist/boot/stage-10-http-server.js +5 -3
  51. package/dist/boot/stores.js +4 -4
  52. package/dist/boot/web-admin.d.ts +5 -2
  53. package/dist/boot/web-admin.js +5 -5
  54. package/dist/brain.js +5 -5
  55. package/dist/budget.d.ts +17 -15
  56. package/dist/budget.js +2 -2
  57. package/dist/capabilities/mcp-probe.d.ts +36 -11
  58. package/dist/capabilities/mcp-probe.js +44 -5
  59. package/dist/capabilities/scenarios.d.ts +1 -1
  60. package/dist/capability-facts.d.ts +21 -7
  61. package/dist/capability-facts.js +13 -3
  62. package/dist/checkpoint-identity.d.ts +13 -6
  63. package/dist/config-catalog.js +14 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +19 -24
  67. package/dist/config-provider.js +27 -12
  68. package/dist/config-types.d.ts +25 -12
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +52 -20
  71. package/dist/delegated-session-owner.d.ts +2 -2
  72. package/dist/deployment-shape.d.ts +7 -7
  73. package/dist/device-enrollment.d.ts +41 -2
  74. package/dist/device-enrollment.js +27 -12
  75. package/dist/device-store.d.ts +58 -85
  76. package/dist/device-store.js +19 -49
  77. package/dist/device-ws-hub.js +2 -1
  78. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  79. package/dist/exec-cut.d.ts +60 -0
  80. package/dist/exec-cut.js +36 -0
  81. package/dist/execution-lane-caps.d.ts +19 -0
  82. package/dist/execution-lane-caps.js +10 -0
  83. package/dist/execution-placement.d.ts +259 -0
  84. package/dist/execution-placement.js +199 -0
  85. package/dist/field-conflict.d.ts +3 -2
  86. package/dist/field-conflict.js +1 -0
  87. package/dist/gate-settlement.d.ts +71 -0
  88. package/dist/gate-settlement.js +40 -0
  89. package/dist/hooks/classic-stations.d.ts +1 -0
  90. package/dist/hooks/classic-stations.js +1 -0
  91. package/dist/hooks/command-spawn.d.ts +2 -2
  92. package/dist/hooks/hook-runner.js +14 -14
  93. package/dist/hosted-posture.d.ts +2 -2
  94. package/dist/http/admission.js +10 -19
  95. package/dist/http/checkpoint-ctx.d.ts +21 -12
  96. package/dist/http/checkpoint-ctx.js +4 -2
  97. package/dist/http/claimed-run.d.ts +1 -1
  98. package/dist/http/dispatch.d.ts +4 -0
  99. package/dist/http/dispatch.js +3 -0
  100. package/dist/http/durable-run-leg.js +12 -6
  101. package/dist/http/execution-context.d.ts +218 -0
  102. package/dist/http/execution-context.js +330 -0
  103. package/dist/http/memory-compliance-failure-reply.js +3 -0
  104. package/dist/http/resume-legs.d.ts +5 -11
  105. package/dist/http/resume-legs.js +179 -104
  106. package/dist/http/route-ctx.d.ts +44 -59
  107. package/dist/http/route-ctx.js +2 -5
  108. package/dist/http/routes/a2a-serve.js +10 -4
  109. package/dist/http/routes/agents-roster.d.ts +1 -1
  110. package/dist/http/routes/agents-roster.js +5 -1
  111. package/dist/http/routes/approvals-assistant.js +6 -6
  112. package/dist/http/routes/background.d.ts +29 -1
  113. package/dist/http/routes/background.js +29 -2
  114. package/dist/http/routes/capabilities.js +12 -10
  115. package/dist/http/routes/devices.d.ts +13 -19
  116. package/dist/http/routes/devices.js +21 -26
  117. package/dist/http/routes/mcp-reconnect.d.ts +3 -2
  118. package/dist/http/routes/mcp-reconnect.js +7 -5
  119. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  120. package/dist/http/routes/memory-origin.d.ts +1 -1
  121. package/dist/http/routes/rules.js +3 -3
  122. package/dist/http/routes/runs.js +11 -7
  123. package/dist/http/routes/session-sync.js +110 -15
  124. package/dist/http/routes/sessions.d.ts +6 -0
  125. package/dist/http/routes/sessions.js +119 -13
  126. package/dist/http/routes/side-query.d.ts +12 -4
  127. package/dist/http/routes/side-query.js +6 -8
  128. package/dist/http/routes/tasks.js +800 -782
  129. package/dist/http/server.d.ts +61 -39
  130. package/dist/http/server.js +11 -0
  131. package/dist/http/session-inherit-env.d.ts +3 -4
  132. package/dist/http/session-inherit-env.js +1 -1
  133. package/dist/http/wire-types.d.ts +22 -18
  134. package/dist/identity-source.d.ts +1 -1
  135. package/dist/index.d.ts +3 -0
  136. package/dist/index.js +1 -0
  137. package/dist/leader/wire.d.ts +7 -0
  138. package/dist/leader/wire.js +5 -4
  139. package/dist/mcp-assertion-header.d.ts +51 -40
  140. package/dist/mcp-assertion-header.js +10 -11
  141. package/dist/mcp-authorization-face.d.ts +27 -0
  142. package/dist/mcp-authorization-face.js +15 -0
  143. package/dist/memory-operator-faces.d.ts +13 -4
  144. package/dist/memory-operator-faces.js +1 -17
  145. package/dist/memory-sync-client.d.ts +53 -14
  146. package/dist/memory-sync-client.js +187 -92
  147. package/dist/memory-sync.d.ts +1 -1
  148. package/dist/memory-sync.js +3 -2
  149. package/dist/model-route-gap.d.ts +6 -22
  150. package/dist/model-route-gap.js +7 -5
  151. package/dist/mods-declaration.d.ts +110 -0
  152. package/dist/mods-declaration.js +109 -0
  153. package/dist/observability/cost-taxonomy.d.ts +2 -2
  154. package/dist/observability/err-text.d.ts +5 -0
  155. package/dist/observability/err-text.js +9 -0
  156. package/dist/observability/fail-open.d.ts +17 -1
  157. package/dist/observability/fail-open.js +17 -1
  158. package/dist/observability/run-terminal.js +0 -1
  159. package/dist/observability/secret-env-scrub.d.ts +1 -1
  160. package/dist/observability/secret-env-scrub.js +1 -1
  161. package/dist/orchestration/workflow-notify-journal.js +4 -4
  162. package/dist/org-memory-admission.d.ts +4 -3
  163. package/dist/org-memory-admission.js +6 -6
  164. package/dist/parked-decide.d.ts +49 -11
  165. package/dist/parked-decide.js +25 -26
  166. package/dist/parked-revive-surface.d.ts +12 -15
  167. package/dist/parked-revive-surface.js +3 -6
  168. package/dist/peer-seat.d.ts +156 -0
  169. package/dist/peer-seat.js +273 -0
  170. package/dist/permission-rule-vocab.d.ts +7 -7
  171. package/dist/permission-rule-vocab.js +3 -3
  172. package/dist/plain-string-record.d.ts +38 -0
  173. package/dist/plain-string-record.js +38 -0
  174. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  175. package/dist/plugins/approval-ask-store-file.js +11 -0
  176. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  177. package/dist/plugins/approval-ask-store-memory.js +12 -0
  178. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  179. package/dist/plugins/approval-ask-store-sql.js +30 -12
  180. package/dist/plugins/background-agent-store-sql.js +6 -3
  181. package/dist/plugins/caching-session-store.d.ts +11 -2
  182. package/dist/plugins/caching-session-store.js +15 -3
  183. package/dist/plugins/checkpoint-store-sql.d.ts +21 -25
  184. package/dist/plugins/checkpoint-store-sql.js +13 -15
  185. package/dist/plugins/device-store-sql.d.ts +25 -51
  186. package/dist/plugins/device-store-sql.js +49 -103
  187. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  188. package/dist/plugins/execution-placement-sql.js +223 -0
  189. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  190. package/dist/plugins/file-history-store-sql.js +8 -7
  191. package/dist/plugins/file-run-store.d.ts +6 -1
  192. package/dist/plugins/file-run-store.js +4 -0
  193. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  194. package/dist/plugins/fork-routing-session-store.js +16 -0
  195. package/dist/plugins/k8s-pod-reaper.d.ts +2 -8
  196. package/dist/plugins/k8s-pod-reaper.js +3 -3
  197. package/dist/plugins/local-checkpoint-store.d.ts +1 -11
  198. package/dist/plugins/local-checkpoint-store.js +0 -1
  199. package/dist/plugins/local-session-store.d.ts +183 -45
  200. package/dist/plugins/local-session-store.js +412 -105
  201. package/dist/plugins/mailbox-store-sql.d.ts +11 -3
  202. package/dist/plugins/mailbox-store-sql.js +103 -15
  203. package/dist/plugins/memory-key-guards.d.ts +4 -0
  204. package/dist/plugins/memory-key-guards.js +1 -0
  205. package/dist/plugins/memory-run-store.d.ts +1 -1
  206. package/dist/plugins/memory-sync-store-pg.d.ts +16 -5
  207. package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
  208. package/dist/plugins/one-time-migrations.d.ts +117 -6
  209. package/dist/plugins/one-time-migrations.js +383 -14
  210. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  211. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  212. package/dist/plugins/permission-rule-store-sql.js +34 -29
  213. package/dist/plugins/pg-pool.d.ts +7 -3
  214. package/dist/plugins/pg-session-storage.d.ts +41 -22
  215. package/dist/plugins/pg-session-storage.js +136 -55
  216. package/dist/plugins/remote-env-adb.d.ts +22 -4
  217. package/dist/plugins/remote-env-adb.js +70 -62
  218. package/dist/plugins/remote-env-device.d.ts +32 -12
  219. package/dist/plugins/remote-env-device.js +18 -17
  220. package/dist/plugins/remote-env-e2b.d.ts +19 -3
  221. package/dist/plugins/remote-env-e2b.js +41 -42
  222. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  223. package/dist/plugins/remote-env-file-error.js +1 -1
  224. package/dist/plugins/remote-env-host.d.ts +23 -8
  225. package/dist/plugins/remote-env-host.js +66 -70
  226. package/dist/plugins/remote-env-k8s.d.ts +13 -16
  227. package/dist/plugins/remote-env-k8s.js +61 -54
  228. package/dist/plugins/remote-env-local-docker.d.ts +30 -5
  229. package/dist/plugins/remote-env-local-docker.js +46 -59
  230. package/dist/plugins/remote-env-ssh.d.ts +18 -4
  231. package/dist/plugins/remote-env-ssh.js +72 -56
  232. package/dist/plugins/remote-shell.d.ts +100 -3
  233. package/dist/plugins/remote-shell.js +166 -2
  234. package/dist/plugins/retention-store-sql.d.ts +23 -8
  235. package/dist/plugins/retention-store-sql.js +13 -1
  236. package/dist/plugins/run-store-sql.d.ts +13 -3
  237. package/dist/plugins/run-store-sql.js +5 -0
  238. package/dist/plugins/schema-consistency.d.ts +15 -5
  239. package/dist/plugins/schema-consistency.js +14 -8
  240. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  241. package/dist/plugins/schema-drift-sentence.js +38 -0
  242. package/dist/plugins/session-fork.d.ts +63 -12
  243. package/dist/plugins/session-fork.js +38 -21
  244. package/dist/plugins/session-ownership.d.ts +7 -0
  245. package/dist/plugins/session-ownership.js +5 -0
  246. package/dist/plugins/session-row-sql.d.ts +22 -0
  247. package/dist/plugins/session-row-sql.js +81 -0
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +17 -0
  250. package/dist/plugins/session-workspace-cell.js +38 -0
  251. package/dist/plugins/store-backend.d.ts +1 -1
  252. package/dist/plugins/store-backend.js +26 -14
  253. package/dist/plugins/store-contracts.d.ts +25 -5
  254. package/dist/plugins/tidb-pool.d.ts +12 -7
  255. package/dist/plugins/tidb-pool.js +15 -3
  256. package/dist/plugins/tidb-session-store.d.ts +74 -28
  257. package/dist/plugins/tidb-session-store.js +132 -53
  258. package/dist/plugins/tool-result-store-sql.d.ts +16 -8
  259. package/dist/plugins/tool-result-store-sql.js +26 -17
  260. package/dist/plugins/worktree-isolation.d.ts +3 -2
  261. package/dist/plugins/worktree-isolation.js +1 -7
  262. package/dist/project-identity.d.ts +1 -14
  263. package/dist/project-identity.js +0 -4
  264. package/dist/project-memory.d.ts +9 -15
  265. package/dist/project-memory.js +5 -4
  266. package/dist/request-key-closure.js +1 -1
  267. package/dist/request-value-closure.d.ts +6 -7
  268. package/dist/request-value-closure.js +1 -3
  269. package/dist/rules-consent.js +6 -4
  270. package/dist/run-local.d.ts +6 -0
  271. package/dist/run-local.js +36 -26
  272. package/dist/runs.d.ts +9 -2
  273. package/dist/runs.js +9 -2
  274. package/dist/sandbox-git-plane.d.ts +2 -2
  275. package/dist/security.d.ts +38 -12
  276. package/dist/security.js +8 -2
  277. package/dist/server-secret-env.d.ts +15 -8
  278. package/dist/server-secret-env.js +4 -2
  279. package/dist/session-children-retire.d.ts +27 -9
  280. package/dist/session-children-retire.js +9 -5
  281. package/dist/session-incarnation.d.ts +19 -10
  282. package/dist/session-incarnation.js +5 -3
  283. package/dist/session-row.d.ts +114 -5
  284. package/dist/session-row.js +55 -5
  285. package/dist/session-titler.d.ts +2 -2
  286. package/dist/session-workspace.d.ts +130 -0
  287. package/dist/session-workspace.js +83 -0
  288. package/dist/spec-fields.d.ts +1 -1
  289. package/dist/standing-answer-grant.d.ts +2 -13
  290. package/dist/standing-answer-grant.js +0 -7
  291. package/dist/task-a2a.d.ts +44 -14
  292. package/dist/task-a2a.js +46 -20
  293. package/dist/task-cwd.d.ts +6 -1
  294. package/dist/task-mcp.d.ts +33 -20
  295. package/dist/task-mcp.js +46 -22
  296. package/dist/tool-approval.d.ts +38 -8
  297. package/dist/tool-approval.js +57 -13
  298. package/dist/trace/core-keyset-guard.d.ts +33 -18
  299. package/dist/trace/engine-notice-wire.d.ts +1 -1
  300. package/dist/trace/engine-notice-wire.js +2 -0
  301. package/dist/trace/exit-seat.d.ts +20 -1
  302. package/dist/trace/exit-seat.js +3 -0
  303. package/dist/trace/extension-call-face.d.ts +33 -0
  304. package/dist/trace/extension-call-face.js +39 -0
  305. package/dist/trace/project.d.ts +26 -7
  306. package/dist/trace/project.js +30 -3
  307. package/dist/trace/redact.js +42 -13
  308. package/dist/trace/self-minted-wire.d.ts +240 -12
  309. package/dist/trace/self-minted-wire.js +100 -4
  310. package/dist/trace/wire-projection-faces.d.ts +6 -6
  311. package/dist/trace/wire-projection-faces.js +6 -5
  312. package/dist/url-refusal.d.ts +1 -1
  313. package/dist/user-authored-text.d.ts +2 -2
  314. package/dist/user-authored-text.js +16 -1
  315. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  316. package/dist/web-admin-client/apply-effective.js +20 -10
  317. package/dist/web-admin-client/center-request.d.ts +3 -2
  318. package/dist/web-admin-client/center-request.js +2 -3
  319. package/dist/web-admin-client/skills-mcp.d.ts +8 -4
  320. package/dist/web-admin-client/skills-mcp.js +32 -18
  321. package/dist/web-admin-client/types.d.ts +11 -0
  322. package/package.json +3 -3
  323. package/wire-changes/server@7.138.0.json +446 -0
  324. package/wire-changes/server@7.139.0.json +348 -0
  325. package/dist/http/device-placement.d.ts +0 -175
  326. package/dist/http/device-placement.js +0 -145
  327. package/dist/http/mcp-remote-text.d.ts +0 -5
  328. package/dist/http/mcp-remote-text.js +0 -7
@@ -3,8 +3,12 @@ import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../pri
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
5
  import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, isDeviceAbsolutePath, sameDeviceOwner, } from "../../device-store.js";
6
+ import { boundDeviceOf } from "../../execution-placement.js";
7
+ import { defaultTaskRegistry } from "@sema-agent/core";
8
+ import { probeSessionChildren, sessionChildrenScope } from "../../session-children-retire.js";
9
+ import { liveChildrenRefusal } from "./background.js";
6
10
  import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
7
- import { deviceOwnerDomainOf } from "../device-placement.js";
11
+ import { deviceOwnerDomainOf } from "../execution-context.js";
8
12
  import { echoKeyName } from "../../request-key-closure.js";
9
13
  const DEVICE_NOT_FOUND_MESSAGE = "device not found";
10
14
  const NO_DEVICE_DOMAIN_MESSAGE = "device management needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) and is not an explicit operator — connect through the registry auth bridge, or configure OPERATOR_PRINCIPALS";
@@ -32,9 +36,6 @@ function deviceListRow(row, online) {
32
36
  ...(row.lastSeenAtMs !== null ? { lastSeenAt: new Date(row.lastSeenAtMs).toISOString() } : {}),
33
37
  };
34
38
  }
35
- function boundDeviceOf(row) {
36
- return { deviceId: row.deviceId, rev: row.rev, boundAt: new Date(row.boundAtMs).toISOString() };
37
- }
38
39
  function sendDeviceRejection(res, rej) {
39
40
  if (rej.retryAfterSec !== undefined)
40
41
  res.setHeader("retry-after", String(rej.retryAfterSec));
@@ -112,7 +113,6 @@ function parseEnrollBody(res, body) {
112
113
  export async function handleDevices(req, res, match, ctx) {
113
114
  switch (match.decl.id) {
114
115
  case "devices-list":
115
- case "device-session":
116
116
  case "device-session-rebind":
117
117
  case "device-enroll-tokens":
118
118
  case "device-enroll":
@@ -248,31 +248,19 @@ export async function handleDevices(req, res, match, ctx) {
248
248
  return;
249
249
  }
250
250
  const rootSessionId = decoded;
251
- if (match.decl.id === "device-session") {
252
- if (!operator && domain === undefined) {
253
- sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
254
- return;
255
- }
256
- const binding = await store.getSessionBinding(rootSessionId);
257
- if (!binding || (!operator && !sameDeviceOwner(binding.owner, domain))) {
258
- sendDeviceNotFound(res);
259
- return;
260
- }
261
- sendJson(res, 200, { rootSessionId, boundDevice: boundDeviceOf(binding) });
262
- return;
263
- }
264
251
  if (!operator && domain === undefined) {
265
252
  sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
266
253
  return;
267
254
  }
268
- const binding = await store.getSessionBinding(rootSessionId);
269
- if (!binding || (!operator && !sameDeviceOwner(binding.owner, domain))) {
255
+ const placement = await enrollment.readPlacement(rootSessionId);
256
+ const binding = placement === undefined ? undefined : boundDeviceOf(placement.cell);
257
+ if (placement === undefined || !binding || (!operator && !sameDeviceOwner(binding.owner, domain))) {
270
258
  sendDeviceNotFound(res);
271
259
  return;
272
260
  }
273
261
  const body = (await readJson(req));
274
262
  if (typeof body !== "object" || body === null || typeof body.toDeviceId !== "string" || body.expectedRev === undefined) {
275
- sendError(res, 400, "request.body_shape", "body must be { toDeviceId: string, expectedRev: number } — expectedRev is the binding rev read from GET /v1/devices/sessions/{rootSessionId} (explicit prior state; two concurrent operators must not silently stomp each other)");
263
+ sendError(res, 400, "request.body_shape", "body must be { toDeviceId: string, expectedRev: number } — expectedRev is the device rev read from GET /v1/sessions/{sessionId}/execution-placement (explicit prior state; two concurrent operators must not silently stomp each other)");
276
264
  return;
277
265
  }
278
266
  if (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0) {
@@ -295,16 +283,24 @@ export async function handleDevices(req, res, match, ctx) {
295
283
  sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId });
296
284
  return;
297
285
  }
298
- const result = await store.rebindSession({ rootSessionId, toDeviceId, expectedRev, owner: binding.owner });
286
+ const tree = await probeSessionChildren({ agentStore: deps.backgroundAgentStore, checkpointStore: deps.checkpointStore, registry: defaultTaskRegistry }, rootSessionId, sessionChildrenScope(placement.occupant.owner));
287
+ if (tree.liveChildren.length > 0) {
288
+ sendError(res, 409, "device.rebind_active_run", "this session still has background agents running or waiting on an approval under it — rebinding now would leave them addressed at the old device; retire them (POST retirePath) or let them finish, then retry", liveChildrenRefusal(rootSessionId, tree.liveChildren));
289
+ return;
290
+ }
291
+ const result = await store.rebindDeviceExecution({ root: rootSessionId, occupant: placement.occupant, owner: binding.owner, expectedRev, targetDeviceId: toDeviceId });
299
292
  switch (result.outcome) {
300
293
  case "rebound":
301
294
  case "idempotent":
302
- sendJson(res, 200, { rootSessionId, deviceId: result.row.deviceId, rev: result.row.rev, workspaceCarryover: "none" });
295
+ sendJson(res, 200, { rootSessionId, deviceId: result.device.id, rev: result.device.rev, workspaceCarryover: "none" });
303
296
  return;
304
297
  case "rev_conflict":
305
- sendError(res, 409, "device.rebind_rev_conflict", "expectedRev does not match the binding's current rev — the session↔device binding changed since it was read; re-read it and retry with currentRev", { currentRev: result.row.rev });
298
+ sendError(res, 409, "device.rebind_rev_conflict", "expectedRev does not match the binding's current rev — the session↔device binding changed since it was read; re-read it and retry with currentRev", { currentRev: result.device.rev });
299
+ return;
300
+ case "active_run":
301
+ sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId: result.activeTaskId });
306
302
  return;
307
- case "binding_not_found":
303
+ case "gone":
308
304
  case "device_not_found":
309
305
  sendDeviceNotFound(res);
310
306
  return;
@@ -321,7 +317,6 @@ export const DEVICES_ROUTES = [
321
317
  { id: "device-enroll", path: "/v1/devices/enroll", credentialGated: ["POST"], methods: ["POST"] },
322
318
  { id: "device-revoke", pattern: /^\/v1\/devices\/(?<deviceId>[^/]+)\/revoke$/, label: "/v1/devices/:deviceId/revoke", methods: ["POST"] },
323
319
  { id: "device-session-rebind", pattern: /^\/v1\/devices\/sessions\/(?<rootSessionId>[^/]+)\/rebind$/, label: "/v1/devices/sessions/:id/rebind", methods: ["POST"] },
324
- { id: "device-session", pattern: /^\/v1\/devices\/sessions\/(?<rootSessionId>[^/]+)$/, label: "/v1/devices/sessions/:id", credentialGated: ["GET"], methods: ["GET"] },
325
320
  { prefix: "/v1/devices/", credentialGated: ["POST"] },
326
321
  ];
327
322
  //# sourceMappingURL=devices.js.map
@@ -25,8 +25,9 @@
25
25
  * `unsupported` 挂 409 更会把「这条 run 没有 MCP 面」谎报成一个活性问题。
26
26
  *
27
27
  * ── 下一步动作按 `status`,不解析 `reason`(core [ref] 键表逐字)────────────────────────────────
28
- * `status.errorCode` / `status.httpStatus` 是机器面(`http_status` 401 是一扇重新授权的门,`spawn_failed` 是
29
- * 缺二进制);`reason` 是**人读**的一句话(它可能嵌远端文本:引擎铸点抹过请求回显与同主体词表,本仓那一遍只读引擎没抹的段、定长截断后投出 —— `http/mcp-remote-text.ts`),
28
+ * `status.errorCode` / `status.httpStatus` 是机器面(`unauthorized` 是一扇要用户授权的门 —— 引擎 7.59.0 起 401 与带 `insufficient_scope` 的 403 读这个词、不再是
29
+ * `http_status` + 状态数;`spawn_failed` 是
30
+ * 缺二进制);`reason` 是**人读**的一句话(它可能嵌远端文本:引擎铸点抹过请求回显与同主体词表,出门经出口座 `mcp-remote-text` 行 —— 同一个脱敏器、界 200 —— 投出),
30
31
  * 消费端**不许**解析它。名册**按 `tools` 的在场性**投成 `toolNames`(present-iff),**不按 `outcome`**。
31
32
  *
32
33
  * ── 查找与拒绝形(与 steer / capture-optout **同一只**,不新造)───────────────────────────────────
@@ -7,7 +7,8 @@ import {} from "../route-ctx.js";
7
7
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
8
8
  import * as facts from "../../capability-facts.js";
9
9
  import { sessionOwnerCellOf } from "../../security.js";
10
- import { redactMcpRemoteText } from "../mcp-remote-text.js";
10
+ import { seatText } from "../../trace/exit-seat.js";
11
+ import { projectMcpAuthorization } from "../../mcp-authorization-face.js";
11
12
  const BodySchema = z.object({ server: z.string().min(1).max(190) }).strict();
12
13
  function outcomeOf(coreOutcome) {
13
14
  switch (coreOutcome) {
@@ -24,8 +25,9 @@ function projectStatus(s) {
24
25
  ...(s.errorCode !== undefined ? { errorCode: s.errorCode } : {}),
25
26
  ...(s.httpStatus !== undefined ? { httpStatus: s.httpStatus } : {}),
26
27
  ...(s.delivered !== undefined ? { delivered: s.delivered } : {}),
28
+ ...((a) => (a !== undefined ? { authorization: a } : {}))(projectMcpAuthorization(s.authorization)),
27
29
  ...(s.serverInfo !== undefined ? { serverInfo: s.serverInfo } : {}),
28
- ...(s.error !== undefined ? { error: redactMcpRemoteText(String(s.error)) } : {}),
30
+ ...(s.error !== undefined ? { error: seatText("mcp-remote-text", String(s.error)) } : {}),
29
31
  ...(s.transportClosed !== undefined ? { transportClosed: s.transportClosed } : {}),
30
32
  };
31
33
  }
@@ -34,7 +36,7 @@ function projectListingIncomplete(l) {
34
36
  reason: l.reason,
35
37
  pages: l.pages,
36
38
  ...(l.budgetMs !== undefined ? { budgetMs: l.budgetMs } : {}),
37
- ...(l.error !== undefined ? { error: redactMcpRemoteText(l.error) } : {}),
39
+ ...(l.error !== undefined ? { error: seatText("mcp-remote-text", l.error) } : {}),
38
40
  };
39
41
  }
40
42
  function projectResult(r) {
@@ -46,7 +48,7 @@ function projectResult(r) {
46
48
  added: r.added,
47
49
  removed: r.removed,
48
50
  ...(r.tools !== undefined ? { toolNames: r.tools.map((t) => t.name) } : {}),
49
- ...(r.reason !== undefined ? { reason: redactMcpRemoteText(r.reason) } : {}),
51
+ ...(r.reason !== undefined ? { reason: seatText("mcp-remote-text", r.reason) } : {}),
50
52
  ...(r.status !== undefined ? { status: projectStatus(r.status) } : {}),
51
53
  ...(r.listingIncomplete !== undefined ? { listingIncomplete: projectListingIncomplete(r.listingIncomplete) } : {}),
52
54
  };
@@ -112,7 +114,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
112
114
  const msg = e instanceof Error ? e.message : String(e);
113
115
  switch (code) {
114
116
  case "mcp.reconnect_unavailable":
115
- sendJson(res, 200, { taskId: activeTaskId, sessionId, server, outcome: "unsupported", reason: redactMcpRemoteText(msg) });
117
+ sendJson(res, 200, { taskId: activeTaskId, sessionId, server, outcome: "unsupported", reason: seatText("mcp-remote-text", msg) });
116
118
  return;
117
119
  case "steering.invalid_content":
118
120
  sendError(res, 422, "steering.invalid_content", msg);
@@ -24,7 +24,7 @@
24
24
  * · 调用方显式给 `scope` ⇒ 用它 —— 哪怕它不在 `MEMORY_CONSOLIDATION_SCOPES` 里。
25
25
  * 🔴 那张表**不是授权边界**:本仓今天没有任何授权源能对一个 operator 收窄 scope(`gateMemoryScope`
26
26
  * 的 operator 支在查目录之前就 allow;`org-memory-admission` 明写 v1 不中央收窄 operator 声明的
27
- * scope —— 全文见 `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注)。在本层拿它当
27
+ * scope —— 全文见 `src/memory-operator-faces.ts` `MemoryComplianceFaces.erase` 的「授权座只有一只」段)。在本层拿它当
28
28
  * 白名单就是**发明第二套授权语义**,与 `gateMemoryScope` 对同一个身份给出相反答案;
29
29
  * · 省略 `scope` ⇒ 只有表**恰好一条**时才有缺省(那条就是这台部署的库);零条或两条以上一律 400,
30
30
  * 并把候选列进文案 —— 让运维一眼看到「我该点哪一个」。
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
114
114
  baseRev: string;
115
115
  origin: {
116
116
  at: number;
117
- cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
117
+ cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
118
118
  taint: "external";
119
119
  };
120
120
  };
@@ -7,7 +7,7 @@ import { createLogger } from "../../observability/logger.js";
7
7
  import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, buildCcImportLayerRootRefusal, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
8
  import { hasControlOrBidi } from "@sema-agent/settings-schema";
9
9
  import { IMPORTED_SETTINGS_LAYERS, MAX_RULE_TEXT_CHARS, PERSISTED_RULE_MATCHES, RULE_ADD_ORIGINS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
10
- import { isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
10
+ import { isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
11
11
  const logger = createLogger();
12
12
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
13
13
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -50,9 +50,9 @@ const LocalImportRowSchema = z
50
50
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
51
51
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
52
52
  behavior: z.enum(RULE_BEHAVIORS),
53
- tool: z.string().min(1).max(190).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
53
+ tool: z.string().min(1).max(190),
54
54
  match: z.enum(PERSISTED_RULE_MATCHES),
55
- command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
55
+ command: z.string().max(MAX_RULE_TEXT_CHARS),
56
56
  adds: z.array(LocalImportAddSchema).max(64),
57
57
  })
58
58
  .strict();
@@ -222,11 +222,16 @@ export async function handleRuns(req, res, match, ctx) {
222
222
  const prepared = await prepareSpec(req, res, keyedBody);
223
223
  if (!prepared)
224
224
  return;
225
- const resp = await createDurableRun(req, res, ctx, prepared, idem);
226
- if (!resp)
225
+ try {
226
+ const resp = await createDurableRun(req, res, ctx, prepared, idem);
227
+ if (!resp)
228
+ return;
229
+ sendJson(res, resp.status, resp.body);
227
230
  return;
228
- sendJson(res, resp.status, resp.body);
229
- return;
231
+ }
232
+ finally {
233
+ prepared.peerLease.release();
234
+ }
230
235
  }
231
236
  if (match.decl.id === "run-detail" || match.decl.id === "run-events" || match.decl.id === "runs-by-idem") {
232
237
  const runs = facts.durableRuns(deps);
@@ -433,13 +438,12 @@ export async function handleRuns(req, res, match, ctx) {
433
438
  const text = body.text;
434
439
  const priority = body.priority;
435
440
  const steerIdemRaw = headerStr(req.headers["idempotency-key"]);
436
- const steerInputId = steerIdemRaw ? `idem-${idemKeyOf(steerIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : undefined;
437
- const messageId = steerInputId ?? uuidv7();
441
+ const messageId = steerIdemRaw ? `idem-${idemKeyOf(steerIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : uuidv7();
438
442
  const trusted = explicitOperatorOk(principal, deps.config);
439
443
  const steerInput = () => ({
440
444
  text,
441
445
  trusted,
442
- ...(steerInputId !== undefined ? { inputId: steerInputId } : {}),
446
+ inputId: messageId,
443
447
  ...(priority !== undefined ? { priority } : {}),
444
448
  });
445
449
  try {
@@ -18,6 +18,10 @@ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
18
18
  import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
19
19
  import { readSessionPinned, readSessionTitle } from "../../session-row.js";
20
20
  import { sessionFaceOfHead } from "../../session-incarnation.js";
21
+ import { defaultTaskRegistry } from "@sema-agent/core";
22
+ import { SessionActiveRunError, SessionForkInProgressError, SessionWriterPresentError } from "../../execution-placement.js";
23
+ import { probeSessionChildren, sessionChildrenScope } from "../../session-children-retire.js";
24
+ import { liveChildrenRefusal } from "./background.js";
21
25
  export function createSessionSyncLocal() {
22
26
  const pendingStagings = new Map();
23
27
  const importLeases = new Map();
@@ -90,7 +94,46 @@ export async function handleSessionSync(req, res, match, ctx) {
90
94
  const replaySessionRow = async (row, face) => {
91
95
  if (Object.keys(row).length === 0)
92
96
  return true;
93
- return face !== undefined && (await sessionStorage.patchSessionMeta(sessionId, row, face)) !== undefined;
97
+ if (face === undefined)
98
+ return false;
99
+ const replayed = await sessionStorage.writeSessionRowCells(sessionId, row, { writer: "replay", row: { row: face } });
100
+ switch (replayed.kind) {
101
+ case "written":
102
+ return true;
103
+ case "absent":
104
+ return false;
105
+ default: {
106
+ const unreachable = replayed;
107
+ throw new Error(`session-sync row replay: no answer for the row write outcome ${JSON.stringify(unreachable)}`);
108
+ }
109
+ }
110
+ };
111
+ const liveChildrenOf = async (face) => face === undefined
112
+ ? []
113
+ : (await probeSessionChildren({ agentStore: deps.backgroundAgentStore, checkpointStore: deps.checkpointStore, registry: defaultTaskRegistry }, sessionId, sessionChildrenScope(face.owner))).liveChildren;
114
+ const activityOf = async (face) => {
115
+ const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
116
+ if (activeTaskId)
117
+ return { kind: "run", activeTaskId };
118
+ const liveChildren = await liveChildrenOf(face);
119
+ return liveChildren.length > 0 ? { kind: "children", liveChildren } : undefined;
120
+ };
121
+ const sendActivity = (activity, moment) => {
122
+ switch (activity.kind) {
123
+ case "run":
124
+ if (moment === "open")
125
+ sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId: activity.activeTaskId });
126
+ else
127
+ sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activity.activeTaskId });
128
+ return;
129
+ case "children":
130
+ sendError(res, 409, "session_active", "session still has background agents running or waiting on an approval under it; retire them (POST retirePath) or let them finish, then sync", liveChildrenRefusal(sessionId, activity.liveChildren));
131
+ return;
132
+ default: {
133
+ const unhandled = activity;
134
+ throw new Error(`session sync: unknown session activity ${JSON.stringify(unhandled)}`);
135
+ }
136
+ }
94
137
  };
95
138
  const canReplayHistory = fileHistoryImportFace(historyStore) !== undefined;
96
139
  const replayHistory = async (envelope, allowReplace) => {
@@ -339,15 +382,56 @@ export async function handleSessionSync(req, res, match, ctx) {
339
382
  if (!payloadVerified)
340
383
  digestSaysDiffer = true;
341
384
  }
385
+ const placed = gateFace === undefined ? undefined : await sessionStorage.executionPlacement?.read(sessionId);
386
+ if (placed !== undefined) {
387
+ switch (placed.cell.role) {
388
+ case "absent":
389
+ case "mirror":
390
+ break;
391
+ case "writer":
392
+ sendSessionWriter(res);
393
+ return;
394
+ case "forking":
395
+ sendError(res, 409, "conflict.fork_in_progress", new SessionForkInProgressError(sessionId).message);
396
+ return;
397
+ default: {
398
+ const unknownRole = placed.cell;
399
+ throw new Error(`session sync: unknown execution placement ${JSON.stringify(unknownRole)}`);
400
+ }
401
+ }
402
+ }
403
+ const activeNow = await activityOf(gateFace);
404
+ if (activeNow !== undefined) {
405
+ sendActivity(activeNow, "open");
406
+ return;
407
+ }
342
408
  if (relA.relation === "identical" && !digestSaysDiffer) {
343
- if (fileHistory !== null && canReplayHistory) {
344
- if (resolution === "overwrite-dst") {
345
- const activeNow = await deps.runStore?.getActiveTaskId(sessionId);
346
- if (activeNow) {
347
- sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId: activeNow });
409
+ const seat = sessionStorage.executionPlacement;
410
+ if (seat !== undefined) {
411
+ if (gateFace === undefined) {
412
+ sendError(res, 409, "conflict", "sync conflict");
413
+ return;
414
+ }
415
+ const mirrored = await seat.markMirror(sessionId, gateFace);
416
+ switch (mirrored.outcome) {
417
+ case "mirrored":
418
+ break;
419
+ case "writer":
420
+ sendSessionWriter(res);
421
+ return;
422
+ case "gone":
423
+ sendError(res, 409, "conflict", "sync conflict");
424
+ return;
425
+ case "active":
426
+ sendActivity({ kind: "run", activeTaskId: mirrored.activeTaskId }, "open");
348
427
  return;
428
+ default: {
429
+ const unhandled = mirrored;
430
+ throw new Error(`session sync: unknown mirror outcome ${JSON.stringify(unhandled)}`);
349
431
  }
350
432
  }
433
+ }
434
+ if (fileHistory !== null && canReplayHistory) {
351
435
  const failed = await replayHistory(fileHistory, resolution === "overwrite-dst");
352
436
  if (failed !== undefined) {
353
437
  sendFileHistoryReplayFailure(res, failed);
@@ -380,11 +464,6 @@ export async function handleSessionSync(req, res, match, ctx) {
380
464
  });
381
465
  return;
382
466
  }
383
- const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
384
- if (activeTaskId) {
385
- sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
386
- return;
387
- }
388
467
  const heldLease = importLeases.get(sessionId);
389
468
  if (heldLease) {
390
469
  const staleSecRaw = deps.config.syncImportLeaseStaleSec;
@@ -511,8 +590,9 @@ export async function handleSessionSync(req, res, match, ctx) {
511
590
  let mayPushAtCommit;
512
591
  let activeAtCommit;
513
592
  try {
514
- mayPushAtCommit = (await judgeSessionGate()).mayPush;
515
- activeAtCommit = mayPushAtCommit ? await deps.runStore?.getActiveTaskId(sessionId) : undefined;
593
+ const gateAtCommit = await judgeSessionGate();
594
+ mayPushAtCommit = gateAtCommit.mayPush;
595
+ activeAtCommit = mayPushAtCommit ? await activityOf(gateAtCommit.face) : undefined;
516
596
  }
517
597
  catch (e) {
518
598
  await pending.handle.abort().catch(() => undefined);
@@ -525,10 +605,10 @@ export async function handleSessionSync(req, res, match, ctx) {
525
605
  sendSessionAbsent();
526
606
  return;
527
607
  }
528
- if (activeAtCommit) {
608
+ if (activeAtCommit !== undefined) {
529
609
  await pending.handle.abort().catch(() => undefined);
530
610
  cleanupStaging(stagingId);
531
- sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
611
+ sendActivity(activeAtCommit, "streamed");
532
612
  return;
533
613
  }
534
614
  try {
@@ -567,6 +647,18 @@ export async function handleSessionSync(req, res, match, ctx) {
567
647
  sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
568
648
  return;
569
649
  }
650
+ if (e instanceof SessionWriterPresentError) {
651
+ sendSessionWriter(res);
652
+ return;
653
+ }
654
+ if (e instanceof SessionForkInProgressError) {
655
+ sendError(res, 409, "conflict.fork_in_progress", e.message);
656
+ return;
657
+ }
658
+ if (e instanceof SessionActiveRunError) {
659
+ sendActivity({ kind: "run", activeTaskId: e.activeTaskId }, "streamed");
660
+ return;
661
+ }
570
662
  if (sendSessionPolicyError(res, e))
571
663
  return;
572
664
  if (sendSessionError(res, e, "import")) {
@@ -635,6 +727,9 @@ function sendPullUnreadable(req, res, logger, e) {
635
727
  }
636
728
  }
637
729
  }
730
+ function sendSessionWriter(res) {
731
+ sendError(res, 409, "conflict.session_writer", "this session is written on this deployment (its execution placement is a writer here) — an import cannot turn it into a read-only mirror; fork it to push your copy, or pull instead; nothing was written");
732
+ }
638
733
  function sendMissingBlob(res) {
639
734
  sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
640
735
  }
@@ -74,6 +74,12 @@ export declare const SESSIONS_ROUTES: readonly [{
74
74
  readonly pattern: RegExp;
75
75
  readonly label: "/v1/sessions/:id/$verb";
76
76
  readonly methods: readonly ["GET"];
77
+ }, {
78
+ readonly id: "session-execution-placement";
79
+ readonly pattern: RegExp;
80
+ readonly label: "/v1/sessions/:id/execution-placement";
81
+ readonly credentialGated: readonly ["GET"];
82
+ readonly methods: readonly ["GET"];
77
83
  }, {
78
84
  readonly id: "session-events";
79
85
  readonly pattern: RegExp;