@sema-agent/server 7.51.0 → 7.52.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 (198) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/engine-lease.d.ts +1 -1
  21. package/dist/boot/execution-env.d.ts +1 -1
  22. package/dist/boot/governance-seams.d.ts +2 -2
  23. package/dist/boot/leader.d.ts +1 -1
  24. package/dist/boot/memory-boundary.d.ts +2 -2
  25. package/dist/boot/memory-consolidation.d.ts +6 -6
  26. package/dist/boot/org-memory.d.ts +1 -1
  27. package/dist/boot/parked-revive-gate.d.ts +3 -3
  28. package/dist/boot/reapers.d.ts +2 -2
  29. package/dist/boot/reapers.js +17 -4
  30. package/dist/boot/resolve-spec.d.ts +12 -0
  31. package/dist/boot/resolve-spec.js +29 -7
  32. package/dist/boot/retention-lane.d.ts +2 -2
  33. package/dist/boot/runner-deps.d.ts +17 -7
  34. package/dist/boot/runner-deps.js +4 -2
  35. package/dist/boot/runtime-caps.d.ts +23 -0
  36. package/dist/boot/runtime-caps.js +24 -2
  37. package/dist/boot/session-faces.d.ts +1 -1
  38. package/dist/boot/session-faces.js +14 -3
  39. package/dist/boot/stores.js +31 -1
  40. package/dist/boot/task-list-lane.d.ts +2 -2
  41. package/dist/brain.js +51 -31
  42. package/dist/budget.d.ts +2 -2
  43. package/dist/capabilities/center-plugins.d.ts +1 -1
  44. package/dist/capabilities/collab-workflows.d.ts +1 -1
  45. package/dist/capabilities/hands-lane.d.ts +3 -3
  46. package/dist/capabilities/scenarios.d.ts +3 -3
  47. package/dist/capabilities/skills.d.ts +1 -1
  48. package/dist/config-center/hot-keys-registry.js +5 -0
  49. package/dist/config-center/http-client.d.ts +2 -2
  50. package/dist/config-center/read-face.d.ts +1 -1
  51. package/dist/config-center/restart-signal.d.ts +1 -1
  52. package/dist/config-types.d.ts +78 -18
  53. package/dist/config.js +45 -10
  54. package/dist/deployment-governance.d.ts +1 -1
  55. package/dist/device-enrollment.d.ts +2 -2
  56. package/dist/device-store.d.ts +3 -3
  57. package/dist/device-ws-protocol.d.ts +2 -2
  58. package/dist/env-facts.d.ts +1 -1
  59. package/dist/execution-lane-caps.d.ts +1 -1
  60. package/dist/fleet/fleet-bus.d.ts +1 -1
  61. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  62. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  63. package/dist/http/active-run-conflict.d.ts +2 -2
  64. package/dist/http/principal-gate.d.ts +1 -1
  65. package/dist/http/route-ctx.d.ts +24 -4
  66. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  67. package/dist/http/routes/admin-memory-optout.js +75 -0
  68. package/dist/http/routes/agents-roster.d.ts +1 -1
  69. package/dist/http/routes/approvals-assistant.js +21 -21
  70. package/dist/http/routes/capabilities.js +20 -9
  71. package/dist/http/routes/diagnostics.d.ts +1 -1
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/memory-bundle.d.ts +1 -1
  74. package/dist/http/routes/memory-bundle.js +3 -1
  75. package/dist/http/routes/memory-compliance.d.ts +4 -4
  76. package/dist/http/routes/memory-origin.d.ts +4 -4
  77. package/dist/http/routes/memory-origin.js +3 -1
  78. package/dist/http/routes/memory-policy.js +6 -2
  79. package/dist/http/routes/retention-ops.d.ts +1 -1
  80. package/dist/http/routes/rules.d.ts +5 -2
  81. package/dist/http/routes/rules.js +84 -5
  82. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  83. package/dist/http/routes/run-memory-capture.js +97 -0
  84. package/dist/http/routes/runs.d.ts +1 -0
  85. package/dist/http/routes/runs.js +212 -6
  86. package/dist/http/routes/session-sync.d.ts +4 -6
  87. package/dist/http/routes/session-sync.js +60 -47
  88. package/dist/http/routes/sessions.js +61 -188
  89. package/dist/http/routes/shared-memory.d.ts +1 -1
  90. package/dist/http/routes/tasks.js +1 -1
  91. package/dist/http/routes/workflows.js +19 -0
  92. package/dist/http/server.d.ts +35 -13
  93. package/dist/http/server.js +105 -12
  94. package/dist/http/verify-rounds.d.ts +1 -1
  95. package/dist/http/wire-types.d.ts +41 -8
  96. package/dist/leader/endpoint.d.ts +1 -1
  97. package/dist/leader/leader.d.ts +1 -1
  98. package/dist/leader/repair-oracle.d.ts +2 -2
  99. package/dist/leader/repair-oracle.js +1 -1
  100. package/dist/main.js +13 -6
  101. package/dist/memory-bundle-engine.d.ts +1 -1
  102. package/dist/memory-operator-faces.d.ts +5 -5
  103. package/dist/memory-origin-fence.d.ts +50 -0
  104. package/dist/memory-origin-fence.js +15 -0
  105. package/dist/memory-posture.d.ts +13 -1
  106. package/dist/memory-posture.js +1 -0
  107. package/dist/memory-scope.d.ts +6 -1
  108. package/dist/memory-scope.js +3 -2
  109. package/dist/model-select.d.ts +2 -2
  110. package/dist/observability/fail-open.d.ts +17 -5
  111. package/dist/observability/fail-open.js +17 -5
  112. package/dist/orchestration/hardened-vm-runner.d.ts +2 -2
  113. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  114. package/dist/plugins/background-shell-support.d.ts +2 -2
  115. package/dist/plugins/blob-backend.d.ts +7 -2
  116. package/dist/plugins/blob-backend.js +22 -18
  117. package/dist/plugins/caching-session-store.d.ts +7 -3
  118. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  119. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  120. package/dist/plugins/file-history-store-sql.js +827 -0
  121. package/dist/plugins/file-run-store.d.ts +44 -5
  122. package/dist/plugins/file-run-store.js +85 -13
  123. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  124. package/dist/plugins/fork-routing-session-store.js +2 -2
  125. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  126. package/dist/plugins/local-session-store.d.ts +9 -1
  127. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  128. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  129. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  130. package/dist/plugins/memory-origin-law.d.ts +2 -2
  131. package/dist/plugins/memory-run-store.d.ts +4 -3
  132. package/dist/plugins/memory-run-store.js +2 -1
  133. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  134. package/dist/plugins/permission-rule-store-file.js +77 -13
  135. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  136. package/dist/plugins/permission-rule-store-sql.js +123 -21
  137. package/dist/plugins/pg-pool.js +59 -0
  138. package/dist/plugins/pg-safe-json.d.ts +1 -1
  139. package/dist/plugins/remote-env-device.d.ts +1 -1
  140. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  141. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  142. package/dist/plugins/retention-store-sql.d.ts +7 -7
  143. package/dist/plugins/retention-store-sql.js +6 -0
  144. package/dist/plugins/run-store-sql.d.ts +9 -3
  145. package/dist/plugins/run-store-sql.js +2 -2
  146. package/dist/plugins/send-user-file.d.ts +1 -1
  147. package/dist/plugins/session-placement.d.ts +1 -1
  148. package/dist/plugins/sql-errors.d.ts +1 -1
  149. package/dist/plugins/store-backend.d.ts +59 -29
  150. package/dist/plugins/store-backend.js +75 -15
  151. package/dist/plugins/store-contracts.d.ts +5 -3
  152. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  153. package/dist/plugins/tidb-pool.js +59 -0
  154. package/dist/plugins/web-search.d.ts +1 -1
  155. package/dist/prompts-domain-validate.js +1 -1
  156. package/dist/resource-suspend.d.ts +1 -1
  157. package/dist/resource-window.d.ts +3 -3
  158. package/dist/rules-consent.d.ts +231 -9
  159. package/dist/rules-consent.js +411 -13
  160. package/dist/run-local.js +1 -0
  161. package/dist/runs.d.ts +15 -1
  162. package/dist/runs.js +19 -0
  163. package/dist/runtime-caps-resolver.d.ts +36 -1
  164. package/dist/runtime-caps-resolver.js +25 -0
  165. package/dist/runtime-governance.d.ts +1 -1
  166. package/dist/security.d.ts +5 -5
  167. package/dist/security.js +1 -1
  168. package/dist/session-sync.d.ts +52 -37
  169. package/dist/session-sync.js +19 -16
  170. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  171. package/dist/sighup-idle.d.ts +1 -1
  172. package/dist/spec-fields.d.ts +22 -1
  173. package/dist/spec-fields.js +11 -0
  174. package/dist/store-live-probe.d.ts +2 -2
  175. package/dist/task-a2a.d.ts +1 -1
  176. package/dist/task-cwd.d.ts +1 -1
  177. package/dist/task-mcp.d.ts +3 -3
  178. package/dist/task-settings.d.ts +3 -3
  179. package/dist/task-workflow.d.ts +1 -1
  180. package/dist/text-bidi.d.ts +2 -2
  181. package/dist/tool-approval.d.ts +191 -9
  182. package/dist/tool-approval.js +248 -29
  183. package/dist/trace/core-keyset-guard.d.ts +1 -1
  184. package/dist/trace/engine-notice-wire.d.ts +6 -4
  185. package/dist/trace/engine-notice-wire.js +2 -0
  186. package/dist/trace/ledger-events.d.ts +1 -1
  187. package/dist/trace/ledger-sink.d.ts +2 -2
  188. package/dist/trace/project.d.ts +3 -2
  189. package/dist/trace/project.js +1 -0
  190. package/dist/turn-activity.d.ts +1 -1
  191. package/dist/wall-clock-jump-guard.d.ts +3 -3
  192. package/package.json +2 -2
  193. package/dist/http/tar.d.ts +0 -17
  194. package/dist/http/tar.js +0 -53
  195. package/dist/http/workspace-content.d.ts +0 -11
  196. package/dist/http/workspace-content.js +0 -20
  197. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  198. package/dist/plugins/file-snapshot-store-sql.js +0 -213
@@ -4,8 +4,6 @@ import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js
4
4
  import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
7
- import { workspaceContentType, looksBinary } from "../workspace-content.js";
8
- import { tarHeader, tarPadding, splitTarPath, TAR_END } from "../tar.js";
9
7
  import { sendJson, sendError, sseHeaders } from "../send.js";
10
8
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
11
9
  const MCP_STATUS_TTL_MS = 5_000;
@@ -411,6 +409,66 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
411
409
  return;
412
410
  }
413
411
  }
412
+ {
413
+ const em = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/memory\/erase$/.exec(url) : null;
414
+ if (em) {
415
+ const sessionId = safeDecode(em[1]);
416
+ if (sessionId === null) {
417
+ sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
418
+ return;
419
+ }
420
+ if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
421
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
422
+ return;
423
+ }
424
+ if (!deps.sessionAudit) {
425
+ 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)");
426
+ return;
427
+ }
428
+ const audit = await deps.sessionAudit(sessionId);
429
+ if (!audit) {
430
+ sendError(res, 404, "not_found.session", "session not found");
431
+ return;
432
+ }
433
+ if (!runOwnerOk(req, res, audit.owner))
434
+ return;
435
+ const faces = deps.memoryCompliance;
436
+ if (faces === undefined) {
437
+ 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)");
438
+ return;
439
+ }
440
+ const raw = (await readJson(req));
441
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
442
+ sendError(res, 400, "request.body_shape", "invalid session memory erase body — expected { requestId: string, allowUnevidenced?: boolean } (no `select`: this face erases exactly this session's own contributions)");
443
+ return;
444
+ }
445
+ if ("select" in raw) {
446
+ sendError(res, 400, "request.body_shape", "invalid session memory erase body — `select` is not accepted on the self-service face: the selector is fixed to { sessionId: <this session> } by the path; the operator face POST /v1/memory/erase takes explicit selectors");
447
+ return;
448
+ }
449
+ if (typeof raw.requestId !== "string" || raw.requestId.length === 0) {
450
+ sendError(res, 400, "request.request_id_required", "memory erasure requires a non-empty `requestId` string — it is this request's idempotency identity and the server never mints one for you (a retry must carry the SAME id)");
451
+ return;
452
+ }
453
+ if (raw.allowUnevidenced !== undefined && typeof raw.allowUnevidenced !== "boolean") {
454
+ sendError(res, 400, "request.field_invalid", "allowUnevidenced must be a boolean when present");
455
+ return;
456
+ }
457
+ try {
458
+ sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
459
+ }
460
+ catch (err) {
461
+ const code = err.code;
462
+ if (code === "config.memory_erasure_request") {
463
+ sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
464
+ return;
465
+ }
466
+ deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
467
+ sendError(res, 500, "internal.error", "session memory erasure failed");
468
+ }
469
+ return;
470
+ }
471
+ }
414
472
  if (deps.sessionAudit && req.method === "GET") {
415
473
  const sm = /^\/v1\/sessions\/([^/]+)$/.exec(url);
416
474
  if (sm) {
@@ -621,192 +679,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
621
679
  {
622
680
  const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
623
681
  if (wm) {
624
- const fs = deps.fileSnapshotStore;
625
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
626
- if (!fs || typeof fs.listKeys !== "function" || typeof fs.exportManifest !== "function" || typeof fs.getBlob !== "function" || !ownerOf) {
627
- sendError(res, 501, "capability.snapshot_store_required", "workspace browse requires a durable snapshot store (rewindFiles wiring) and a durable session store");
628
- return;
629
- }
630
- const scope = sessionOwnerScope(req);
631
- if (!scope.fleetWide && scope.gateOwner === null) {
632
- sendError(res, 401, "auth.unauthorized", "unauthorized");
633
- return;
634
- }
635
- const wsSession = safeDecode(wm[1]);
636
- if (wsSession === null || !isUuidShape(wsSession)) {
637
- sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
638
- return;
639
- }
640
- const wsOwner = await ownerOf(wsSession);
641
- if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, wsOwner))) {
642
- sendError(res, 404, "not_found.session", "session not found");
643
- return;
644
- }
645
- const q = new URL(req.url ?? "", "http://x").searchParams;
646
- const keys = (await fs.listKeys(wsSession)).sort().reverse();
647
- const sub = wm[3];
648
- if (wm[2] === undefined) {
649
- const limRaw = q.get("limit");
650
- if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
651
- sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
652
- return;
653
- }
654
- const lim = limRaw === null ? 20 : Number(limRaw);
655
- const beforeRaw = q.get("before");
656
- const scoped = beforeRaw !== null ? keys.filter((k) => k < beforeRaw) : keys;
657
- const page = scoped.slice(0, lim);
658
- const snapshots = [];
659
- for (const k of page) {
660
- const m = await fs.exportManifest(wsSession, k);
661
- if (m === null)
662
- continue;
663
- let bytes;
664
- if (typeof fs.blobSizes === "function") {
665
- const sizes = await fs.blobSizes([...new Set(m.values())]);
666
- bytes = 0;
667
- for (const h of m.values())
668
- bytes += sizes.get(h) ?? 0;
669
- }
670
- snapshots.push({ key: k, files: m.size, ...(bytes !== undefined ? { bytes } : {}) });
671
- }
672
- const lastPageKey = page.length > 0 ? page[page.length - 1] : undefined;
673
- const hasMore = lastPageKey !== undefined && scoped.length > page.length;
674
- sendJson(res, 200, {
675
- sessionId: wsSession,
676
- snapshots,
677
- ...(snapshots.length > 0 ? { latest: snapshots[0].key } : {}),
678
- total: keys.length,
679
- ...(hasMore ? { nextBefore: lastPageKey } : {}),
680
- });
681
- return;
682
- }
683
- const keyRaw = safeDecode(wm[2]);
684
- if (keyRaw === null) {
685
- sendError(res, 400, "request.path_malformed", "malformed snapshot key");
686
- return;
687
- }
688
- const key = keyRaw === "latest" ? keys[0] : keyRaw;
689
- const manifest = key !== undefined ? await fs.exportManifest(wsSession, key) : null;
690
- if (key === undefined || manifest === null) {
691
- sendError(res, 404, "not_found.snapshot", "snapshot not found");
692
- return;
693
- }
694
- if (sub === "tree") {
695
- const limRaw = q.get("limit");
696
- if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
697
- sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..2000");
698
- return;
699
- }
700
- const offRaw = q.get("offset");
701
- if (offRaw !== null && !/^[0-9]+$/.test(offRaw)) {
702
- sendError(res, 400, "request.query_invalid", "offset must be a non-negative integer");
703
- return;
704
- }
705
- const lim = limRaw === null ? 500 : Number(limRaw);
706
- const off = offRaw === null ? 0 : Number(offRaw);
707
- const all = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
708
- const page = all.slice(off, off + lim);
709
- const sizes = typeof fs.blobSizes === "function" ? await fs.blobSizes([...new Set(page.map(([, h]) => h))]) : undefined;
710
- sendJson(res, 200, {
711
- sessionId: wsSession,
712
- key,
713
- entries: page.map(([path, hash]) => ({ path, hash, ...(sizes?.get(hash) !== undefined ? { size: sizes.get(hash) } : {}) })),
714
- total: all.length,
715
- ...(off + lim < all.length ? { nextOffset: off + lim } : {}),
716
- });
717
- return;
718
- }
719
- if (sub === "file") {
720
- const pathParam = q.get("path");
721
- if (!pathParam) {
722
- sendError(res, 400, "request.query_invalid", "missing ?path=<relPath from the tree>");
723
- return;
724
- }
725
- const hash = manifest.get(pathParam);
726
- if (hash === undefined) {
727
- sendError(res, 404, "not_found.file", "no such file in this snapshot (paths are the tree's relPaths, verbatim)");
728
- return;
729
- }
730
- const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
731
- if (typeof fs.blobSizes === "function") {
732
- const known = (await fs.blobSizes([hash])).get(hash);
733
- if (known !== undefined && known > cap) {
734
- sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: known, limit: cap });
735
- return;
736
- }
737
- }
738
- const bytes = await fs.getBlob(hash);
739
- if (bytes === undefined) {
740
- sendError(res, 404, "not_found.file", "file content no longer available (snapshot raced a reap)");
741
- return;
742
- }
743
- if (bytes.byteLength > cap) {
744
- sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: bytes.byteLength, limit: cap });
745
- return;
746
- }
747
- const ct = workspaceContentType(pathParam);
748
- const bin = looksBinary(bytes);
749
- res.writeHead(200, {
750
- "content-type": ct,
751
- "content-length": bytes.byteLength,
752
- "x-content-type-options": "nosniff",
753
- "x-sema-content-binary": bin ? "true" : "false",
754
- });
755
- res.end(Buffer.from(bytes));
756
- return;
757
- }
758
- if (sub === "archive") {
759
- const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
760
- const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
761
- if (bad.length > 0) {
762
- sendError(res, 422, "request.content_unrepresentable", `snapshot contains ${bad.length} path(s) not representable in ustar (>255 bytes or unsplittable)`, { paths: bad.slice(0, 5) });
763
- return;
764
- }
765
- if (typeof fs.hasBlobs === "function") {
766
- const present = await fs.hasBlobs([...new Set(entries.map(([, h]) => h))]);
767
- const missing = entries.filter(([, h]) => !present.has(h)).length;
768
- if (missing > 0) {
769
- sendError(res, 502, "internal.snapshot_blobs_missing", `snapshot is missing ${missing} blob(s) (raced a reap or a partial import) — retry, or pick another snapshot`);
770
- return;
771
- }
772
- }
773
- res.writeHead(200, {
774
- "content-type": "application/x-tar",
775
- "content-disposition": `attachment; filename="workspace-${key}.tar"`,
776
- "x-content-type-options": "nosniff",
777
- });
778
- const write = async (buf) => {
779
- if (res.writableEnded || res.destroyed)
780
- throw new Error("client gone");
781
- if (!res.write(buf)) {
782
- await new Promise((resolve, reject) => {
783
- const onDrain = () => { cleanup(); resolve(); };
784
- const onGone = () => { cleanup(); reject(new Error("client gone (closed while draining)")); };
785
- const cleanup = () => { res.off("drain", onDrain); res.off("close", onGone); res.off("error", onGone); };
786
- res.once("drain", onDrain);
787
- res.once("close", onGone);
788
- res.once("error", onGone);
789
- });
790
- }
791
- };
792
- try {
793
- for (const [p, h] of entries) {
794
- const bytes = await fs.getBlob(h);
795
- if (bytes === undefined)
796
- throw new Error(`blob vanished mid-stream: ${h}`);
797
- await write(tarHeader(p, bytes.byteLength, 0));
798
- await write(Buffer.from(bytes));
799
- await write(tarPadding(bytes.byteLength));
800
- }
801
- await write(TAR_END);
802
- res.end();
803
- }
804
- catch {
805
- res.destroy();
806
- }
807
- return;
808
- }
809
- sendError(res, 404, "request.route_unsupported", "unknown workspace verb — use /tree, /file?path=, or /archive");
682
+ sendError(res, 501, "capability.workspace_retired", "workspace browse is retired: the whole-tree snapshot epoch it projected was replaced by per-edited-file rewind history (which holds only files the agent edited, not the workspace). There is no whole-tree manifest to browse anymore.");
810
683
  return;
811
684
  }
812
685
  }
@@ -8,7 +8,7 @@
8
8
  * 挂载条件 = store ∧ org 折叠面,**双在场**才有这个域(v2/F4 硬化)
9
9
  * ─────────────────────────────────────────────────────────────────────────────────────────────────
10
10
  * 供给面在场但授权面缺席时,这个域整体**不挂载** —— 请求落到全局 404 `not_found.route`。刻意不是 501:
11
- * · 501 的自述是「这个面存在,只是本部署没开」,它对一个连身份都无法被判定的调用方泄露了「这套部署
11
+ * · 501 的自述是「这个面存在,只是本部署没开」,它对一个连身份都无法被判定的调用方暴露了「这套部署
12
12
  * 有共享记忆库」,并暗示补个配置就能读;
13
13
  * · 「store 好了先挂载、授权后补」是这条纪律真正要堵的中间态 —— 一个没有成员性判据的读面,唯一能做的
14
14
  * 要么是全放(跨租户读)要么是全拒(伪装成故障),两个都比「这个面不存在」差。
@@ -78,7 +78,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
78
78
  }
79
79
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
80
80
  return;
81
- const prepared = await prepareSpec(req, res);
81
+ const prepared = await prepareSpec(req, res, undefined, undefined, { liveStream: url === "/v1/tasks/stream" });
82
82
  if (!prepared)
83
83
  return;
84
84
  const principal = prepared.auth?.principal;
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
2
3
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
3
4
  import { redactSecrets } from "../../trace/redact.js";
@@ -201,6 +202,22 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
201
202
  }
202
203
  miss.fell = true;
203
204
  }
205
+ const LegApprovalKeysSchema = z
206
+ .object({
207
+ pendingApproval: z.object({ askIds: z.array(z.string().min(1)).min(1), oldestCreatedAtMs: z.number().finite() }).optional(),
208
+ approvalWaitedMs: z.number().finite().nonnegative().optional(),
209
+ })
210
+ .loose();
211
+ function projectLegApprovalKeys(agent) {
212
+ const parsed = LegApprovalKeysSchema.safeParse(agent);
213
+ if (!parsed.success)
214
+ return {};
215
+ const { pendingApproval, approvalWaitedMs } = parsed.data;
216
+ return {
217
+ ...(pendingApproval !== undefined ? { pendingApproval: { askIds: [...pendingApproval.askIds], oldestCreatedAtMs: pendingApproval.oldestCreatedAtMs } } : {}),
218
+ ...(approvalWaitedMs !== undefined ? { approvalWaitedMs } : {}),
219
+ };
220
+ }
204
221
  function summarizeWorkflowDetail(run) {
205
222
  const isDone = (s) => s === "completed" || s === "failed";
206
223
  const durationOf = (a) => a.startedAt === undefined || a.endedAt === undefined ? undefined : a.endedAt - a.startedAt;
@@ -245,6 +262,8 @@ function summarizeWorkflowDetail(run) {
245
262
  replayed: a.replayed,
246
263
  prompt: a.prompt,
247
264
  output: a.output,
265
+ ...projectLegApprovalKeys(a),
266
+ taskRunId: a.taskRunId,
248
267
  }));
249
268
  const byId = new Map();
250
269
  for (const g of runGroups) {
@@ -5,7 +5,7 @@ import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
7
7
  import { type OwnerAwareSessionStore } from "../security.js";
8
- import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js";
8
+ import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileHistoryStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js";
9
9
  import type { RuleConsentLane } from "../rules-consent.js";
10
10
  import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
11
11
  import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
@@ -86,8 +86,14 @@ export interface ServiceCoreDeps {
86
86
  * task). The disposition lives INSIDE resolveSpec because only it derives the effective scenario (principal
87
87
  * ruling + defaultScenario) and holds the center-pack snapshot the provider pick uses — an HTTP-side probe
88
88
  * could disagree with the actual assembly (scenario mismatch / centerPrompts refresh TOCTOU). */
89
+ /** `opts.liveStream` (S-46 件C):这条腿是不是 `POST /v1/tasks/stream` —— **唯一**挂 HITL 活流投递面
90
+ * (`question.runWithContext`)的腿。resolveSpec 拿它做逐腿 `interactionPosture` 判据(判据全文见
91
+ * `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture ——
92
+ * bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。
93
+ * ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */
89
94
  resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
90
95
  leg?: "fresh" | "resume";
96
+ liveStream?: boolean;
91
97
  }) => TaskSpec | Promise<TaskSpec>;
92
98
  /**
93
99
  * #196 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
@@ -116,13 +122,14 @@ export interface ServiceStoreDeps {
116
122
  /** [3321] tool-results 读面([3315] 请求,core [3322] `buildToolResultRef` 到货):`GET /v1/tasks/:id/
117
123
  * tool-results/:ref` 的数据源 —— 与 RunnerDeps 侧的 offload 店**同实例**(main.ts 的 `toolResultStore`,
118
124
  * 即「durable 后端在场」那一只;无 backend 部署里 Runner 用的内存兜底 `runnerOffloadStore` **不接**这里)。
119
- * 🔴 缺席 ⇒ 路由回 404 而不是 501:501 会向一个无权 caller 泄露「这台机器配没配 durable 店」的部署形,
125
+ * 🔴 缺席 ⇒ 路由回 404 而不是 501:501 会向一个无权 caller 暴露「这台机器配没配 durable 店」的部署形,
120
126
  * 与 unknown-ref / 归属不符两臂**同形** 404 才是防 oracle 的收口([3321] 的「另定」项按防 oracle 收紧)。
121
127
  * 这是本组「absent ⇒ 501 诚实缺席」通例的**唯一具名例外**,例外理由即上一句。 */
122
128
  toolResultStore?: ToolResultStoreFull;
123
- /** E19 rewind-files snapshot store. Present ⇒ the `rewindFiles` capability is advertised; core uses it via
124
- * RunnerDeps.fileSnapshotStore (snapshot per turn + restore on resumeAt, ANY env since gate-split 1.134.0). Also the 2c artifact store. */
125
- fileSnapshotStore?: ServiceFileSnapshotStore;
129
+ /** S-15 per-edited-file rewind history store. Present ⇒ the rewind capabilities are advertised; core uses it
130
+ * via RunnerDeps.fileHistoryStore (first-touch tracking + per-turn boundaries + tracked-set restore, ANY
131
+ * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces). */
132
+ fileHistoryStore?: ServiceFileHistoryStore;
126
133
  /** D-1 通用文件上传(clay 拍 2026-07-27:独立附件 store,不骑 snapshot blob 面):enables
127
134
  * POST/GET/DELETE /v1/attachments*。缺席 ⇒ 501(与其他可选 store 面同姿)。绑定/物化半场在
128
135
  * main.ts(resolveSpec + env factory 包装);本面只管上传/取回/删除三动词 + server 权威上限。 */
@@ -153,7 +160,7 @@ export interface ServiceStoreDeps {
153
160
  * bake + resumable SSE + runner claim/ingest + status-monotonic auto-register). Set when a TiDB pool is
154
161
  * present AND `IMAGE_BAKES_ENABLED`. The auto-register path writes into `imageIndex` (so both are wired
155
162
  * together). 🔴 main.ts boot-asserts `operatorPrincipals` is non-empty when this is on (never a
156
- * world-writable build-host-RCE door, §P2.4a). */
163
+ * unrestricted-write build-host exec door, §P2.4a). */
157
164
  imageBakes?: ImageBake;
158
165
  /** P2 (design/73 §7.2): the outcome-fact sink, for the `GET /v1/outcomes` read aggregate. `summary` is
159
166
  * present on the SQL ledgers; a File sink (no query surface) → the endpoint 501s honestly. */
@@ -172,7 +179,7 @@ export interface ServiceStoreDeps {
172
179
  * OOM 形)。理由仍成立、结论改了 —— backend 依旧要整个传,因为四个 store 必须同源。
173
180
  * The individual stores above are wired for the per-feature routes; the sync routes need the backend
174
181
  * itself because they coordinate the four together (a route can't reassemble it from the parts —
175
- * `backend.session()`/`fileSnapshot()`/`sessionPolicy()`/`resumeAnchor()` must come from ONE backend). Present
182
+ * `backend.session()`/`fileHistory()`/`sessionPolicy()`/`resumeAnchor()` must come from ONE backend). Present
176
183
  * whenever a durable backend (tidb/pg/local) is; absent on the env-only/no-backend worker (the routes 501). */
177
184
  backend?: StoreBackend;
178
185
  /** #154 车二:**持久化权限规则的同意车道**(core design/179 三步协议的宿主半场)。
@@ -312,7 +319,7 @@ export interface ServiceSeamDeps {
312
319
  * #345(core 5.57.0 `routePairingStatus`)—— **key↔URL 配对读面**:`GET /v1/models` 每行的
313
320
  * `routePairing` 标注就是拿这一只判的。main.ts 接的是**装配好的那只 brain 本身**(不是摘出来的
314
321
  * 函数引用:A-057.2 那条「裸摘引用丢接收者」的教训同款,且 core 的装饰器族是按 `stream` 的同一条
315
- * 路再分派 `adjudicateRoute` 的,整只传才不会绕过路由/快切层)。
322
+ * 路再分派 `adjudicateRoute` 的,整只传才不会跳过路由/快切层)。
316
323
  *
317
324
  * 🔴 缺席 ⇒ 该面每行标 `"unknown"`,**绝不猜**:一只没有 `adjudicateRoute` 面的自定义 brain 是
318
325
  * core 明令「callers must treat that as 'no judgment', never as OK or as broken」的形。测试夹具
@@ -403,6 +410,12 @@ export interface ServiceSeamDeps {
403
410
  run(scope: string): Promise<ConsolidationRunReceipt>;
404
411
  lastRun(scope: string): ConsolidationDriverRunRow | undefined;
405
412
  };
413
+ /** design/383 §3.1 / S-50 §1.3(S-49 S-2):per-principal memory-capture opt-out 授权表的管理面
414
+ * (`/v1/admin/memory-optout` 四口,operator-only)—— `backend.memoryOptOutGrant()` 的同一只(与
415
+ * `RuntimeCaps.allowMemoryOptOut` 的 resolver 腿读同一张表,零缓存直读 ⇒ 写后下一次 prepare 即生效)。
416
+ * 缺席 = 本部署没有 SQL 后端(local 车道刻意无源)⇒ 四口 501 `capability.memory_optout_grant_required`
417
+ * (「换部署形态」族;与 consolidation 阀门的 404 分家:这里不是旋钮没开,是店在本部署上不存在)。 */
418
+ memoryOptOutGrant?: import("../plugins/memory-optout-grant-store-sql.js").MemoryOptOutGrantStore;
406
419
  /** design/170 件A §7 —— org 记忆授权目录,`boot/org-memory.ts` 装配的**同一个实例**(准入 seam 与
407
420
  * 本面共享它的 TTL 缓存/退避窗/gen 高水位)。memory-policy 面的 `org:` 属主门用它回答成员性:
408
421
  * 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键
@@ -605,6 +618,15 @@ export interface ServiceDeploymentDeps {
605
618
  * proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
606
619
  * proxy when absent (older callers / tests that don't pass it). */
607
620
  workflowsCapable?: boolean;
621
+ /** [5673]③ + codex R1-[medium]:`capabilities.workflowModels` 的**引擎快照席**。main.ts 在 boot 期用
622
+ * `workflowModelAllowlistFor(config)` 算一次(与 createRunnerDeps 喂给 core `workflowGovernanceBaseline.
623
+ * workflowModelAllowlist` 的同一只函数、同一 config 代际),多租户 fail-closed 的 undefined 在装配点
624
+ * 折 `[]` ⇒ 生产装配下本席**恒在场**。为什么不让能力面每请求活派生:core 5.49 起 models plane 经
625
+ * `Runner.swapModels` 永久热,而 governance allowlist 是 Runner 构造期快照 —— 活派生会在热加目录键后
626
+ * 长期广告一个 core 必拒的词(教学面超前执法面,直到重启)。广告**引擎真快照**让「能力面教的」与
627
+ * 「core 收到的 allowlist」结构性恒等。缺席(遗留 stub 装配)⇒ 能力面回落活 config 派生(测试便利,
628
+ * 生产路径到不了)。 */
629
+ workflowModelAllowlist?: readonly string[];
608
630
  }
609
631
  /** **数值旋钮**:部署可调的上限/拍距。缺省值写在消费点(`?? 256` 之类),此处只承载覆写。 */
610
632
  export interface ServiceKnobDeps {
@@ -711,22 +733,22 @@ export declare function isBillableSubmitPath(url: string): boolean;
711
733
  * · `POST /v1/rules/cc-import/*` —— principal lane 的**持久 allow 规则**铸造口;一条持久 allow 规则对后续
712
734
  * 同命令的 classify 档 ask 常驻消音(core #144 之后仍然如此,只是不再消 mandated 的那些)。
713
735
  * · `DELETE /v1/rules` —— 同一个规则店的**撤销**口。方向相反(收紧)但同样是持久改写,而且它多一条
714
- * operator 越权域(`?principal=` / body 的 `principal` 让 operator 收回**任一**租户的规则)⇒ 伪造一个
736
+ * operator 超范围域(`?principal=` / body 的 `principal` 让 operator 收回**任一**租户的规则)⇒ 自造一个
715
737
  * 列在 `OPERATOR_PRINCIPALS` 里的头,就能把每一位租户的规则一次清空。
716
738
  * 无 service token 的部署上 `verifiedPrincipal` 走的是「BFF/gated:头已由上游验过」那一支,而这一形恰恰
717
- * 是**没有**那个上游 —— 于是伪造一个头就能收编别人、替任意租户种下常驻放行、或替他们全删。bake 门当年
739
+ * 是**没有**那个上游 —— 于是自造一个头就能收编别人、替任意租户种下常驻放行、或替他们全删。bake 门当年
718
740
  * 补的就是同一句话,`isDestructiveSessionWrite` 从逐条路由白名单改成族判定也是同一句话(那次的措辞逐字是
719
741
  * 「a forged principal header alone could **tighten/DoS** a session's tools」——撤销口落的正是 DoS 那一半)。
720
742
  *
721
- * 🔴 **方法感知**(codex 对抗复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
743
+ * 🔴 **方法感知**(codex 交叉复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
722
744
  * 合取里 —— 于是一个自称「持久改写」的谓词把爆炸半径最大的那条动词漏在门外,正是 `isDestructiveSessionWrite`
723
745
  * 当年被抓到的同一形。签名因此收方法,与那只谓词逐字同形。
724
746
  * 🔴 读面**原则上**不进本表:`GET /v1/adoption/:id` 与 `GET /v1/rules` 各有自己的**属主门**兜底,本门只拦写。
725
747
  *
726
- * 🔴 **第二个合取项的加宽**(codex 对抗复审 [high],#277 提货批,验真后修)——判据现在是
748
+ * 🔴 **第二个合取项的加宽**(codex 交叉复审 [high],#277 提货批,验真后修)——判据现在是
727
749
  * 「持久改写 **或** 授权唯一输入是 principal 头的**跨租户治理读**」。加宽是为了收进 #264 v2-c 的两口:
728
750
  * · `POST /v1/memory/import` —— 直接落在**原**判据上,而且是本仓最深的那一种持久改写:它把别处铸的
729
- * 条目 / 挑战 / 污染标记 / 托管链落进本部署的记忆平面。伪造一个在册 operator 头 ⇒ 一份**攻击者自制的
751
+ * 条目 / 挑战 / 污染标记 / 托管链落进本部署的记忆平面。自造一个在册 operator 头 ⇒ 一份**不当调用方自制的
730
752
  * 治理史**成为本店的账;而记忆是模型**每次挂载都读**的面,比种一条常驻 allow 规则更深(那条只影响
731
753
  * ask 会不会问,这条影响模型相信什么)。
732
754
  * · `POST /v1/memory/export` —— 是**读**,按原措辞本不该进。但被点名豁免的那两条读各有属主门兜底,