@sema-agent/server 7.55.0 → 7.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +3 -3
- package/README.md +14 -7
- package/README.zh-CN.md +10 -6
- package/USAGE.md +207 -59
- package/deploy/sema-up/chart/values.yaml +1 -1
- 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-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- 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/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- 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 -8
- package/dist/boot/runtime-caps.js +50 -20
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/config-catalog.js +14 -5
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/http/route-ctx.d.ts +90 -3
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +21 -8
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/leader.js +2 -2
- 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 +20 -5
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +220 -34
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +49 -15
- 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 +80 -1
- package/dist/leader/wire.js +55 -26
- package/dist/main.js +44 -8
- 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 +1 -1
- 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 +26 -0
- package/dist/plugins/checkpoint-store-sql.js +23 -4
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +21 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +5 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +21 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +25 -11
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +39 -23
- package/dist/plugins/pg-session-storage.js +17 -13
- 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 +36 -5
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +70 -1
- package/dist/plugins/tidb-session-store.js +18 -14
- 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.d.ts +5 -4
- package/dist/rules-consent.js +25 -46
- 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 +44 -4
- package/dist/security.d.ts +7 -0
- 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 +38 -63
- package/dist/tool-approval.js +102 -74
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +10 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +655 -20
- package/dist/trace/sema-provenance.d.ts +26 -0
- package/dist/trace/sema-provenance.js +11 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +3 -3
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
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,26 +1874,66 @@ export function createHttpServer(rawDeps) {
|
|
|
1804
1874
|
taskId, sessionId, status: result.status,
|
|
1805
1875
|
bindingEnforced: true,
|
|
1806
1876
|
...(result.errorCode ? { errorCode: result.errorCode } : {}),
|
|
1807
|
-
...(result.errorMessage ? { errorMessage:
|
|
1877
|
+
...(result.errorMessage ? { errorMessage: redactErrorMessage(result.errorMessage) ?? "" } : {}),
|
|
1808
1878
|
...(checkpointReopened ? { retriable: true } : {}),
|
|
1809
1879
|
},
|
|
1810
1880
|
};
|
|
1811
1881
|
}
|
|
1812
|
-
async function
|
|
1882
|
+
async function mayDiscloseRowError(cs, token, decider) {
|
|
1883
|
+
if (decider === undefined || decider.explicitOperator)
|
|
1884
|
+
return true;
|
|
1885
|
+
let scope;
|
|
1886
|
+
try {
|
|
1887
|
+
scope = await cs.peekScopeByToken(token);
|
|
1888
|
+
}
|
|
1889
|
+
catch (e) {
|
|
1890
|
+
deps.logger?.warn?.("checkpoint_owner_probe_unreadable", { note: "peekScopeByToken failed — row-error disclosure withheld (fail-closed)", err: e instanceof Error ? e.message : String(e) });
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
if (scope === undefined)
|
|
1894
|
+
return false;
|
|
1895
|
+
const owner = decodeCheckpointScope(scope ?? CHECKPOINT_PUBLIC_SCOPE);
|
|
1896
|
+
return owner === undefined || owner === decider.principal;
|
|
1897
|
+
}
|
|
1898
|
+
async function resumePreempted(sessionId, req, acceptEarly, decider) {
|
|
1813
1899
|
const cs = deps.checkpointStore;
|
|
1814
1900
|
const RESUMABLE_GATE_KIND = "resource_limit";
|
|
1815
|
-
const
|
|
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" } };
|
|
1932
|
+
}
|
|
1933
|
+
if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
|
|
1934
|
+
deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1935
|
+
return noSuspension;
|
|
1936
|
+
}
|
|
1827
1937
|
const gateKind = cp.gate?.kind;
|
|
1828
1938
|
if (gateKind !== RESUMABLE_GATE_KIND) {
|
|
1829
1939
|
return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
|
|
@@ -1835,7 +1945,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1835
1945
|
const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
|
|
1836
1946
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1837
1947
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1838
|
-
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}) });
|
|
1948
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1839
1949
|
}
|
|
1840
1950
|
async function resumeWake(sessionId, message, caller, req, acceptEarly) {
|
|
1841
1951
|
const cs = deps.checkpointStore;
|
|
@@ -1856,10 +1966,18 @@ export function createHttpServer(rawDeps) {
|
|
|
1856
1966
|
cp = await cs.get(token);
|
|
1857
1967
|
}
|
|
1858
1968
|
catch (e) {
|
|
1969
|
+
if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
|
|
1970
|
+
deps.logger?.warn?.("wake_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
|
|
1971
|
+
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
1972
|
+
}
|
|
1859
1973
|
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
1860
1974
|
}
|
|
1861
|
-
if (!cp)
|
|
1975
|
+
if (!cp) {
|
|
1976
|
+
if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
|
|
1977
|
+
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
1978
|
+
}
|
|
1862
1979
|
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1980
|
+
}
|
|
1863
1981
|
if (!operator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== caller) {
|
|
1864
1982
|
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
1865
1983
|
}
|
|
@@ -1897,9 +2015,10 @@ export function createHttpServer(rawDeps) {
|
|
|
1897
2015
|
verifyRounds: undefined,
|
|
1898
2016
|
mintFreshRun: { source: "wake" },
|
|
1899
2017
|
...(acceptEarly === true ? { acceptEarly: true } : {}),
|
|
2018
|
+
...(req !== undefined ? { callerInitiated: true } : {}),
|
|
1900
2019
|
});
|
|
1901
2020
|
}
|
|
1902
|
-
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly) {
|
|
2021
|
+
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
|
|
1903
2022
|
const cs = deps.checkpointStore;
|
|
1904
2023
|
const log404 = (r) => deps.logger?.info?.("decide_404", {
|
|
1905
2024
|
reason: r,
|
|
@@ -1907,22 +2026,45 @@ export function createHttpServer(rawDeps) {
|
|
|
1907
2026
|
...(log?.principalPresent !== undefined ? { principalPresent: log.principalPresent } : {}),
|
|
1908
2027
|
});
|
|
1909
2028
|
const PLAN_REVIEW_GATE_KIND = "plan_review";
|
|
1910
|
-
const
|
|
2029
|
+
const planReviewBinding = { gateKinds: [PLAN_REVIEW_GATE_KIND] };
|
|
2030
|
+
const token = (await cs.findPendingTokenBySession(sessionId, undefined, planReviewBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
2031
|
+
const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
|
|
1911
2032
|
if (!token) {
|
|
1912
2033
|
log404("no-pending-checkpoint");
|
|
1913
|
-
return
|
|
2034
|
+
return noPending;
|
|
2035
|
+
}
|
|
2036
|
+
if (decider !== undefined && !decider.explicitOperator) {
|
|
2037
|
+
const peeked = await cs
|
|
2038
|
+
.peekPendingScope(sessionId, planReviewBinding)
|
|
2039
|
+
.then((sc) => (sc === undefined ? cs.peekPendingScope(sessionId) : sc))
|
|
2040
|
+
.catch(() => undefined);
|
|
2041
|
+
const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
|
|
2042
|
+
if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
|
|
2043
|
+
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
|
|
2044
|
+
return noPending;
|
|
2045
|
+
}
|
|
1914
2046
|
}
|
|
1915
2047
|
let cp;
|
|
1916
2048
|
try {
|
|
1917
2049
|
cp = await cs.get(token);
|
|
1918
2050
|
}
|
|
1919
2051
|
catch (e) {
|
|
2052
|
+
if (!(await mayDiscloseRowError(cs, token, decider))) {
|
|
2053
|
+
deps.logger?.warn?.("plan_review_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
|
|
2054
|
+
return noPending;
|
|
2055
|
+
}
|
|
1920
2056
|
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
1921
2057
|
}
|
|
1922
2058
|
if (!cp) {
|
|
1923
2059
|
log404("checkpoint-missing");
|
|
2060
|
+
if (!(await mayDiscloseRowError(cs, token, decider)))
|
|
2061
|
+
return noPending;
|
|
1924
2062
|
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1925
2063
|
}
|
|
2064
|
+
if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
|
|
2065
|
+
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
2066
|
+
return noPending;
|
|
2067
|
+
}
|
|
1926
2068
|
const gateKind = cp.gate?.kind;
|
|
1927
2069
|
if (gateKind !== PLAN_REVIEW_GATE_KIND) {
|
|
1928
2070
|
return {
|
|
@@ -1946,7 +2088,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1946
2088
|
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
1947
2089
|
...(reason ? { reason } : {}),
|
|
1948
2090
|
};
|
|
1949
|
-
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}) });
|
|
2091
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1950
2092
|
}
|
|
1951
2093
|
function recordTaskResult(result) {
|
|
1952
2094
|
deps.metrics?.inc("tasks_total", { status: result.status });
|
|
@@ -1985,6 +2127,44 @@ export function createHttpServer(rawDeps) {
|
|
|
1985
2127
|
sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
|
|
1986
2128
|
return true;
|
|
1987
2129
|
}
|
|
2130
|
+
function ownerCostQuotaDenied(principal) {
|
|
2131
|
+
if (!deps.costQuota || !principal)
|
|
2132
|
+
return undefined;
|
|
2133
|
+
const d = deps.costQuota.check(principal);
|
|
2134
|
+
if (d.allowed)
|
|
2135
|
+
return undefined;
|
|
2136
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
2137
|
+
return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
|
|
2138
|
+
}
|
|
2139
|
+
async function ownerFleetLeaseDenied(owner) {
|
|
2140
|
+
if (!deps.fleetLease || !owner)
|
|
2141
|
+
return undefined;
|
|
2142
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
2143
|
+
if (adm.ok)
|
|
2144
|
+
return undefined;
|
|
2145
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
2146
|
+
return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
|
|
2147
|
+
}
|
|
2148
|
+
async function ownerLeaseDenied(res, owner) {
|
|
2149
|
+
if (!deps.fleetLease || !owner)
|
|
2150
|
+
return false;
|
|
2151
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
2152
|
+
if (adm.ok)
|
|
2153
|
+
return false;
|
|
2154
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
2155
|
+
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
2156
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
function ownerQuotaDenied(res, owner) {
|
|
2160
|
+
const denial = ownerCostQuotaDenied(owner);
|
|
2161
|
+
if (!denial)
|
|
2162
|
+
return false;
|
|
2163
|
+
const { errorCode, error, ...extras } = denial.body;
|
|
2164
|
+
res.setHeader("retry-after", String(extras.retryAfterSec));
|
|
2165
|
+
sendError(res, denial.status, errorCode, error, extras);
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
1988
2168
|
async function leaseDenied(req, res) {
|
|
1989
2169
|
if (!deps.fleetLease)
|
|
1990
2170
|
return false;
|
|
@@ -2106,7 +2286,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2106
2286
|
if (chunks.length === 0)
|
|
2107
2287
|
return {};
|
|
2108
2288
|
try {
|
|
2109
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2289
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
|
|
2110
2290
|
}
|
|
2111
2291
|
catch {
|
|
2112
2292
|
throw new HttpError(400, "invalid JSON body");
|
|
@@ -2168,7 +2348,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2168
2348
|
continue;
|
|
2169
2349
|
}
|
|
2170
2350
|
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
|
|
2171
|
-
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status,
|
|
2351
|
+
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, errorCode: r.body?.errorCode }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
|
|
2172
2352
|
}
|
|
2173
2353
|
if (parkedSkipped > 0)
|
|
2174
2354
|
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
@@ -2220,6 +2400,10 @@ export function createHttpServer(rawDeps) {
|
|
|
2220
2400
|
cp = await cs.get(req.gateToken);
|
|
2221
2401
|
}
|
|
2222
2402
|
catch (e) {
|
|
2403
|
+
if (!(await mayDiscloseRowError(cs, req.gateToken, { principal: req.principal, explicitOperator: false }))) {
|
|
2404
|
+
deps.logger?.warn?.("parked_late_decision_unreadable_row_withheld", { askId: req.askId });
|
|
2405
|
+
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
2406
|
+
}
|
|
2223
2407
|
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
2224
2408
|
}
|
|
2225
2409
|
if (!cp) {
|
|
@@ -2334,7 +2518,9 @@ const ROUTE_LABEL_PATTERNS = [
|
|
|
2334
2518
|
[/^\/v1\/assistant\/tasks\/[^/]+\/preempt$/, "/v1/assistant/tasks/:id/preempt"],
|
|
2335
2519
|
[/^\/v1\/assistant\/tasks\/[^/]+\/resume$/, "/v1/assistant/tasks/:id/resume"],
|
|
2336
2520
|
[/^\/v1\/assistant\/tasks\/[^/]+\/plan_review$/, "/v1/assistant/tasks/:id/plan_review"],
|
|
2337
|
-
[/^\/v1\/
|
|
2521
|
+
[/^\/v1\/elicitations\/[^/]+\/respond$/, "/v1/elicitations/:id/respond"],
|
|
2522
|
+
[/^\/v1\/questions\/[^/]+\/respond$/, "/v1/questions/:id/respond"],
|
|
2523
|
+
[/^\/v1\/tool-approvals\/[^/]+\/respond$/, "/v1/tool-approvals/:id/respond"],
|
|
2338
2524
|
[/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
|
|
2339
2525
|
[/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
|
|
2340
2526
|
[/^\/v1\/workflows\/[^/]+$/, "/v1/workflows/:id"],
|
|
@@ -364,10 +364,15 @@ export interface TaskRequestBody {
|
|
|
364
364
|
* shared settings keys on every path, while `settings.webSearch` has its OWN validator on the single-user
|
|
365
365
|
* scenario lane (capabilities/scenarios.ts `webSearchConfigFromSettings` — the multi-tenant lane ignores it
|
|
366
366
|
* by design). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
|
|
367
|
-
* runtime drops garbage): a non-array object bag, or omit the key.
|
|
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)` };
|