@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
@@ -17,7 +17,8 @@ import { recordResumeResourceWindow } from "../resource-window.js";
17
17
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
18
18
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
19
19
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
20
- import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
20
+ import { currentApprovalGateToken } from "../approval-gate-currency.js";
21
+ import { createApprovalCardEmitter, declaredApprovalFrameFamilies, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
21
22
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
22
23
  import { APPROVAL_ENDING_CAUSES } from "../approval-deny-reasons.js";
23
24
  import { serviceVersion } from "../service-version.js";
@@ -47,9 +48,9 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
47
48
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
48
49
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
49
50
  import { deploymentListAsks } from "../approval.js";
50
- import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
51
- import { buildCheckpointVersionKeys, resumeBlockedByPolicyAnswer, resumeRefusalWithCard, storedBodyReplayAnswer, storedBodyReplayLogFields, withPermissionModeRewritten } from "./checkpoint-ctx.js";
52
- import { refusalBody } from "./refusal-body.js";
51
+ import { readResumeCtx, resumeCtxCorruptAnswer, resumeCtxUnsealableAnswer } from "./resume-ctx-read.js";
52
+ import { buildCheckpointVersionKeys, resumeBlockedByPolicyAnswer, resumeRefusalWithCard, sealCheckpointCtx, storedBodyReplayAnswer, storedBodyReplayLogFields, withPermissionModeRewritten } from "./checkpoint-ctx.js";
53
+ import { refusalBody, resumeRefusalFacts } from "./refusal-body.js";
53
54
  import { storedBodyReplayRefusal } from "./stored-body-replay.js";
54
55
  import { httpErrorRefusal } from "./send.js";
55
56
  import { createRunAcceptance } from "../run-acceptance.js";
@@ -125,6 +126,9 @@ const CHECKPOINT_ERROR_DISPOSITION = {
125
126
  "steering.invalid_content": "terminal",
126
127
  "steering.queue_full": "terminal",
127
128
  };
129
+ function windowZeroSeatOf(toolApproval) {
130
+ return (identity) => toolApproval.boundUnattendedAsk(identity, { arm: "window_zero" });
131
+ }
128
132
  export function resumeRefusalDisposition(e) {
129
133
  const word = Object.hasOwn(CHECKPOINT_ERROR_DISPOSITION, e.code) ? CHECKPOINT_ERROR_DISPOSITION[e.code] : undefined;
130
134
  switch (word) {
@@ -186,9 +190,7 @@ export function checkpointRefusalAnswer(e, at) {
186
190
  errorCode: e.code === "checkpoint.invalid_outcome" ? (refusalCause !== undefined ? WIRE_CODE_OF_DECISION_REFUSAL[refusalCause] : UNCLASSIFIED_INVALID_OUTCOME_CODE) : e.code,
187
191
  facts: {
188
192
  ...(at.taskId !== undefined ? { taskId: at.taskId } : {}),
189
- ...(detail?.code !== undefined ? { blockedBy: detail.code } : {}),
190
- ...(detail?.field ? { field: detail.field } : {}),
191
- ...(detail?.reason !== undefined ? { reason: detail.reason } : {}),
193
+ ...resumeRefusalFacts(detail),
192
194
  ...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof detail?.retryAfterMs === "number" && Number.isFinite(detail.retryAfterMs) && detail.retryAfterMs > 0
193
195
  ? { retryAfterSec: Math.max(1, Math.ceil(detail.retryAfterMs / 1000)) }
194
196
  : {}),
@@ -198,11 +200,12 @@ export function checkpointRefusalAnswer(e, at) {
198
200
  }, at.retriable);
199
201
  }
200
202
  export function createResumeLegs(ctx) {
201
- const { 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 } = ctx;
203
+ const { 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 } = ctx;
204
+ const ctxOpening = (sessionId) => ({ sessionId, opener: deps.checkpointCtxSealing });
202
205
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
203
206
  async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
204
207
  const cs = deps.checkpointStore;
205
- const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? (await cs.findPendingTokenBySession(sessionId));
208
+ const token = await currentApprovalGateToken(cs, sessionId);
206
209
  if (binding?.checkpointToken && binding.checkpointToken !== token) {
207
210
  let seenUnreadable = false;
208
211
  const seen = await cs.get(binding.checkpointToken).catch((e) => {
@@ -386,7 +389,7 @@ export function createResumeLegs(ctx) {
386
389
  const parkedReq = {
387
390
  token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
388
391
  decision, hostDecision, ...(reason !== undefined ? { reason } : {}),
389
- ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
392
+ ...(binding !== undefined ? { binding: (({ checkpointToken: _staleGuardOnly, ...rest }) => rest)(binding) } : {}),
390
393
  ...(answer !== undefined ? { answer } : {}),
391
394
  ...(boundGrant ? { grantRemember: (rootSessionId) => boundGrant(rootSessionId) } : {}),
392
395
  };
@@ -399,7 +402,7 @@ export function createResumeLegs(ctx) {
399
402
  sessionFaceOf: deps.sessionStorage?.sessionFaceOf ? deps.sessionStorage.sessionFaceOf.bind(deps.sessionStorage) : sessionFaceUnavailable,
400
403
  ...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
401
404
  ...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
402
- ...(deps.toolApproval ? { approverSeatFor: deps.toolApproval.boundUnattendedAsk } : {}),
405
+ ...(deps.toolApproval ? { approverSeatFor: windowZeroSeatOf(deps.toolApproval) } : {}),
403
406
  ...(deps.logger ? { warn: parkWarn } : {}),
404
407
  }, parkedReq, { handle: origin.handle, row: origin.row }));
405
408
  void ctxPromise.catch(() => { });
@@ -463,9 +466,11 @@ export function createResumeLegs(ctx) {
463
466
  return out;
464
467
  }
465
468
  }
466
- const ctxRead0 = await readResumeCtx(ctxPromise);
469
+ const ctxRead0 = await readResumeCtx(ctxPromise, ctxOpening(sessionId));
467
470
  if (ctxRead0.kind === "corrupt")
468
471
  return resumeCtxCorruptAnswer(ctxRead0.where);
472
+ if (ctxRead0.kind === "unsealable")
473
+ return resumeCtxUnsealableAnswer(ctxRead0);
469
474
  const ctx = ctxRead0.kind === "ok" ? ctxRead0.ctx : null;
470
475
  if (!ctx)
471
476
  return { status: 409, body: refusalBody({ errorCode: "conflict.resume_context_unavailable", message: "resume context missing — cannot rebuild task config" }) };
@@ -492,6 +497,7 @@ export function createResumeLegs(ctx) {
492
497
  decision: decision === "approve" ? "allow" : "deny",
493
498
  ...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
494
499
  ...(reason ? { reason } : {}),
500
+ ...(binding?.feedback !== undefined ? { feedback: binding.feedback } : {}),
495
501
  ...(answer !== undefined ? { answer } : {}),
496
502
  hostDecision,
497
503
  };
@@ -554,7 +560,7 @@ export function createResumeLegs(ctx) {
554
560
  const resumeLegStartedAt = Date.now();
555
561
  const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
556
562
  const reparkStatus = outcome.gate === "plan_review" ? "needs_review" : "suspended";
557
- const commitResumeCtx = () => deps.checkpointStore.putCtx(resumeCtx.sessionId, { ...resumeCtx.ctx, writerVersion: serviceVersion() }).catch((e) => {
563
+ const commitResumeCtx = () => Promise.resolve().then(() => deps.checkpointStore.putCtx(resumeCtx.sessionId, sealCheckpointCtx({ ...resumeCtx.ctx, writerVersion: serviceVersion() }, deps.checkpointCtxSealing, resumeCtx.sessionId))).catch((e) => {
558
564
  deps.metrics?.inc("resume_ctx_not_persisted_total");
559
565
  deps.logger?.warn?.("resume_ctx_not_persisted", { sessionId: resumeCtx.sessionId, gate: outcome.gate, error: errorText(e), note: "this continuation runs with the body it was given; the durable rebuild input still holds the PREVIOUS body, so the NEXT continuation of this session rebuilds from that one (never wider than this leg's)" });
560
566
  });
@@ -563,7 +569,7 @@ export function createResumeLegs(ctx) {
563
569
  toolApprovalEnabled: Boolean(deps.toolApproval),
564
570
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
565
571
  backend: deps.backend,
566
- parkFacility: deps.checkpointStore !== undefined,
572
+ checkpointStore: deps.checkpointStore,
567
573
  }).active;
568
574
  const approvalLeg = resolveApprovalLeg({
569
575
  streamApprovalOn,
@@ -571,6 +577,7 @@ export function createResumeLegs(ctx) {
571
577
  windowMarginMs: deps.config.streamAskWindowMarginMs,
572
578
  ...(typeof taskConfig.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: taskConfig.limits.maxWalltimeMs } : {}),
573
579
  nowMonotonicMs: performance.now(),
580
+ declaredFamilies: declaredApprovalFrameFamilies(resumeCtx.ctx.body.settings),
574
581
  });
575
582
  const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
576
583
  const ownerQuotaDenial = args.callerInitiated === true ? ownerCostQuotaDenied(principal) : undefined;
@@ -609,8 +616,8 @@ export function createResumeLegs(ctx) {
609
616
  const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
610
617
  if (deps.toolApproval && taskId) {
611
618
  const toolApproval = deps.toolApproval;
612
- resumeTaskConfig.onAsk = approvalLeg.windowZero
613
- ? toolApproval.boundUnattendedAsk({ owner: principal ?? null, taskId, sessionId })
619
+ resumeTaskConfig.onAsk = approvalLeg.unattended
620
+ ? toolApproval.boundUnattendedAsk({ owner: principal ?? null, taskId, sessionId }, approvalLeg.unattended)
614
621
  : toolApproval.boundAsk({
615
622
  owner: principal ?? null,
616
623
  taskId,
@@ -1097,7 +1104,7 @@ export function createResumeLegs(ctx) {
1097
1104
  const { type, ...rest } = frame;
1098
1105
  return append(type, rest);
1099
1106
  };
1100
- const withApproval = () => deps.toolApproval && taskId && approvalLeg.active && !approvalLeg.windowZero
1107
+ const withApproval = () => deps.toolApproval && taskId && approvalLeg.active && approvalLeg.unattended === undefined
1101
1108
  ? deps.toolApproval.runWithContext({
1102
1109
  taskId,
1103
1110
  owner: principal ?? null,
@@ -1274,9 +1281,11 @@ export function createResumeLegs(ctx) {
1274
1281
  if (gateKind !== RESUMABLE_GATE_KIND) {
1275
1282
  return { status: 409, body: refusalBody({ errorCode: "gate_not_resumable", message: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide` }) };
1276
1283
  }
1277
- const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1284
+ const ctxRead = await readResumeCtx(cs.getCtx(sessionId), ctxOpening(sessionId));
1278
1285
  if (ctxRead.kind === "corrupt")
1279
1286
  return resumeCtxCorruptAnswer(ctxRead.where);
1287
+ if (ctxRead.kind === "unsealable")
1288
+ return resumeCtxUnsealableAnswer(ctxRead);
1280
1289
  if (ctxRead.kind === "absent")
1281
1290
  return { status: 409, body: refusalBody({ errorCode: "conflict.resume_context_unavailable", message: "resume context missing — cannot rebuild task config" }) };
1282
1291
  const ctx = ctxRead.ctx;
@@ -1294,9 +1303,11 @@ export function createResumeLegs(ctx) {
1294
1303
  if (!deps.runStore) {
1295
1304
  return { status: 501, body: refusalBody({ errorCode: "capability.run_store_required", message: "delivering this decision requires a durable run store (DB_BACKEND=mysql|pg|local)" }) };
1296
1305
  }
1297
- const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1306
+ const ctxRead = await readResumeCtx(cs.getCtx(sessionId), ctxOpening(sessionId));
1298
1307
  if (ctxRead.kind === "corrupt")
1299
1308
  return resumeCtxCorruptAnswer(ctxRead.where);
1309
+ if (ctxRead.kind === "unsealable")
1310
+ return resumeCtxUnsealableAnswer(ctxRead);
1300
1311
  const ctx = ctxRead.kind === "ok" ? ctxRead.ctx : null;
1301
1312
  if (!ctx) {
1302
1313
  return {
@@ -1385,9 +1396,11 @@ export function createResumeLegs(ctx) {
1385
1396
  throw e;
1386
1397
  }
1387
1398
  }
1388
- const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1399
+ const ctxRead = await readResumeCtx(cs.getCtx(sessionId), ctxOpening(sessionId));
1389
1400
  if (ctxRead.kind === "corrupt")
1390
1401
  return resumeCtxCorruptAnswer(ctxRead.where);
1402
+ if (ctxRead.kind === "unsealable")
1403
+ return resumeCtxUnsealableAnswer(ctxRead);
1391
1404
  if (ctxRead.kind === "absent")
1392
1405
  return { status: 409, body: refusalBody({ errorCode: "conflict.resume_context_unavailable", message: "resume context missing — cannot rebuild task config" }) };
1393
1406
  const ctx = ctxRead.ctx;
@@ -1474,9 +1487,11 @@ export function createResumeLegs(ctx) {
1474
1487
  }),
1475
1488
  });
1476
1489
  }
1477
- const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1490
+ const ctxRead = await readResumeCtx(cs.getCtx(sessionId), ctxOpening(sessionId));
1478
1491
  if (ctxRead.kind === "corrupt")
1479
1492
  return refuse(resumeCtxCorruptAnswer(ctxRead.where));
1493
+ if (ctxRead.kind === "unsealable")
1494
+ return refuse(resumeCtxUnsealableAnswer(ctxRead));
1480
1495
  if (ctxRead.kind === "absent")
1481
1496
  return refuse({ status: 409, body: refusalBody({ errorCode: "conflict.resume_context_unavailable", message: "resume context missing — cannot rebuild task config" }) });
1482
1497
  const replay = storedBodyReplayRefusal(ctxRead.ctx, deps.config);
@@ -1514,7 +1529,7 @@ export function createResumeLegs(ctx) {
1514
1529
  const routeCtxBase = {
1515
1530
  deps,
1516
1531
  registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
1517
- helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1532
+ helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk },
1518
1533
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1519
1534
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
1520
1535
  };
@@ -73,21 +73,12 @@ export interface RouteHelpers {
73
73
  /** [ref]-T1:治理窗 pre-admission(异步 202 车道的唯一 429 出路;key 公式与 core 单源)。 */
74
74
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
75
75
  safeDecode(seg: string): string | null;
76
- isFleetWide(req: IncomingMessage): boolean;
77
76
  /** run 面的属主门(谓词 `runOwnerReadable`;非属主 ⇒ 404 `not_found.run` + 长文,与 run 读口的 unknown 臂同码同串,[ref])。 */
78
77
  runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
79
78
  /** 会话面的属主门(谓词 `sessionOwnerReadable`,S-1452:没有身份边界的部署不钉人,与会话列表同读;非属主 ⇒ 404 `not_found.session` +
80
79
  * {@link SESSION_NOT_FOUND_MESSAGE},与会话读口的 unknown 臂同码同串 —— G52 [ref] / S-989)。每一个按会话寻址的属主判都走它。
81
80
  * `cell` = 调用点经 `security.ts` 的 `sessionOwnerCellOf` 读出的那一格(S-1770:无主的委派子代 placed 行只对 fleet 调用方放行,判据 `ownerlessRowOpen`)。 */
82
81
  sessionOwnerOk(req: IncomingMessage, res: ServerResponse, cell: SessionOwnerCell): boolean;
83
- sessionOwnerScope(req: IncomingMessage): {
84
- fleetWide: boolean;
85
- gateOwner: string | null;
86
- };
87
- sessionOwnerScopeForWrite(req: IncomingMessage): {
88
- fleetWide: boolean;
89
- gateOwner: string | null;
90
- };
91
82
  runSessionAcceptOk(req: IncomingMessage, res: ServerResponse, run: {
92
83
  sessionId: string | null;
93
84
  }, face: string, notFoundError?: string): boolean;
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { sendUnauthorized } from "../principal-gate.js";
2
+ import { fleetViewOf, sendUnauthorized } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
5
  export const ROSTER_SCAN_LIMIT = 500;
@@ -56,10 +56,9 @@ function decodeCursor(raw) {
56
56
  }
57
57
  export async function handleAgentRoster(req, res, match, ctx) {
58
58
  const { deps } = ctx;
59
- const { sessionOwnerScope } = ctx.helpers;
60
59
  switch (match.decl.id) {
61
60
  case "agents-roster": {
62
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
61
+ const { fleetWide, gateOwner } = fleetViewOf(req, deps, "read");
63
62
  if (!fleetWide && gateOwner === null) {
64
63
  sendUnauthorized(res, "identity");
65
64
  return;
@@ -14,7 +14,7 @@ import { redactRiskDescriptor } from "../../observability/run-terminal.js";
14
14
  import { buildRunName } from "../../fleet/fleet-bus.js";
15
15
  import { sleep } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
- import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
17
+ import { sendJson, sendError, sseHeaders, logRequestError, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sessionCallerOf } from "../principal-gate.js";
19
19
  import { readOwnedRun, readOwnedSession } from "../owner-access.js";
20
20
  import { governanceOriginOf } from "../../runtime-governance.js";
@@ -23,17 +23,17 @@ import { callGrantBindingOf, questionGrantBindingOf } from "../../standing-answe
23
23
  import { sendResumeOutcome } from "../route-ctx.js";
24
24
  import { assertNever } from "../route-ctx.js";
25
25
  import * as facts from "../../capability-facts.js";
26
- import { readResumeCtx } from "../resume-ctx-read.js";
26
+ import { KEEP_SEALED, readResumeCtx } from "../resume-ctx-read.js";
27
27
  import { StoreCorruptError } from "../../plugins/sql-json-column.js";
28
28
  import { recordFailOpen } from "../../observability/fail-open.js";
29
29
  import { durableRowIdentity } from "../../checkpoint-identity.js";
30
30
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
31
31
  import { fieldConflictFacts } from "../../field-conflict.js";
32
32
  const DECIDE_BODY_DECLARED_KEYS = [
33
- "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
33
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "feedback", "reason", "remember", "updatedInput",
34
34
  ];
35
35
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
36
- const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
36
+ const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "feedback", "remember", "updatedInput"];
37
37
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
38
38
  "boundCallId", "decidedBy", "decision", "editedPlan", "permissionModeAfter", "reason",
39
39
  ];
@@ -285,8 +285,19 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
285
285
  const scope = operator
286
286
  ? (new URLSearchParams(match.search).get("owner") ?? undefined)
287
287
  : ownScope;
288
+ let durableRows;
289
+ try {
290
+ durableRows = await cs.listPending(scope);
291
+ }
292
+ catch (err) {
293
+ if (!isApprovalsReadCorrupt(err))
294
+ throw err;
295
+ logRequestError(deps.logger, req, err);
296
+ sendError(res, 500, "approvals.read_corrupt", "a stored approval row cannot be read - retrying this request will fail the same way until an operator repairs or removes the row");
297
+ return;
298
+ }
288
299
  sendJson(res, 200, {
289
- pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
300
+ pending: durableRows.map((r) => projectPendingForWire(r, deps.config)),
290
301
  serverNowMs: Date.now(),
291
302
  ...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
292
303
  ...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
@@ -342,10 +353,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
342
353
  const scope = operator && ownerParam ? ownerParam : ownScope;
343
354
  const summaries = await cs.listByScope(scope);
344
355
  const inbox = (await Promise.all(summaries.map(async (s) => {
345
- const ctxRead = await readResumeCtx(cs.getCtx(s.sessionId));
356
+ const ctxRead = await readResumeCtx(cs.getCtx(s.sessionId), KEEP_SEALED);
346
357
  if (ctxRead.kind === "corrupt")
347
358
  recordFailOpen("server.assistant-inbox.objective-ctx-corrupt", ctxRead.where);
348
- const objective = ctxRead.kind === "ok" ? (ctxRead.ctx.body?.objective ?? null) : null;
359
+ const storedBody = ctxRead.kind === "ok" ? ctxRead.ctx.body : undefined;
360
+ const storedObjective = typeof storedBody === "object" && storedBody !== null ? storedBody.objective : undefined;
361
+ const objective = typeof storedObjective === "string" ? storedObjective : null;
349
362
  const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
350
363
  return {
351
364
  sessionId: s.sessionId,
@@ -655,7 +668,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
655
668
  const body = (await readJson(req));
656
669
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
657
670
  if (!decision) {
658
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }", retiredDecisionKeyFacts(body)?.extra);
671
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy?, feedback? }", retiredDecisionKeyFacts(body)?.extra);
659
672
  return;
660
673
  }
661
674
  {
@@ -719,6 +732,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
719
732
  return;
720
733
  }
721
734
  }
735
+ if (body.feedback !== undefined && typeof body.feedback !== "string") {
736
+ sendError(res, 400, "request.field_invalid", "feedback must be a string (the words the approving person wrote on the card; omit the key when there are none)");
737
+ return;
738
+ }
722
739
  if (deps.config.directDoorActive && body.updatedInput !== undefined) {
723
740
  sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
724
741
  return;
@@ -727,6 +744,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
727
744
  sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
728
745
  return;
729
746
  }
747
+ if (deps.config.directDoorActive && body.feedback !== undefined) {
748
+ sendError(res, 400, "feedback_not_in_proof", "feedback is not supported on a direct-door worker (not covered by the decision proof)");
749
+ return;
750
+ }
730
751
  const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
731
752
  switch (decidedByReading.kind) {
732
753
  case "not_in_proof":
@@ -743,11 +764,20 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
743
764
  }
744
765
  }
745
766
  const decidedBy = decidedByReading.decidedBy;
767
+ if (body.feedback !== undefined && decision !== "approve") {
768
+ sendError(res, 400, "request.field_conflict", "feedback is only valid with decision 'approve' — the words of a denial are its reason", fieldConflictFacts("approval_feedback", ["feedback", "decision"]));
769
+ return;
770
+ }
771
+ if (body.feedback !== undefined && decidedBy !== "person") {
772
+ sendError(res, 400, "request.field_conflict", "feedback is only valid on a person's decision — the agent reads it as the user's own words, which a policy decision cannot carry", fieldConflictFacts("approval_feedback", ["feedback", "decidedBy"]));
773
+ return;
774
+ }
746
775
  const binding = {
747
776
  checkpointToken: body.checkpointToken,
748
777
  boundCallId: body.boundCallId,
749
778
  boundInputHash: body.boundInputHash,
750
779
  updatedInput: body.updatedInput,
780
+ ...(typeof body.feedback === "string" ? { feedback: body.feedback } : {}),
751
781
  };
752
782
  let deciderPrincipal = principal;
753
783
  if (deps.config.directDoorActive) {
@@ -811,6 +841,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
811
841
  ...(decision === "approve" && binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
812
842
  ...(answer !== undefined ? { answer } : {}),
813
843
  ...((r) => (r !== undefined ? { reason: r } : {}))(decisionReasonOf(body)),
844
+ ...(binding.feedback !== undefined ? { feedback: binding.feedback } : {}),
814
845
  },
815
846
  deciderPrincipal,
816
847
  explicitOperator,
@@ -827,6 +858,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
827
858
  return assertNever(match.decl.id);
828
859
  }
829
860
  const APPROVALS_STREAM_POLL_MS = 3000;
861
+ function isApprovalsReadCorrupt(err) {
862
+ return err instanceof StoreCorruptError;
863
+ }
830
864
  function redactPendingDisclosures(row) {
831
865
  const rd = row.riskDescriptor;
832
866
  if (rd === null || rd === undefined)
@@ -898,7 +932,7 @@ export async function streamApprovals(req, res, cs, live, scope, governance, pol
898
932
  snapshot = { pending, livePending: live ? live.listLivePending(scope) : [] };
899
933
  }
900
934
  catch (err) {
901
- if (err instanceof StoreCorruptError) {
935
+ if (isApprovalsReadCorrupt(err)) {
902
936
  if (!res.writableEnded)
903
937
  res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "approvals.read_corrupt", message: "a stored approval row cannot be read - this stream is closing, and reconnecting will fail the same way until an operator repairs or removes the row" })}\n\n`);
904
938
  break;
@@ -131,7 +131,7 @@ export async function handleCapabilities(req, res, match, ctx) {
131
131
  scratchpadDir: true,
132
132
  subagentSteer: facts.subagentSteer(deps).available,
133
133
  subagentResume: facts.subagentSteer(deps).available,
134
- taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config), pluginHooks: taskHooksHonored(deps.config), managedHookGroups: taskHooksHonored(deps.config), tightenOnlySeats: true },
134
+ taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config), pluginHooks: taskHooksHonored(deps.config), managedHookGroups: taskHooksHonored(deps.config), tightenOnlySeats: true, approvalFrameFamilies: true },
135
135
  settings: { permissions: { allowGrant: true, allowLayer: true, callerTrust: callerTrust(req, deps.config).trust } },
136
136
  permissionModeAuto: {
137
137
  accepted: true,
@@ -43,8 +43,8 @@ export interface ServerWiringGateInput {
43
43
  streamApprovalEnabled: boolean;
44
44
  /** 谓词自己的入参类型(结构形,不 import store 依赖树)—— 取型而非重述,两处不可能漂。 */
45
45
  backend: StreamApprovalGateInput["backend"];
46
- /** 在场性即事实;本面不取实例,故 `unknown` 足够(取窄型会把整棵 store 类型树拖进诊断域)。 */
47
- checkpointStore: unknown;
46
+ /** 谓词自己的入参类型(只收店的形,工厂方法过不了 tsc)—— 取型而非重述,与 `backend` 同理;在场性即事实,本面不读实例的内容。 */
47
+ checkpointStore: StreamApprovalGateInput["checkpointStore"];
48
48
  durableApproval: boolean;
49
49
  /** S-178:三个 posture 旋钮的**来源**,取自 config 上与值同处写的那一格(不在这里重判)。 */
50
50
  durableApprovalSource: PostureSource;
@@ -68,7 +68,7 @@ export interface StaticWiringAudit {
68
68
  export interface StaticWiringFacts {
69
69
  /** core `describeStaticWiring(deps, specTemplate)` 的产物。 */
70
70
  manifest: WiringManifest;
71
- /** 我方喂给 `resolveStreamApprovalGate` 的 `parkFacility` 实参(= checkpoint 店在场 ∧ DURABLE_APPROVAL)。 */
71
+ /** 我方喂给 `resolveStreamApprovalGate` 的 `checkpointStore` 实参的在场性(= checkpoint 店在场 ∧ DURABLE_APPROVAL)。 */
72
72
  parkFacility: boolean;
73
73
  /** store backend 的种类;`undefined` = env-only worker(无 backend)。 */
74
74
  backendKind: "mysql" | "pg" | "local" | undefined;
@@ -12,7 +12,7 @@ export function buildServerWiringGates(input) {
12
12
  toolApprovalEnabled: input.toolApprovalEnabled,
13
13
  streamApprovalEnabled: input.streamApprovalEnabled,
14
14
  backend: input.backend,
15
- parkFacility: input.checkpointStore !== undefined,
15
+ checkpointStore: input.checkpointStore,
16
16
  });
17
17
  const noCenterArm = (knob) => `this knob has no web-admin arm on this deployment — it is set by ${knob} on this machine, or derived from the deployment posture`;
18
18
  const noLaneArm = (knob) => `this knob has no execution-lane default — ${knob} does not depend on which execution lane this deployment runs`;
@@ -2,12 +2,11 @@ import { hookNoticeWire } from "../../fleet/fleet-bus.js";
2
2
  import { FLEET_SNAPSHOT_TERMINAL_MAX_ROWS, isTerminalWorkflowRowStatus, recentTerminalWorkflowRows } from "../../fleet/fleet-terminal-window.js";
3
3
  import { taskNotificationFoldKey } from "../../orchestration/workflow-completion-inbox.js";
4
4
  import { sendJson, sendError, sseHeaders } from "../send.js";
5
- import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { fleetViewOf, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
7
  import * as facts from "../../capability-facts.js";
8
8
  export async function handleFleet(req, res, match, ctx) {
9
9
  const { deps } = ctx;
10
- const { sessionOwnerScope } = ctx.helpers;
11
10
  switch (match.decl.id) {
12
11
  case "fleet-stream": {
13
12
  const fleet = facts.fleetStream(deps);
@@ -15,7 +14,7 @@ export async function handleFleet(req, res, match, ctx) {
15
14
  sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
16
15
  return;
17
16
  }
18
- const { fleetWide } = sessionOwnerScope(req);
17
+ const { fleetWide } = fleetViewOf(req, deps, "read");
19
18
  const principal = gatedPrincipal(req, deps.config);
20
19
  if (deps.config.requirePrincipal && !principal && !fleetWide) {
21
20
  sendPrincipalRefusal(res, req, deps.config);
@@ -77,7 +76,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
77
76
  return rest;
78
77
  };
79
78
  const SCOPED_WORKFLOW_KEY_DISPOSITION = {
80
- id: "keep", status: "keep", doneCount: "keep", totalCount: "keep", failedCount: "keep",
79
+ id: "keep", status: "keep", errorCode: "keep", doneCount: "keep", totalCount: "keep", failedCount: "keep",
81
80
  startedCount: "keep", elapsedMs: "keep", tokens: "keep",
82
81
  name: "rewrite", scope: "strip", sessionId: "strip",
83
82
  description: "scrub",
@@ -90,6 +89,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
90
89
  id: rest.id,
91
90
  status: rest.status,
92
91
  name: "(unattributed)",
92
+ ...(rest.errorCode !== undefined ? { errorCode: rest.errorCode } : {}),
93
93
  ...(rest.doneCount !== undefined ? { doneCount: rest.doneCount } : {}),
94
94
  ...(rest.totalCount !== undefined ? { totalCount: rest.totalCount } : {}),
95
95
  ...(rest.failedCount !== undefined ? { failedCount: rest.failedCount } : {}),
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
114
114
  baseRev: string;
115
115
  origin: {
116
116
  at: number;
117
- cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
117
+ cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
118
118
  taint: "external";
119
119
  };
120
120
  };
@@ -6,8 +6,8 @@ import * as facts from "../../capability-facts.js";
6
6
  import { createLogger } from "../../observability/logger.js";
7
7
  import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, buildCcImportLayerRootRefusal, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
8
  import { hasControlOrBidi } from "@sema-agent/settings-schema";
9
- import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
10
- import { PERSISTED_RULE_MATCHES, isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
9
+ import { IMPORTED_SETTINGS_LAYERS, MAX_RULE_TEXT_CHARS, PERSISTED_RULE_MATCHES, RULE_ADD_ORIGINS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
10
+ import { isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
11
11
  const logger = createLogger();
12
12
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
13
13
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -23,7 +23,7 @@ const CcImportPrepareBodySchema = z
23
23
  layers: z
24
24
  .array(z
25
25
  .object({
26
- layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
26
+ layer: z.enum(IMPORTED_SETTINGS_LAYERS),
27
27
  path: z.string().min(1).max(1024),
28
28
  root: z.string().max(1024),
29
29
  content: z.string().max(MAX_LAYER_CONTENT_CHARS),
@@ -41,7 +41,7 @@ const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict
41
41
  const LocalImportAddSchema = z
42
42
  .object({
43
43
  dot: z.object({ actor: z.string().min(1).max(190), counter: z.number().int().nonnegative().safe() }).strict(),
44
- origin: z.enum(["user", "imported-cc", "starter"]),
44
+ origin: z.enum(RULE_ADD_ORIGINS),
45
45
  createdAt: z.string().min(1).max(64),
46
46
  })
47
47
  .strict();
@@ -13,7 +13,8 @@ import { streamSseLog } from "../sse-log.js";
13
13
  import { bindSseLifecycle } from "../sse-lifecycle.js";
14
14
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
15
15
  import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
16
- import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
16
+ import { ASK_DECISION_CONSUMER_ABSENT, approvalPreambleReplays, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
17
+ import { readRunDeclaredFamilies } from "../run-declared-families.js";
17
18
  import { MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt } from "../../approval-ask-machine.js";
18
19
  import { causeOfAskRow } from "../../approval-deny-reasons.js";
19
20
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
@@ -109,17 +110,26 @@ function sendHandleMiss(res, run, thisInstanceId) {
109
110
  }
110
111
  return false;
111
112
  }
112
- async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
113
+ async function streamRunEvents(req, res, deps, runStore, taskId, sessionId, staleMs) {
113
114
  await streamSseLog(req, res, {
114
115
  preamble: async (signal) => {
115
116
  const gate = resolveStreamApprovalGate({
116
117
  toolApprovalEnabled: Boolean(deps.toolApproval),
117
118
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
118
119
  backend: deps.backend,
119
- parkFacility: deps.checkpointStore !== undefined,
120
+ checkpointStore: deps.checkpointStore,
120
121
  });
121
122
  if (!gate.active)
122
123
  return [];
124
+ const declaredFamilies = await readRunDeclaredFamilies(gate.checkpointStore, sessionId, (where) => deps.logger?.warn?.("approval_preamble_declaration_unreadable", { taskId, where, face: "run.events" }));
125
+ if (!approvalPreambleReplays({
126
+ streamApprovalOn: gate.active,
127
+ windowMs: deps.config.streamApproval?.windowMs,
128
+ windowMarginMs: deps.config.streamAskWindowMarginMs,
129
+ declaredFamilies,
130
+ })) {
131
+ return [];
132
+ }
123
133
  const store = gate.askStore;
124
134
  const rows = await store.listPendingByTask(taskId, signal);
125
135
  const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
@@ -255,7 +265,7 @@ export async function handleRuns(req, res, match, ctx) {
255
265
  if (!runSessionAcceptOk(req, res, run, match.decl.id === "run-events" ? "run.events" : "run.poll"))
256
266
  return;
257
267
  if (match.decl.id === "run-events") {
258
- await streamRunEvents(req, res, deps, runStore, taskId, deps.config.runStaleSec * 1000);
268
+ await streamRunEvents(req, res, deps, runStore, taskId, run.sessionId, deps.config.runStaleSec * 1000);
259
269
  }
260
270
  else {
261
271
  sendJson(res, 200, await readRunFace(deps, runStore, run));
@@ -613,11 +623,15 @@ export async function handleRuns(req, res, match, ctx) {
613
623
  const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
614
624
  if (leafId === undefined || leafId === null)
615
625
  return;
616
- const resumeRead = await readResumeCtx(cs.getCtx(run.sessionId));
626
+ const resumeRead = await readResumeCtx(cs.getCtx(run.sessionId), { sessionId: run.sessionId, opener: deps.checkpointCtxSealing });
617
627
  if (resumeRead.kind === "corrupt") {
618
628
  mintResult.corruptCtxWhere = resumeRead.where;
619
629
  return;
620
630
  }
631
+ if (resumeRead.kind === "unsealable") {
632
+ mintResult.unsealableCtx = resumeRead;
633
+ return;
634
+ }
621
635
  if (resumeRead.kind === "absent") {
622
636
  mintResult.noResumeCtx = true;
623
637
  return;
@@ -641,10 +655,13 @@ export async function handleRuns(req, res, match, ctx) {
641
655
  createdAt: Date.now(),
642
656
  sourceTaskId: taskId,
643
657
  });
644
- const stillRead = await readResumeCtx(cs.getCtx(run.sessionId));
645
- if (stillRead.kind === "corrupt") {
646
- await withdrawWakeCard(wakeToken, cpScope, "resume_context_corrupt");
647
- mintResult.corruptCtxWhere = stillRead.where;
658
+ const stillRead = await readResumeCtx(cs.getCtx(run.sessionId), { sessionId: run.sessionId, opener: deps.checkpointCtxSealing });
659
+ if (stillRead.kind === "corrupt" || stillRead.kind === "unsealable") {
660
+ await withdrawWakeCard(wakeToken, cpScope, stillRead.kind === "corrupt" ? "resume_context_corrupt" : "resume_context_unsealable");
661
+ if (stillRead.kind === "corrupt")
662
+ mintResult.corruptCtxWhere = stillRead.where;
663
+ else
664
+ mintResult.unsealableCtx = stillRead;
648
665
  return;
649
666
  }
650
667
  if (stillRead.kind !== "ok") {
@@ -681,6 +698,10 @@ export async function handleRuns(req, res, match, ctx) {
681
698
  sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
682
699
  return;
683
700
  }
701
+ if (mintResult.unsealableCtx !== undefined) {
702
+ sendError(res, 409, "conflict.checkpoint_ctx_unsealable", "run already ended and this session's stored resume context holds credentials this server cannot unseal (reason says why) — nothing was queued, because POST /v1/sessions/:id/wake could not resume it; restore the sealed-key directory that sealed them, restart this server (it reads its keys only at startup) and steer again, or submit a new task instead", { field: mintResult.unsealableCtx.field, reason: mintResult.unsealableCtx.reason });
703
+ return;
704
+ }
684
705
  if (mintResult.unservable !== undefined) {
685
706
  deps.logger?.warn?.("steer_wake_park_refused_stored_body", { taskId, sessionId: run.sessionId, ...storedBodyReplayLogFields(mintResult.unservable) });
686
707
  const { status, body: { error, errorCode, ...rest } } = storedBodyReplayAnswer(mintResult.unservable, serviceVersion(), false);
@@ -34,8 +34,9 @@ export type PendingStaging = {
34
34
  declaredEntryIds: string[];
35
35
  /** The principal that opened this staging — a different caller may not drive it (404, no oracle). */
36
36
  openedBy: string | null;
37
- /** Fleet-wide caller (ops on-behalf) — bypasses the openedBy identity check (like the route's owner gates). */
38
- openedFleetWide: boolean;
37
+ /** The owner cell the gate judged when it opened this staging (`ownerOf` reading: `undefined` = no such session) — the commit lands only on
38
+ * this same cell (`StagedCommitOptions.expectedOwner`, `session-sync-kernel.ts`); `owner` above (the re-stamped importing principal) is derived from it for a fleet caller. */
39
+ expectedOwner: string | null | undefined;
39
40
  };
40
41
  /** 单域独占状态 + 它的两个操作。每个 server 实例一份。 */
41
42
  export interface SessionSyncLocal {