@sema-agent/server 7.55.0 → 7.57.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 (129) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +14 -7
  3. package/README.zh-CN.md +10 -6
  4. package/USAGE.md +207 -59
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +100 -1
  7. package/dist/approval-ask-audit-store.js +103 -2
  8. package/dist/approval-card.d.ts +148 -13
  9. package/dist/approval-card.js +82 -13
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval.d.ts +31 -0
  13. package/dist/approval.js +18 -0
  14. package/dist/auto-mode-face.d.ts +111 -0
  15. package/dist/auto-mode-face.js +99 -0
  16. package/dist/bench/s1/arms.js +5 -5
  17. package/dist/bench/s1/live-deps.js +11 -11
  18. package/dist/bench/s1/run-firm.js +3 -0
  19. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  20. package/dist/bench/s1/runner-ctx.js +7 -0
  21. package/dist/boot/config-center.js +23 -1
  22. package/dist/boot/coordinators.js +13 -1
  23. package/dist/boot/leader.d.ts +21 -0
  24. package/dist/boot/leader.js +6 -0
  25. package/dist/boot/parked-revive-gate.d.ts +7 -2
  26. package/dist/boot/parked-revive-gate.js +12 -1
  27. package/dist/boot/resolve-spec.d.ts +3 -0
  28. package/dist/boot/resolve-spec.js +3 -1
  29. package/dist/boot/runner-deps.d.ts +12 -0
  30. package/dist/boot/runner-deps.js +28 -3
  31. package/dist/boot/runtime-caps.d.ts +19 -8
  32. package/dist/boot/runtime-caps.js +50 -20
  33. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  34. package/dist/boot/session-shell-gate-registry.js +21 -0
  35. package/dist/config-catalog.js +14 -5
  36. package/dist/config-center/types.d.ts +2 -1
  37. package/dist/config-provider.js +1 -0
  38. package/dist/config-types.d.ts +41 -10
  39. package/dist/config.d.ts +21 -0
  40. package/dist/config.js +40 -5
  41. package/dist/http/route-ctx.d.ts +90 -3
  42. package/dist/http/routes/a2a-serve.js +5 -3
  43. package/dist/http/routes/approvals-assistant.js +21 -8
  44. package/dist/http/routes/capabilities.js +21 -4
  45. package/dist/http/routes/leader.js +2 -2
  46. package/dist/http/routes/memory-origin.d.ts +2 -2
  47. package/dist/http/routes/rules.js +3 -2
  48. package/dist/http/routes/runs.d.ts +0 -14
  49. package/dist/http/routes/runs.js +20 -8
  50. package/dist/http/routes/tasks.js +34 -8
  51. package/dist/http/routes/workflows.js +20 -5
  52. package/dist/http/server.d.ts +11 -1
  53. package/dist/http/server.js +220 -34
  54. package/dist/http/wire-types.d.ts +9 -4
  55. package/dist/leader/diffout.js +2 -1
  56. package/dist/leader/endpoint.js +49 -15
  57. package/dist/leader/fanout.js +6 -5
  58. package/dist/leader/leader.js +17 -14
  59. package/dist/leader/merge.js +17 -12
  60. package/dist/leader/planner.js +3 -2
  61. package/dist/leader/repair-oracle.js +6 -4
  62. package/dist/leader/repair-wire.js +5 -3
  63. package/dist/leader/wire.d.ts +80 -1
  64. package/dist/leader/wire.js +55 -26
  65. package/dist/main.js +44 -8
  66. package/dist/observability/err-text.d.ts +6 -0
  67. package/dist/observability/err-text.js +10 -0
  68. package/dist/observability/fail-open.d.ts +40 -0
  69. package/dist/observability/fail-open.js +19 -0
  70. package/dist/observability/metrics.js +1 -1
  71. package/dist/observability/run-terminal-log.d.ts +120 -0
  72. package/dist/observability/run-terminal-log.js +360 -0
  73. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  74. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  75. package/dist/permission-rule-vocab.d.ts +40 -0
  76. package/dist/permission-rule-vocab.js +17 -0
  77. package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
  78. package/dist/plugins/checkpoint-store-sql.js +23 -4
  79. package/dist/plugins/file-run-store.d.ts +3 -34
  80. package/dist/plugins/file-run-store.js +21 -0
  81. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  82. package/dist/plugins/local-checkpoint-store.js +5 -0
  83. package/dist/plugins/local-session-store.d.ts +9 -9
  84. package/dist/plugins/local-session-store.js +5 -3
  85. package/dist/plugins/memory-run-store.d.ts +3 -15
  86. package/dist/plugins/memory-run-store.js +21 -0
  87. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  88. package/dist/plugins/permission-rule-store-file.js +25 -11
  89. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  90. package/dist/plugins/permission-rule-store-sql.js +39 -23
  91. package/dist/plugins/pg-session-storage.js +17 -13
  92. package/dist/plugins/remote-scratchpad.js +3 -2
  93. package/dist/plugins/run-store-sql.d.ts +3 -42
  94. package/dist/plugins/run-store-sql.js +36 -5
  95. package/dist/plugins/store-backend.d.ts +1 -1
  96. package/dist/plugins/store-contracts.d.ts +70 -1
  97. package/dist/plugins/tidb-session-store.js +18 -14
  98. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  99. package/dist/plugins/workflow-run-store-sql.js +10 -2
  100. package/dist/rules-consent.d.ts +5 -4
  101. package/dist/rules-consent.js +25 -46
  102. package/dist/run-cancel-context.d.ts +13 -0
  103. package/dist/run-cancel-context.js +15 -0
  104. package/dist/run-local.js +2 -2
  105. package/dist/runs.d.ts +4 -1
  106. package/dist/runs.js +41 -10
  107. package/dist/runtime-caps-resolver.d.ts +133 -17
  108. package/dist/runtime-caps-resolver.js +44 -4
  109. package/dist/security.d.ts +7 -0
  110. package/dist/task-settings.d.ts +57 -3
  111. package/dist/task-settings.js +78 -5
  112. package/dist/task-workflow.d.ts +32 -2
  113. package/dist/task-workflow.js +8 -3
  114. package/dist/tool-approval.d.ts +38 -63
  115. package/dist/tool-approval.js +102 -74
  116. package/dist/trace/core-keyset-guard.d.ts +2 -2
  117. package/dist/trace/ledger-sink.d.ts +13 -4
  118. package/dist/trace/ledger-sink.js +14 -6
  119. package/dist/trace/project.d.ts +13 -0
  120. package/dist/trace/project.js +10 -1
  121. package/dist/trace/redact.d.ts +22 -11
  122. package/dist/trace/redact.js +655 -20
  123. package/dist/trace/sema-provenance.d.ts +26 -0
  124. package/dist/trace/sema-provenance.js +11 -0
  125. package/dist/turn-activity.d.ts +32 -2
  126. package/dist/turn-activity.js +29 -4
  127. package/package.json +3 -3
  128. package/skills/find-skills.md +1 -1
  129. package/skills/loop.md +1 -1
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
2
3
  import { once } from "node:events";
3
4
  import { createHash } from "node:crypto";
4
5
  import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, RUN_WORKFLOW_TOOL_NAME } from "@sema-agent/core";
@@ -8,7 +9,9 @@ import {} from "../config-center/facade.js";
8
9
  import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
9
10
  import { verifyDirectDoorProof } from "../principal-jwt.js";
10
11
  import {} from "../memory-sync.js";
11
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../task-settings.js";
12
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, taskSettingsKeyIssue } from "../task-settings.js";
13
+ import { selfOrchestrationDenial } from "../task-workflow.js";
14
+ import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
12
15
  import { parseHooksConfig } from "../hooks/hook-runner.js";
13
16
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
14
17
  import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
@@ -23,6 +26,7 @@ import { HANDS_BAND_TOOL_NAMES } from "../capabilities/hands-lane.js";
23
26
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
24
27
  import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
25
28
  import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
29
+ import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
26
30
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
27
31
  import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
28
32
  import {} from "../observability/rate-limit.js";
@@ -31,6 +35,7 @@ import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEve
31
35
  import { redactSecrets } from "../trace/redact.js";
32
36
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
33
37
  import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
38
+ import { errorText } from "../observability/err-text.js";
34
39
  import { recordFailOpen } from "../observability/fail-open.js";
35
40
  import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
36
41
  export { scopedIdempotencyKey };
@@ -246,7 +251,7 @@ export function createHttpServer(rawDeps) {
246
251
  res.end();
247
252
  return;
248
253
  }
249
- deps.logger?.error("request_error", { method: req.method, url: req.url, err: msg(err) });
254
+ deps.logger?.error("request_error", { method: req.method, url: req.url, err: redactHead(msg(err), 2000) });
250
255
  if (!res.headersSent)
251
256
  sendError(res, 500, "internal.error", "internal error");
252
257
  else
@@ -257,7 +262,7 @@ export function createHttpServer(rawDeps) {
257
262
  const routeCtxBase = {
258
263
  deps,
259
264
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
260
- helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
265
+ helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
261
266
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
262
267
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
263
268
  };
@@ -289,7 +294,7 @@ export function createHttpServer(rawDeps) {
289
294
  deps.metrics?.inc("http_requests_total", { route, method, status });
290
295
  deps.metrics?.observe("http_request_duration_seconds", seconds, { route });
291
296
  const log = route === "/health" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
292
- log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(aborted ? { aborted: true, ...(reqState.streamTaskId ? { taskId: reqState.streamTaskId } : {}), ...(reqState.streamDetached ? { detached: true } : {}) } : {}) });
297
+ log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(reqState.runStatus !== undefined ? { runStatus: reqState.runStatus } : {}), ...(reqState.runErrorCode !== undefined ? { errorCode: reqState.runErrorCode } : {}), ...(aborted ? { aborted: true, ...(reqState.streamTaskId ? { taskId: reqState.streamTaskId } : {}), ...(reqState.streamDetached ? { detached: true } : {}) } : {}) });
293
298
  };
294
299
  res.on("finish", () => record(false));
295
300
  res.on("close", () => record(true));
@@ -582,6 +587,13 @@ export function createHttpServer(rawDeps) {
582
587
  return null;
583
588
  }
584
589
  const st = settingsRaw;
590
+ {
591
+ const keyIssue = taskSettingsKeyIssue(st);
592
+ if (keyIssue) {
593
+ sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
594
+ return null;
595
+ }
596
+ }
585
597
  if (typeof st.outputStyle === "string" && st.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
586
598
  sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`);
587
599
  return null;
@@ -764,6 +776,19 @@ export function createHttpServer(rawDeps) {
764
776
  sendError(res, 400, "request.field_invalid", "enableFork must be a boolean");
765
777
  return null;
766
778
  }
779
+ if (body.selfOrchestration !== undefined && typeof body.selfOrchestration !== "boolean") {
780
+ sendError(res, 400, "request.field_invalid", "selfOrchestration must be a boolean");
781
+ return null;
782
+ }
783
+ {
784
+ const denial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
785
+ const wantsSelfOrchestration = body.selfOrchestration === true
786
+ || parseTaskSettings(body.settings).settings?.ultracode === true;
787
+ if (denial !== null && wantsSelfOrchestration) {
788
+ sendError(res, 501, "capability.self_orchestration_required", "self-orchestration is enabled on this worker (SELF_ORCHESTRATION_ENABLED) but it is multi-tenant (REQUIRE_PRINCIPAL) and wires NO per-principal entitlement source, so the engine's allowWorkflows gate — which is tighten-only, meaning an absent resolver leaves it UNGATED — is fail-closed for every principal: no request can ever get the workflow engine here (GET /v1/capabilities reports workflows:false with workflowsGate.denial \"entitlement_resolver_absent\"). Wire a config center (SEMA_REGISTRY_URL + token, not dry-run) to gate it per principal, or set SELF_ORCHESTRATION_ENABLED=false — the honest posture for a worker that cannot grant it. To submit this task without the workflow engine, drop selfOrchestration / settings.ultracode; ultracode's other half (the max thinking tier) is available as reasoningEffort: \"max\"");
789
+ return null;
790
+ }
791
+ }
767
792
  if (body.resilience !== undefined) {
768
793
  const r = body.resilience;
769
794
  if (typeof r !== "object" || r === null || Array.isArray(r)) {
@@ -895,6 +920,11 @@ export function createHttpServer(rawDeps) {
895
920
  sendError(res, 400, "request.field_invalid", 'settings.permissions.defaultMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
896
921
  return null;
897
922
  }
923
+ const d = p.disableAutoMode;
924
+ if (d !== undefined && d !== "disable" && d !== true && d !== false) {
925
+ sendError(res, 400, "request.field_invalid", 'settings.permissions.disableAutoMode must be the literal "disable" or a boolean when present (omit it to leave auto mode available)');
926
+ return null;
927
+ }
898
928
  }
899
929
  }
900
930
  }
@@ -1039,16 +1069,25 @@ export function createHttpServer(rawDeps) {
1039
1069
  }
1040
1070
  const cpPromise = cs.get(token);
1041
1071
  const ctxPromise = cs.getCtx(sessionId);
1072
+ void ctxPromise.catch(() => { });
1073
+ const noApproval = { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
1042
1074
  let cp;
1043
1075
  try {
1044
1076
  cp = await cpPromise;
1045
1077
  }
1046
1078
  catch (e) {
1047
1079
  void ctxPromise.catch(() => { });
1080
+ if (!(await mayDiscloseRowError(cs, token, decider))) {
1081
+ deps.logger?.warn?.("approval_decide_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
1082
+ return noApproval;
1083
+ }
1048
1084
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1049
1085
  }
1050
- if (!cp)
1086
+ if (!cp) {
1087
+ if (!(await mayDiscloseRowError(cs, token, decider)))
1088
+ return noApproval;
1051
1089
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1090
+ }
1052
1091
  if (decider !== undefined && !decider.explicitOperator) {
1053
1092
  const cpOwner = decodeCheckpointScope(cp.scope);
1054
1093
  if (cpOwner !== undefined && cpOwner !== decider.principal) {
@@ -1109,12 +1148,41 @@ export function createHttpServer(rawDeps) {
1109
1148
  const boundGrant = onResumeCommitted
1110
1149
  ? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), validatedToolName: loadedGrantTool })
1111
1150
  : undefined;
1151
+ const lateSettleSink = deps.approvalLateSettle;
1152
+ const lateSettleAnchor = lateSettleSink !== undefined && req !== undefined && token
1153
+ ? {
1154
+ sessionId: cp.sessionId,
1155
+ ...(paForGrant?.toolCallId !== undefined ? { toolCallId: paForGrant.toolCallId } : {}),
1156
+ checkpointTokenHash: buildCheckpointAnchorHash(token),
1157
+ }
1158
+ : undefined;
1159
+ const noteLateSettleIfAccepted = (out) => {
1160
+ if (lateSettleSink === undefined || lateSettleAnchor === undefined || out.status !== 200)
1161
+ return;
1162
+ if (out.body?.errorCode === "cancelled")
1163
+ return;
1164
+ try {
1165
+ lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", settledBy, settledAtMs: Date.now() });
1166
+ }
1167
+ catch (e) {
1168
+ recordFailOpen("server.approval-ask-audit.append-failed", `kind=late_settle err=${e instanceof Error ? e.message : String(e)}`);
1169
+ }
1170
+ };
1171
+ const parkedForkQuotaDenial = req !== undefined ? ownerCostQuotaDenied(decodeCheckpointScope(cp.scope)) : undefined;
1172
+ if (parkedForkQuotaDenial)
1173
+ return parkedForkQuotaDenial;
1174
+ const parkedForkLeaseDenial = req !== undefined ? await ownerFleetLeaseDenied(decodeCheckpointScope(cp.scope)) : undefined;
1175
+ if (parkedForkLeaseDenial)
1176
+ return parkedForkLeaseDenial;
1177
+ const ownerLeaseAdmittedHere = req !== undefined;
1112
1178
  if (req !== undefined && deps.backgroundAgentStore !== undefined && deps.parkedReviveTool !== undefined) {
1113
- const parked = await decideParkedAgent({
1114
- agentStore: deps.backgroundAgentStore,
1179
+ const parkedAgentStore = deps.backgroundAgentStore;
1180
+ const parkedTool = deps.parkedReviveTool;
1181
+ const parked = await withPrincipal(decodeCheckpointScope(cp.scope), () => decideParkedAgent({
1182
+ agentStore: parkedAgentStore,
1115
1183
  checkpointStore: cs,
1116
1184
  registry: defaultTaskRegistry,
1117
- reviveTool: deps.parkedReviveTool,
1185
+ reviveTool: parkedTool,
1118
1186
  ...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
1119
1187
  ...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
1120
1188
  ...(deps.logger ? { warn: (event, fields) => deps.logger?.warn?.(event, fields) } : {}),
@@ -1124,9 +1192,10 @@ export function createHttpServer(rawDeps) {
1124
1192
  ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
1125
1193
  ...(boundGrant ? { grantRemember: (rootSessionId) => boundGrant(rootSessionId) } : {}),
1126
1194
  ...(answer !== undefined ? { answer } : {}),
1127
- });
1195
+ }));
1128
1196
  if (parked !== undefined) {
1129
1197
  void ctxPromise.catch(() => { });
1198
+ noteLateSettleIfAccepted(parked);
1130
1199
  return parked;
1131
1200
  }
1132
1201
  }
@@ -1191,7 +1260,9 @@ export function createHttpServer(rawDeps) {
1191
1260
  settledBy,
1192
1261
  };
1193
1262
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1194
- return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}) });
1263
+ const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
1264
+ noteLateSettleIfAccepted(driven);
1265
+ return driven;
1195
1266
  }
1196
1267
  async function driveResumeIntoRunLog(args) {
1197
1268
  if (args.acceptEarly !== true)
@@ -1208,7 +1279,7 @@ export function createHttpServer(rawDeps) {
1208
1279
  }, (err) => {
1209
1280
  if (!wasAccepted)
1210
1281
  return;
1211
- deps.logger?.error?.("resume_leg_threw_after_accept", { sessionId: args.sessionId, err: err instanceof Error ? err.message : String(err) });
1282
+ deps.logger?.error?.("resume_leg_threw_after_accept", { sessionId: args.sessionId, err: redactHead(err instanceof Error ? err.message : String(err), 2000) });
1212
1283
  deps.metrics?.inc("resume_post_accept_failed_total");
1213
1284
  });
1214
1285
  return await Promise.race([accepted, leg]);
@@ -1224,19 +1295,18 @@ export function createHttpServer(rawDeps) {
1224
1295
  }).active;
1225
1296
  const approvalLeg = resolveApprovalLeg({
1226
1297
  streamApprovalOn,
1227
- windowMs: deps.config.streamApproval?.windowMs ?? 0,
1298
+ windowMs: deps.config.streamApproval?.windowMs,
1228
1299
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1229
1300
  ...(typeof taskConfig.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: taskConfig.limits.maxWalltimeMs } : {}),
1230
1301
  nowMonotonicMs: performance.now(),
1231
1302
  });
1232
1303
  const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
1233
- if (deps.fleetLease && principal) {
1234
- const adm = await deps.fleetLease.admit(principal);
1235
- if (!adm.ok) {
1236
- deps.metrics?.inc("fleet_lease_rejected_total");
1237
- return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
1238
- }
1239
- }
1304
+ const ownerQuotaDenial = args.callerInitiated === true ? ownerCostQuotaDenied(principal) : undefined;
1305
+ if (ownerQuotaDenial)
1306
+ return ownerQuotaDenial;
1307
+ const ownerLeaseDenial = args.callerInitiated === true && args.ownerLeaseAdmitted !== true ? await ownerFleetLeaseDenied(principal) : undefined;
1308
+ if (ownerLeaseDenial)
1309
+ return ownerLeaseDenial;
1240
1310
  let taskId = await deps.runStore?.getActiveTaskId(sessionId);
1241
1311
  let freshRunClaimed = false;
1242
1312
  if (args.mintFreshRun && taskId && deps.runStore) {
@@ -1764,7 +1834,7 @@ export function createHttpServer(rawDeps) {
1764
1834
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1765
1835
  }
1766
1836
  if (claimedRow && taskId && deps.runStore) {
1767
- await deps.runStore.setTerminal(taskId, "failed", null, e instanceof Error ? e.message : String(e)).catch(() => undefined);
1837
+ await deps.runStore.setTerminal(taskId, "failed", null, errorText(e)).catch(() => undefined);
1768
1838
  }
1769
1839
  settleFleet("failed");
1770
1840
  throw e;
@@ -1804,26 +1874,66 @@ export function createHttpServer(rawDeps) {
1804
1874
  taskId, sessionId, status: result.status,
1805
1875
  bindingEnforced: true,
1806
1876
  ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1807
- ...(result.errorMessage ? { errorMessage: redactSecrets(result.errorMessage) } : {}),
1877
+ ...(result.errorMessage ? { errorMessage: redactErrorMessage(result.errorMessage) ?? "" } : {}),
1808
1878
  ...(checkpointReopened ? { retriable: true } : {}),
1809
1879
  },
1810
1880
  };
1811
1881
  }
1812
- async function resumePreempted(sessionId, req, acceptEarly) {
1882
+ async function mayDiscloseRowError(cs, token, decider) {
1883
+ if (decider === undefined || decider.explicitOperator)
1884
+ return true;
1885
+ let scope;
1886
+ try {
1887
+ scope = await cs.peekScopeByToken(token);
1888
+ }
1889
+ catch (e) {
1890
+ deps.logger?.warn?.("checkpoint_owner_probe_unreadable", { note: "peekScopeByToken failed — row-error disclosure withheld (fail-closed)", err: e instanceof Error ? e.message : String(e) });
1891
+ return false;
1892
+ }
1893
+ if (scope === undefined)
1894
+ return false;
1895
+ const owner = decodeCheckpointScope(scope ?? CHECKPOINT_PUBLIC_SCOPE);
1896
+ return owner === undefined || owner === decider.principal;
1897
+ }
1898
+ async function resumePreempted(sessionId, req, acceptEarly, decider) {
1813
1899
  const cs = deps.checkpointStore;
1814
1900
  const RESUMABLE_GATE_KIND = "resource_limit";
1815
- const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: [RESUMABLE_GATE_KIND] })) ?? (await cs.findPendingTokenBySession(sessionId));
1901
+ const preemptBinding = { gateKinds: [RESUMABLE_GATE_KIND] };
1902
+ const token = (await cs.findPendingTokenBySession(sessionId, undefined, preemptBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
1903
+ const noSuspension = { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1816
1904
  if (!token)
1817
- return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1905
+ return noSuspension;
1906
+ if (decider !== undefined && !decider.explicitOperator) {
1907
+ const peeked = await cs
1908
+ .peekPendingScope(sessionId, preemptBinding)
1909
+ .then((sc) => (sc === undefined ? cs.peekPendingScope(sessionId) : sc))
1910
+ .catch(() => undefined);
1911
+ const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
1912
+ if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
1913
+ deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
1914
+ return noSuspension;
1915
+ }
1916
+ }
1818
1917
  let cp;
1819
1918
  try {
1820
1919
  cp = await cs.get(token);
1821
1920
  }
1822
1921
  catch (e) {
1922
+ if (!(await mayDiscloseRowError(cs, token, decider))) {
1923
+ deps.logger?.warn?.("assistant_resume_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
1924
+ return noSuspension;
1925
+ }
1823
1926
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1824
1927
  }
1825
- if (!cp)
1928
+ if (!cp) {
1929
+ if (!(await mayDiscloseRowError(cs, token, decider)))
1930
+ return noSuspension;
1826
1931
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1932
+ }
1933
+ if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
1934
+ deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1935
+ return noSuspension;
1936
+ }
1827
1937
  const gateKind = cp.gate?.kind;
1828
1938
  if (gateKind !== RESUMABLE_GATE_KIND) {
1829
1939
  return { status: 409, body: { error: `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`, errorCode: "gate_not_resumable" } };
@@ -1835,7 +1945,7 @@ export function createHttpServer(rawDeps) {
1835
1945
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
1836
1946
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1837
1947
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1838
- return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}) });
1948
+ return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1839
1949
  }
1840
1950
  async function resumeWake(sessionId, message, caller, req, acceptEarly) {
1841
1951
  const cs = deps.checkpointStore;
@@ -1856,10 +1966,18 @@ export function createHttpServer(rawDeps) {
1856
1966
  cp = await cs.get(token);
1857
1967
  }
1858
1968
  catch (e) {
1969
+ if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
1970
+ deps.logger?.warn?.("wake_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
1971
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
1972
+ }
1859
1973
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1860
1974
  }
1861
- if (!cp)
1975
+ if (!cp) {
1976
+ if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
1977
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
1978
+ }
1862
1979
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1980
+ }
1863
1981
  if (!operator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== caller) {
1864
1982
  return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
1865
1983
  }
@@ -1897,9 +2015,10 @@ export function createHttpServer(rawDeps) {
1897
2015
  verifyRounds: undefined,
1898
2016
  mintFreshRun: { source: "wake" },
1899
2017
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
2018
+ ...(req !== undefined ? { callerInitiated: true } : {}),
1900
2019
  });
1901
2020
  }
1902
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly) {
2021
+ async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
1903
2022
  const cs = deps.checkpointStore;
1904
2023
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1905
2024
  reason: r,
@@ -1907,22 +2026,45 @@ export function createHttpServer(rawDeps) {
1907
2026
  ...(log?.principalPresent !== undefined ? { principalPresent: log.principalPresent } : {}),
1908
2027
  });
1909
2028
  const PLAN_REVIEW_GATE_KIND = "plan_review";
1910
- const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: [PLAN_REVIEW_GATE_KIND] })) ?? (await cs.findPendingTokenBySession(sessionId));
2029
+ const planReviewBinding = { gateKinds: [PLAN_REVIEW_GATE_KIND] };
2030
+ const token = (await cs.findPendingTokenBySession(sessionId, undefined, planReviewBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
2031
+ const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1911
2032
  if (!token) {
1912
2033
  log404("no-pending-checkpoint");
1913
- return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
2034
+ return noPending;
2035
+ }
2036
+ if (decider !== undefined && !decider.explicitOperator) {
2037
+ const peeked = await cs
2038
+ .peekPendingScope(sessionId, planReviewBinding)
2039
+ .then((sc) => (sc === undefined ? cs.peekPendingScope(sessionId) : sc))
2040
+ .catch(() => undefined);
2041
+ const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
2042
+ if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
2043
+ deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
2044
+ return noPending;
2045
+ }
1914
2046
  }
1915
2047
  let cp;
1916
2048
  try {
1917
2049
  cp = await cs.get(token);
1918
2050
  }
1919
2051
  catch (e) {
2052
+ if (!(await mayDiscloseRowError(cs, token, decider))) {
2053
+ deps.logger?.warn?.("plan_review_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
2054
+ return noPending;
2055
+ }
1920
2056
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1921
2057
  }
1922
2058
  if (!cp) {
1923
2059
  log404("checkpoint-missing");
2060
+ if (!(await mayDiscloseRowError(cs, token, decider)))
2061
+ return noPending;
1924
2062
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1925
2063
  }
2064
+ if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
2065
+ deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
2066
+ return noPending;
2067
+ }
1926
2068
  const gateKind = cp.gate?.kind;
1927
2069
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
1928
2070
  return {
@@ -1946,7 +2088,7 @@ export function createHttpServer(rawDeps) {
1946
2088
  ...(editedPlan !== undefined ? { editedPlan } : {}),
1947
2089
  ...(reason ? { reason } : {}),
1948
2090
  };
1949
- return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}) });
2091
+ return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1950
2092
  }
1951
2093
  function recordTaskResult(result) {
1952
2094
  deps.metrics?.inc("tasks_total", { status: result.status });
@@ -1985,6 +2127,44 @@ export function createHttpServer(rawDeps) {
1985
2127
  sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
1986
2128
  return true;
1987
2129
  }
2130
+ function ownerCostQuotaDenied(principal) {
2131
+ if (!deps.costQuota || !principal)
2132
+ return undefined;
2133
+ const d = deps.costQuota.check(principal);
2134
+ if (d.allowed)
2135
+ return undefined;
2136
+ deps.metrics?.inc("cost_quota_rejected_total");
2137
+ return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
2138
+ }
2139
+ async function ownerFleetLeaseDenied(owner) {
2140
+ if (!deps.fleetLease || !owner)
2141
+ return undefined;
2142
+ const adm = await deps.fleetLease.admit(owner);
2143
+ if (adm.ok)
2144
+ return undefined;
2145
+ deps.metrics?.inc("fleet_lease_rejected_total");
2146
+ return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
2147
+ }
2148
+ async function ownerLeaseDenied(res, owner) {
2149
+ if (!deps.fleetLease || !owner)
2150
+ return false;
2151
+ const adm = await deps.fleetLease.admit(owner);
2152
+ if (adm.ok)
2153
+ return false;
2154
+ deps.metrics?.inc("fleet_lease_rejected_total");
2155
+ res.setHeader("retry-after", String(adm.retryAfterSec));
2156
+ sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
2157
+ return true;
2158
+ }
2159
+ function ownerQuotaDenied(res, owner) {
2160
+ const denial = ownerCostQuotaDenied(owner);
2161
+ if (!denial)
2162
+ return false;
2163
+ const { errorCode, error, ...extras } = denial.body;
2164
+ res.setHeader("retry-after", String(extras.retryAfterSec));
2165
+ sendError(res, denial.status, errorCode, error, extras);
2166
+ return true;
2167
+ }
1988
2168
  async function leaseDenied(req, res) {
1989
2169
  if (!deps.fleetLease)
1990
2170
  return false;
@@ -2106,7 +2286,7 @@ export function createHttpServer(rawDeps) {
2106
2286
  if (chunks.length === 0)
2107
2287
  return {};
2108
2288
  try {
2109
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2289
+ return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
2110
2290
  }
2111
2291
  catch {
2112
2292
  throw new HttpError(400, "invalid JSON body");
@@ -2168,7 +2348,7 @@ export function createHttpServer(rawDeps) {
2168
2348
  continue;
2169
2349
  }
2170
2350
  await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
2171
- deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, body: r.body }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: e instanceof Error ? e.message : String(e) }));
2351
+ deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, errorCode: r.body?.errorCode }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
2172
2352
  }
2173
2353
  if (parkedSkipped > 0)
2174
2354
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
@@ -2220,6 +2400,10 @@ export function createHttpServer(rawDeps) {
2220
2400
  cp = await cs.get(req.gateToken);
2221
2401
  }
2222
2402
  catch (e) {
2403
+ if (!(await mayDiscloseRowError(cs, req.gateToken, { principal: req.principal, explicitOperator: false }))) {
2404
+ deps.logger?.warn?.("parked_late_decision_unreadable_row_withheld", { askId: req.askId });
2405
+ return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
2406
+ }
2223
2407
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
2224
2408
  }
2225
2409
  if (!cp) {
@@ -2334,7 +2518,9 @@ const ROUTE_LABEL_PATTERNS = [
2334
2518
  [/^\/v1\/assistant\/tasks\/[^/]+\/preempt$/, "/v1/assistant/tasks/:id/preempt"],
2335
2519
  [/^\/v1\/assistant\/tasks\/[^/]+\/resume$/, "/v1/assistant/tasks/:id/resume"],
2336
2520
  [/^\/v1\/assistant\/tasks\/[^/]+\/plan_review$/, "/v1/assistant/tasks/:id/plan_review"],
2337
- [/^\/v1\/(elicitations|questions|tool-approvals)\/[^/]+\/respond$/, "/v1/$hitl/:id/respond"],
2521
+ [/^\/v1\/elicitations\/[^/]+\/respond$/, "/v1/elicitations/:id/respond"],
2522
+ [/^\/v1\/questions\/[^/]+\/respond$/, "/v1/questions/:id/respond"],
2523
+ [/^\/v1\/tool-approvals\/[^/]+\/respond$/, "/v1/tool-approvals/:id/respond"],
2338
2524
  [/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
2339
2525
  [/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
2340
2526
  [/^\/v1\/workflows\/[^/]+$/, "/v1/workflows/:id"],
@@ -364,10 +364,15 @@ export interface TaskRequestBody {
364
364
  * shared settings keys on every path, while `settings.webSearch` has its OWN validator on the single-user
365
365
  * scenario lane (capabilities/scenarios.ts `webSearchConfigFromSettings` — the multi-tenant lane ignores it
366
366
  * by design). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
367
- * runtime drops garbage): a non-array object bag, or omit the key. Runtime is more tolerant than the type on
368
- * purpose — a `null` reads as absence on both the fresh-submit wall (prepareSpec) and replay
369
- * (parseTaskSettings), and malformed forms 400 on submit / no-op on replay. ([ref] 件1 leftover key,
370
- * declared 2026-08-31; ownership wording per codex adversarial round.) */
367
+ * runtime drops garbage): a non-array object bag, or omit the key. A `null` reads as absence on both the
368
+ * fresh-submit wall (prepareSpec) and replay (parseTaskSettings), and malformed forms 400 on submit /
369
+ * no-op on replay. ([ref] 件1 leftover key, declared 2026-08-31; ownership wording per codex adversarial round.)
370
+ *
371
+ * 🔴 [ref] 件①(PM 裁「拒」[ref])—— **KEY SET 上运行时不再比类型宽**:`Record<string, unknown>` 的开放
372
+ * 下标是 TS 形状,不是受理面。FRESH 提交腿上顶层 / `settings.permissions` 的**受理键是闭集**
373
+ * ({@link taskSettingsKeyIssue} 的两张表),集外键 400 `request.body_shape` + 逐字列名(`unknownKeys` /
374
+ * `unsupportedKeys`)。RESUME 重放腿照旧宽容(`parseTaskSettings` 忽略未知键,4xx 会把存量 parked 任务
375
+ * 砖死)——「运行时比类型宽」这句现在**只对值形成立,对键集不成立**。 */
371
376
  settings?: Record<string, unknown>;
372
377
  /** [R3] Caller-supplied per-request MCP servers (the TOC client's local `.mcp.json`), aligned to core
373
378
  * `McpServerSpec`. 🔒 honored on any SINGLE-USER deployment (task-mcp.ts `mcpInjectionHonored` = `requirePrincipal!==true`)
@@ -1,3 +1,4 @@
1
+ import { redactHead } from "../observability/run-terminal-log.js";
1
2
  export const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
2
3
  const DEFAULT_READ_TIMEOUT_MS = 30_000;
3
4
  const SAFE_REV = /^[A-Za-z0-9._/-]{1,200}$/;
@@ -44,7 +45,7 @@ export async function pullDiff(env, baseSha, opts = {}) {
44
45
  return { ok: false, truncated: true, error: "stream ended without an exit chunk (truncated or aborted)" };
45
46
  }
46
47
  if (exitCode !== 0) {
47
- return { ok: false, exitCode, error: `git format-patch exited ${exitCode}: ${stderr.slice(0, 500).trim()}` };
48
+ return { ok: false, exitCode, error: `git format-patch exited ${exitCode}: ${redactHead(stderr, 500).trim()}` };
48
49
  }
49
50
  if (patch.trim() === "") {
50
51
  return { ok: false, exitCode, error: `empty diff: no commits in ${baseSha}..${ref} (worker produced nothing)` };