@sema-agent/server 7.56.0 → 7.58.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 (121) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +2 -2
  3. package/USAGE.md +101 -9
  4. package/dist/approval-ask-audit-store.d.ts +100 -1
  5. package/dist/approval-ask-audit-store.js +103 -2
  6. package/dist/approval-card.d.ts +148 -13
  7. package/dist/approval-card.js +82 -13
  8. package/dist/approval.d.ts +31 -0
  9. package/dist/approval.js +18 -0
  10. package/dist/auto-mode-face.d.ts +111 -0
  11. package/dist/auto-mode-face.js +99 -0
  12. package/dist/bench/s1/arms.js +5 -5
  13. package/dist/bench/s1/live-deps.js +11 -11
  14. package/dist/bench/s1/run-firm.js +3 -0
  15. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  16. package/dist/bench/s1/runner-ctx.js +7 -0
  17. package/dist/boot/config-center.js +23 -1
  18. package/dist/boot/coordinators.js +13 -1
  19. package/dist/boot/parked-revive-gate.d.ts +7 -2
  20. package/dist/boot/parked-revive-gate.js +12 -1
  21. package/dist/boot/resolve-spec.d.ts +3 -0
  22. package/dist/boot/resolve-spec.js +3 -1
  23. package/dist/boot/runner-deps.d.ts +12 -0
  24. package/dist/boot/runner-deps.js +28 -3
  25. package/dist/boot/runtime-caps.d.ts +19 -9
  26. package/dist/boot/runtime-caps.js +49 -21
  27. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  28. package/dist/boot/session-shell-gate-registry.js +21 -0
  29. package/dist/boot/stores.js +12 -5
  30. package/dist/config-catalog.js +14 -5
  31. package/dist/config-center/http-client.js +7 -11
  32. package/dist/config-center/read-warnings.d.ts +20 -0
  33. package/dist/config-center/read-warnings.js +36 -0
  34. package/dist/config-provider.d.ts +3 -0
  35. package/dist/config-provider.js +4 -8
  36. package/dist/config-types.d.ts +41 -10
  37. package/dist/config.d.ts +21 -0
  38. package/dist/config.js +40 -5
  39. package/dist/hooks/hook-runner.js +8 -8
  40. package/dist/http/route-ctx.d.ts +75 -0
  41. package/dist/http/routes/a2a-serve.js +5 -3
  42. package/dist/http/routes/approvals-assistant.js +19 -6
  43. package/dist/http/routes/capabilities.js +21 -4
  44. package/dist/http/routes/memory-origin.d.ts +2 -2
  45. package/dist/http/routes/rules.js +3 -2
  46. package/dist/http/routes/runs.d.ts +0 -14
  47. package/dist/http/routes/runs.js +20 -8
  48. package/dist/http/routes/tasks.js +34 -8
  49. package/dist/http/routes/workflows.js +19 -4
  50. package/dist/http/server.d.ts +11 -1
  51. package/dist/http/server.js +215 -43
  52. package/dist/http/wire-types.d.ts +9 -4
  53. package/dist/leader/diffout.js +2 -1
  54. package/dist/leader/endpoint.js +45 -12
  55. package/dist/leader/fanout.js +6 -5
  56. package/dist/leader/leader.js +17 -14
  57. package/dist/leader/merge.js +17 -12
  58. package/dist/leader/planner.js +3 -2
  59. package/dist/leader/repair-oracle.js +6 -4
  60. package/dist/leader/repair-wire.js +5 -3
  61. package/dist/leader/wire.d.ts +30 -1
  62. package/dist/leader/wire.js +36 -19
  63. package/dist/main.js +43 -7
  64. package/dist/observability/err-text.d.ts +6 -0
  65. package/dist/observability/err-text.js +10 -0
  66. package/dist/observability/fail-open.d.ts +40 -0
  67. package/dist/observability/fail-open.js +19 -0
  68. package/dist/observability/metrics.js +2 -2
  69. package/dist/observability/run-terminal-log.d.ts +120 -0
  70. package/dist/observability/run-terminal-log.js +360 -0
  71. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  72. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  73. package/dist/permission-rule-vocab.d.ts +40 -0
  74. package/dist/permission-rule-vocab.js +17 -0
  75. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  76. package/dist/plugins/checkpoint-store-sql.js +19 -3
  77. package/dist/plugins/file-run-store.d.ts +3 -34
  78. package/dist/plugins/file-run-store.js +19 -0
  79. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  80. package/dist/plugins/local-checkpoint-store.js +3 -0
  81. package/dist/plugins/mailbox-store-sql.d.ts +28 -9
  82. package/dist/plugins/mailbox-store-sql.js +72 -10
  83. package/dist/plugins/memory-run-store.d.ts +3 -15
  84. package/dist/plugins/memory-run-store.js +19 -0
  85. package/dist/plugins/permission-rule-store-file.js +8 -3
  86. package/dist/plugins/permission-rule-store-sql.js +16 -10
  87. package/dist/plugins/remote-scratchpad.js +3 -2
  88. package/dist/plugins/run-store-sql.d.ts +3 -42
  89. package/dist/plugins/run-store-sql.js +30 -1
  90. package/dist/plugins/store-backend.d.ts +1 -1
  91. package/dist/plugins/store-contracts.d.ts +66 -1
  92. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  93. package/dist/plugins/workflow-run-store-sql.js +10 -2
  94. package/dist/rules-consent.js +20 -13
  95. package/dist/run-cancel-context.d.ts +13 -0
  96. package/dist/run-cancel-context.js +15 -0
  97. package/dist/run-local.js +2 -2
  98. package/dist/runs.d.ts +4 -1
  99. package/dist/runs.js +41 -10
  100. package/dist/runtime-caps-resolver.d.ts +133 -17
  101. package/dist/runtime-caps-resolver.js +41 -3
  102. package/dist/task-settings.d.ts +57 -3
  103. package/dist/task-settings.js +78 -5
  104. package/dist/task-workflow.d.ts +32 -2
  105. package/dist/task-workflow.js +8 -3
  106. package/dist/tool-approval.d.ts +26 -3
  107. package/dist/tool-approval.js +96 -13
  108. package/dist/trace/core-keyset-guard.d.ts +7 -8
  109. package/dist/trace/engine-notice-wire.d.ts +1 -1
  110. package/dist/trace/engine-notice-wire.js +2 -0
  111. package/dist/trace/ledger-sink.d.ts +13 -4
  112. package/dist/trace/ledger-sink.js +14 -6
  113. package/dist/trace/project.d.ts +58 -3
  114. package/dist/trace/project.js +33 -1
  115. package/dist/trace/redact.d.ts +22 -11
  116. package/dist/trace/redact.js +660 -21
  117. package/dist/trace/sema-provenance.d.ts +35 -0
  118. package/dist/trace/sema-provenance.js +14 -0
  119. package/dist/turn-activity.d.ts +32 -2
  120. package/dist/turn-activity.js +29 -4
  121. package/package.json +3 -3
@@ -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,32 +1874,65 @@ 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
  }
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
+ }
1812
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" } };
1827
- if (decider !== undefined && !decider.explicitOperator) {
1828
- const cpOwner = decodeCheckpointScope(cp.scope);
1829
- if (cpOwner !== undefined && cpOwner !== decider.principal) {
1830
- deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1831
- return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1832
- }
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;
1833
1936
  }
1834
1937
  const gateKind = cp.gate?.kind;
1835
1938
  if (gateKind !== RESUMABLE_GATE_KIND) {
@@ -1842,7 +1945,7 @@ export function createHttpServer(rawDeps) {
1842
1945
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
1843
1946
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1844
1947
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1845
- 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 } : {}) });
1846
1949
  }
1847
1950
  async function resumeWake(sessionId, message, caller, req, acceptEarly) {
1848
1951
  const cs = deps.checkpointStore;
@@ -1863,10 +1966,18 @@ export function createHttpServer(rawDeps) {
1863
1966
  cp = await cs.get(token);
1864
1967
  }
1865
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
+ }
1866
1973
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1867
1974
  }
1868
- 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
+ }
1869
1979
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1980
+ }
1870
1981
  if (!operator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== caller) {
1871
1982
  return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
1872
1983
  }
@@ -1904,6 +2015,7 @@ export function createHttpServer(rawDeps) {
1904
2015
  verifyRounds: undefined,
1905
2016
  mintFreshRun: { source: "wake" },
1906
2017
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
2018
+ ...(req !== undefined ? { callerInitiated: true } : {}),
1907
2019
  });
1908
2020
  }
1909
2021
  async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
@@ -1914,28 +2026,44 @@ export function createHttpServer(rawDeps) {
1914
2026
  ...(log?.principalPresent !== undefined ? { principalPresent: log.principalPresent } : {}),
1915
2027
  });
1916
2028
  const PLAN_REVIEW_GATE_KIND = "plan_review";
1917
- 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" } };
1918
2032
  if (!token) {
1919
2033
  log404("no-pending-checkpoint");
1920
- 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
+ }
1921
2046
  }
1922
2047
  let cp;
1923
2048
  try {
1924
2049
  cp = await cs.get(token);
1925
2050
  }
1926
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
+ }
1927
2056
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1928
2057
  }
1929
2058
  if (!cp) {
1930
2059
  log404("checkpoint-missing");
2060
+ if (!(await mayDiscloseRowError(cs, token, decider)))
2061
+ return noPending;
1931
2062
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1932
2063
  }
1933
- if (decider !== undefined && !decider.explicitOperator) {
1934
- const cpOwner = decodeCheckpointScope(cp.scope);
1935
- if (cpOwner !== undefined && cpOwner !== decider.principal) {
1936
- deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1937
- return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1938
- }
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;
1939
2067
  }
1940
2068
  const gateKind = cp.gate?.kind;
1941
2069
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
@@ -1960,7 +2088,7 @@ export function createHttpServer(rawDeps) {
1960
2088
  ...(editedPlan !== undefined ? { editedPlan } : {}),
1961
2089
  ...(reason ? { reason } : {}),
1962
2090
  };
1963
- 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 } : {}) });
1964
2092
  }
1965
2093
  function recordTaskResult(result) {
1966
2094
  deps.metrics?.inc("tasks_total", { status: result.status });
@@ -1999,6 +2127,44 @@ export function createHttpServer(rawDeps) {
1999
2127
  sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
2000
2128
  return true;
2001
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
+ }
2002
2168
  async function leaseDenied(req, res) {
2003
2169
  if (!deps.fleetLease)
2004
2170
  return false;
@@ -2182,7 +2348,7 @@ export function createHttpServer(rawDeps) {
2182
2348
  continue;
2183
2349
  }
2184
2350
  await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
2185
- 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) }));
2186
2352
  }
2187
2353
  if (parkedSkipped > 0)
2188
2354
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
@@ -2234,6 +2400,10 @@ export function createHttpServer(rawDeps) {
2234
2400
  cp = await cs.get(req.gateToken);
2235
2401
  }
2236
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
+ }
2237
2407
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
2238
2408
  }
2239
2409
  if (!cp) {
@@ -2348,7 +2518,9 @@ const ROUTE_LABEL_PATTERNS = [
2348
2518
  [/^\/v1\/assistant\/tasks\/[^/]+\/preempt$/, "/v1/assistant/tasks/:id/preempt"],
2349
2519
  [/^\/v1\/assistant\/tasks\/[^/]+\/resume$/, "/v1/assistant/tasks/:id/resume"],
2350
2520
  [/^\/v1\/assistant\/tasks\/[^/]+\/plan_review$/, "/v1/assistant/tasks/:id/plan_review"],
2351
- [/^\/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"],
2352
2524
  [/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
2353
2525
  [/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
2354
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)` };
@@ -1,4 +1,5 @@
1
1
  import { uuidv7 } from "@sema-agent/core";
2
+ import { redactErrorMessage, redactHead, redactLeaderResult, redactLeaderResultWithFlag } from "../observability/run-terminal-log.js";
2
3
  import { withPrincipal } from "../observability/principal-context.js";
3
4
  import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
4
5
  function statusForLeaderResult(result) {
@@ -42,13 +43,13 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
42
43
  void (async () => {
43
44
  let terminal;
44
45
  try {
45
- const result = await withPrincipal(owner ?? undefined, () => runLeader(body));
46
+ const result = redactLeaderResult(await withPrincipal(owner ?? undefined, () => runLeader(body)));
46
47
  const status = statusForLeaderResult(result);
47
48
  terminal = { status: status, result };
48
49
  opts.logger?.info?.("leader_run_done", { id, ok: result.ok, status, ...(result.repairTerminal ? { repairTerminal: result.repairTerminal } : {}), merged: result.merge && result.merge.ok ? result.merge.merged : undefined });
49
50
  }
50
51
  catch (e) {
51
- terminal = { status: "failed", error: e instanceof Error ? e.message : String(e) };
52
+ terminal = { status: "failed", error: redactErrorMessage(e instanceof Error ? e.message : String(e)) ?? "" };
52
53
  opts.logger?.error?.("leader_run_error", { id, err: terminal.error });
53
54
  }
54
55
  if (opts.store) {
@@ -61,7 +62,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
61
62
  }
62
63
  catch (e) {
63
64
  if (attempt >= TERMINAL_WRITE_ATTEMPTS) {
64
- opts.logger?.error?.("leader_run_terminal_write_failed", { id, status: terminal.status, attempts: attempt, err: e instanceof Error ? e.message : String(e) });
65
+ opts.logger?.error?.("leader_run_terminal_write_failed", { id, status: terminal.status, attempts: attempt, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
65
66
  break;
66
67
  }
67
68
  await new Promise((r) => setTimeout(r, TERMINAL_WRITE_RETRY_MS));
@@ -79,16 +80,48 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
79
80
  }
80
81
  })();
81
82
  }
83
+ const CLEAN_MEMO_CAP = 4096;
84
+ const cleanIds = new Set();
85
+ const DIRTY_MEMO_CAP = 256;
86
+ const DIRTY_MEMO_MAX_ENTRY_BYTES = 1_048_576;
87
+ const DIRTY_MEMO_MAX_TOTAL_BYTES = 16 * 1_048_576;
88
+ const dirtyMemo = new Map();
89
+ let dirtyMemoBytes = 0;
90
+ function redactedView(run) {
91
+ const terminal = run.status !== "running";
92
+ if (terminal) {
93
+ if (cleanIds.has(run.id))
94
+ return { ...(run.result ? { result: run.result } : {}), ...(run.error ? { error: run.error } : {}) };
95
+ const hit = dirtyMemo.get(run.id);
96
+ if (hit)
97
+ return hit.view;
98
+ }
99
+ const rr = run.result ? redactLeaderResultWithFlag(run.result) : undefined;
100
+ const err = run.error ? (redactErrorMessage(run.error) ?? "") : undefined;
101
+ const changed = (rr?.changed ?? false) || (err !== undefined && err !== run.error);
102
+ const view = { ...(rr ? { result: rr.value } : {}), ...(err !== undefined ? { error: err } : {}) };
103
+ if (!terminal)
104
+ return view;
105
+ if (!changed) {
106
+ if (cleanIds.size >= CLEAN_MEMO_CAP)
107
+ cleanIds.delete(cleanIds.values().next().value);
108
+ cleanIds.add(run.id);
109
+ return view;
110
+ }
111
+ const bytes = Buffer.byteLength(JSON.stringify(view));
112
+ if (bytes <= DIRTY_MEMO_MAX_ENTRY_BYTES) {
113
+ while (dirtyMemo.size > 0 && (dirtyMemo.size >= DIRTY_MEMO_CAP || dirtyMemoBytes + bytes > DIRTY_MEMO_MAX_TOTAL_BYTES)) {
114
+ const oldest = dirtyMemo.keys().next().value;
115
+ dirtyMemoBytes -= dirtyMemo.get(oldest).bytes;
116
+ dirtyMemo.delete(oldest);
117
+ }
118
+ dirtyMemo.set(run.id, { view, bytes });
119
+ dirtyMemoBytes += bytes;
120
+ }
121
+ return view;
122
+ }
82
123
  function getBody(run) {
83
- return {
84
- status: 200,
85
- body: {
86
- id: run.id,
87
- status: run.status,
88
- ...(run.result ? { result: run.result } : {}),
89
- ...(run.error ? { error: run.error } : {}),
90
- },
91
- };
124
+ return { status: 200, body: { id: run.id, status: run.status, ...redactedView(run) } };
92
125
  }
93
126
  const NOT_FOUND = { status: 404, body: { error: "leader run not found", errorCode: "not_found.leader_run" } };
94
127
  async function handle(method, url, body, requester, idempotencyKey) {