@sema-agent/server 7.133.0 → 7.134.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 (185) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +129 -11
  5. package/deploy/sema-up/README.md +16 -0
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
  9. package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
  10. package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
  11. package/deploy/sema-up/chart/templates/server.yaml +14 -0
  12. package/deploy/sema-up/chart/values-full-single.yaml +5 -1
  13. package/deploy/sema-up/chart/values.yaml +34 -0
  14. package/dist/approval-ask-audit-store.d.ts +1 -1
  15. package/dist/approval-card.d.ts +4 -4
  16. package/dist/approval-card.js +1 -2
  17. package/dist/approval-content-kind.d.ts +5 -2
  18. package/dist/approval-content-kind.js +16 -3
  19. package/dist/approval-gate-currency.d.ts +72 -0
  20. package/dist/approval-gate-currency.js +94 -0
  21. package/dist/approval.d.ts +6 -0
  22. package/dist/approval.js +31 -14
  23. package/dist/boot/coordinators.d.ts +6 -1
  24. package/dist/boot/coordinators.js +6 -3
  25. package/dist/boot/leader.d.ts +2 -0
  26. package/dist/boot/leader.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -2
  28. package/dist/boot/runner-deps.d.ts +18 -5
  29. package/dist/boot/runner-deps.js +9 -0
  30. package/dist/boot/stage-06-runners.d.ts +4 -3
  31. package/dist/boot/stage-06-runners.js +15 -2
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  33. package/dist/boot/stage-08-reapers.d.ts +2 -1
  34. package/dist/boot/stage-09-leader.d.ts +2 -1
  35. package/dist/boot/stage-09-leader.js +2 -2
  36. package/dist/boot/stage-10-http-server.d.ts +2 -1
  37. package/dist/boot/stage-10-http-server.js +2 -1
  38. package/dist/boot/stores.d.ts +8 -0
  39. package/dist/boot/stores.js +8 -12
  40. package/dist/boot/web-admin.d.ts +6 -0
  41. package/dist/boot/web-admin.js +11 -2
  42. package/dist/budget.js +5 -1
  43. package/dist/caller-text-echo.d.ts +2 -0
  44. package/dist/caller-text-echo.js +2 -1
  45. package/dist/canonical-json.d.ts +2 -41
  46. package/dist/canonical-json.js +2 -13
  47. package/dist/capabilities/composite-members.d.ts +1 -1
  48. package/dist/capabilities/composite-members.js +1 -1
  49. package/dist/capabilities/mcp-probe.js +2 -2
  50. package/dist/capability-facts.d.ts +2 -0
  51. package/dist/capability-facts.js +4 -4
  52. package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
  53. package/dist/checkpoint-ctx-seal-m28.js +19 -0
  54. package/dist/field-conflict.d.ts +2 -1
  55. package/dist/field-conflict.js +1 -0
  56. package/dist/file-history-disclosure.d.ts +34 -5
  57. package/dist/file-history-disclosure.js +2 -2
  58. package/dist/fleet/fleet-bus.d.ts +4 -0
  59. package/dist/fleet/fleet-bus.js +1 -0
  60. package/dist/hooks/hook-runner.js +2 -2
  61. package/dist/http/admission.js +8 -0
  62. package/dist/http/approval-sweeps.js +3 -1
  63. package/dist/http/checkpoint-ctx.d.ts +157 -2
  64. package/dist/http/checkpoint-ctx.js +158 -2
  65. package/dist/http/dispatch.js +3 -3
  66. package/dist/http/durable-run-leg.js +3 -2
  67. package/dist/http/idempotency.d.ts +2 -2
  68. package/dist/http/idempotency.js +2 -2
  69. package/dist/http/principal-gate.d.ts +32 -6
  70. package/dist/http/principal-gate.js +27 -4
  71. package/dist/http/refusal-body.d.ts +12 -0
  72. package/dist/http/refusal-body.js +11 -0
  73. package/dist/http/request-guards.d.ts +0 -9
  74. package/dist/http/request-guards.js +2 -16
  75. package/dist/http/resume-ctx-read.d.ts +48 -1
  76. package/dist/http/resume-ctx-read.js +26 -3
  77. package/dist/http/resume-legs.d.ts +2 -1
  78. package/dist/http/resume-legs.js +37 -22
  79. package/dist/http/route-ctx.d.ts +0 -9
  80. package/dist/http/routes/agents-roster.js +2 -3
  81. package/dist/http/routes/approvals-assistant.js +43 -9
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -3
  84. package/dist/http/routes/diagnostics.js +1 -1
  85. package/dist/http/routes/fleet.js +4 -4
  86. package/dist/http/routes/memory-origin.d.ts +1 -1
  87. package/dist/http/routes/rules.js +4 -4
  88. package/dist/http/routes/runs.js +30 -9
  89. package/dist/http/routes/session-sync.d.ts +3 -2
  90. package/dist/http/routes/session-sync.js +38 -20
  91. package/dist/http/routes/sessions-list.d.ts +1 -1
  92. package/dist/http/routes/sessions-list.js +3 -4
  93. package/dist/http/routes/sessions.js +4 -4
  94. package/dist/http/routes/tasks.js +21 -16
  95. package/dist/http/routes/trace-usage.js +7 -9
  96. package/dist/http/run-declared-families.d.ts +16 -0
  97. package/dist/http/run-declared-families.js +12 -0
  98. package/dist/http/server.d.ts +6 -0
  99. package/dist/http/server.js +5 -2
  100. package/dist/http/submit-ctx.d.ts +4 -3
  101. package/dist/http/submit-ctx.js +19 -3
  102. package/dist/http/wire-types.d.ts +7 -3
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +1 -0
  105. package/dist/leader/wire.d.ts +4 -0
  106. package/dist/leader/wire.js +8 -7
  107. package/dist/main.js +1 -1
  108. package/dist/memory-origin-fence.js +2 -2
  109. package/dist/model-compat.d.ts +1 -1
  110. package/dist/observability/prompt-manifest.d.ts +19 -8
  111. package/dist/observability/prompt-manifest.js +9 -8
  112. package/dist/observability/tool-trace.js +8 -9
  113. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  114. package/dist/orchestration/workflow-park-decision.js +1 -0
  115. package/dist/parked-decide.d.ts +4 -5
  116. package/dist/parked-decide.js +3 -1
  117. package/dist/permission-rule-vocab.d.ts +1 -23
  118. package/dist/permission-rule-vocab.js +0 -13
  119. package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
  120. package/dist/plugins/approval-payload-fingerprint.js +1 -1
  121. package/dist/plugins/caching-session-store.d.ts +1 -1
  122. package/dist/plugins/caching-session-store.js +4 -3
  123. package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
  124. package/dist/plugins/checkpoint-store-sql.js +45 -1
  125. package/dist/plugins/local-checkpoint-store.d.ts +13 -2
  126. package/dist/plugins/local-checkpoint-store.js +26 -0
  127. package/dist/plugins/local-session-store.d.ts +12 -9
  128. package/dist/plugins/local-session-store.js +23 -14
  129. package/dist/plugins/local-session-window.d.ts +12 -6
  130. package/dist/plugins/local-session-window.js +1 -17
  131. package/dist/plugins/one-time-migrations.d.ts +11 -0
  132. package/dist/plugins/one-time-migrations.js +12 -0
  133. package/dist/plugins/permission-rule-store-file.js +10 -6
  134. package/dist/plugins/permission-rule-store-sql.js +8 -8
  135. package/dist/plugins/pg-session-storage.d.ts +1 -1
  136. package/dist/plugins/pg-session-storage.js +39 -17
  137. package/dist/plugins/remote-env-device.d.ts +14 -7
  138. package/dist/plugins/remote-env-device.js +23 -15
  139. package/dist/plugins/session-ownership.d.ts +11 -0
  140. package/dist/plugins/session-ownership.js +11 -0
  141. package/dist/plugins/session-placement.d.ts +12 -14
  142. package/dist/plugins/session-placement.js +2 -15
  143. package/dist/plugins/tidb-session-store.d.ts +1 -1
  144. package/dist/plugins/tidb-session-store.js +39 -17
  145. package/dist/request-tool-axes.d.ts +12 -0
  146. package/dist/request-tool-axes.js +20 -0
  147. package/dist/run-local.js +1 -0
  148. package/dist/runs.d.ts +2 -1
  149. package/dist/runs.js +4 -3
  150. package/dist/sealed-key.d.ts +32 -0
  151. package/dist/sealed-key.js +55 -1
  152. package/dist/security.d.ts +9 -7
  153. package/dist/session-error-wire.d.ts +12 -5
  154. package/dist/session-error-wire.js +4 -1
  155. package/dist/session-sync-kernel.d.ts +37 -10
  156. package/dist/session-sync-kernel.js +10 -0
  157. package/dist/task-a2a.js +4 -3
  158. package/dist/task-mcp.d.ts +2 -2
  159. package/dist/task-mcp.js +20 -8
  160. package/dist/task-settings.d.ts +5 -1
  161. package/dist/task-settings.js +1 -1
  162. package/dist/tool-approval.d.ts +151 -52
  163. package/dist/tool-approval.js +113 -20
  164. package/dist/tool-rule-names.d.ts +6 -0
  165. package/dist/trace/core-keyset-guard.d.ts +4 -3
  166. package/dist/trace/exit-seat.d.ts +13 -1
  167. package/dist/trace/exit-seat.js +55 -1
  168. package/dist/trace/ledger-events.d.ts +3 -1
  169. package/dist/trace/project.d.ts +18 -1
  170. package/dist/trace/project.js +16 -4
  171. package/dist/trace/redact.d.ts +6 -3
  172. package/dist/trace/redact.js +191 -23
  173. package/dist/trace/self-minted-wire.d.ts +4 -0
  174. package/dist/trace/self-minted-wire.js +1 -0
  175. package/dist/trace/wire-projection-faces.d.ts +6 -0
  176. package/dist/trace/wire-projection-faces.js +6 -0
  177. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  178. package/dist/web-admin-client/skills-mcp.js +2 -3
  179. package/dist/web-admin-client/types.d.ts +1 -1
  180. package/package.json +2 -2
  181. package/scripts/drain-check-local.mjs +34 -2
  182. package/scripts/drain-check.mysql.sql +20 -2
  183. package/scripts/drain-check.pg.sql +20 -2
  184. package/dist/mcp-tool-faces.d.ts +0 -69
  185. package/dist/mcp-tool-faces.js +0 -104
@@ -5,10 +5,10 @@ import { StringDecoder } from "node:string_decoder";
5
5
  import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
6
6
  import * as facts from "../../capability-facts.js";
7
7
  import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
8
- import { isUuidShape } from "../../security.js";
8
+ import { isUuidShape, legacyUnownedSessionOk, sessionOwnerCellOf } from "../../security.js";
9
9
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, logRequestError, msg } from "../send.js";
10
10
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
11
- import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
11
+ import { fleetViewOf, gatedPrincipal, explicitOperatorOk, sendUnauthorized, sessionCallerOf } from "../principal-gate.js";
12
12
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
13
13
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
14
14
  const SYNC_IMPORT_BATCH = 500;
@@ -34,7 +34,7 @@ export function createSessionSyncLocal() {
34
34
  }
35
35
  export async function handleSessionSync(req, res, match, ctx) {
36
36
  const { deps } = ctx;
37
- const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite, rateLimited } = ctx.helpers;
37
+ const { readJson, readRawBody, safeDecode, rateLimited } = ctx.helpers;
38
38
  const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
39
39
  {
40
40
  {
@@ -44,7 +44,7 @@ export async function handleSessionSync(req, res, match, ctx) {
44
44
  return;
45
45
  }
46
46
  const { backend, sessionStorage, historyStore } = sync;
47
- const scope = sessionOwnerScopeForWrite(req);
47
+ const scope = fleetViewOf(req, deps, "write");
48
48
  if (!scope.fleetWide && scope.gateOwner === null) {
49
49
  sendUnauthorized(res, "identity");
50
50
  return;
@@ -54,23 +54,25 @@ export async function handleSessionSync(req, res, match, ctx) {
54
54
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
55
55
  return;
56
56
  }
57
- const gateFace = await sessionStorage.sessionFaceOf(sessionId);
57
+ const judgeSessionGate = async () => {
58
+ const face = await sessionStorage.sessionFaceOf(sessionId);
59
+ const mayTouch = face !== undefined && (scope.fleetWide || face.owner === scope.gateOwner || legacyUnownedSessionOk(deps.config.requirePrincipal, sessionCallerOf(req, deps), await sessionOwnerCellOf(sessionStorage, sessionId, face.owner)));
60
+ return { face, mayPull: mayTouch, mayPush: face === undefined || mayTouch };
61
+ };
62
+ const gate = await judgeSessionGate();
63
+ const gateFace = gate.face;
58
64
  const owner = gateFace === undefined ? undefined : gateFace.owner;
59
- const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
65
+ const sendSessionAbsent = () => sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
60
66
  const pullGateOk = () => {
61
- if (owner === undefined) {
62
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
63
- return false;
64
- }
65
- if (!ownsOrFleet(owner)) {
66
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
67
+ if (!gate.mayPull) {
68
+ sendSessionAbsent();
67
69
  return false;
68
70
  }
69
71
  return true;
70
72
  };
71
73
  const pushGateOk = () => {
72
- if (owner !== undefined && !ownsOrFleet(owner)) {
73
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
74
+ if (!gate.mayPush) {
75
+ sendSessionAbsent();
74
76
  return false;
75
77
  }
76
78
  return true;
@@ -93,7 +95,7 @@ export async function handleSessionSync(req, res, match, ctx) {
93
95
  throw e;
94
96
  }
95
97
  if (out === null) {
96
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
98
+ sendSessionAbsent();
97
99
  return;
98
100
  }
99
101
  sendJson(res, 200, { manifest: out.manifest });
@@ -114,7 +116,7 @@ export async function handleSessionSync(req, res, match, ctx) {
114
116
  }
115
117
  const it = await sessionStorage.exportEntriesStream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
116
118
  if (it === null) {
117
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
119
+ sendSessionAbsent();
118
120
  return;
119
121
  }
120
122
  res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
@@ -387,7 +389,7 @@ export async function handleSessionSync(req, res, match, ctx) {
387
389
  importLeases.set(sessionId, { at: Date.now(), stagingId });
388
390
  pendingStagings.set(stagingId, {
389
391
  handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
390
- fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
392
+ fileHistory, policy, anchors, row, declaredEntryIds: entryIds, openedBy: scope.gateOwner, expectedOwner: owner,
391
393
  });
392
394
  sendJson(res, 200, { stagingId, relation: relA.relation });
393
395
  return;
@@ -403,7 +405,7 @@ export async function handleSessionSync(req, res, match, ctx) {
403
405
  sendError(res, 404, "not_found.staging", "staging not found");
404
406
  return;
405
407
  }
406
- if (!pending.openedFleetWide && !scope.fleetWide && pending.openedBy !== scope.gateOwner) {
408
+ if (!scope.fleetWide && pending.openedBy !== scope.gateOwner) {
407
409
  sendError(res, 404, "not_found.staging", "staging not found");
408
410
  return;
409
411
  }
@@ -485,7 +487,23 @@ export async function handleSessionSync(req, res, match, ctx) {
485
487
  return;
486
488
  throw e;
487
489
  }
488
- const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
490
+ let mayPushAtCommit;
491
+ let activeAtCommit;
492
+ try {
493
+ mayPushAtCommit = (await judgeSessionGate()).mayPush;
494
+ activeAtCommit = mayPushAtCommit ? await deps.runStore?.getActiveTaskId(sessionId) : undefined;
495
+ }
496
+ catch (e) {
497
+ await pending.handle.abort().catch(() => undefined);
498
+ cleanupStaging(stagingId);
499
+ throw e;
500
+ }
501
+ if (!mayPushAtCommit) {
502
+ await pending.handle.abort().catch(() => undefined);
503
+ cleanupStaging(stagingId);
504
+ sendSessionAbsent();
505
+ return;
506
+ }
489
507
  if (activeAtCommit) {
490
508
  await pending.handle.abort().catch(() => undefined);
491
509
  cleanupStaging(stagingId);
@@ -493,7 +511,7 @@ export async function handleSessionSync(req, res, match, ctx) {
493
511
  return;
494
512
  }
495
513
  try {
496
- const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
514
+ const committed = await pending.handle.commit(pending.owner, { expectedOwner: pending.expectedOwner, ...(pending.resolution ? { resolution: pending.resolution } : {}) });
497
515
  if (pending.resolution === "overwrite-dst" && (committed.relation === "fork" || committed.relation === "stale")) {
498
516
  await wipeSessionAttendants({ fileHistoryStore: historyStore, sessionPolicyStore: deps.sessionPolicyStore, resumeAnchorStore: deps.resumeAnchorStore }, sessionId);
499
517
  }
@@ -2,7 +2,7 @@
2
2
  * [ref] A9 域模块 —— 会话列表 / 发件链接列表。
3
3
  *
4
4
  * 路由:`GET /v1/sessions`(CC /resume picker,keyset 分页)、`GET /v1/sendfile-links`。
5
- * owner-scope 与 trace 列表同一权威模型(system/traceToken=fleet-wide,principal=钉住本人)。本域零可变状态。
5
+ * owner-scope 与 trace 列表同一权威模型(`fleetViewOf` 读档:service 凭据不报主体 / 只读 trace 凭据 / dev-open = fleet-wide;带验过的主体 = 钉住那位主体)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
8
  import { type SessionListItem } from "../../security.js";
@@ -1,16 +1,15 @@
1
1
  import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { sendUnauthorized } from "../principal-gate.js";
3
+ import { fleetViewOf, sendUnauthorized } from "../principal-gate.js";
4
4
  import { filterFingerprint } from "../cursor-fingerprint.js";
5
5
  import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
7
  import * as facts from "../../capability-facts.js";
8
8
  export async function handleSessionsList(req, res, match, ctx) {
9
9
  const { deps } = ctx;
10
- const { isFleetWide, sessionOwnerScope } = ctx.helpers;
11
10
  switch (match.decl.id) {
12
11
  case "sessions-list": {
13
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
12
+ const { fleetWide, gateOwner } = fleetViewOf(req, deps, "read");
14
13
  if (!fleetWide && gateOwner === null) {
15
14
  sendUnauthorized(res, "identity");
16
15
  return;
@@ -26,7 +25,7 @@ export async function handleSessionsList(req, res, match, ctx) {
26
25
  return;
27
26
  }
28
27
  case "sendfile-links": {
29
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
28
+ const { fleetWide, gateOwner } = fleetViewOf(req, deps, "read");
30
29
  if (!fleetWide && gateOwner === null) {
31
30
  sendUnauthorized(res, "identity");
32
31
  return;
@@ -9,7 +9,7 @@ import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.
9
9
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
10
10
  import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
11
11
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
12
- import { gatedPrincipal, explicitOperatorOk, sessionCallerOf, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
12
+ import { fleetViewOf, gatedPrincipal, explicitOperatorOk, sessionCallerOf, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
13
13
  import { runRowReadableOnSessionFace } from "../owner-access.js";
14
14
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
15
15
  import * as facts from "../../capability-facts.js";
@@ -71,7 +71,7 @@ async function readLastLegMcp(deps, sessionId, caller) {
71
71
  }
72
72
  export async function handleSessions(req, res, match, ctx) {
73
73
  const { deps } = ctx;
74
- const { readJson, safeDecode, sessionOwnerOk, sessionOwnerScopeForWrite } = ctx.helpers;
74
+ const { readJson, safeDecode, sessionOwnerOk } = ctx.helpers;
75
75
  const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
76
76
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
77
77
  if (ownerOf !== undefined) {
@@ -699,7 +699,7 @@ export async function handleSessions(req, res, match, ctx) {
699
699
  }
700
700
  const fork = forkFact.sessionStorage.fork.bind(forkFact.sessionStorage);
701
701
  const ownerOf = forkFact.sessionStorage.ownerOf.bind(forkFact.sessionStorage);
702
- const scope = sessionOwnerScopeForWrite(req);
702
+ const scope = fleetViewOf(req, deps, "write");
703
703
  if (!scope.fleetWide && scope.gateOwner === null) {
704
704
  sendUnauthorized(res, "identity");
705
705
  return;
@@ -737,7 +737,7 @@ export async function handleSessions(req, res, match, ctx) {
737
737
  }
738
738
  const purge = deleteFact.purge;
739
739
  const ownerOf = deleteFact.sessionStorage.ownerOf.bind(deleteFact.sessionStorage);
740
- const scope = sessionOwnerScopeForWrite(req);
740
+ const scope = fleetViewOf(req, deps, "write");
741
741
  if (!scope.fleetWide && scope.gateOwner === null) {
742
742
  sendUnauthorized(res, "identity");
743
743
  return;
@@ -21,7 +21,7 @@ import { replayOfExistingRun } from "../submit-replay.js";
21
21
  import { httpErrorCode, sendJson, sendError, sseHeaders } from "../send.js";
22
22
  import { collectSsePreamble } from "../sse-log.js";
23
23
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
24
- import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGate } from "../../tool-approval.js";
24
+ import { approvalPreambleReplays, createApprovalCardEmitter, declaredApprovalFrameFamilies, resolveApprovalLeg, resolveStreamApprovalGate } from "../../tool-approval.js";
25
25
  import { buildActiveRunConflict, isActiveRunConflictBody, toDoneFrameResult } from "../active-run-conflict.js";
26
26
  import { commitPlacement, devicePlacementErrorBody } from "../device-placement.js";
27
27
  import { commitSessionInheritEnv } from "../session-inherit-env.js";
@@ -150,6 +150,18 @@ export async function handleTasks(req, res, match, ctx) {
150
150
  const prepared = await prepareSpec(req, res, keyedBody, undefined, { liveStream });
151
151
  if (!prepared)
152
152
  return;
153
+ const declaredFamilies = declaredApprovalFrameFamilies(prepared.body.settings);
154
+ const approvalLegInput = (streamApprovalOn) => {
155
+ const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
156
+ return {
157
+ streamApprovalOn,
158
+ windowMs: deps.config.streamApproval?.windowMs,
159
+ windowMarginMs: deps.config.streamAskWindowMarginMs,
160
+ ...(legWalltimeMs !== undefined ? { legWalltimeMs } : {}),
161
+ declaredFamilies,
162
+ };
163
+ };
164
+ const assembleApprovalLeg = (streamApprovalOn) => resolveApprovalLeg({ ...approvalLegInput(streamApprovalOn), nowMonotonicMs: performance.now() });
153
165
  const principal = prepared.auth?.principal;
154
166
  const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config);
155
167
  let wiringReceipt;
@@ -215,9 +227,9 @@ export async function handleTasks(req, res, match, ctx) {
215
227
  toolApprovalEnabled: Boolean(deps.toolApproval),
216
228
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
217
229
  backend: deps.backend,
218
- parkFacility: deps.checkpointStore !== undefined,
230
+ checkpointStore: deps.checkpointStore,
219
231
  });
220
- const replaySessionId = replayGate.active ? prepared.spec.sessionId : undefined;
232
+ const replaySessionId = approvalPreambleReplays(approvalLegInput(replayGate.active)) ? prepared.spec.sessionId : undefined;
221
233
  const presentedOnConnection = new Set();
222
234
  const presentedCompleted = new Set();
223
235
  const admitPresentedComplete = (askId) => {
@@ -663,18 +675,11 @@ export async function handleTasks(req, res, match, ctx) {
663
675
  toolApprovalEnabled: Boolean(deps.toolApproval),
664
676
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
665
677
  backend: deps.backend,
666
- parkFacility: deps.checkpointStore !== undefined,
678
+ checkpointStore: deps.checkpointStore,
667
679
  }).active;
668
- const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
669
- const approvalLeg = resolveApprovalLeg({
670
- streamApprovalOn,
671
- windowMs: deps.config.streamApproval?.windowMs,
672
- windowMarginMs: deps.config.streamAskWindowMarginMs,
673
- ...(legWalltimeMs !== undefined ? { legWalltimeMs } : {}),
674
- nowMonotonicMs: performance.now(),
675
- });
680
+ const approvalLeg = assembleApprovalLeg(streamApprovalOn);
676
681
  const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
677
- const windowZero = approvalLeg.windowZero;
682
+ const unattended = approvalLeg.unattended;
678
683
  const emitCard = createApprovalCardEmitter({
679
684
  ...(ledgerSink
680
685
  ? {
@@ -696,7 +701,7 @@ export async function handleTasks(req, res, match, ctx) {
696
701
  if (!res.writableEnded && !res.destroyed)
697
702
  emitAsk(frame);
698
703
  };
699
- const approvalCtx = deps.toolApproval && !windowZero
704
+ const approvalCtx = deps.toolApproval && unattended === undefined
700
705
  ? {
701
706
  taskId: askTaskId,
702
707
  owner: askOwner,
@@ -714,8 +719,8 @@ export async function handleTasks(req, res, match, ctx) {
714
719
  : undefined;
715
720
  if (deps.toolApproval) {
716
721
  const toolApproval = deps.toolApproval;
717
- prepared.spec.onAsk = windowZero
718
- ? toolApproval.boundUnattendedAsk({ owner: askOwner, taskId: askTaskId, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })
722
+ prepared.spec.onAsk = unattended
723
+ ? toolApproval.boundUnattendedAsk({ owner: askOwner, taskId: askTaskId, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) }, unattended)
719
724
  : toolApproval.boundAsk({
720
725
  owner: askOwner,
721
726
  taskId: askTaskId,
@@ -6,16 +6,16 @@ import { sleep } from "../sse-log.js";
6
6
  import { bindSseLifecycle } from "../sse-lifecycle.js";
7
7
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
8
8
  import { filterFingerprint } from "../cursor-fingerprint.js";
9
- import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
9
+ import { fleetViewOf, sendUnauthorized } from "../principal-gate.js";
10
10
  import { assertNever } from "../route-ctx.js";
11
11
  import * as facts from "../../capability-facts.js";
12
12
  const TOOL_RESULT_LIMIT_CAP = 262_144;
13
13
  const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
14
14
  export async function handleTraceUsage(req, res, match, ctx) {
15
15
  const { deps } = ctx;
16
- const { isFleetWide, safeDecode } = ctx.helpers;
16
+ const { safeDecode } = ctx.helpers;
17
17
  if (match.decl.id === "tasks-source-summary") {
18
- if (!isFleetWide(req)) {
18
+ if (!fleetViewOf(req, deps, "read").fleetWide) {
19
19
  sendUnauthorized(res, "operator");
20
20
  return;
21
21
  }
@@ -35,8 +35,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
35
35
  match.decl.id === "task-trace-stream" ||
36
36
  match.decl.id === "task-trace-artifacts" ||
37
37
  match.decl.id === "task-tool-result") {
38
- const fleetWide = isFleetWide(req);
39
- const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
38
+ const { fleetWide, gateOwner } = fleetViewOf(req, deps, "read");
40
39
  if (!fleetWide && gateOwner === null) {
41
40
  sendUnauthorized(res, "identity");
42
41
  return;
@@ -89,9 +88,8 @@ export async function handleTraceUsage(req, res, match, ctx) {
89
88
  return;
90
89
  }
91
90
  const runStore = runs.runStore;
92
- const fleetWide = isFleetWide(req);
93
- const jwtOwner = fleetWide ? undefined : gatedPrincipal(req, deps.config);
94
- if (!fleetWide && !jwtOwner) {
91
+ const { fleetWide, gateOwner } = fleetViewOf(req, deps, "read");
92
+ if (!fleetWide && gateOwner === null) {
95
93
  sendUnauthorized(res, "identity");
96
94
  return;
97
95
  }
@@ -109,7 +107,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
109
107
  sendError(res, 400, "request.query_invalid", "invalid from/to (ISO-8601 or epoch-ms; from < to)");
110
108
  return;
111
109
  }
112
- const owner = jwtOwner ?? (q.get("principal") ?? undefined);
110
+ const owner = gateOwner ?? (q.get("principal") ?? undefined);
113
111
  try {
114
112
  const scan = await runStore.usageScan(from, to, owner !== undefined ? { owner } : undefined);
115
113
  const base = { from: new Date(from).toISOString(), to: new Date(to).toISOString(), ...(scan.truncated ? { truncated: true } : {}) };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 一条 run 的**持久化声明**读口(H5):它声明了哪些审批开卡帧族(`settings.approvalFrameFamilies`)。
3
+ *
4
+ * 声明随受理过的提交体落盘 —— 提交腿(`POST /v1/tasks` 两腿 / `POST /v1/runs` / A2A)在 run 认领之后把整份提交体写进续跑输入(`checkpoint_ctx`,会话键,
5
+ * `http/submit-ctx.ts` 的 `persistSubmitCtx`),所以**事后**才发生的读(耐久事件流的开流 preamble)读得到「这条 run 当初声明了什么」。读法只有一个:
6
+ * `declaredApprovalFrameFamilies`(S-1914 的唯一读者;缺席 / 形坏 ⇒ 空集),本模块不另判声明的合法性。
7
+ *
8
+ * 失效方向是**不重放**:续跑输入不在 / 体读不出 ⇒ 空集 ⇒ preamble 零帧(与「未声明」同形;空集 = 这条腿没有活审批通道)。店与会话 id 由类型保证在场:
9
+ * 唯一的调用方(`routes/runs.ts` 事件流 preamble)只在上场门为真时读 —— 上场门的 park 设施项就是「checkpoint 店在场」,run 行的会话 id 是必填串。
10
+ * 体读不出(店的拒绝契约)时报一次 `onUnreadable(where)`,让「为什么这条 run 的卡没被重放」在运维日志里有迹可循;店抖动 / 连接错按读口的刻意保守折成「不在」(`readResumeCtx` 头注)。
11
+ * 读的是 `KEEP_SEALED` 形:`approvalFrameFamilies` 不是凭据位,不为一个声明位开封任何凭据。
12
+ */
13
+ import type { ApprovalFrameFamily } from "../tool-approval.js";
14
+ import type { CheckpointStoreFull } from "../plugins/store-backend.js";
15
+ export declare function readRunDeclaredFamilies(cs: Pick<CheckpointStoreFull, "getCtx">, sessionId: string, onUnreadable?: (where: string) => void): Promise<ReadonlySet<ApprovalFrameFamily>>;
16
+ //# sourceMappingURL=run-declared-families.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { declaredApprovalFrameFamilies } from "../tool-approval.js";
2
+ import { KEEP_SEALED, readResumeCtx } from "./resume-ctx-read.js";
3
+ export async function readRunDeclaredFamilies(cs, sessionId, onUnreadable) {
4
+ const read = await readResumeCtx(cs.getCtx(sessionId), KEEP_SEALED);
5
+ if (read.kind === "corrupt")
6
+ onUnreadable?.(read.where);
7
+ if (read.kind !== "ok")
8
+ return new Set();
9
+ const body = read.ctx.body;
10
+ return declaredApprovalFrameFamilies(typeof body === "object" && body !== null ? Reflect.get(body, "settings") : undefined);
11
+ }
12
+ //# sourceMappingURL=run-declared-families.js.map
@@ -153,6 +153,12 @@ export interface ServiceStoreDeps {
153
153
  /** Durable-checkpoint store ([ref], core 1.67): durable F4 suspend/resume. When present, the resume
154
154
  * endpoint + suspended-run surfacing are active. Set only when DURABLE_APPROVAL + a TiDB pool. */
155
155
  checkpointStore?: CheckpointStoreFull;
156
+ /** S-1907:停驻行凭据的**封签器**(`sealed-key.ts` `ensureCheckpointCtxSealing`,这一域自己的保管店 `<数据根>/sealed-key/checkpoint-ctx-keys/`;规则属主 = `checkpoint-ctx.ts` 末段
157
+ * 「凭据位密封」)。提交体里请求面凭据位上的值在 `checkpoint_ctx` 里只以封签存在:写 ctx 的每一笔经它封、续跑上下文读口经它开。
158
+ * 🔴 **与 `checkpointStore` 同在**:店在而它缺席 ⇒ `createHttpServer` 拒启(句子 = `CHECKPOINT_CTX_SEALING_REQUIRED`)—— 凭据只以封签落盘,
159
+ * 封不了就不落,没有「明文照写」的第二种装配。嵌入方:`checkpointCtxSealing: await ensureCheckpointCtxSealing(sealedKeyDir)`
160
+ * (保管目录要跨重启持久、多副本各副本是同一份钥匙,否则封它的那把钥匙不在 ⇒ 409 `conflict.checkpoint_ctx_unsealable` `reason:"key_not_held"`)。 */
161
+ checkpointCtxSealing?: import("./checkpoint-ctx.js").CheckpointCtxSealing;
156
162
  /** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). Enables PUT/GET /v1/sessions/:id/policy
157
163
  * when present; core reads the rules at prepare-time (subtract-only) via RunnerDeps.sessionPolicyStore (wired separately). */
158
164
  sessionPolicyStore?: ServiceSessionPolicyStore;
@@ -21,6 +21,7 @@ import { sendError, sendHttpError, sendSessionError, logRequestError } from "./s
21
21
  import { createRequestGuards } from "./request-guards.js";
22
22
  import { sqlEngineSeatIssue } from "../sql-engine-posture.js";
23
23
  import { parkDecisionStoreRefusal } from "../orchestration/workflow-park-decision.js";
24
+ import { CHECKPOINT_CTX_SEALING_REQUIRED } from "./checkpoint-ctx.js";
24
25
  import { retiredServiceDepsRefusal } from "./retired-service-deps.js";
25
26
  import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
26
27
  import { createResumeLegs } from "./resume-legs.js";
@@ -85,6 +86,8 @@ export function createHttpServer(rawDeps) {
85
86
  if (refusal !== undefined)
86
87
  throw new Error(`createHttpServer: ${refusal}`);
87
88
  }
89
+ if (deps.checkpointStore !== undefined && deps.checkpointCtxSealing === undefined)
90
+ throw new Error(`createHttpServer: ${CHECKPOINT_CTX_SEALING_REQUIRED}`);
88
91
  const idemCache = new IdempotencyCache();
89
92
  const inflightRuns = new Map();
90
93
  const preemptableRuns = new DrainYieldingPreemptBook(() => deps.drainState?.draining === true);
@@ -140,9 +143,9 @@ export function createHttpServer(rawDeps) {
140
143
  }
141
144
  };
142
145
  }
143
- const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
146
+ const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
144
147
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
145
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
148
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
146
149
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
147
150
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
148
151
  const server = http.createServer((req, res) => {
@@ -1,11 +1,11 @@
1
1
  import type { RunStore } from "../plugins/store-backend.js";
2
2
  import type { Logger } from "../observability/logger.js";
3
3
  import type { Metrics } from "../observability/metrics.js";
4
- import { buildCheckpointCtx } from "./checkpoint-ctx.js";
4
+ import { buildCheckpointCtx, type CheckpointCtxSealer, type SealedCheckpointCtx } from "./checkpoint-ctx.js";
5
5
  import { type RefusalBody } from "./refusal-body.js";
6
6
  /** 写不进的那一种可答复的结局(各腿把它交回自己的出口:同步 / durable 腿直接回,活流腿编码成终帧)。 */
7
7
  export interface SubmitCtxRefusal {
8
- readonly status: 413;
8
+ readonly status: 413 | 409;
9
9
  readonly body: RefusalBody;
10
10
  }
11
11
  /**
@@ -14,10 +14,11 @@ export interface SubmitCtxRefusal {
14
14
  * 返回 `undefined` = 写进了,腿照常起跑。
15
15
  */
16
16
  export declare function persistSubmitCtx(store: {
17
- putCtx(sessionId: string, ctx: object): Promise<void>;
17
+ putCtx(sessionId: string, ctx: SealedCheckpointCtx): Promise<void>;
18
18
  }, deps: {
19
19
  readonly logger?: Logger | undefined;
20
20
  readonly metrics?: Metrics | undefined;
21
+ readonly checkpointCtxSealing?: CheckpointCtxSealer | undefined;
21
22
  }, claim: {
22
23
  readonly runStore: RunStore;
23
24
  readonly taskId: string;
@@ -1,8 +1,18 @@
1
1
  import { StoreRowTooLargeError } from "../plugins/sql-errors.js";
2
2
  import { releaseClaimedRun } from "./claimed-run.js";
3
- import { buildCheckpointCtx } from "./checkpoint-ctx.js";
3
+ import { buildCheckpointCtx, CheckpointCtxUnsealableError } from "./checkpoint-ctx.js";
4
4
  import { refusalBody } from "./refusal-body.js";
5
5
  import { limitFactsUnmeasured } from "../limit-refusal.js";
6
+ function unsealableRefusal(e) {
7
+ return {
8
+ status: 409,
9
+ body: refusalBody({
10
+ errorCode: "conflict.checkpoint_ctx_unsealable",
11
+ message: "this deployment cannot store this task's credentials (MCP / A2A headers or env values, settings.env, settings.webSearch.apiKey, or http hook headers) for resuming it: they are kept only sealed, and this server has no sealed-key store — nothing was run; ask whoever operates this deployment to give the service a writable sealed-key directory (<data root>/sealed-key/), or submit the task without those credentials",
12
+ facts: { field: e.field, reason: e.reason },
13
+ }),
14
+ };
15
+ }
6
16
  function rowTooLargeRefusal(tooLarge) {
7
17
  switch (tooLarge.wall) {
8
18
  case "row":
@@ -19,18 +29,24 @@ function rowTooLargeRefusal(tooLarge) {
19
29
  }
20
30
  export async function persistSubmitCtx(store, deps, claim, sessionId, fields) {
21
31
  try {
22
- await store.putCtx(sessionId, buildCheckpointCtx(fields));
32
+ await store.putCtx(sessionId, buildCheckpointCtx(fields, deps.checkpointCtxSealing, sessionId));
23
33
  return undefined;
24
34
  }
25
35
  catch (err) {
26
36
  const tooLarge = err instanceof StoreRowTooLargeError ? err : undefined;
37
+ const unsealable = err instanceof CheckpointCtxUnsealableError ? err : undefined;
38
+ const known = tooLarge ?? unsealable;
27
39
  if (claim !== undefined) {
28
- const why = tooLarge !== undefined ? tooLarge.message : `the run's resume inputs could not be stored: ${err instanceof Error ? err.message : String(err)}`;
40
+ const why = known !== undefined ? known.message : `the run's resume inputs could not be stored: ${err instanceof Error ? err.message : String(err)}`;
29
41
  await releaseClaimedRun(claim.runStore, claim.taskId, why, (releaseErr, step) => {
30
42
  deps.logger?.warn("submit_ctx_run_release_failed", { taskId: claim.taskId, sessionId, step, err: releaseErr instanceof Error ? releaseErr.message : String(releaseErr) });
31
43
  deps.metrics?.inc("submit_ctx_run_release_failed_total", { step });
32
44
  });
33
45
  }
46
+ if (unsealable !== undefined) {
47
+ deps.logger?.warn("submit_ctx_unsealable", { sessionId, taskId: claim?.taskId, field: unsealable.field, reason: unsealable.reason });
48
+ return unsealableRefusal(unsealable);
49
+ }
34
50
  if (tooLarge === undefined)
35
51
  throw err;
36
52
  deps.logger?.warn("submit_ctx_row_too_large", { sessionId, taskId: claim?.taskId, wall: tooLarge.wall, db: tooLarge.dbMessage });
@@ -402,10 +402,10 @@ export interface TaskRequestBody {
402
402
  * 记忆面暗的部署(引擎未接线 / 多租户)上 `memory` 整键本就缺席,本声明无处落也无事可关。
403
403
  * Rides the persisted body onto resume legs(与 `memoryWrite` 同姿势)。
404
404
  * ✅ **子代地板自 core 7.22.0 到货,本仓零接线**:core 的 `parentSpecSeatOf` 从**这条 run 自己的**
405
- * `spec.memory.enabled === false` 铸 `ParentSpecSeat.memoryOff`,`applyParentSpecSeat` 作**无条件
406
- * REMOVAL** 施加(改写成 `{ …子代自己的 scopes, enabled:false }`,永不删键)⇒ 父关 ⇒ 子代 / 孙代 /
405
+ * `spec.memory.enabled === false` 铸座位的 `memory: { enabled: false }`(core 7.54.0 起是夹表座的一行;此前叫 `memoryOff`,已退役),
406
+ * `applyParentSpecSeat` 按表**夹**到子代上(改写成 `{ …子代自己的 scopes, enabled:false }`,永不删键;子代的 `enabled:true` 抬不起)⇒ 父关 ⇒ 子代 / 孙代 /
407
407
  * workflow 子 spec / 跨 run 唤醒与保留 resume 一律无记忆面。🚧 **tier-3 耐久复活不带**(core 已记残余)。
408
- * 链路的机器锚:`test/memory-off-request.test.ts` ⑥ 段(亲读 core dist 的铸点与施加点)。 */
408
+ * 链路的机器锚:`test/memory-off-request.test.ts` ⑥ 段(经 core 包根 `parentSpecSeatOf` / `applyParentSpecSeat` 读铸与施)。 */
409
409
  memory?: "off";
410
410
  /** 142-S4 projectId 线程化([ref] §2):这个 run 归属的项目(center 登记簿键,generic lowercase
411
411
  * UUID — core S1 `resolveProjectId` marker 或 S3 无仓 mint 的产物;客户端只透传,零铸造权威)。是「哪个
@@ -572,6 +572,10 @@ export type DecideBinding = {
572
572
  boundInputHash?: string;
573
573
  /** [ref] edit: operator-corrected tool args, applied by core AFTER the binding check, on approve only. */
574
574
  updatedInput?: unknown;
575
+ /** core 7.54.0: the words the approving person wrote on the card ("and tell the agent what to do next"). The decide door admits
576
+ * the key only on a person's `approve` (a `deny` or a policy decide with it is a 400 `request.field_conflict`), and the outcome
577
+ * mints carry it verbatim when present — the engine owns what counts as written (blank = nothing) and judges it again pre-CAS. */
578
+ feedback?: string;
575
579
  };
576
580
  /** [ref]([ref]④→[ref] 坐标轮换判定):409 `approval_stale` 拒体上的 additive 机读指路 ——
577
581
  * **本会话当前 pending** 的 D-1 回显坐标,让持旧坐标的壳一跳重定位(免重拉列表)。
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task
23
23
  export { createAuthorizer, principalFrom, HttpError, type AuthContext, type OwnerAwareSessionStore, } from "./security.js";
24
24
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
25
25
  export type { TaskRequestBody } from "./http/wire-types.js";
26
+ export { ensureCheckpointCtxSealing } from "./sealed-key.js";
27
+ export type { CheckpointCtxSealing } from "./http/checkpoint-ctx.js";
26
28
  export { createHttpServer, type ServiceDeps, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
27
29
  export { createSideQueryLane, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
28
30
  export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS } from "./bo
20
20
  export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task-list-store-sql.js";
21
21
  export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
22
22
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
23
+ export { ensureCheckpointCtxSealing } from "./sealed-key.js";
23
24
  export { createHttpServer, } from "./http/server.js";
24
25
  export { createSideQueryLane } from "./boot/side-query-lane.js";
25
26
  export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
@@ -1,5 +1,6 @@
1
1
  import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core";
2
2
  import type { Autonomy } from "../runtime-governance.js";
3
+ import { type CheckpointCtxSealer } from "../http/checkpoint-ctx.js";
3
4
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
4
5
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
5
6
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
@@ -7,6 +8,9 @@ import type { LeaderRequestBody } from "./endpoint.js";
7
8
  /** leader worker 的 durable 审批座(`LeaderWireConfig.durable` 函数席每次产出的一份)。 */
8
9
  export interface LeaderDurableSeat {
9
10
  readonly checkpointStore: CheckpointStoreFull;
11
+ /** S-1907:写 worker 续跑上下文的封口(`ServiceDeps.checkpointCtxSealing` 同一只;规则属主 `http/checkpoint-ctx.ts` 末段「凭据位密封」)。
12
+ * worker 的续跑体只有入口词 + 子任务 objective、不带凭据位 ⇒ 实际不封任何值,但写 ctx 的每一笔都经同一只封口(没有不封的第二种写法)。 */
13
+ readonly ctxSealer: CheckpointCtxSealer | undefined;
10
14
  readonly requireApproval: string[];
11
15
  readonly deny?: string[];
12
16
  readonly ttlMs?: number;
@@ -274,9 +274,10 @@ export function createLeaderRunner(cfg) {
274
274
  const provisionWorker = async (sub) => {
275
275
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
276
276
  const durable = cfg.durable?.();
277
- const resumeCtx = buildCheckpointCtx({ body: { ...entry, objective: sub.spec.objective }, unattendedLeg: true, writerVersion: serviceVersion() });
277
+ const resumeBody = { ...entry, objective: sub.spec.objective };
278
+ const storedResumeCtx = durable !== undefined ? buildCheckpointCtx({ body: resumeBody, unattendedLeg: true, writerVersion: serviceVersion() }, durable.ctxSealer, sessionId) : undefined;
278
279
  let durableSpec = {};
279
- if (durable) {
280
+ if (durable && storedResumeCtx !== undefined) {
280
281
  const d = durable;
281
282
  durableSpec = {
282
283
  toolPolicy: combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({ requireApproval: d.requireApproval, ...(d.deny ? { deny: d.deny } : {}), ...(d.autoBudget ? { autoBudget: d.autoBudget } : {}), ...(d.neverAuto ? { neverAuto: d.neverAuto } : {}) })),
@@ -284,7 +285,7 @@ export function createLeaderRunner(cfg) {
284
285
  durableApproval: { scope: "_leader", ...(d.ttlMs ? { ttlMs: d.ttlMs } : {}) },
285
286
  onQuestion: QUESTION_AWAITS_RESUME,
286
287
  };
287
- await d.checkpointStore.putCtx(sessionId, resumeCtx).catch((e) => {
288
+ await d.checkpointStore.putCtx(sessionId, storedResumeCtx).catch((e) => {
288
289
  cfg.logger?.warn?.("leader_putctx_failed", { workerId: sub.workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
289
290
  });
290
291
  }
@@ -313,13 +314,13 @@ export function createLeaderRunner(cfg) {
313
314
  }
314
315
  const keepCtxWarm = (runner) => {
315
316
  const d = durable;
316
- if (!d)
317
+ if (!d || storedResumeCtx === undefined)
317
318
  return runner;
318
319
  const orig = runner.runTaskStream.bind(runner);
319
320
  runner.runTaskStream = (spec, resume) => {
320
321
  const stream = orig(spec, resume);
321
322
  const iv = setInterval(() => {
322
- void d.checkpointStore.putCtx(sessionId, resumeCtx).catch(() => { });
323
+ void d.checkpointStore.putCtx(sessionId, storedResumeCtx).catch(() => { });
323
324
  }, 45_000);
324
325
  iv.unref?.();
325
326
  void stream.result().then(() => clearInterval(iv), () => clearInterval(iv));
@@ -359,7 +360,7 @@ export function createLeaderRunner(cfg) {
359
360
  },
360
361
  })),
361
362
  fetchDiff: () => fetchUploadedDiff(getUrl),
362
- spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }, resumeCtx.body),
363
+ spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }, resumeBody),
363
364
  };
364
365
  }
365
366
  if (!cfg.e2bApiKey)
@@ -374,7 +375,7 @@ export function createLeaderRunner(cfg) {
374
375
  runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
375
376
  diffEnv: env,
376
377
  destroy: () => env.destroy().then(() => { }),
377
- spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }, resumeCtx.body),
378
+ spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }, resumeBody),
378
379
  };
379
380
  };
380
381
  const provisionIntegrationSandbox = async () => {