@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.
- package/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +101 -9
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -9
- package/dist/boot/runtime-caps.js +49 -21
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/boot/stores.js +12 -5
- package/dist/config-catalog.js +14 -5
- package/dist/config-center/http-client.js +7 -11
- package/dist/config-center/read-warnings.d.ts +20 -0
- package/dist/config-center/read-warnings.js +36 -0
- package/dist/config-provider.d.ts +3 -0
- package/dist/config-provider.js +4 -8
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/hooks/hook-runner.js +8 -8
- package/dist/http/route-ctx.d.ts +75 -0
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +19 -6
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +19 -4
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +215 -43
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +45 -12
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +30 -1
- package/dist/leader/wire.js +36 -19
- package/dist/main.js +43 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +19 -3
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +19 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +3 -0
- package/dist/plugins/mailbox-store-sql.d.ts +28 -9
- package/dist/plugins/mailbox-store-sql.js +72 -10
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +19 -0
- package/dist/plugins/permission-rule-store-file.js +8 -3
- package/dist/plugins/permission-rule-store-sql.js +16 -10
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +30 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +66 -1
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.js +20 -13
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +41 -3
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +26 -3
- package/dist/tool-approval.js +96 -13
- package/dist/trace/core-keyset-guard.d.ts +7 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +58 -3
- package/dist/trace/project.js +33 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +660 -21
- package/dist/trace/sema-provenance.d.ts +35 -0
- package/dist/trace/sema-provenance.js +14 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +3 -3
package/dist/http/server.js
CHANGED
|
@@ -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
|
|
1114
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1935
|
-
|
|
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,
|
|
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\/
|
|
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.
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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`)
|
package/dist/leader/diffout.js
CHANGED
|
@@ -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
|
|
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)` };
|
package/dist/leader/endpoint.js
CHANGED
|
@@ -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) {
|