@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
@@ -12,8 +12,8 @@ export function bodyCarriesAllowList(body) {
12
12
  const allow = permissions["allow"];
13
13
  return Array.isArray(allow) && allow.length > 0;
14
14
  }
15
- export function buildCheckpointCtx(fields) {
16
- return {
15
+ export function buildCheckpointCtx(fields, sealer, sessionId) {
16
+ const ctx = {
17
17
  body: fields.body,
18
18
  writerVersion: fields.writerVersion,
19
19
  ...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
@@ -21,6 +21,7 @@ export function buildCheckpointCtx(fields) {
21
21
  ...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "allow_layer" } : {}),
22
22
  permissionModeReading: "entry_word",
23
23
  };
24
+ return sealCheckpointCtx(ctx, sealer, sessionId);
24
25
  }
25
26
  export function withPermissionModeRewritten(ctx, body) {
26
27
  return { ...ctx, body, permissionModeReading: "entry_word" };
@@ -122,4 +123,159 @@ function storedBodyReplayArmAnswer(refusal, readerVersion) {
122
123
  }
123
124
  }
124
125
  }
126
+ export const CHECKPOINT_CTX_SEAL_ALG = "checkpoint-ctx-sealedbox-v1";
127
+ export const CHECKPOINT_CTX_CREDENTIAL_SLOTS = [
128
+ { path: ["mcpServers", "*", "transport", "headers"], holds: "map" },
129
+ { path: ["mcpServers", "*", "transport", "env"], holds: "map" },
130
+ { path: ["a2aPeers", "*", "headers"], holds: "map" },
131
+ { path: ["settings", "env"], holds: "map" },
132
+ { path: ["settings", "webSearch", "apiKey"], holds: "value" },
133
+ { path: ["settings", "hooks", "*", "*", "hooks", "*", "headers"], holds: "map" },
134
+ ];
135
+ export const CHECKPOINT_CTX_UNSEALABLE_REASONS = ["no_key_store", "key_not_held", "unopenable", "plaintext", "unrecognized_seal"];
136
+ export class CheckpointCtxUnsealableError extends Error {
137
+ field;
138
+ reason;
139
+ constructor(field, reason) {
140
+ super(`checkpoint ctx credential at ${field} cannot be ${reason === "no_key_store" ? "sealed or opened without a sealed-key store" : `opened (${reason})`}`);
141
+ this.name = "CheckpointCtxUnsealableError";
142
+ this.field = field;
143
+ this.reason = reason;
144
+ }
145
+ }
146
+ function mintSealedCheckpointCtx(sealed) {
147
+ return sealed;
148
+ }
149
+ export const CHECKPOINT_CTX_SEALING_REQUIRED = "a durable checkpoint store is wired but checkpointCtxSealing is not: suspended tasks keep the request's credentials (MCP / A2A headers and env values, settings.env, settings.webSearch.apiKey, http hook headers) in their resume context, and they are stored sealed or not at all — give the service a sealed-key store for them (this service builds it under <data root>/sealed-key/checkpoint-ctx-keys/ at boot; a failure there is the earlier checkpoint_ctx_sealing_unavailable warning — make that directory creatable and chmod-able by the service, or repair or remove a key file there whose public and private halves do not belong together), or, when embedding, pass checkpointCtxSealing: await ensureCheckpointCtxSealing(sealedKeyDir); refusing to start rather than write credentials in plaintext";
150
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
151
+ export function isSealedCheckpointValue(v) {
152
+ if (!isRecord(v))
153
+ return false;
154
+ const keys = Object.keys(v);
155
+ return keys.length === 3 && v["alg"] === CHECKPOINT_CTX_SEAL_ALG && typeof v["publicKeyId"] === "string" && typeof v["sealed"] === "string";
156
+ }
157
+ function defineOwn(target, key, value) {
158
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
159
+ }
160
+ function mapCredentialSlots(body, visit) {
161
+ let out = body;
162
+ for (const slot of CHECKPOINT_CTX_CREDENTIAL_SLOTS)
163
+ out = mapAt(out, slot.path, 0, slot.holds, "", visit);
164
+ return out;
165
+ }
166
+ function mapAt(node, path, i, holds, field, visit) {
167
+ if (i === path.length) {
168
+ if (holds === "value")
169
+ return visit(node, field);
170
+ if (!isRecord(node) || isSealedCheckpointValue(node))
171
+ return visit(node, field);
172
+ let copy;
173
+ for (const [k, v] of Object.entries(node)) {
174
+ const next = visit(v, memberField(field, k));
175
+ if (next !== v)
176
+ defineOwn((copy ??= { ...node }), k, next);
177
+ }
178
+ return copy ?? node;
179
+ }
180
+ const seg = path[i];
181
+ if (seg === "*") {
182
+ if (Array.isArray(node)) {
183
+ let copy;
184
+ node.forEach((el, idx) => {
185
+ const next = mapAt(el, path, i + 1, holds, `${field}[${idx}]`, visit);
186
+ if (next !== el)
187
+ (copy ??= [...node])[idx] = next;
188
+ });
189
+ return copy ?? node;
190
+ }
191
+ if (!isRecord(node))
192
+ return node;
193
+ let copy;
194
+ for (const [k, v] of Object.entries(node)) {
195
+ const next = mapAt(v, path, i + 1, holds, memberField(field, k), visit);
196
+ if (next !== v)
197
+ defineOwn((copy ??= { ...node }), k, next);
198
+ }
199
+ return copy ?? node;
200
+ }
201
+ if (!isRecord(node) || !Object.hasOwn(node, seg))
202
+ return node;
203
+ const child = node[seg];
204
+ const next = mapAt(child, path, i + 1, holds, memberField(field, seg), visit);
205
+ if (next === child)
206
+ return node;
207
+ const copy = { ...node };
208
+ defineOwn(copy, seg, next);
209
+ return copy;
210
+ }
211
+ function memberField(field, key) {
212
+ return field === "" ? key : `${field}.${key}`;
213
+ }
214
+ function sealBody(body, sealer, sessionId) {
215
+ let valuesSealed = 0;
216
+ const sealed = mapCredentialSlots(body, (value, field) => {
217
+ if (sealer === undefined)
218
+ throw new CheckpointCtxUnsealableError(field, "no_key_store");
219
+ valuesSealed += 1;
220
+ return sealer.seal(value, sessionId);
221
+ });
222
+ return { body: sealed, valuesSealed };
223
+ }
224
+ export function sealCheckpointCtx(ctx, sealer, sessionId) {
225
+ const { body } = sealBody(ctx.body, sealer, sessionId);
226
+ return mintSealedCheckpointCtx({ ...ctx, body: body, credentialSeal: CHECKPOINT_CTX_SEAL_ALG });
227
+ }
228
+ export function openCheckpointCtx(stored, opener, sessionId) {
229
+ const { credentialSeal, ...rest } = stored;
230
+ const stamp = credentialSeal;
231
+ try {
232
+ const body = mapCredentialSlots(stored.body, (value, field) => {
233
+ if (stamp === undefined)
234
+ throw new CheckpointCtxUnsealableError(field, "plaintext");
235
+ if (stamp !== CHECKPOINT_CTX_SEAL_ALG)
236
+ throw new CheckpointCtxUnsealableError(field, "unrecognized_seal");
237
+ if (isSealedCheckpointValue(value)) {
238
+ if (opener === undefined)
239
+ throw new CheckpointCtxUnsealableError(field, "no_key_store");
240
+ if (!opener.holds(value.publicKeyId))
241
+ throw new CheckpointCtxUnsealableError(field, "key_not_held");
242
+ try {
243
+ return opener.open(value, sessionId);
244
+ }
245
+ catch {
246
+ throw new CheckpointCtxUnsealableError(field, "unopenable");
247
+ }
248
+ }
249
+ if (isRecord(value) && Object.hasOwn(value, "sealed"))
250
+ throw new CheckpointCtxUnsealableError(field, "unrecognized_seal");
251
+ throw new CheckpointCtxUnsealableError(field, "plaintext");
252
+ });
253
+ return { kind: "ok", ctx: { ...rest, body: body } };
254
+ }
255
+ catch (e) {
256
+ if (e instanceof CheckpointCtxUnsealableError)
257
+ return { kind: "unsealable", field: e.field, reason: e.reason };
258
+ throw e;
259
+ }
260
+ }
261
+ export function sealedCredentialFieldOf(stored) {
262
+ if (!isRecord(stored))
263
+ return undefined;
264
+ let first;
265
+ mapCredentialSlots(stored["body"], (value, field) => {
266
+ if (first === undefined && isSealedCheckpointValue(value))
267
+ first = field;
268
+ return value;
269
+ });
270
+ return first;
271
+ }
272
+ export function sealStoredCheckpointCtxM28(stored, sealer, sessionId) {
273
+ if (stored.credentialSeal !== undefined)
274
+ return undefined;
275
+ const body = stored.body;
276
+ if (!isRecord(body))
277
+ return undefined;
278
+ const out = sealBody(body, sealer, sessionId);
279
+ return out.valuesSealed === 0 ? undefined : { ctx: mintSealedCheckpointCtx({ ...stored, body: out.body, credentialSeal: CHECKPOINT_CTX_SEAL_ALG }), valuesSealed: out.valuesSealed };
280
+ }
125
281
  //# sourceMappingURL=checkpoint-ctx.js.map
@@ -8,14 +8,14 @@ import { sendJson, sendError, isSseResponse, endSseWithError } from "./send.js";
8
8
  import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal, billableRefusalOf, billableRefusalFrame } from "./readiness.js";
9
9
  import { ENGINE_SIGNAL_KEY, buildEngineSignal, fleetMembershipKeys, healthConfigLedgerKeys, healthDrainingKeys, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
10
10
  import { buildHealthSelfReport } from "./health-self-report.js";
11
- import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized, verifiedIdentitySource, hasServiceAuth, devOpenDeployment } from "./principal-gate.js";
11
+ import { actsForDeployment, authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized, traceTokenPresented, verifiedIdentitySource, hasServiceAuth, devOpenDeployment } from "./principal-gate.js";
12
12
  const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
13
13
  export function readCredentialOk(req, config) {
14
14
  if (!hasServiceAuth(config))
15
15
  return true;
16
16
  if (verifiedIdentitySource(req, config) !== undefined)
17
17
  return true;
18
- return config.traceToken !== undefined && config.traceToken !== "" && authorized(req, config.traceToken);
18
+ return traceTokenPresented(req, config);
19
19
  }
20
20
  function cors(res, req, origins, principalHeader) {
21
21
  if (!origins || origins.length === 0)
@@ -191,7 +191,7 @@ export function createDispatch(ctx) {
191
191
  }
192
192
  const mt = deps.config.metricsToken;
193
193
  const devOpen = devOpenDeployment(deps) && !mt;
194
- const ok = devOpen || systemFor(req, deps.config) !== undefined || (!!mt && authorized(req, mt));
194
+ const ok = devOpen || (!!mt && authorized(req, mt)) || actsForDeployment(req, deps.config, () => gatedPrincipal(req, deps.config));
195
195
  if (!ok) {
196
196
  sendUnauthorized(res, "operator");
197
197
  return;
@@ -3,7 +3,7 @@ import { heldWorkflowParkDecisions } from "../parked-decide.js";
3
3
  import { HEARTBEAT_MS, runInBackground } from "../runs.js";
4
4
  import { fleetRunPublisher, fleetRunLabels } from "../fleet/fleet-bus.js";
5
5
  import { cascadeConfig, runMeta } from "./run-meta.js";
6
- import { resolveStreamApprovalGate } from "../tool-approval.js";
6
+ import { declaredApprovalFrameFamilies, resolveStreamApprovalGate } from "../tool-approval.js";
7
7
  import { buildActiveRunConflict } from "./active-run-conflict.js";
8
8
  import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
9
9
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
@@ -84,10 +84,11 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
84
84
  toolApprovalEnabled: true,
85
85
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
86
86
  backend: deps.backend,
87
- parkFacility: deps.checkpointStore !== undefined,
87
+ checkpointStore: deps.checkpointStore,
88
88
  }).active,
89
89
  windowMs: deps.config.streamApproval?.windowMs,
90
90
  windowMarginMs: deps.config.streamAskWindowMarginMs,
91
+ declaredFamilies: declaredApprovalFrameFamilies(args.prepared.body.settings),
91
92
  }
92
93
  : undefined, deps.checkpointStore, args.prepared.engineNotices, trustedInternals, deps.userInputLedger, args.prepared.acceptance);
93
94
  return { status: 202, body: { taskId, sessionId, status: "running", ...(args.prepared.warnings !== undefined ? { warnings: args.prepared.warnings } : {}) } };
@@ -80,11 +80,11 @@ export declare function idemIdentityOf(caller: IdemCaller): string;
80
80
  export declare function idemKeyOf(raw: string, caller: IdemCaller, family: IdemFamily): string;
81
81
  /**
82
82
  * 一次提交的**请求体指纹** —— HTTP 三条提交腿(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)同一只规范化函数:
83
- * `sha256(canonicalJson(体 − taskId))` 的 hex(定宽 64)。
83
+ * `sha256(canonicalJsonStringify(体 − taskId))` 的 hex(定宽 64)。
84
84
  *
85
85
  * 幂等 = 同一调用方、同一键、**同一请求**:账本行记下绑定 `Idempotency-Key` 的那次请求的指纹(`task_run.idem_body_fingerprint`),
86
86
  * 同键再投而指纹不同 ⇒ 唯一回执函数答 409(`http/submit-replay.ts`)。规范化只有两步:
87
- * · JSON 按键递归排序(唯一序列化器 `canonicalJson`;值为 `undefined` 的键与缺席同义)—— 键序不同的同一份体是同一请求;
87
+ * · JSON 按键递归排序(唯一序列化器 core `canonicalJsonStringify`;值为 `undefined` 的键与缺席同义)—— 键序不同的同一份体是同一请求;
88
88
  * · 去掉 `taskId` —— 它是同一条律的**另一把钥匙**(调用方自铸的主键),由回执函数的「钥匙指向别的行 ⇒ `conflict.run_exists`」判,
89
89
  * 不是请求内容(换腿重投:流式腿不带 → durable 腿带回 `x-task-id`,仍是同一请求)。
90
90
  * 头一概不进:身份已在键里(`idemKeyOf` 的身份位),其余头(`Idempotency-Key` 本身、`x-detach-on-disconnect` 一类连接语义)不是提交内容。
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { canonicalJson } from "../canonical-json.js";
2
+ import { canonicalJsonStringify } from "@sema-agent/core";
3
3
  import { gatedPrincipal } from "./principal-gate.js";
4
4
  export class IdempotencyCache {
5
5
  ttlMs;
@@ -80,7 +80,7 @@ export function idemKeyOf(raw, caller, family) {
80
80
  }
81
81
  export function submissionFingerprintOf(body) {
82
82
  const content = body !== null && typeof body === "object" && !Array.isArray(body) ? { ...body, taskId: undefined } : body;
83
- return createHash("sha256").update(canonicalJson(content)).digest("hex");
83
+ return createHash("sha256").update(canonicalJsonStringify(content)).digest("hex");
84
84
  }
85
85
  export function submissionIdemOf(raw, caller, body) {
86
86
  return { key: idemKeyOf(raw, caller, "run"), bodyFingerprint: submissionFingerprintOf(body) };
@@ -12,6 +12,9 @@ export declare function systemFor(req: IncomingMessage, config: ServiceConfig):
12
12
  /** "presented credentials must verify":Authorization 带 Bearer/token scheme 且三路全验不过 → true。
13
13
  * 其他 scheme(Basic 等,非我方凭证空间)不管——由原有门语义处置,避免误伤未知代理注头。 */
14
14
  export declare function bearerPresentedButUnverified(req: IncomingMessage, config: ServiceConfig): boolean;
15
+ /** 「这次请求出示了这台部署的只读 trace 凭据(`TRACE_TOKEN`)」—— 一句,三处读:凭据验不过判({@link bearerPresentedButUnverified})、门前读路谓词
16
+ * (`dispatch.ts` 的 `readCredentialOk`)、fleet 读档的只读臂({@link fleetViewOf})。没配 ⇒ 恒 false。 */
17
+ export declare function traceTokenPresented(req: IncomingMessage, config: ServiceConfig): boolean;
15
18
  /** loopback 三形(IPv4 / IPv6 / IPv4-mapped)—— 一处定义,diagnostics 门与任何「本机自用」判据同吃。缺席 ⇒ false(fail-closed)。 */
16
19
  export declare function isLoopbackAddress(addr: string | undefined): boolean;
17
20
  /**
@@ -62,8 +65,8 @@ export declare function sendPrincipalRefusal(res: ServerResponse, req: IncomingM
62
65
  * 每面一句模块级常量(冻结门按常量解引用逐字钉)。三句都**不含任何值**(令牌 / 头名 / 路径实参一概不回显),且**不区分**
63
66
  * 「没带」与「带错」—— 同一道门上两者逐字同体,区分就是探测面。
64
67
  * · `credential` —— 全局凭据门(`dispatch.ts`):部署配了 service token,请求没带或带的不认(registry 登录令牌验不过同属此面);
65
- * · `operator` —— 只给运维凭据开的口(`/metrics*` 与 `GET /v1/tasks/source-summary`):service token 开得了,指标口另认只读的
66
- * `METRICS_TOKEN`(它在全局门之前判);
68
+ * · `operator` —— 只给运维凭据开的口(`/metrics*` 与 `GET /v1/tasks/source-summary`):不带主体头的 service token 开得了(带主体头的是那位主体,
69
+ * {@link actsForDeployment}),指标口另认只读的 `METRICS_TOKEN`、`source-summary` 另认只读的 `TRACE_TOKEN`(都在全局门之前判);
67
70
  * · `identity` —— 过了凭据门(或这台部署没配 service token)、但这一口要一个**验过的调用方身份**:属主寻址的读 / 写面、规则面、
68
71
  * 记忆面。在配了 service token 的部署上,「带 service token 却没身份」只会落在只认身份的口(规则 / 记忆 / 会话策略)上 ——
69
72
  * 这时再叫调用方「去带 service token」是假出路,所以这一面的句子只讲身份。
@@ -71,7 +74,7 @@ export declare function sendPrincipalRefusal(res: ServerResponse, req: IncomingM
71
74
  */
72
75
  export type UnauthorizedFace = "credential" | "operator" | "identity";
73
76
  export declare const UNAUTHORIZED_CREDENTIAL = "unauthorized \u2014 the request carries no credential this deployment accepts: send the deployment's service token as a Bearer token in the Authorization header (its operator set it as SERVICE_AUTH_TOKEN or SERVICE_AUTH_TOKENS; a sema-up deployment keeps SERVICE_AUTH_TOKEN in ~/.sema-up/<project>/.env on the machine that ran sema-up)";
74
- export declare const UNAUTHORIZED_OPERATOR = "unauthorized \u2014 this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header (the metrics endpoints also accept the read-only METRICS_TOKEN)";
77
+ export declare const UNAUTHORIZED_OPERATOR = "unauthorized \u2014 this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header without a principal header (a service-token request that names a principal acts for that principal, not for the deployment), or a read-only token this endpoint accepts (METRICS_TOKEN for the metrics endpoints, TRACE_TOKEN for the task source summary)";
75
78
  export declare const UNAUTHORIZED_IDENTITY = "unauthorized \u2014 this endpoint acts for a named caller and the request carries no caller identity this deployment verifies: identify the caller the way this deployment does (a registry sign-in token as the Bearer token, a signed principal token on a direct-door deployment, or the principal header set by a trusted front end)";
76
79
  export declare function sendUnauthorized(res: ServerResponse, face: UnauthorizedFace): void;
77
80
  /**
@@ -113,13 +116,36 @@ export declare function wiredIdentitySources(w: IdentityWiring): readonly Identi
113
116
  * 没配 service token、没装鉴权桥、没开直连门)、**也没声明**只读 trace token 或 `REQUIRE_PRINCIPAL`:请求里没有任何东西替主体头作保,头是谁都能自报的
114
117
  * **标签**(过滤 / 展示用),不是身份。装了鉴权桥的部署(SSO 单开)带登录令牌的调用方是验过的身份 ⇒ 不是 dev-open(修前只看 token / `REQUIRE_PRINCIPAL`,
115
118
  * 这一形被读成 dev-open,任何人 fleet-wide 读全部会话)。
116
- * 四处读它、不各写一份:会话列表的 fleet-wide 第一臂(`isFleetWide`)、会话写面的 fleet-wide 第一臂(`sessionOwnerScopeForWrite`)、
119
+ * 三处读它、不各写一份:fleet 调用方的第一臂({@link sessionCallerOf} —— fleet 面的 {@link fleetViewOf} 与会话行属主门都经它)、
117
120
  * 会话面的属主谓词({@link sessionOwnerReadable})、`/metrics` 族的免凭据臂(`dispatch.ts`)—— 于是「调用方是谁」在列表、单会话面与指标面上只有一只读法。
118
121
  */
119
122
  export declare function devOpenDeployment(w: IdentityWiring): boolean;
120
- /** 会话行属主门的调用方的唯一铸点({@link SessionCaller}):验过的主体 + 是不是 fleet 调用方 —— {@link devOpenDeployment} 上的任何调用方,
121
- * 或出示了这台部署的 service 凭据(`systemFor`)、不报主体的调用方(带主体头的是那位主体,不是 fleet;只读 trace token 不是 service 凭据)。 */
123
+ /**
124
+ * service 凭据**代部署本身**说话:出示了这台部署的 service 凭据({@link systemFor}),且不代任何验过的主体。带主体头的 service 凭据请求是前端(BFF)
125
+ * 代那位主体发的 —— 它读那位主体,不读全舰队;部署姿态(单用户 / 多租)不改这一句(单用户形上的主体头同样是「这次请求为谁」)。
126
+ * 两处读它:fleet 调用方的凭据臂({@link sessionCallerOf})与 `/metrics` 族的 service 臂(`dispatch.ts`)。先判凭据、后取主体:`principal` 只在出示了
127
+ * service 凭据时才求值(调用点那一次 {@link gatedPrincipal}),没带凭据的请求不因主体头的形而改答。
128
+ */
129
+ export declare function actsForDeployment(req: IncomingMessage, config: ServiceConfig, principal: () => Principal | undefined): boolean;
130
+ /**
131
+ * 🔴 **「这个调用方是不是 fleet 调用方(看得到所有租户)」的唯一铸点**({@link SessionCaller}):验过的主体 + 是不是 fleet —— {@link devOpenDeployment} 上的任何
132
+ * 调用方,或 {@link actsForDeployment}(出示 service 凭据、不报主体)。带主体头的是那位主体,不是 fleet;只读 trace token 不是 service 凭据;operator 不在这里
133
+ * (运维口各自读 {@link explicitOperatorOk})。读者:会话行属主门(无主 placed 行的放行臂)与 fleet 面的 {@link fleetViewOf} —— 同一问一只读法。
134
+ */
122
135
  export declare function sessionCallerOf(req: IncomingMessage, w: IdentityWiring): SessionCaller;
136
+ /** fleet 面的两档(闭集):`read` = 只读面,只读 trace 凭据在这一档也是 fleet;`write` = 写面,只认 {@link sessionCallerOf}(读凭据不能变成跨租户写权)。 */
137
+ export type FleetFace = "read" | "write";
138
+ /** 一次 fleet 面判定的读数:`fleetWide` = 看全舰队(不钉人);否则 `gateOwner` = 钉住的那位验过的主体,`null` = 没有可用身份(调用点答 401 identity)。 */
139
+ export interface FleetView {
140
+ readonly fleetWide: boolean;
141
+ readonly gateOwner: Principal | null;
142
+ }
143
+ /**
144
+ * 🔴 **fleet 面「看全舰队,还是钉住一位主体」的唯一读法**。fleet 判据只有一个来源 —— {@link sessionCallerOf};本函数只把它投成 fleet 面要的两格,
145
+ * 并按档并上只读 trace 凭据(读档)。读者 —— 读档:舰队流、会话列表、发件链接列表、后台名册、trace 五口、用量三口、`/v1/tasks/source-summary`;
146
+ * 写档:删除、fork、同步族(拉取与推送同门)。
147
+ */
148
+ export declare function fleetViewOf(req: IncomingMessage, w: IdentityWiring, face: FleetFace): FleetView;
123
149
  /**
124
150
  * **「这个调用方能不能动这只会话」的唯一谓词**(会话面:视图 / head / 事件流 / 记忆状态与自助抹除 / 改名置顶 / 后台任务 / MCP 重连 /
125
151
  * 面板的上一腿 MCP 投影)。没有身份边界的部署({@link devOpenDeployment})上不钉任何人(与会话列表同读,列表把这只会话列给谁,谁就读得到它);
@@ -30,10 +30,13 @@ export function bearerPresentedButUnverified(req, config) {
30
30
  return false;
31
31
  if (systemFor(req, config) !== undefined)
32
32
  return false;
33
- if (config.traceToken && authorized(req, config.traceToken))
33
+ if (traceTokenPresented(req, config))
34
34
  return false;
35
35
  return true;
36
36
  }
37
+ export function traceTokenPresented(req, config) {
38
+ return !!config.traceToken && authorized(req, config.traceToken);
39
+ }
37
40
  export function isLoopbackAddress(addr) {
38
41
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
39
42
  }
@@ -68,7 +71,7 @@ export function sendPrincipalRefusal(res, req, config) {
68
71
  sendError(res, 401, "auth.principal_required", `missing principal header '${config.principalHeader}'`);
69
72
  }
70
73
  export const UNAUTHORIZED_CREDENTIAL = "unauthorized — the request carries no credential this deployment accepts: send the deployment's service token as a Bearer token in the Authorization header (its operator set it as SERVICE_AUTH_TOKEN or SERVICE_AUTH_TOKENS; a sema-up deployment keeps SERVICE_AUTH_TOKEN in ~/.sema-up/<project>/.env on the machine that ran sema-up)";
71
- export const UNAUTHORIZED_OPERATOR = "unauthorized — this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header (the metrics endpoints also accept the read-only METRICS_TOKEN)";
74
+ export const UNAUTHORIZED_OPERATOR = "unauthorized — this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header without a principal header (a service-token request that names a principal acts for that principal, not for the deployment), or a read-only token this endpoint accepts (METRICS_TOKEN for the metrics endpoints, TRACE_TOKEN for the task source summary)";
72
75
  export const UNAUTHORIZED_IDENTITY = "unauthorized — this endpoint acts for a named caller and the request carries no caller identity this deployment verifies: identify the caller the way this deployment does (a registry sign-in token as the Bearer token, a signed principal token on a direct-door deployment, or the principal header set by a trusted front end)";
73
76
  export function sendUnauthorized(res, face) {
74
77
  switch (face) {
@@ -120,9 +123,29 @@ export function wiredIdentitySources(w) {
120
123
  export function devOpenDeployment(w) {
121
124
  return wiredIdentitySources(w).length === 0 && !w.config.traceToken && !w.config.requirePrincipal;
122
125
  }
126
+ export function actsForDeployment(req, config, principal) {
127
+ return systemFor(req, config) !== undefined && principal() === undefined;
128
+ }
123
129
  export function sessionCallerOf(req, w) {
124
130
  const principal = gatedPrincipal(req, w.config);
125
- return { principal, fleet: devOpenDeployment(w) || (principal === undefined && systemFor(req, w.config) !== undefined) };
131
+ return { principal, fleet: devOpenDeployment(w) || actsForDeployment(req, w.config, () => principal) };
132
+ }
133
+ export function fleetViewOf(req, w, face) {
134
+ const caller = sessionCallerOf(req, w);
135
+ const fleetWide = caller.fleet || readOnlyFleetCredential(req, w.config, face);
136
+ return { fleetWide, gateOwner: fleetWide ? null : caller.principal || null };
137
+ }
138
+ function readOnlyFleetCredential(req, config, face) {
139
+ switch (face) {
140
+ case "read":
141
+ return traceTokenPresented(req, config);
142
+ case "write":
143
+ return false;
144
+ default: {
145
+ const unknownFace = face;
146
+ throw new Error(`fleetViewOf: unknown fleet face ${String(unknownFace)}`);
147
+ }
148
+ }
126
149
  }
127
150
  export function sessionOwnerReadable(caller, cell, w) {
128
151
  if (devOpenDeployment(w))
@@ -146,7 +169,7 @@ export function buildOperatorUndeclaredNotice(config, approvalFaceWired) {
146
169
  event: "operator_undeclared_single_user",
147
170
  fields: {
148
171
  detail: "this single-user deployment declares no operator: OPERATOR_PRINCIPALS is empty and OPERATOR_SINGLE_USER is not set, so no caller is an operator — " +
149
- "GET /v1/approvals, its stream, /v1/assistant/inbox and /v1/assistant/tasks show each caller only its own pending approvals (a caller that sends no principal sees none) and ignore ?owner=. " +
172
+ "GET /v1/approvals, its stream, /v1/assistant/inbox and /v1/assistant/tasks show each caller only its own pending approvals (a caller that sends no principal sees only the anonymous ones - the cards and tasks that were submitted without a principal - never a named principal's) and ignore ?owner=. " +
150
173
  "To act as this deployment's operator, add one line: OPERATOR_SINGLE_USER=true",
151
174
  },
152
175
  };
@@ -18,4 +18,16 @@ export type RefusalBody = {
18
18
  } & Record<string, unknown>;
19
19
  /** 拒体:`{ error, errorCode, ...facts }`。 */
20
20
  export declare function refusalBody(r: Refusal): RefusalBody;
21
+ /**
22
+ * 引擎拒一次续跑时的**三个机读判别位** → 拒体的同名事实键(唯一读法;续跑家族的每一只拒答都读它):
23
+ * `detail.code` ⇒ `blockedBy`(挡住它的准入码)· `detail.field` ⇒ `field`(哪一侧 / 哪一格)· `detail.reason` ⇒ `reason`(同码多臂时哪一臂)。
24
+ * 一员在场 ⇔ 它是非空串;其余(缺席、非串、空串)一律不铸键 —— 引擎没说就是没说,本层不编值,也不把一个非串原样推上 wire。
25
+ * 载体有两种,读法同一只:同步腿的 `CheckpointError.detail`(`http/resume-legs.ts` `checkpointRefusalAnswer`),与后台停驻子代赎回回执的
26
+ * `details.detail`(`parked-decide.ts` 的 422 `parked_resume.startup_failed`;引擎 7.54.0 起它把认领前那只失败的同三位交到回执上)。
27
+ */
28
+ export declare function resumeRefusalFacts(detail: unknown): {
29
+ blockedBy?: string;
30
+ field?: string;
31
+ reason?: string;
32
+ };
21
33
  //# sourceMappingURL=refusal-body.d.ts.map
@@ -1,4 +1,15 @@
1
1
  export function refusalBody(r) {
2
2
  return { error: r.message, errorCode: r.errorCode, ...r.facts };
3
3
  }
4
+ export function resumeRefusalFacts(detail) {
5
+ if (typeof detail !== "object" || detail === null)
6
+ return {};
7
+ const d = detail;
8
+ const word = (v) => typeof v === "string" && v !== "";
9
+ return {
10
+ ...(word(d.code) ? { blockedBy: d.code } : {}),
11
+ ...(word(d.field) ? { field: d.field } : {}),
12
+ ...(word(d.reason) ? { reason: d.reason } : {}),
13
+ };
14
+ }
4
15
  //# sourceMappingURL=refusal-body.js.map
@@ -95,15 +95,6 @@ export declare function createRequestGuards(ctx: {
95
95
  usageWindowDenied: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
96
96
  rateLimited: (req: IncomingMessage, res: ServerResponse) => boolean;
97
97
  safeDecode: (seg: string) => string | null;
98
- isFleetWide: (req: IncomingMessage) => boolean;
99
- sessionOwnerScope: (req: IncomingMessage) => {
100
- fleetWide: boolean;
101
- gateOwner: string | null;
102
- };
103
- sessionOwnerScopeForWrite: (req: IncomingMessage) => {
104
- fleetWide: boolean;
105
- gateOwner: string | null;
106
- };
107
98
  runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
108
99
  sessionOwnerOk: (req: IncomingMessage, res: ServerResponse, cell: SessionOwnerCell) => boolean;
109
100
  runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
@@ -4,7 +4,7 @@ import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js
4
4
  import { invalidJsonError, sendError } from "./send.js";
5
5
  import { refusalBody } from "./refusal-body.js";
6
6
  import { limitFactsUnmeasured } from "../limit-refusal.js";
7
- import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sessionCallerOf, devOpenDeployment, sendPrincipalRefusal } from "./principal-gate.js";
7
+ import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sessionCallerOf, sendPrincipalRefusal } from "./principal-gate.js";
8
8
  import { runRowReadable } from "./owner-access.js";
9
9
  import { quotaWindowResetsAt } from "../observability/cost-quota.js";
10
10
  import { escapedStatementBytes, packetWallStatementBytes, rowWallPayloadBytes } from "../plugins/sql-driver.js";
@@ -184,20 +184,6 @@ export function createRequestGuards(ctx) {
184
184
  return null;
185
185
  }
186
186
  }
187
- function isFleetWide(req) {
188
- const tt = deps.config.traceToken;
189
- return devOpenDeployment(deps) || systemFor(req, deps.config) !== undefined || (!!tt && authorized(req, tt));
190
- }
191
- function sessionOwnerScope(req) {
192
- const fleetWide = isFleetWide(req);
193
- const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
194
- return { fleetWide, gateOwner };
195
- }
196
- function sessionOwnerScopeForWrite(req) {
197
- const fleetWide = devOpenDeployment(deps) || systemFor(req, deps.config) !== undefined;
198
- const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
199
- return { fleetWide, gateOwner };
200
- }
201
187
  function ownerGate(req, res, readable, notFound) {
202
188
  const principal = gatedPrincipal(req, deps.config);
203
189
  if (deps.config.requirePrincipal && !principal) {
@@ -265,6 +251,6 @@ export function createRequestGuards(ctx) {
265
251
  }
266
252
  return Buffer.concat(chunks);
267
253
  }
268
- return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
254
+ return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
269
255
  }
270
256
  //# sourceMappingURL=request-guards.js.map
@@ -21,8 +21,20 @@
21
21
  * 早把列错误**折**成了一只普通 `Error` ⇒ 折后的错落进下面那条「店抖动 ⇒ 当缺席」臂,坏体与缺席在
22
22
  * 用户面上仍然同答 409 —— 正是 S-577 第二半要修的那个病,只是它当时只在 local 车道上修成了(那一侧
23
23
  * 恰好没折码,把列错误漏了出来 = 巧合式正确)。两条车道现在同一份拒绝契约,本口只认它。
24
+ *
25
+ * 🔴 **第四态 `unsealable`(S-1907,7.134.0):读出来了,但凭据位上的封签开不开**。店交出的是**落盘形**(凭据位上是封签,`StoredCheckpointCtx`);
26
+ * 本口在任何消费者看到之前经**唯一**开口 `openCheckpointCtx` 开封(规则属主 = `checkpoint-ctx.ts` 末段「凭据位密封」)—— 所以 `ok` 交出的恒是开封形,
27
+ * 没有哪条腿拿着封签去重建 spec。开不开(没有保管店 / 钥匙不在本副本 / 密文坏或不是本行封的 / 明文 / 认不出的封签)⇒ `unsealable`(`field` + `reason`),
28
+ * 各续跑入口答 {@link resumeCtxUnsealableAnswer}(409 `conflict.checkpoint_ctx_unsealable`)。**不折成缺席**(出路不同:缺席 = 重提;开不开 = 看 `reason`
29
+ * —— `key_not_held` 多半是多副本没共享保管目录,运维修好之后同一个决定原样重发即可),**不明文回落、不丢键**。
30
+ * 开封要知道这一行的会话(封签绑会话)与开口(`ServiceDeps.checkpointCtxSealing`;缺席 = 本副本没有保管店):两件一起交({@link CheckpointCtxOpening})。
31
+ *
32
+ * **两种读法,一只读口**(第二个实参):① 开封形({@link CheckpointCtxOpening})—— 续跑重建 spec 的每一条腿;② {@link KEEP_SEALED}「信封原样留着」——
33
+ * 只读**非凭据字段**的读者(今天 = inbox 的展示位 `objective`):不开封、不判凭据位(活凭据不为展示进内存),交回落盘形 `StoredCheckpointCtx`,
34
+ * 结局里没有 `unsealable` 这一态(型面就没有;店边界的三态与开封形逐字同一段)。凭据位上的值在这种读法里仍是封签 —— 谁要用它们就必须走读法 ①。
24
35
  */
25
36
  import type { CheckpointCtx } from "./route-ctx.js";
37
+ import { type CheckpointCtxOpener, type CheckpointCtxUnsealableReason, type StoredCheckpointCtx } from "./checkpoint-ctx.js";
26
38
  import { type RefusalBody } from "./refusal-body.js";
27
39
  export type ResumeCtxRead = {
28
40
  readonly kind: "ok";
@@ -32,8 +44,43 @@ export type ResumeCtxRead = {
32
44
  } | {
33
45
  readonly kind: "corrupt";
34
46
  readonly where: string;
47
+ } | {
48
+ readonly kind: "unsealable";
49
+ readonly field: string;
50
+ readonly reason: CheckpointCtxUnsealableReason;
51
+ };
52
+ /** 读法 ② 的结局:店边界的三态,`ok` 交落盘形(凭据位上仍是封签)。 */
53
+ export type SealedResumeCtxRead = {
54
+ readonly kind: "ok";
55
+ readonly ctx: StoredCheckpointCtx;
56
+ } | {
57
+ readonly kind: "absent";
58
+ } | {
59
+ readonly kind: "corrupt";
60
+ readonly where: string;
61
+ };
62
+ /** 开封的两件:这一行的会话(= 交给店 `getCtx` 的那一个)与开口(缺席 = 本副本没有保管店)。 */
63
+ export interface CheckpointCtxOpening {
64
+ readonly sessionId: string;
65
+ readonly opener: CheckpointCtxOpener | undefined;
66
+ }
67
+ /** 读法 ② 的词:信封原样留着(只读非凭据字段)。 */
68
+ export declare const KEEP_SEALED = "keep_sealed";
69
+ export declare function readResumeCtx(load: Promise<StoredCheckpointCtx | null>, opening: CheckpointCtxOpening): Promise<ResumeCtxRead>;
70
+ export declare function readResumeCtx(load: Promise<StoredCheckpointCtx | null>, opening: typeof KEEP_SEALED): Promise<SealedResumeCtxRead>;
71
+ /**
72
+ * 开不开的对外答复(S-1907)—— 409 `conflict.checkpoint_ctx_unsealable`,机读 `field`(哪一个凭据位,提交体路径;键名不是秘密,值从不进来)+ `reason`
73
+ * (闭集 `CHECKPOINT_CTX_UNSEALABLE_REASONS`)。句子说「这条任务的续跑上下文里带的凭据本服务开不开」、五种成因与出路(按 `reason` 修好保管目录后原样重发,
74
+ * 或作为新任务重提、仍停驻的先取消)。拒在任何状态改动之前:卡仍待决、行字节不动、`plan_review` 的一次性 nonce 未烧。
75
+ * 🔴 形是给冻结门看的(与 {@link resumeCtxCorruptAnswer} 同一条):`status: <三位数>, body: refusalBody({ errorCode: "<码>", message: "<整句双引号字面>"`,中间只许有空白。
76
+ */
77
+ export declare function resumeCtxUnsealableAnswer(read: {
78
+ readonly field: string;
79
+ readonly reason: CheckpointCtxUnsealableReason;
80
+ }): {
81
+ status: number;
82
+ body: RefusalBody;
35
83
  };
36
- export declare function readResumeCtx(load: Promise<CheckpointCtx | null>): Promise<ResumeCtxRead>;
37
84
  /** 坏体的对外答复 —— 与会话策略面**同码**(`session-policy-wire.ts` 的 `corrupt` ⇒ 500),不铸新词。
38
85
  * 文案只说「哪一行读不出来 + 该找谁」,不把店层 detail 拼上 wire(内部机件不上 wire,S-376 同律)。 */
39
86
  export declare function resumeCtxCorruptAnswer(where: string): {
@@ -1,9 +1,18 @@
1
+ import { openCheckpointCtx } from "./checkpoint-ctx.js";
1
2
  import { SqlJsonColumnError, StoreCorruptError } from "../plugins/sql-json-column.js";
2
3
  import { refusalBody } from "./refusal-body.js";
3
- export async function readResumeCtx(load) {
4
+ export const KEEP_SEALED = "keep_sealed";
5
+ export async function readResumeCtx(load, opening) {
6
+ const read = await readStoredCtx(load);
7
+ if (read.kind !== "ok" || opening === KEEP_SEALED)
8
+ return read;
9
+ const opened = openCheckpointCtx(read.ctx, opening.opener, opening.sessionId);
10
+ return opened.kind === "ok" ? { kind: "ok", ctx: opened.ctx } : { kind: "unsealable", field: opened.field, reason: opened.reason };
11
+ }
12
+ async function readStoredCtx(load) {
13
+ let stored;
4
14
  try {
5
- const ctx = await load;
6
- return ctx == null ? { kind: "absent" } : { kind: "ok", ctx };
15
+ stored = await load;
7
16
  }
8
17
  catch (e) {
9
18
  if (e instanceof StoreCorruptError)
@@ -12,6 +21,20 @@ export async function readResumeCtx(load) {
12
21
  throw e;
13
22
  return { kind: "absent" };
14
23
  }
24
+ return stored == null ? { kind: "absent" } : { kind: "ok", ctx: stored };
25
+ }
26
+ export function resumeCtxUnsealableAnswer(read) {
27
+ return {
28
+ status: 409,
29
+ body: refusalBody({
30
+ errorCode: "conflict.checkpoint_ctx_unsealable",
31
+ message: "this task's stored resume context holds credentials from its request (MCP / A2A headers or env values, settings.env, settings.webSearch.apiKey, or http hook headers) that this server cannot unseal — reason says why: no_key_store (this server has no sealed-key store), key_not_held (they were sealed with a key this server does not hold: another replica's, or a replaced sealed-key directory), unopenable (the sealed value is damaged or belongs to another session), plaintext (an older server version stored them unsealed), unrecognized_seal (a seal format this version does not know). Nothing was decided and nothing was resumed. If whoever operates this deployment restores the sealed-key directory that sealed them and restarts this server (it reads its keys only at startup), the same request can be sent again; otherwise submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel).",
32
+ facts: {
33
+ field: read.field,
34
+ reason: read.reason,
35
+ },
36
+ }),
37
+ };
15
38
  }
16
39
  export function resumeCtxCorruptAnswer(where) {
17
40
  return {
@@ -35,7 +35,8 @@ export declare function resumeRefusalDisposition(e: CheckpointError): "cancelled
35
35
  * 规则(一条):**一次拒 = 一个答复形,与「谁发现的」无关**。引擎在认领前 / CAS 前拒的,与本服务在重放存量体之前自己拒的(`resume_blocked_by_policy`,
36
36
  * `checkpoint-ctx.ts` `resumeBlockedByPolicyAnswer`;续跑入口上卡仍 pending ⇒ 同样带 `retriable:true`)读同一组机读键;体经拒体组句器 `refusalBody` 铸,
37
37
  * `retriable` 经续跑家族的唯一写点 `resumeRefusalWithCard` 追加。
38
- * 一处如实例外:后台停驻子代的 parked 赎回不经本函数 —— 引擎的赎回回执只交成因词,`parked-decide.ts` 答 `422 parked_resume.startup_failed`,体只带 `cause`:
38
+ * 后台停驻子代的 parked 赎回不经本函数(引擎交的是赎回回执,不是 `CheckpointError`):`parked-decide.ts` 答 `422 parked_resume.startup_failed`,体带成因词 `cause`,
39
+ * 引擎 7.54.0 起回执的 `details.detail` 交出同三位判别位,经**同一只**读法 `resumeRefusalFacts` 落成同名三键(下面第二条)。键律:
39
40
  * · `errorCode` —— 引擎码的原码镜像(`checkpoint.invalid_outcome` 按决断成因改铸,{@link decisionRefusalCauseOf});
40
41
  * · 引擎 `detail` 的三个机读判别位原样过境,键名与 `resume_blocked_by_policy` 同义:`detail.code` ⇒ `blockedBy`(挡住它的准入码 = 同一份坏配置在新跑上报的 `errorCode`;
41
42
  * 今天只有 `checkpoint.config_refused` 带它)· `detail.field` ⇒ `field`(哪一侧 / 哪一格)· `detail.reason` ⇒ `reason`(同码多臂时哪一臂,闭集 `CheckpointErrorReason`)。