@sema-agent/server 7.79.0 → 7.79.2
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/dist/file-history-disclosure.d.ts +53 -0
- package/dist/file-history-disclosure.js +89 -0
- package/dist/http/admission.d.ts +56 -0
- package/dist/http/admission.js +560 -0
- package/dist/http/approval-sweeps.d.ts +21 -0
- package/dist/http/approval-sweeps.js +138 -0
- package/dist/http/dispatch.d.ts +32 -0
- package/dist/http/dispatch.js +230 -0
- package/dist/http/request-guards.d.ts +51 -0
- package/dist/http/request-guards.js +205 -0
- package/dist/http/resume-legs.d.ts +80 -0
- package/dist/http/resume-legs.js +1230 -0
- package/dist/http/route-table.d.ts +113 -0
- package/dist/http/route-table.js +235 -0
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/memory-compliance.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +4 -4
- package/dist/http/server.d.ts +20 -104
- package/dist/http/server.js +24 -2589
- package/dist/main.js +2 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/plugins/file-history-store-sql.js +12 -11
- package/dist/trace/redact.d.ts +9 -0
- package/dist/trace/redact.js +210 -78
- package/package.json +1 -1
package/dist/http/server.js
CHANGED
|
@@ -1,95 +1,33 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
|
|
6
|
-
import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
|
|
7
|
-
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
2
|
+
import { redactHead } from "../observability/run-terminal-log.js";
|
|
3
|
+
import { validatePendingSteer, terminalProjection } from "@sema-agent/core";
|
|
8
4
|
import {} from "../config-center/facade.js";
|
|
9
|
-
import { HttpError
|
|
10
|
-
import { verifyDirectDoorProof } from "../principal-jwt.js";
|
|
5
|
+
import { HttpError } from "../security.js";
|
|
11
6
|
import {} from "../memory-sync.js";
|
|
12
|
-
import {
|
|
13
|
-
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
14
|
-
import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
15
|
-
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
16
|
-
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
17
|
-
import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
18
|
-
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
19
|
-
import { runInBackground, evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
20
|
-
import { failureCodeOf, nestedPauseOf, stripResumeToken } from "../terminal.js";
|
|
21
|
-
import { mintCancelledResult } from "../run-cancel-context.js";
|
|
22
|
-
import { personDecision } from "../host-decision.js";
|
|
23
|
-
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
24
|
-
import { recordResumeResourceWindow } from "../resource-window.js";
|
|
25
|
-
import { publicStoreProbeError } from "../store-live-probe.js";
|
|
26
|
-
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
7
|
+
import { evictIfConflict } from "../runs.js";
|
|
27
8
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
|
|
32
|
-
import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
|
|
33
|
-
import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
|
|
34
|
-
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
9
|
+
import {} from "../orchestration/workflow-completion-inbox.js";
|
|
10
|
+
import {} from "../fleet/fleet-bus.js";
|
|
11
|
+
import {} from "../tool-approval.js";
|
|
35
12
|
import {} from "../observability/rate-limit.js";
|
|
36
|
-
import { withPrincipal } from "../observability/principal-context.js";
|
|
37
|
-
import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
|
|
38
|
-
import { redactSecrets } from "../trace/redact.js";
|
|
39
|
-
import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
|
|
40
|
-
import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
|
|
41
|
-
import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-segment.js";
|
|
42
|
-
import { errorText } from "../observability/err-text.js";
|
|
43
|
-
import { recordFailOpen } from "../observability/fail-open.js";
|
|
44
13
|
import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
|
|
45
14
|
export { scopedIdempotencyKey };
|
|
46
|
-
import {
|
|
47
|
-
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
48
|
-
import { acceptedResumingReceipt } from "./route-ctx.js";
|
|
49
|
-
import { mintAndDriveDurableRun } from "./durable-run-leg.js";
|
|
50
|
-
import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
51
|
-
import { handleCapabilities } from "./routes/capabilities.js";
|
|
52
|
-
import { handleObservability } from "./routes/observability.js";
|
|
53
|
-
import { handleDiagnostics } from "./routes/diagnostics.js";
|
|
54
|
-
import { handleAdoption } from "./routes/adoption.js";
|
|
55
|
-
import { handleRetention } from "./routes/retention-ops.js";
|
|
56
|
-
import { handleAdminDrain } from "./routes/admin-drain.js";
|
|
57
|
-
import { handleDevices } from "./routes/devices.js";
|
|
58
|
-
import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js";
|
|
59
|
-
import { handleConfigCatalog } from "./routes/config-catalog.js";
|
|
60
|
-
import { handleMemoryOptOut } from "./routes/admin-memory-optout.js";
|
|
61
|
-
import { handleMemoryPolicy } from "./routes/memory-policy.js";
|
|
62
|
-
import { handleMemoryBundle } from "./routes/memory-bundle.js";
|
|
63
|
-
import { handleMemoryCompliance } from "./routes/memory-compliance.js";
|
|
64
|
-
import { handleMemoryOrigin } from "./routes/memory-origin.js";
|
|
65
|
-
import { handleMemoryConsolidation } from "./routes/memory-consolidation.js";
|
|
66
|
-
import { handleSharedMemory } from "./routes/shared-memory.js";
|
|
67
|
-
import { handleRules } from "./routes/rules.js";
|
|
68
|
-
import { handleSessionsList } from "./routes/sessions-list.js";
|
|
69
|
-
import { handleAgentRoster } from "./routes/agents-roster.js";
|
|
70
|
-
import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
|
|
71
|
-
import { handleSessionSync, createSessionSyncLocal } from "./routes/session-sync.js";
|
|
72
|
-
import { handleAttachments } from "./routes/attachments.js";
|
|
73
|
-
import { handleFleet } from "./routes/fleet.js";
|
|
74
|
-
import { handleA2aServe, handleA2a } from "./routes/a2a-serve.js";
|
|
75
|
-
import { handleTraceUsage } from "./routes/trace-usage.js";
|
|
76
|
-
import { handleWorkflows, handleWorkflowAgentSteer } from "./routes/workflows.js";
|
|
77
|
-
import { handleNotifyWake, SESSION_WAKE_RE } from "./routes/notify-wake.js";
|
|
78
|
-
import { handleApprovalsAssistant, streamApprovals, isQuestionAnswer, ASSISTANT_RESUME_RE, ASSISTANT_PLAN_REVIEW_RE } from "./routes/approvals-assistant.js";
|
|
15
|
+
import { streamApprovals, isQuestionAnswer } from "./routes/approvals-assistant.js";
|
|
79
16
|
export { streamApprovals, isQuestionAnswer };
|
|
80
|
-
import { handleRuns, handleRunVerbs, RUN_SUBAGENT_RESUME_RE } from "./routes/runs.js";
|
|
81
|
-
import { handleRunMemoryCapture } from "./routes/run-memory-capture.js";
|
|
82
|
-
import { handleSideQuery } from "./routes/side-query.js";
|
|
83
|
-
import { handleTasks } from "./routes/tasks.js";
|
|
84
|
-
import { handleLeader } from "./routes/leader.js";
|
|
85
17
|
import { cascadeConfig } from "./run-meta.js";
|
|
86
18
|
export { cascadeConfig };
|
|
87
|
-
import {
|
|
19
|
+
import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
|
|
88
20
|
export { coarseStatusForState, errorCodeForExit };
|
|
89
|
-
import {
|
|
90
|
-
import {
|
|
91
|
-
import {
|
|
92
|
-
import {
|
|
21
|
+
import { sendError, httpErrorCode, msg } from "./send.js";
|
|
22
|
+
import { createRequestGuards } from "./request-guards.js";
|
|
23
|
+
import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
|
|
24
|
+
import { createResumeLegs } from "./resume-legs.js";
|
|
25
|
+
import { createDispatch, hasServiceAuth } from "./dispatch.js";
|
|
26
|
+
import { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath } from "./route-table.js";
|
|
27
|
+
import { createApprovalSweeps } from "./approval-sweeps.js";
|
|
28
|
+
export { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, hasServiceAuth };
|
|
29
|
+
export { MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills };
|
|
30
|
+
import { explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
93
31
|
export { explicitOperatorOk, isOperator };
|
|
94
32
|
export function flattenServiceDeps(deps) {
|
|
95
33
|
const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
|
|
@@ -97,84 +35,6 @@ export function flattenServiceDeps(deps) {
|
|
|
97
35
|
return deps;
|
|
98
36
|
return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
|
|
99
37
|
}
|
|
100
|
-
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
101
|
-
export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
|
|
102
|
-
export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
|
|
103
|
-
export function validateUserSkills(skills) {
|
|
104
|
-
if (skills === undefined)
|
|
105
|
-
return null;
|
|
106
|
-
if (!Array.isArray(skills))
|
|
107
|
-
return "skills must be an array of { name, description, content }";
|
|
108
|
-
for (const s of skills) {
|
|
109
|
-
if (s === null || typeof s !== "object")
|
|
110
|
-
return "each skill must be an object { name, description, content }";
|
|
111
|
-
if (typeof s["name"] !== "string" || s["name"].length === 0)
|
|
112
|
-
return "skill.name must be a non-empty string";
|
|
113
|
-
if (typeof s["description"] !== "string")
|
|
114
|
-
return "skill.description must be a string";
|
|
115
|
-
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
116
|
-
return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
121
|
-
const MAX_BODY = 8 * 1024 * 1024;
|
|
122
|
-
const MAX_IMAGES_PER_REQUEST = 20;
|
|
123
|
-
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
124
|
-
function mentionableNames(config, catalog) {
|
|
125
|
-
const allowlist = config.atModelAllowlist ?? [];
|
|
126
|
-
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
|
|
127
|
-
}
|
|
128
|
-
function isCheckpointReopenedFailure(r) {
|
|
129
|
-
const code = failureCodeOf(r.terminal);
|
|
130
|
-
return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
|
|
131
|
-
}
|
|
132
|
-
function nestedParkStatusOf(r) {
|
|
133
|
-
const paused = nestedPauseOf(r.terminal);
|
|
134
|
-
if (paused === undefined)
|
|
135
|
-
return undefined;
|
|
136
|
-
return PAUSE_REGISTRY[paused.gate.kind].taskStatus;
|
|
137
|
-
}
|
|
138
|
-
const ROUTE_DOMAINS = [
|
|
139
|
-
handleTraceUsage,
|
|
140
|
-
handleWorkflows,
|
|
141
|
-
handleFleet,
|
|
142
|
-
handleA2aServe,
|
|
143
|
-
handleCapabilities,
|
|
144
|
-
handleSideQuery,
|
|
145
|
-
handleTasks,
|
|
146
|
-
handleRuns,
|
|
147
|
-
handleA2a,
|
|
148
|
-
handleWorkflowAgentSteer,
|
|
149
|
-
handleRunVerbs,
|
|
150
|
-
handleRunMemoryCapture,
|
|
151
|
-
handleLeader,
|
|
152
|
-
handleImages,
|
|
153
|
-
handleApprovalsAssistant,
|
|
154
|
-
handleObservability,
|
|
155
|
-
handleDiagnostics,
|
|
156
|
-
handleAdoption,
|
|
157
|
-
handleRetention,
|
|
158
|
-
handleAdminDrain,
|
|
159
|
-
handleDevices,
|
|
160
|
-
handleAdminConfigRefresh,
|
|
161
|
-
handleConfigCatalog,
|
|
162
|
-
handleMemoryOptOut,
|
|
163
|
-
handleMemoryPolicy,
|
|
164
|
-
handleMemoryBundle,
|
|
165
|
-
handleMemoryCompliance,
|
|
166
|
-
handleMemoryOrigin,
|
|
167
|
-
handleMemoryConsolidation,
|
|
168
|
-
handleRules,
|
|
169
|
-
handleSharedMemory,
|
|
170
|
-
handleSessionsList,
|
|
171
|
-
handleAgentRoster,
|
|
172
|
-
handleSessions,
|
|
173
|
-
handleSessionSync,
|
|
174
|
-
handleAttachments,
|
|
175
|
-
handleNotifyWake,
|
|
176
|
-
];
|
|
177
|
-
void ROUTE_DOMAINS;
|
|
178
38
|
export function createHttpServer(rawDeps) {
|
|
179
39
|
const deps = flattenServiceDeps(rawDeps);
|
|
180
40
|
const idemCache = new IdempotencyCache();
|
|
@@ -194,18 +54,6 @@ export function createHttpServer(rawDeps) {
|
|
|
194
54
|
};
|
|
195
55
|
if (deps.drainState)
|
|
196
56
|
deps.drainState.lastActivityAt = () => lastBillableActivityAt;
|
|
197
|
-
const openResponses = new Set();
|
|
198
|
-
let lastAttachEndedAt = 0;
|
|
199
|
-
if (deps.drainState) {
|
|
200
|
-
deps.drainState.attachedStreams = () => {
|
|
201
|
-
let n = 0;
|
|
202
|
-
for (const r of openResponses)
|
|
203
|
-
if (isSseResponse(r))
|
|
204
|
-
n++;
|
|
205
|
-
return n;
|
|
206
|
-
};
|
|
207
|
-
deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
|
|
208
|
-
}
|
|
209
57
|
if (deps.hookWakeBus) {
|
|
210
58
|
deps.hookWakeBus.deliver = async (sessionId, text) => {
|
|
211
59
|
try {
|
|
@@ -243,6 +91,11 @@ export function createHttpServer(rawDeps) {
|
|
|
243
91
|
}
|
|
244
92
|
};
|
|
245
93
|
}
|
|
94
|
+
const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
|
|
95
|
+
const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
|
|
96
|
+
const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
|
|
97
|
+
const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
|
|
98
|
+
const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
|
|
246
99
|
const server = http.createServer((req, res) => {
|
|
247
100
|
void handle(req, res).catch((err) => {
|
|
248
101
|
if (err instanceof HttpError) {
|
|
@@ -260,1934 +113,9 @@ export function createHttpServer(rawDeps) {
|
|
|
260
113
|
});
|
|
261
114
|
});
|
|
262
115
|
deps.deviceHub?.attach(server);
|
|
263
|
-
const routeCtxBase = {
|
|
264
|
-
deps,
|
|
265
|
-
registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
|
|
266
|
-
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
267
|
-
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
|
|
268
|
-
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
|
|
269
|
-
};
|
|
270
|
-
async function handle(req, res) {
|
|
271
|
-
const startedAt = Date.now();
|
|
272
|
-
const method = req.method ?? "GET";
|
|
273
|
-
const url = (req.url ?? "/").split("?")[0];
|
|
274
|
-
noteActivity(url);
|
|
275
|
-
const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
|
|
276
|
-
if (admitted)
|
|
277
|
-
counters.admittedInflight++;
|
|
278
|
-
if (admitted)
|
|
279
|
-
openResponses.add(res);
|
|
280
|
-
let logged = false;
|
|
281
|
-
const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
|
|
282
|
-
const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
|
|
283
|
-
const record = (viaClose) => {
|
|
284
|
-
if (logged)
|
|
285
|
-
return;
|
|
286
|
-
logged = true;
|
|
287
|
-
if (admitted)
|
|
288
|
-
counters.admittedInflight--;
|
|
289
|
-
if (openResponses.delete(res) && isSseResponse(res))
|
|
290
|
-
lastAttachEndedAt = performance.now();
|
|
291
|
-
const route = routeLabel(method, url);
|
|
292
|
-
const seconds = (Date.now() - startedAt) / 1000;
|
|
293
|
-
const aborted = viaClose && !res.writableEnded;
|
|
294
|
-
const status = aborted ? "499" : String(res.statusCode);
|
|
295
|
-
deps.metrics?.inc("http_requests_total", { route, method, status });
|
|
296
|
-
deps.metrics?.observe("http_request_duration_seconds", seconds, { route });
|
|
297
|
-
const log = route === "/health" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
|
|
298
|
-
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 } : {}) } : {}) });
|
|
299
|
-
};
|
|
300
|
-
res.on("finish", () => record(false));
|
|
301
|
-
res.on("close", () => record(true));
|
|
302
|
-
cors(res, req, deps.config.corsOrigins, deps.config.principalHeader);
|
|
303
|
-
if (req.method === "OPTIONS") {
|
|
304
|
-
res.writeHead(204);
|
|
305
|
-
res.end();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (req.method === "GET" && url === "/health") {
|
|
309
|
-
const restart = deps.restartState?.();
|
|
310
|
-
const configHash = createHash("sha256")
|
|
311
|
-
.update(JSON.stringify({ model: deps.config.model.id, sessionBackend: deps.config.sessionBackend, dbBackend: deps.config.dbBackend, dataRoot: deps.config.localDataRoot, configProvider: deps.config.configProvider, remoteExec: deps.config.remoteExec?.provider, port: deps.config.port }))
|
|
312
|
-
.digest("hex")
|
|
313
|
-
.slice(0, 16);
|
|
314
|
-
sendJson(res, 200, {
|
|
315
|
-
status: "ok",
|
|
316
|
-
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
317
|
-
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}), ...(deps.drainState.reason ? { drainReason: deps.drainState.reason } : {}) } : {}),
|
|
318
|
-
...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
|
|
319
|
-
...(deps.storeDegraded ? { durable: false, storeDegraded: true } : {}),
|
|
320
|
-
...(deps.modelReady && !deps.modelReady() ? { ready: false, readyReason: "model roster pending (first effective-config pull)" } : {}),
|
|
321
|
-
model: deps.config.model.id,
|
|
322
|
-
sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
|
|
323
|
-
sessions: deps.runner.sessions.size,
|
|
324
|
-
...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
|
|
325
|
-
...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
|
|
326
|
-
configHash,
|
|
327
|
-
pid: process.pid,
|
|
328
|
-
startedAt: PROCESS_STARTED_AT_MS,
|
|
329
|
-
port: deps.config.port,
|
|
330
|
-
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
331
|
-
...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
|
|
332
|
-
...(() => {
|
|
333
|
-
const stuck = deps.planeDeferredState?.();
|
|
334
|
-
return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
|
|
335
|
-
})(),
|
|
336
|
-
...(() => {
|
|
337
|
-
const gen = deps.configApplyState?.();
|
|
338
|
-
if (!gen)
|
|
339
|
-
return {};
|
|
340
|
-
return {
|
|
341
|
-
configTargetVersion: gen.configTargetVersion,
|
|
342
|
-
configAppliedVersion: gen.configAppliedVersion,
|
|
343
|
-
configTargetOrdinal: gen.configTargetOrdinal,
|
|
344
|
-
configAppliedOrdinal: gen.configAppliedOrdinal,
|
|
345
|
-
...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
|
|
346
|
-
};
|
|
347
|
-
})(),
|
|
348
|
-
...(() => {
|
|
349
|
-
const probe = deps.storeLiveState?.();
|
|
350
|
-
if (!probe)
|
|
351
|
-
return {};
|
|
352
|
-
return {
|
|
353
|
-
...(probe.live ? {} : { storeLive: false }),
|
|
354
|
-
storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
|
|
355
|
-
};
|
|
356
|
-
})(),
|
|
357
|
-
});
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
361
|
-
if (!deps.metrics) {
|
|
362
|
-
sendError(res, 404, "feature.metrics_disabled", "metrics disabled");
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const { authToken: t, metricsToken: mt } = deps.config;
|
|
366
|
-
const anyT = Boolean(t) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
367
|
-
const devOpen = !anyT && !mt && !deps.config.requirePrincipal;
|
|
368
|
-
const ok = devOpen || (anyT && systemFor(req, deps.config) !== undefined) || (!!mt && authorized(req, mt));
|
|
369
|
-
if (!ok) {
|
|
370
|
-
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (url === "/metrics/summary") {
|
|
374
|
-
sendJson(res, 200, { model: deps.config.model.id, ...deps.metrics.summarize() });
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (url === "/metrics/plan-cache") {
|
|
378
|
-
sendJson(res, 200, { scopes: deps.planCacheProbe?.dump() ?? {} });
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
res.writeHead(200, { "content-type": "text/plain; version=0.0.4" });
|
|
382
|
-
res.end(deps.metrics.render());
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (deps.registryJwtVerifier) {
|
|
386
|
-
const authz = req.headers.authorization ?? "";
|
|
387
|
-
const rawBearer = /^Bearer\s/i.test(authz) ? authz.replace(/^Bearer\s+/i, "") : "";
|
|
388
|
-
if (rawBearer && looksLikeJwt(rawBearer)) {
|
|
389
|
-
const v = await deps.registryJwtVerifier.verify(rawBearer);
|
|
390
|
-
if (v.ok) {
|
|
391
|
-
setSsoPrincipal(req, v.identity.principal);
|
|
392
|
-
req.headers[deps.config.principalHeader.toLowerCase()] = v.identity.principal;
|
|
393
|
-
if (v.identity.scope)
|
|
394
|
-
setSsoScope(req, v.identity.scope);
|
|
395
|
-
deps.logger?.info?.("auth_bridge_principal_accepted", { principal: v.identity.principal, ...(v.identity.scope ? { scope: v.identity.scope } : {}) });
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (await handleTraceUsage(req, res, url, ctx))
|
|
400
|
-
return;
|
|
401
|
-
if (await handleWorkflows(req, res, url, ctx))
|
|
402
|
-
return;
|
|
403
|
-
if (await handleFleet(req, res, url, ctx))
|
|
404
|
-
return;
|
|
405
|
-
if (await handleA2aServe(req, res, url, ctx))
|
|
406
|
-
return;
|
|
407
|
-
let source = null;
|
|
408
|
-
const anyServiceAuth = hasServiceAuth(deps.config);
|
|
409
|
-
if (anyServiceAuth) {
|
|
410
|
-
const sys = systemFor(req, deps.config);
|
|
411
|
-
if (sys === undefined) {
|
|
412
|
-
if (ssoVerifiedPrincipal(req)) {
|
|
413
|
-
source = "sso";
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
source = sys;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
reqState.source = source;
|
|
425
|
-
if (!anyServiceAuth &&
|
|
426
|
-
!deps.config.allowUnauthedWrites &&
|
|
427
|
-
((req.method === "POST" && (isBillableSubmitPath(url) || url.startsWith("/v1/images/bakes"))) ||
|
|
428
|
-
isCredentialGatedRewrite(req.method ?? "", url))) {
|
|
429
|
-
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
|
|
433
|
-
res.setHeader("retry-after", "15");
|
|
434
|
-
sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance", ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}) });
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
|
|
438
|
-
res.setHeader("retry-after", "5");
|
|
439
|
-
sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
if (isDestructiveSessionWrite(req.method ?? "", url) && !anyServiceAuth && !deps.config.allowUnauthedWrites) {
|
|
443
|
-
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
if (await handleCapabilities(req, res, url, ctx))
|
|
447
|
-
return;
|
|
448
|
-
if (await handleSideQuery(req, res, url, ctx))
|
|
449
|
-
return;
|
|
450
|
-
if (await handleTasks(req, res, url, ctx))
|
|
451
|
-
return;
|
|
452
|
-
if (await handleRuns(req, res, url, ctx))
|
|
453
|
-
return;
|
|
454
|
-
if (await handleA2a(req, res, url, ctx))
|
|
455
|
-
return;
|
|
456
|
-
if (await handleWorkflowAgentSteer(req, res, url, ctx))
|
|
457
|
-
return;
|
|
458
|
-
if (await handleRunVerbs(req, res, url, ctx))
|
|
459
|
-
return;
|
|
460
|
-
if (await handleRunMemoryCapture(req, res, url, ctx))
|
|
461
|
-
return;
|
|
462
|
-
if (await handleLeader(req, res, url, ctx))
|
|
463
|
-
return;
|
|
464
|
-
if (await handleImages(req, res, url, ctx))
|
|
465
|
-
return;
|
|
466
|
-
if (await handleApprovalsAssistant(req, res, url, ctx))
|
|
467
|
-
return;
|
|
468
|
-
if (await handleObservability(req, res, url, ctx))
|
|
469
|
-
return;
|
|
470
|
-
if (await handleDiagnostics(req, res, url, ctx))
|
|
471
|
-
return;
|
|
472
|
-
if (await handleAdoption(req, res, url, ctx))
|
|
473
|
-
return;
|
|
474
|
-
if (await handleRetention(req, res, url, ctx))
|
|
475
|
-
return;
|
|
476
|
-
if (await handleAdminDrain(req, res, url, ctx))
|
|
477
|
-
return;
|
|
478
|
-
if (await handleDevices(req, res, url, ctx))
|
|
479
|
-
return;
|
|
480
|
-
if (await handleAdminConfigRefresh(req, res, url, ctx))
|
|
481
|
-
return;
|
|
482
|
-
if (await handleConfigCatalog(req, res, url, ctx))
|
|
483
|
-
return;
|
|
484
|
-
if (await handleMemoryOptOut(req, res, url, ctx))
|
|
485
|
-
return;
|
|
486
|
-
if (await handleMemoryPolicy(req, res, url, ctx))
|
|
487
|
-
return;
|
|
488
|
-
if (await handleMemoryBundle(req, res, url, ctx))
|
|
489
|
-
return;
|
|
490
|
-
if (await handleMemoryCompliance(req, res, url, ctx))
|
|
491
|
-
return;
|
|
492
|
-
if (await handleMemoryOrigin(req, res, url, ctx))
|
|
493
|
-
return;
|
|
494
|
-
if (deps.memoryConsolidation && (await handleMemoryConsolidation(req, res, url, ctx)))
|
|
495
|
-
return;
|
|
496
|
-
if (await handleRules(req, res, url, ctx))
|
|
497
|
-
return;
|
|
498
|
-
if (deps.sharedMemoryStore && deps.orgMemoryDirectory && (await handleSharedMemory(req, res, url, ctx)))
|
|
499
|
-
return;
|
|
500
|
-
if (await handleSessionsList(req, res, url, ctx))
|
|
501
|
-
return;
|
|
502
|
-
if (await handleAgentRoster(req, res, url, ctx))
|
|
503
|
-
return;
|
|
504
|
-
if (await handleSessions(req, res, url, ctx))
|
|
505
|
-
return;
|
|
506
|
-
if (await handleSessionSync(req, res, url, ctx))
|
|
507
|
-
return;
|
|
508
|
-
if (await handleAttachments(req, res, url, ctx))
|
|
509
|
-
return;
|
|
510
|
-
if (await handleNotifyWake(req, res, url, ctx))
|
|
511
|
-
return;
|
|
512
|
-
sendError(res, 404, "not_found.route", "not found");
|
|
513
|
-
}
|
|
514
116
|
function runnerFor(spec) {
|
|
515
117
|
return deps.runnerFor?.(spec) ?? deps.runner;
|
|
516
118
|
}
|
|
517
|
-
async function prepareSpec(req, res, bodyIn, onTypedFailure, legFacts) {
|
|
518
|
-
const body = bodyIn ?? (await readJson(req));
|
|
519
|
-
if (!body || typeof body.objective !== "string") {
|
|
520
|
-
sendError(res, 400, "request.field_invalid", "missing 'objective' string");
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
if (body.objective.trim().length === 0) {
|
|
524
|
-
sendError(res, 400, "request.field_invalid", "objective must not be empty or whitespace-only (an empty user message poisons the session history on strict providers)");
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
{
|
|
528
|
-
const bodyKeyIssue = taskBodyKeyIssue(body);
|
|
529
|
-
if (bodyKeyIssue) {
|
|
530
|
-
sendError(res, 400, "request.body_shape", bodyKeyIssue.message, { unknownKeys: bodyKeyIssue.unknownKeys, unsupportedKeys: bodyKeyIssue.unsupportedKeys });
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
|
|
535
|
-
sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
if (typeof body.sessionId === "string" && body.sessionId.length > 64) {
|
|
539
|
-
sendError(res, 400, "request.field_invalid", "sessionId must be at most 64 characters");
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
543
|
-
sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
if (body.appendSystemPrompt !== undefined) {
|
|
547
|
-
if (typeof body.appendSystemPrompt !== "string" || body.appendSystemPrompt.length === 0) {
|
|
548
|
-
sendError(res, 400, "request.field_invalid", "appendSystemPrompt must be a non-empty string");
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
if (body.appendSystemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
552
|
-
sendError(res, 400, "request.field_invalid", `appendSystemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (body.cwd !== undefined && !isValidCwd(body.cwd)) {
|
|
557
|
-
sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
const addDirsRaw = body.additionalDirectories;
|
|
561
|
-
if (addDirsRaw !== undefined) {
|
|
562
|
-
if (!Array.isArray(addDirsRaw)) {
|
|
563
|
-
sendError(res, 400, "request.field_invalid", "additionalDirectories must be an array of absolute host paths");
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
if (addDirsRaw.length > MAX_ADDITIONAL_DIRS) {
|
|
567
|
-
sendError(res, 400, "request.field_invalid", `additionalDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
const bad = addDirsRaw.find((d) => !isValidCwd(d));
|
|
571
|
-
if (bad !== undefined) {
|
|
572
|
-
sendError(res, 400, "request.field_invalid", "each additionalDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
const addReadDirsRaw = body.additionalReadDirectories;
|
|
577
|
-
if (addReadDirsRaw !== undefined) {
|
|
578
|
-
if (!Array.isArray(addReadDirsRaw)) {
|
|
579
|
-
sendError(res, 400, "request.field_invalid", "additionalReadDirectories must be an array of absolute host paths");
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
if (addReadDirsRaw.length > MAX_ADDITIONAL_DIRS) {
|
|
583
|
-
sendError(res, 400, "request.field_invalid", `additionalReadDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
const badRead = addReadDirsRaw.find((d) => !isValidCwd(d));
|
|
587
|
-
if (badRead !== undefined) {
|
|
588
|
-
sendError(res, 400, "request.field_invalid", "each additionalReadDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const settingsRaw = body.settings;
|
|
593
|
-
if (settingsRaw !== undefined && settingsRaw !== null) {
|
|
594
|
-
if (typeof settingsRaw !== "object" || Array.isArray(settingsRaw)) {
|
|
595
|
-
sendError(res, 400, "request.field_invalid", "settings must be an object");
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
const st = settingsRaw;
|
|
599
|
-
{
|
|
600
|
-
const keyIssue = taskSettingsKeyIssue(st);
|
|
601
|
-
if (keyIssue) {
|
|
602
|
-
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (typeof st.outputStyle === "string" && st.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
|
|
607
|
-
sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`);
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
if (typeof st.outputStyle === "string" &&
|
|
611
|
-
st.outputStyle.length > 0 &&
|
|
612
|
-
typeof body.appendSystemPrompt === "string" &&
|
|
613
|
-
body.appendSystemPrompt.length + 2 + st.outputStyle.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
614
|
-
sendError(res, 400, "request.field_conflict", `appendSystemPrompt + settings.outputStyle fold into one system-prompt block — combined they must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters (including the 2-char joiner)`);
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
const perms = st.permissions;
|
|
618
|
-
if (perms !== undefined && perms !== null && (typeof perms !== "object" || Array.isArray(perms))) {
|
|
619
|
-
sendError(res, 400, "request.field_invalid", "settings.permissions must be an object");
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
|
|
623
|
-
for (const k of ["allow", "deny", "ask"]) {
|
|
624
|
-
const arr = perms[k];
|
|
625
|
-
if (Array.isArray(arr) && arr.length > MAX_SETTINGS_PERMISSION_RULES) {
|
|
626
|
-
sendError(res, 400, "request.field_invalid", `settings.permissions.${k} must have at most ${MAX_SETTINGS_PERMISSION_RULES} entries`);
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
const envRaw = st.env;
|
|
632
|
-
if (envRaw !== undefined && envRaw !== null) {
|
|
633
|
-
if (typeof envRaw !== "object" || Array.isArray(envRaw)) {
|
|
634
|
-
sendError(res, 400, "request.field_invalid", "settings.env must be an object of string→string");
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
const entries = Object.entries(envRaw);
|
|
638
|
-
if (entries.length > MAX_SETTINGS_ENV_VARS) {
|
|
639
|
-
sendError(res, 400, "request.field_invalid", `settings.env must have at most ${MAX_SETTINGS_ENV_VARS} variables`);
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
for (const [k, val] of entries) {
|
|
643
|
-
if (k.length > MAX_SETTINGS_ENV_KEY_CHARS || (typeof val === "string" && val.length > MAX_SETTINGS_ENV_VALUE_CHARS)) {
|
|
644
|
-
sendError(res, 400, "request.field_invalid", `settings.env keys must be ≤${MAX_SETTINGS_ENV_KEY_CHARS} chars and values ≤${MAX_SETTINGS_ENV_VALUE_CHARS} chars`);
|
|
645
|
-
return null;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (st.ultracode !== undefined && typeof st.ultracode !== "boolean") {
|
|
650
|
-
sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
if (st.skillListingBudgetFraction !== undefined && !skillListingBudgetFractionOk(st.skillListingBudgetFraction)) {
|
|
654
|
-
sendError(res, 400, "request.field_invalid", "settings.skillListingBudgetFraction must be a number greater than 0 and at most 1");
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
if (st.skillListingMaxDescChars !== undefined && !skillListingMaxDescOk(st.skillListingMaxDescChars)) {
|
|
658
|
-
sendError(res, 400, "request.field_invalid", "settings.skillListingMaxDescChars must be a positive integer");
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
if (st.hooks !== undefined && st.hooks !== null) {
|
|
662
|
-
const hooksParsed = parseHooksConfig(st.hooks);
|
|
663
|
-
if (!hooksParsed.config) {
|
|
664
|
-
sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
const skillsErr = validateUserSkills(body.skills);
|
|
670
|
-
if (skillsErr) {
|
|
671
|
-
sendError(res, 400, "request.field_invalid", skillsErr);
|
|
672
|
-
return null;
|
|
673
|
-
}
|
|
674
|
-
if (Array.isArray(body.images)) {
|
|
675
|
-
if (body.images.length > MAX_IMAGES_PER_REQUEST) {
|
|
676
|
-
sendError(res, 413, "request.payload_too_large", `at most ${MAX_IMAGES_PER_REQUEST} images per request`);
|
|
677
|
-
return null;
|
|
678
|
-
}
|
|
679
|
-
for (const img of body.images) {
|
|
680
|
-
const data = img.data;
|
|
681
|
-
if (typeof data === "string" && Buffer.byteLength(data, "utf8") > MAX_IMAGE_BASE64_BYTES) {
|
|
682
|
-
sendError(res, 413, "request.payload_too_large", `each inline image must be at most ${MAX_IMAGE_BASE64_BYTES} base64 bytes`);
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (body.clientContext !== undefined) {
|
|
688
|
-
const cc = body.clientContext;
|
|
689
|
-
if (typeof cc !== "object" || cc === null || Array.isArray(cc)) {
|
|
690
|
-
sendError(res, 400, "request.field_invalid", "clientContext must be an object");
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
if (cc.timeZone !== undefined && (typeof cc.timeZone !== "string" || cc.timeZone.length > 64)) {
|
|
694
|
-
sendError(res, 400, "request.field_invalid", "clientContext.timeZone must be a string of at most 64 characters");
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
if (cc.userEmail !== undefined && (typeof cc.userEmail !== "string" || cc.userEmail.length > 256)) {
|
|
698
|
-
sendError(res, 400, "request.field_invalid", "clientContext.userEmail must be a string of at most 256 characters");
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (body.sandboxImageProfile !== undefined && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0 || body.sandboxImageProfile.length > 128)) {
|
|
703
|
-
sendError(res, 400, "request.field_invalid", "sandboxImageProfile must be a non-empty string of at most 128 characters");
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
if (body.capabilitiesNeeded !== undefined && (!Array.isArray(body.capabilitiesNeeded) || body.capabilitiesNeeded.some((c) => typeof c !== "string"))) {
|
|
707
|
-
sendError(res, 400, "request.field_invalid", "capabilitiesNeeded must be an array of strings");
|
|
708
|
-
return null;
|
|
709
|
-
}
|
|
710
|
-
if (Array.isArray(body.capabilitiesNeeded) && body.capabilitiesNeeded.length > 0 && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0)) {
|
|
711
|
-
sendError(res, 400, "request.field_conflict", "capabilitiesNeeded requires sandboxImageProfile (it constrains the selected image)");
|
|
712
|
-
return null;
|
|
713
|
-
}
|
|
714
|
-
if (typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 && (body.cascade === true || body.verify === true)) {
|
|
715
|
-
sendError(res, 400, "request.field_conflict", "sandboxImageProfile is not supported together with cascade/verify in v1 (the per-task image would not bind to the cascade rungs / verifier sub-run)");
|
|
716
|
-
return null;
|
|
717
|
-
}
|
|
718
|
-
if (body.outputSchema !== undefined) {
|
|
719
|
-
if (typeof body.outputSchema !== "object" || body.outputSchema === null || Array.isArray(body.outputSchema)) {
|
|
720
|
-
sendError(res, 400, "request.field_invalid", "outputSchema must be a JSON Schema object");
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
if (JSON.stringify(body.outputSchema).length > MAX_OUTPUT_SCHEMA_CHARS) {
|
|
724
|
-
sendError(res, 400, "request.field_invalid", `outputSchema must serialize to at most ${MAX_OUTPUT_SCHEMA_CHARS} characters`);
|
|
725
|
-
return null;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (body.reasoningEffort !== undefined && !isThinkingLevel(body.reasoningEffort)) {
|
|
729
|
-
sendError(res, 400, "request.field_invalid", "reasoningEffort must be one of: off, minimal, low, medium, high, xhigh, max");
|
|
730
|
-
return null;
|
|
731
|
-
}
|
|
732
|
-
if (body.resumeAt !== undefined) {
|
|
733
|
-
if (typeof body.resumeAt !== "string" || body.resumeAt.length === 0 || body.resumeAt.length > 64) {
|
|
734
|
-
sendError(res, 400, "request.field_invalid", "resumeAt must be a non-empty message eventId string (≤64 chars)");
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
if (body.verify === true || body.cascade === true) {
|
|
738
|
-
sendError(res, 400, "request.field_conflict", "resumeAt is not supported with verify or cascade (those start a fresh/replaced session)");
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
if (body.resumeAtMode !== undefined) {
|
|
743
|
-
if (body.resumeAtMode !== "at" && body.resumeAtMode !== "before") {
|
|
744
|
-
sendError(res, 400, "request.field_invalid", 'resumeAtMode must be "at" or "before"');
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
if (body.resumeAt === undefined) {
|
|
748
|
-
sendError(res, 400, "request.field_conflict", "resumeAtMode requires resumeAt (it qualifies the rewind target)");
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (body.suggestNextPrompts !== undefined && typeof body.suggestNextPrompts !== "boolean") {
|
|
753
|
-
const s = body.suggestNextPrompts;
|
|
754
|
-
if (typeof s !== "object" || s === null || Array.isArray(s)) {
|
|
755
|
-
sendError(res, 400, "request.field_invalid", "suggestNextPrompts must be a boolean or an object { count?, role? }");
|
|
756
|
-
return null;
|
|
757
|
-
}
|
|
758
|
-
const o = s;
|
|
759
|
-
if (o.count !== undefined && (typeof o.count !== "number" || !Number.isFinite(o.count) || o.count < 1)) {
|
|
760
|
-
sendError(res, 400, "request.field_invalid", "suggestNextPrompts.count must be a positive number");
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
if (o.role !== undefined && typeof o.role !== "string") {
|
|
764
|
-
sendError(res, 400, "request.field_invalid", "suggestNextPrompts.role must be a string (a model role name)");
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
if (body.rewindFiles !== undefined && typeof body.rewindFiles !== "boolean") {
|
|
769
|
-
sendError(res, 400, "request.field_invalid", "rewindFiles must be a boolean");
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
if (body.restoreFiles !== undefined && typeof body.restoreFiles !== "boolean") {
|
|
773
|
-
sendError(res, 400, "request.field_invalid", "restoreFiles must be a boolean");
|
|
774
|
-
return null;
|
|
775
|
-
}
|
|
776
|
-
if (body.acceptPartialRestore !== undefined && typeof body.acceptPartialRestore !== "boolean") {
|
|
777
|
-
sendError(res, 400, "request.field_invalid", "acceptPartialRestore must be a boolean");
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
if (body.memoryWrite !== undefined && typeof body.memoryWrite !== "boolean") {
|
|
781
|
-
sendError(res, 400, "request.field_invalid", "memoryWrite must be a boolean (false = pause memory writes for this run)");
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
if (body.memoryCapture !== undefined && body.memoryCapture !== "off") {
|
|
785
|
-
sendError(res, 400, "request.field_invalid", "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)");
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
|
|
789
|
-
sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
if (body.enableFork !== undefined && typeof body.enableFork !== "boolean") {
|
|
793
|
-
sendError(res, 400, "request.field_invalid", "enableFork must be a boolean");
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
796
|
-
if (body.selfOrchestration !== undefined && typeof body.selfOrchestration !== "boolean") {
|
|
797
|
-
sendError(res, 400, "request.field_invalid", "selfOrchestration must be a boolean");
|
|
798
|
-
return null;
|
|
799
|
-
}
|
|
800
|
-
{
|
|
801
|
-
const denial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
|
|
802
|
-
const wantsSelfOrchestration = body.selfOrchestration === true
|
|
803
|
-
|| parseTaskSettings(body.settings).settings?.ultracode === true;
|
|
804
|
-
if (denial !== null && wantsSelfOrchestration) {
|
|
805
|
-
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\"");
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
if (body.resilience !== undefined) {
|
|
810
|
-
const r = body.resilience;
|
|
811
|
-
if (typeof r !== "object" || r === null || Array.isArray(r)) {
|
|
812
|
-
sendError(res, 400, "request.field_invalid", "resilience must be an object { allowDegrade?, allowFailover?, bypassBreaker? } (booleans)");
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
for (const k of ["allowDegrade", "allowFailover", "bypassBreaker"]) {
|
|
816
|
-
const v = r[k];
|
|
817
|
-
if (v !== undefined && typeof v !== "boolean") {
|
|
818
|
-
sendError(res, 400, "request.field_invalid", `resilience.${k} must be a boolean`);
|
|
819
|
-
return null;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
if (body.finalVerification !== undefined && typeof body.finalVerification !== "boolean") {
|
|
824
|
-
sendError(res, 400, "request.field_invalid", "finalVerification must be a boolean");
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
|
-
if (body.limits !== undefined) {
|
|
828
|
-
const l = body.limits;
|
|
829
|
-
if (typeof l !== "object" || l === null || Array.isArray(l)) {
|
|
830
|
-
sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
|
|
831
|
-
return null;
|
|
832
|
-
}
|
|
833
|
-
if (l.timeoutSec !== undefined) {
|
|
834
|
-
sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
|
|
835
|
-
return null;
|
|
836
|
-
}
|
|
837
|
-
if (l.deadlineNudge !== undefined) {
|
|
838
|
-
sendError(res, 400, "request.field_invalid", "limits.deadlineNudge was retired in core 5.8.0 — use limits.approachNotice (default on at 80%/95%; false turns it off, {at: [r1, r2]} tunes thresholds)");
|
|
839
|
-
return null;
|
|
840
|
-
}
|
|
841
|
-
for (const k of ["callCapByDeadline", "gracefulFinalize"]) {
|
|
842
|
-
if (l[k] !== undefined) {
|
|
843
|
-
sendError(res, 400, "request.field_invalid", `limits.${k} was retired in core 5.8.0 (the deadline-pacing mechanism family is gone; there is no replacement key — remove it)`);
|
|
844
|
-
return null;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
for (const k of ["maxCostUsd", "maxTokens"]) {
|
|
848
|
-
if (l[k] !== undefined) {
|
|
849
|
-
sendError(res, 400, "request.field_invalid", `limits.${k} is not read from limits here — send it at the request body top level (${k}); it is capped by the operator ceiling ${k === "maxCostUsd" ? "MAX_TASK_COST_USD" : "MAX_TASK_TOKENS"}`);
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (l.degrade !== undefined) {
|
|
854
|
-
sendError(res, 400, "request.field_invalid", "limits.degrade is operator-controlled (env MODEL_DEGRADE_TO / MODEL_DEGRADE_AT_COST_FRACTION), not caller-settable — remove it");
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
857
|
-
if (l.budgetStreamCancel !== undefined) {
|
|
858
|
-
sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
862
|
-
const v = l[k];
|
|
863
|
-
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
864
|
-
sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer${k === "maxWalltimeMs" ? " (milliseconds)" : ""}`);
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
{
|
|
869
|
-
const v = l.approachNotice;
|
|
870
|
-
if (v !== undefined && normalizeApproachNotice(v) === undefined) {
|
|
871
|
-
sendError(res, 400, "request.field_invalid", "limits.approachNotice accepts false (turn off) or {at?: [r1, r2]} with fractions in (0, 1], r1 <= r2 (default on at 80%/95%)");
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
if (body.agents !== undefined) {
|
|
877
|
-
const agentsErr = validateTaskAgents(body.agents);
|
|
878
|
-
if (agentsErr) {
|
|
879
|
-
sendError(res, 400, "request.field_invalid", agentsErr);
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
const agentsBare = deps.config.models ?? {};
|
|
883
|
-
const agentsCatalog = expandTiers(agentsBare, deps.config.tiers ?? {}) ?? agentsBare;
|
|
884
|
-
const um = unknownTaskAgentModelRef(body.agents, agentsCatalog);
|
|
885
|
-
if (um !== undefined) {
|
|
886
|
-
sendError(res, 400, "request.unknown_reference", `unknown agents[${um.index}].model "${um.ref.slice(0, 120)}" — not in the configured catalog (a catalog name or tier word)`, { available: mentionableNames(deps.config, agentsCatalog) });
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
if (body.interactiveTools !== undefined && typeof body.interactiveTools !== "boolean") {
|
|
891
|
-
sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
|
|
895
|
-
sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
|
|
896
|
-
return null;
|
|
897
|
-
}
|
|
898
|
-
if (body.retainBackgroundProcesses !== undefined && typeof body.retainBackgroundProcesses !== "boolean") {
|
|
899
|
-
sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
for (const key of ["excludeTools", "deferTools"]) {
|
|
903
|
-
const v = body[key];
|
|
904
|
-
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
905
|
-
sendError(res, 400, "request.field_invalid", `${key} must be an array of non-empty tool-name strings`);
|
|
906
|
-
return null;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
{
|
|
910
|
-
const v = body.promptProfile;
|
|
911
|
-
if (v !== undefined && v !== "simple" && v !== "classic") {
|
|
912
|
-
sendError(res, 400, "request.field_invalid", 'promptProfile must be "simple" or "classic"');
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
{
|
|
917
|
-
const v = body.toolMaterializeStrategy;
|
|
918
|
-
if (v !== undefined && v !== "static" && v !== "swap") {
|
|
919
|
-
sendError(res, 400, "request.field_invalid", 'toolMaterializeStrategy must be "static" or "swap"');
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
{
|
|
924
|
-
const v = body.permissionMode;
|
|
925
|
-
if (v !== undefined && v !== "default" && v !== "acceptEdits" && v !== "plan" && v !== "bypassPermissions" && v !== "auto") {
|
|
926
|
-
sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
927
|
-
return null;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
{
|
|
931
|
-
const s = body.settings;
|
|
932
|
-
if (s !== null && typeof s === "object" && !Array.isArray(s)) {
|
|
933
|
-
const p = s.permissions;
|
|
934
|
-
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
935
|
-
const m = p.defaultMode;
|
|
936
|
-
if (m !== undefined && m !== "default" && m !== "acceptEdits" && m !== "plan" && m !== "bypassPermissions" && m !== "auto") {
|
|
937
|
-
sendError(res, 400, "request.field_invalid", 'settings.permissions.defaultMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
938
|
-
return null;
|
|
939
|
-
}
|
|
940
|
-
const d = p.disableAutoMode;
|
|
941
|
-
if (d !== undefined && d !== "disable" && d !== true && d !== false) {
|
|
942
|
-
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)');
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (body.model !== undefined) {
|
|
949
|
-
if (typeof body.model !== "string" || body.model.length === 0) {
|
|
950
|
-
sendError(res, 400, "request.field_invalid", "model must be a non-empty string (a configured catalog name, tier word, or model id)");
|
|
951
|
-
return null;
|
|
952
|
-
}
|
|
953
|
-
const bare = deps.config.models ?? {};
|
|
954
|
-
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
955
|
-
if (matchCatalogModel(body.model, catalog) === undefined) {
|
|
956
|
-
sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
if (body.compactionModel !== undefined) {
|
|
961
|
-
if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0) {
|
|
962
|
-
sendError(res, 400, "request.field_invalid", "compactionModel must be a non-empty string (a configured catalog name, tier word, or model id)");
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
const bare = deps.config.models ?? {};
|
|
966
|
-
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
967
|
-
if (matchCatalogModel(body.compactionModel, catalog) === undefined) {
|
|
968
|
-
sendError(res, 400, "request.unknown_reference", `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
if (body.attachments !== undefined) {
|
|
973
|
-
const a = body.attachments;
|
|
974
|
-
if (typeof a !== "object" || a === null || Array.isArray(a)) {
|
|
975
|
-
sendError(res, 400, "request.field_invalid", "attachments must be an object { todoReminder?, todoReminderMode?, changedFiles?, planModeReminder?, budgetUsd?, backgroundTasks?, toolsDelta?, agentListing?, skillsListing?, mcpInstructions? }");
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
const rec = a;
|
|
979
|
-
for (const k of ["todoReminder", "planModeReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "agentListing", "skillsListing", "mcpInstructions"]) {
|
|
980
|
-
if (rec[k] !== undefined && typeof rec[k] !== "boolean") {
|
|
981
|
-
sendError(res, 400, "request.field_invalid", `attachments.${k} must be a boolean`);
|
|
982
|
-
return null;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
if (rec.todoReminderMode !== undefined && rec.todoReminderMode !== "baseline" && rec.todoReminderMode !== "off") {
|
|
986
|
-
sendError(res, 400, "request.field_invalid", 'attachments.todoReminderMode must be "baseline" | "off"');
|
|
987
|
-
return null;
|
|
988
|
-
}
|
|
989
|
-
const cf = rec.changedFiles;
|
|
990
|
-
if (cf !== undefined && typeof cf !== "boolean") {
|
|
991
|
-
if (typeof cf !== "object" || cf === null || Array.isArray(cf)) {
|
|
992
|
-
sendError(res, 400, "request.field_invalid", "attachments.changedFiles must be a boolean or { maxFiles?: number }");
|
|
993
|
-
return null;
|
|
994
|
-
}
|
|
995
|
-
const mf = cf.maxFiles;
|
|
996
|
-
if (mf !== undefined && (typeof mf !== "number" || !Number.isFinite(mf) || mf < 1)) {
|
|
997
|
-
sendError(res, 400, "request.field_invalid", "attachments.changedFiles.maxFiles must be a number ≥ 1");
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
const verify = verifyRoundsFromBody(body);
|
|
1003
|
-
const cascade = body.cascade === true;
|
|
1004
|
-
if (cascade && verify) {
|
|
1005
|
-
sendError(res, 400, "request.field_conflict", "verify and cascade are mutually exclusive (verify = adversarial gate; cascade = cheap→strong ladder)");
|
|
1006
|
-
return null;
|
|
1007
|
-
}
|
|
1008
|
-
if (body.suggestNextPrompts && (verify || cascade)) {
|
|
1009
|
-
sendError(res, 400, "request.field_conflict", "suggestNextPrompts is not supported with verify or cascade (those return a result, not a streamed run)");
|
|
1010
|
-
return null;
|
|
1011
|
-
}
|
|
1012
|
-
if (cascade && deps.config.cascadeLadder.length === 0) {
|
|
1013
|
-
sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
|
|
1014
|
-
return null;
|
|
1015
|
-
}
|
|
1016
|
-
try {
|
|
1017
|
-
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
1018
|
-
const engineNotices = [];
|
|
1019
|
-
const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
|
|
1020
|
-
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body, ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
1021
|
-
}
|
|
1022
|
-
catch (err) {
|
|
1023
|
-
if (err instanceof HttpError) {
|
|
1024
|
-
if (onTypedFailure) {
|
|
1025
|
-
onTypedFailure(err);
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
throw err;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
1035
|
-
async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
|
|
1036
|
-
const cs = deps.checkpointStore;
|
|
1037
|
-
const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
1038
|
-
if (binding?.checkpointToken && binding.checkpointToken !== token) {
|
|
1039
|
-
let seenUnreadable = false;
|
|
1040
|
-
const seen = await cs.get(binding.checkpointToken).catch((e) => {
|
|
1041
|
-
seenUnreadable = true;
|
|
1042
|
-
recordFailOpen("server.approvals.stale-echoed-token-unreadable", `session=${sessionId} err=${e instanceof Error ? e.message : String(e)}`);
|
|
1043
|
-
return null;
|
|
1044
|
-
});
|
|
1045
|
-
const attributable = seen !== null && seen.sessionId === sessionId;
|
|
1046
|
-
if (!attributable && seen !== null) {
|
|
1047
|
-
deps.logger?.warn?.("approval_stale_token_foreign", { sessionId, tokenSession: seen.sessionId ?? null });
|
|
1048
|
-
}
|
|
1049
|
-
if (!token && !attributable && !seenUnreadable) {
|
|
1050
|
-
deps.logger?.info?.("approval_decide_no_pending", { sessionId, staleTokenEchoed: true });
|
|
1051
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
1052
|
-
}
|
|
1053
|
-
let currentPending;
|
|
1054
|
-
if (token && decider !== undefined) {
|
|
1055
|
-
const cur = await cs.get(token).catch((e) => {
|
|
1056
|
-
recordFailOpen("server.approvals.stale-current-pending-unreadable", `session=${sessionId} err=${e instanceof Error ? e.message : String(e)}`);
|
|
1057
|
-
return null;
|
|
1058
|
-
});
|
|
1059
|
-
const curPa = cur?.pendingAction;
|
|
1060
|
-
const curOwner = cur ? decodeCheckpointScope(cur.scope) : undefined;
|
|
1061
|
-
const scopeOk = cur !== null && (decider.explicitOperator || curOwner === undefined || curOwner === decider.principal);
|
|
1062
|
-
if (cur && cur.sessionId === sessionId && !scopeOk) {
|
|
1063
|
-
deps.logger?.info?.("approval_decide_no_pending", { sessionId, staleTokenEchoed: true, foreignCurrentSuppressed: true });
|
|
1064
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
1065
|
-
}
|
|
1066
|
-
if (cur && cur.status === "pending" && cur.sessionId === sessionId && scopeOk && curPa?.kind === "tool_approval" && curPa.toolCallId) {
|
|
1067
|
-
currentPending = {
|
|
1068
|
-
toolName: curPa.toolName,
|
|
1069
|
-
boundCallId: curPa.toolCallId,
|
|
1070
|
-
...(curPa.boundInputHash ? { boundInputHash: curPa.boundInputHash } : {}),
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
return {
|
|
1075
|
-
status: 409,
|
|
1076
|
-
body: {
|
|
1077
|
-
error: "the approval you are deciding is no longer current (already resolved or superseded since you viewed it)",
|
|
1078
|
-
errorCode: "approval_stale",
|
|
1079
|
-
...(attributable ? { terminal: "resolved" } : {}),
|
|
1080
|
-
...(currentPending ? { currentPending } : {}),
|
|
1081
|
-
},
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
if (!token) {
|
|
1085
|
-
deps.logger?.info?.("approval_decide_no_pending", { sessionId });
|
|
1086
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
1087
|
-
}
|
|
1088
|
-
const cpPromise = cs.get(token);
|
|
1089
|
-
const ctxPromise = cs.getCtx(sessionId);
|
|
1090
|
-
void ctxPromise.catch(() => { });
|
|
1091
|
-
const noApproval = { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
1092
|
-
let cp;
|
|
1093
|
-
try {
|
|
1094
|
-
cp = await cpPromise;
|
|
1095
|
-
}
|
|
1096
|
-
catch (e) {
|
|
1097
|
-
void ctxPromise.catch(() => { });
|
|
1098
|
-
if (!(await mayDiscloseRowError(cs, token, decider))) {
|
|
1099
|
-
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" });
|
|
1100
|
-
return noApproval;
|
|
1101
|
-
}
|
|
1102
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
1103
|
-
}
|
|
1104
|
-
if (!cp) {
|
|
1105
|
-
if (!(await mayDiscloseRowError(cs, token, decider)))
|
|
1106
|
-
return noApproval;
|
|
1107
|
-
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1108
|
-
}
|
|
1109
|
-
if (decider !== undefined && !decider.explicitOperator) {
|
|
1110
|
-
const cpOwner = decodeCheckpointScope(cp.scope);
|
|
1111
|
-
if (cpOwner !== undefined && cpOwner !== decider.principal) {
|
|
1112
|
-
deps.logger?.warn?.("approval_decide_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1113
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
const decideGateKind = cp.gate?.kind;
|
|
1117
|
-
if (!isApprovalGateKind(decideGateKind)) {
|
|
1118
|
-
const activeForPath = decideGateKind
|
|
1119
|
-
? await deps.runStore?.getActiveTaskId(sessionId).catch(() => undefined)
|
|
1120
|
-
: undefined;
|
|
1121
|
-
const gnPath = decideGateKind && activeForPath
|
|
1122
|
-
? resumeEntryForGate(decideGateKind, { sessionId, taskId: activeForPath })
|
|
1123
|
-
: null;
|
|
1124
|
-
return {
|
|
1125
|
-
status: 409,
|
|
1126
|
-
body: {
|
|
1127
|
-
error: `task is suspended on a '${decideGateKind ?? "unknown"}' gate, not a tool approval — a plan_review gate is resolved via POST /v1/assistant/tasks/:id/plan_review, a resource_limit suspension via POST /v1/assistant/tasks/:id/resume`,
|
|
1128
|
-
errorCode: "gate_not_tool_approval",
|
|
1129
|
-
...(decideGateKind ? { pendingGate: { kind: decideGateKind, ...(gnPath ? { decidePath: gnPath } : {}) } } : {}),
|
|
1130
|
-
},
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
if (req !== undefined && !binding?.checkpointToken && !binding?.boundCallId) {
|
|
1134
|
-
return {
|
|
1135
|
-
status: 400,
|
|
1136
|
-
body: {
|
|
1137
|
-
error: "decide requires the decision-action binding — echo checkpointToken and/or boundCallId from the pending approval (GET /v1/approvals)",
|
|
1138
|
-
errorCode: "decide_requires_binding",
|
|
1139
|
-
bindingEnforced: true,
|
|
1140
|
-
},
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
const s57RowCallId = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
1144
|
-
if (binding?.boundCallId !== undefined && s57RowCallId && binding.boundCallId !== s57RowCallId) {
|
|
1145
|
-
let s57CurrentPending;
|
|
1146
|
-
if (decider !== undefined && cp.status === "pending" && cp.sessionId === sessionId && cp.pendingAction.kind === "tool_approval") {
|
|
1147
|
-
s57CurrentPending = {
|
|
1148
|
-
toolName: cp.pendingAction.toolName,
|
|
1149
|
-
boundCallId: s57RowCallId,
|
|
1150
|
-
...(cp.pendingAction.boundInputHash ? { boundInputHash: cp.pendingAction.boundInputHash } : {}),
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
return {
|
|
1154
|
-
status: 409,
|
|
1155
|
-
body: {
|
|
1156
|
-
error: "the approval you are deciding is no longer current (already resolved or superseded since you viewed it)",
|
|
1157
|
-
errorCode: "approval_stale",
|
|
1158
|
-
...(s57CurrentPending ? { currentPending: s57CurrentPending } : {}),
|
|
1159
|
-
},
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
const paForGrant = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction : undefined;
|
|
1163
|
-
const loadedGrantTool = paForGrant !== undefined && paForGrant.toolName && paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME
|
|
1164
|
-
? paForGrant.toolName
|
|
1165
|
-
: null;
|
|
1166
|
-
const boundGrant = onResumeCommitted
|
|
1167
|
-
? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), validatedToolName: loadedGrantTool })
|
|
1168
|
-
: undefined;
|
|
1169
|
-
const lateSettleSink = deps.approvalLateSettle;
|
|
1170
|
-
const lateSettleAnchor = lateSettleSink !== undefined && req !== undefined && token
|
|
1171
|
-
? {
|
|
1172
|
-
sessionId: cp.sessionId,
|
|
1173
|
-
...(paForGrant?.toolCallId !== undefined ? { toolCallId: paForGrant.toolCallId } : {}),
|
|
1174
|
-
checkpointTokenHash: buildCheckpointAnchorHash(token),
|
|
1175
|
-
}
|
|
1176
|
-
: undefined;
|
|
1177
|
-
const noteLateSettleIfAccepted = (out) => {
|
|
1178
|
-
if (lateSettleSink === undefined || lateSettleAnchor === undefined || out.status !== 200)
|
|
1179
|
-
return;
|
|
1180
|
-
if (out.body?.errorCode === "cancelled")
|
|
1181
|
-
return;
|
|
1182
|
-
try {
|
|
1183
|
-
lateSettleSink.recordLateSettle({ ...lateSettleAnchor, outcome: decision === "approve" ? "allowed" : "denied", decidedBy: hostDecision.decidedBy, ...(hostDecision.approver !== undefined ? { approver: hostDecision.approver } : {}), settledAtMs: Date.now() });
|
|
1184
|
-
}
|
|
1185
|
-
catch (e) {
|
|
1186
|
-
recordFailOpen("server.approval-ask-audit.append-failed", `kind=late_settle err=${e instanceof Error ? e.message : String(e)}`);
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
const parkedForkQuotaDenial = req !== undefined ? ownerCostQuotaDenied(decodeCheckpointScope(cp.scope)) : undefined;
|
|
1190
|
-
if (parkedForkQuotaDenial)
|
|
1191
|
-
return parkedForkQuotaDenial;
|
|
1192
|
-
const parkedForkLeaseDenial = req !== undefined ? await ownerFleetLeaseDenied(decodeCheckpointScope(cp.scope)) : undefined;
|
|
1193
|
-
if (parkedForkLeaseDenial)
|
|
1194
|
-
return parkedForkLeaseDenial;
|
|
1195
|
-
const ownerLeaseAdmittedHere = req !== undefined;
|
|
1196
|
-
if (req !== undefined) {
|
|
1197
|
-
const parkedAgentStore = deps.backgroundAgentStore;
|
|
1198
|
-
const parkedTool = deps.parkedReviveTool;
|
|
1199
|
-
const parkWarn = (event, fields) => deps.logger?.warn?.(event, fields);
|
|
1200
|
-
const origin = await findParkOrigin({
|
|
1201
|
-
...(parkedAgentStore !== undefined && parkedTool !== undefined ? { agentStore: parkedAgentStore } : {}),
|
|
1202
|
-
...(deps.workflowAgentSessionIndex !== undefined && deps.workflowRunStore !== undefined
|
|
1203
|
-
? { workflowIndex: deps.workflowAgentSessionIndex, workflowRunStore: deps.workflowRunStore }
|
|
1204
|
-
: {}),
|
|
1205
|
-
...(deps.logger ? { warn: parkWarn } : {}),
|
|
1206
|
-
}, { token, scope: cp.scope, sessionId: cp.sessionId });
|
|
1207
|
-
const parkedReq = {
|
|
1208
|
-
token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
|
|
1209
|
-
decision, hostDecision, ...(reason !== undefined ? { reason } : {}),
|
|
1210
|
-
...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
|
|
1211
|
-
...(answer !== undefined ? { answer } : {}),
|
|
1212
|
-
...(boundGrant ? { grantRemember: (rootSessionId) => boundGrant(rootSessionId) } : {}),
|
|
1213
|
-
};
|
|
1214
|
-
if (origin?.kind === "background" && parkedAgentStore !== undefined && parkedTool !== undefined) {
|
|
1215
|
-
const parked = await withPrincipal(decodeCheckpointScope(cp.scope), () => decideParkedAgent({
|
|
1216
|
-
agentStore: parkedAgentStore,
|
|
1217
|
-
checkpointStore: cs,
|
|
1218
|
-
registry: defaultTaskRegistry,
|
|
1219
|
-
reviveTool: parkedTool,
|
|
1220
|
-
...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
|
|
1221
|
-
...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
|
|
1222
|
-
...(deps.logger ? { warn: parkWarn } : {}),
|
|
1223
|
-
}, parkedReq, { handle: origin.handle, row: origin.row }));
|
|
1224
|
-
void ctxPromise.catch(() => { });
|
|
1225
|
-
noteLateSettleIfAccepted(parked);
|
|
1226
|
-
return parked;
|
|
1227
|
-
}
|
|
1228
|
-
if (origin?.kind === "workflow_parks_unreadable") {
|
|
1229
|
-
void ctxPromise.catch(() => { });
|
|
1230
|
-
return denyWorkflowParksUnreadable(origin);
|
|
1231
|
-
}
|
|
1232
|
-
if (origin?.kind === "workflow") {
|
|
1233
|
-
void ctxPromise.catch(() => { });
|
|
1234
|
-
const plan = planWorkflowParkedDecide(origin, parkedReq);
|
|
1235
|
-
if (!plan.ok)
|
|
1236
|
-
return plan.deny;
|
|
1237
|
-
const parkedResume = { workflowParkedResume: [{ runId: plan.runId, token, outcome: plan.outcome }] };
|
|
1238
|
-
const hostToken = await cs
|
|
1239
|
-
.findPendingTokenBySession(plan.hostSessionId, undefined, { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" })
|
|
1240
|
-
.catch(() => undefined);
|
|
1241
|
-
const admittedPayer = decodeCheckpointScope(cp.scope);
|
|
1242
|
-
return hostToken
|
|
1243
|
-
? await resumeWake(plan.hostSessionId, plan.message, decider?.principal, req, acceptEarly, parkedResume.workflowParkedResume, admittedPayer)
|
|
1244
|
-
: await startFreshHostTurn(plan.hostSessionId, plan.message, admittedPayer, req, parkedResume, plan.runId);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
let ctx;
|
|
1248
|
-
try {
|
|
1249
|
-
ctx = await ctxPromise;
|
|
1250
|
-
}
|
|
1251
|
-
catch (e) {
|
|
1252
|
-
return { status: 409, body: { error: `resume context load failed: ${e instanceof Error ? e.message : String(e)}`, errorCode: "conflict.resume_context_unavailable" } };
|
|
1253
|
-
}
|
|
1254
|
-
if (!ctx)
|
|
1255
|
-
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1256
|
-
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1257
|
-
let spec;
|
|
1258
|
-
let engineNotices = [];
|
|
1259
|
-
try {
|
|
1260
|
-
({ spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth));
|
|
1261
|
-
}
|
|
1262
|
-
catch (e) {
|
|
1263
|
-
if (e instanceof HttpError) {
|
|
1264
|
-
return {
|
|
1265
|
-
status: 409,
|
|
1266
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
throw e;
|
|
1270
|
-
}
|
|
1271
|
-
const pendingTool = cp.pendingAction.toolName;
|
|
1272
|
-
if (decision === "approve" && pendingTool === ASK_USER_QUESTION_TOOL_NAME && !answer) {
|
|
1273
|
-
return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
|
|
1274
|
-
}
|
|
1275
|
-
if (answer && (decision !== "approve" || pendingTool !== ASK_USER_QUESTION_TOOL_NAME)) {
|
|
1276
|
-
return { status: 400, body: { error: `body.answer is only valid when approving a pending AskUserQuestion (decision: "${decision}", pending: "${pendingTool ?? "unknown"}")`, errorCode: "request.field_conflict" } };
|
|
1277
|
-
}
|
|
1278
|
-
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1279
|
-
const pa = cp.pendingAction;
|
|
1280
|
-
const persistedCallId = pa.kind === "tool_approval" ? pa.toolCallId : "";
|
|
1281
|
-
const persistedHash = pa.kind === "tool_approval" ? (pa.boundInputHash ?? "") : "";
|
|
1282
|
-
const outcome = {
|
|
1283
|
-
gate: "policy_ask",
|
|
1284
|
-
boundCallId: binding?.boundCallId ?? persistedCallId,
|
|
1285
|
-
boundInputHash: binding?.boundInputHash ?? persistedHash,
|
|
1286
|
-
decision: decision === "approve" ? "allow" : "deny",
|
|
1287
|
-
...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
|
|
1288
|
-
...(reason ? { reason } : {}),
|
|
1289
|
-
...(answer !== undefined ? { answer } : {}),
|
|
1290
|
-
hostDecision,
|
|
1291
|
-
};
|
|
1292
|
-
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1293
|
-
const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
|
|
1294
|
-
noteLateSettleIfAccepted(driven);
|
|
1295
|
-
return driven;
|
|
1296
|
-
}
|
|
1297
|
-
const ONE_SHOT_HISTORY_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "rewindFiles"];
|
|
1298
|
-
const ONE_SHOT_HISTORY_STRIP = Object.fromEntries(ONE_SHOT_HISTORY_KEYS.map((k) => [k, undefined]));
|
|
1299
|
-
const resolveResumeSpec = async (body, req, auth) => {
|
|
1300
|
-
const engineNotices = [];
|
|
1301
|
-
const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
|
|
1302
|
-
return { spec, engineNotices };
|
|
1303
|
-
};
|
|
1304
|
-
async function driveResumeIntoRunLog(args) {
|
|
1305
|
-
if (args.acceptEarly !== true)
|
|
1306
|
-
return await driveResumeLeg(args);
|
|
1307
|
-
let settleAccepted;
|
|
1308
|
-
let wasAccepted = false;
|
|
1309
|
-
const accepted = new Promise((resolve) => { settleAccepted = resolve; });
|
|
1310
|
-
const leg = driveResumeLeg(args, (receipt) => { wasAccepted = true; settleAccepted?.(receipt); });
|
|
1311
|
-
void leg.then((out) => {
|
|
1312
|
-
if (wasAccepted && out.status >= 400) {
|
|
1313
|
-
deps.logger?.warn?.("resume_leg_failed_after_accept", { sessionId: args.sessionId, status: out.status, body: out.body });
|
|
1314
|
-
deps.metrics?.inc("resume_post_accept_failed_total");
|
|
1315
|
-
}
|
|
1316
|
-
}, (err) => {
|
|
1317
|
-
if (!wasAccepted)
|
|
1318
|
-
return;
|
|
1319
|
-
deps.logger?.error?.("resume_leg_threw_after_accept", { sessionId: args.sessionId, err: redactHead(err instanceof Error ? err.message : String(err), 2000) });
|
|
1320
|
-
deps.metrics?.inc("resume_post_accept_failed_total");
|
|
1321
|
-
});
|
|
1322
|
-
return await Promise.race([accepted, leg]);
|
|
1323
|
-
}
|
|
1324
|
-
async function driveResumeLeg(args, onAccepted) {
|
|
1325
|
-
const resumeLegStartedAt = Date.now();
|
|
1326
|
-
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
|
|
1327
|
-
const legKey = createHash("sha256").update(String(token)).digest("hex");
|
|
1328
|
-
const streamApprovalOn = resolveStreamApprovalGate({
|
|
1329
|
-
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
1330
|
-
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
1331
|
-
backend: deps.backend,
|
|
1332
|
-
parkFacility: deps.checkpointStore !== undefined,
|
|
1333
|
-
}).active;
|
|
1334
|
-
const approvalLeg = resolveApprovalLeg({
|
|
1335
|
-
streamApprovalOn,
|
|
1336
|
-
windowMs: deps.config.streamApproval?.windowMs,
|
|
1337
|
-
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
1338
|
-
...(typeof taskConfig.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: taskConfig.limits.maxWalltimeMs } : {}),
|
|
1339
|
-
nowMonotonicMs: performance.now(),
|
|
1340
|
-
});
|
|
1341
|
-
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
1342
|
-
const ownerQuotaDenial = args.callerInitiated === true ? ownerCostQuotaDenied(principal) : undefined;
|
|
1343
|
-
if (ownerQuotaDenial)
|
|
1344
|
-
return ownerQuotaDenial;
|
|
1345
|
-
const ownerLeaseDenial = args.callerInitiated === true && args.ownerLeaseAdmitted !== true ? await ownerFleetLeaseDenied(principal) : undefined;
|
|
1346
|
-
if (ownerLeaseDenial)
|
|
1347
|
-
return ownerLeaseDenial;
|
|
1348
|
-
let taskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
1349
|
-
let freshRunClaimed = false;
|
|
1350
|
-
if (args.mintFreshRun && taskId && deps.runStore) {
|
|
1351
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, taskId) };
|
|
1352
|
-
}
|
|
1353
|
-
if (!taskId && args.mintFreshRun && deps.runStore) {
|
|
1354
|
-
const freshId = uuidv7();
|
|
1355
|
-
const created = await deps.runStore.createRun(freshId, sessionId, principal ?? null, deps.instanceId ?? "default", {
|
|
1356
|
-
source: args.mintFreshRun.source,
|
|
1357
|
-
objectivePreview: resumeObjective ? redactSecrets(resumeObjective).slice(0, 120) : null,
|
|
1358
|
-
});
|
|
1359
|
-
if (!created.ok) {
|
|
1360
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
1361
|
-
}
|
|
1362
|
-
taskId = freshId;
|
|
1363
|
-
freshRunClaimed = true;
|
|
1364
|
-
}
|
|
1365
|
-
const fleetPub = taskId && deps.fleetBus
|
|
1366
|
-
? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, ...fleetRunLabels(resumeObjective) })
|
|
1367
|
-
: undefined;
|
|
1368
|
-
const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
|
|
1369
|
-
if (deps.toolApproval && taskId) {
|
|
1370
|
-
const toolApproval = deps.toolApproval;
|
|
1371
|
-
resumeTaskConfig.onAsk = approvalLeg.windowZero
|
|
1372
|
-
? (askReq) => Promise.resolve(toolApproval.unattendedAskOutcome({ taskId, owner: principal ?? null, toolName: askReq.toolName }))
|
|
1373
|
-
: toolApproval.boundAsk({
|
|
1374
|
-
owner: principal ?? null,
|
|
1375
|
-
taskId,
|
|
1376
|
-
sessionId,
|
|
1377
|
-
...(approvalLeg.active ? { legKey, ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}) } : {}),
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
if (taskId && resumeTaskConfig.resourceSuspend)
|
|
1381
|
-
recordResumeResourceWindow(taskId, resumeTaskConfig.resourceSuspend);
|
|
1382
|
-
let fleetSettled = false;
|
|
1383
|
-
const settleFleet = (status, residuals) => {
|
|
1384
|
-
if (fleetSettled)
|
|
1385
|
-
return;
|
|
1386
|
-
fleetSettled = true;
|
|
1387
|
-
fleetPub?.onTerminal(status, residuals);
|
|
1388
|
-
};
|
|
1389
|
-
let result;
|
|
1390
|
-
let reopenConfirmedPark = false;
|
|
1391
|
-
const confirmCheckpointReopened = async () => {
|
|
1392
|
-
const store = deps.checkpointStore;
|
|
1393
|
-
if (!store)
|
|
1394
|
-
return true;
|
|
1395
|
-
try {
|
|
1396
|
-
return (await store.get(token))?.status === "pending";
|
|
1397
|
-
}
|
|
1398
|
-
catch {
|
|
1399
|
-
return true;
|
|
1400
|
-
}
|
|
1401
|
-
};
|
|
1402
|
-
let resumeHeartbeat;
|
|
1403
|
-
let resumeManifestLease = 0;
|
|
1404
|
-
let resumeStreamRef;
|
|
1405
|
-
let resumeLegLive = false;
|
|
1406
|
-
const subagentHandleEvictions = [];
|
|
1407
|
-
let claimedRow = false;
|
|
1408
|
-
const cancelCtrl = new AbortController();
|
|
1409
|
-
const preemptCtrl = new AbortController();
|
|
1410
|
-
if (taskId && deps.runStore) {
|
|
1411
|
-
claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
|
|
1412
|
-
if (!claimedRow)
|
|
1413
|
-
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
1414
|
-
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
|
|
1415
|
-
fleetPub?.onStart();
|
|
1416
|
-
const rs = deps.runStore;
|
|
1417
|
-
const owner = principal ?? null;
|
|
1418
|
-
inflightRuns.set(taskId, cancelCtrl);
|
|
1419
|
-
preemptableRuns.set(taskId, preemptCtrl);
|
|
1420
|
-
resumeHeartbeat = setInterval(() => {
|
|
1421
|
-
void rs.heartbeat(taskId, owner).catch(() => undefined);
|
|
1422
|
-
if (!cancelCtrl.signal.aborted) {
|
|
1423
|
-
void rs.isCancelRequested(taskId, owner).then((c) => { if (c)
|
|
1424
|
-
cancelCtrl.abort(); }).catch(() => undefined);
|
|
1425
|
-
}
|
|
1426
|
-
if (!preemptCtrl.signal.aborted) {
|
|
1427
|
-
void rs.isPreemptRequested(taskId, owner).then((p) => { if (p)
|
|
1428
|
-
preemptCtrl.abort(); }).catch(() => undefined);
|
|
1429
|
-
}
|
|
1430
|
-
}, HEARTBEAT_MS);
|
|
1431
|
-
resumeHeartbeat.unref?.();
|
|
1432
|
-
deps.modelUsage?.register(taskId);
|
|
1433
|
-
resumeManifestLease = deps.promptManifests?.register(taskId) ?? 0;
|
|
1434
|
-
}
|
|
1435
|
-
if (args.onResumeCommitted)
|
|
1436
|
-
await args.onResumeCommitted();
|
|
1437
|
-
let unregisterEngineNotice;
|
|
1438
|
-
try {
|
|
1439
|
-
result = await withPrincipal(principal, async () => {
|
|
1440
|
-
const resumeNotifiedKeys = new NotifiedKeys();
|
|
1441
|
-
if (deps.runStore && taskId) {
|
|
1442
|
-
const rs0 = deps.runStore;
|
|
1443
|
-
let drainSeq = (await rs0.maxSeq(taskId)) + 1;
|
|
1444
|
-
await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, sessionId, principal ?? null, (frame) => {
|
|
1445
|
-
const { type, ...rest } = frame;
|
|
1446
|
-
const f = frame;
|
|
1447
|
-
const write = rs0.appendEvent(taskId, drainSeq++, type, rest);
|
|
1448
|
-
if (f.type === "task_notification" && f.task_id)
|
|
1449
|
-
resumeNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
|
|
1450
|
-
return write;
|
|
1451
|
-
}, { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
|
|
1452
|
-
}
|
|
1453
|
-
if (verifyRounds !== undefined) {
|
|
1454
|
-
const rsV = deps.runStore;
|
|
1455
|
-
const vSeqBase = rsV && taskId ? await rsV.maxSeq(taskId) : 0;
|
|
1456
|
-
const verifySink = createLedgerSink({
|
|
1457
|
-
appendEvent: (seq, type, data) => (rsV && taskId ? rsV.appendEvent(taskId, vSeqBase + seq, type, data) : Promise.resolve()),
|
|
1458
|
-
persistThinking: deps.config.traceThinking,
|
|
1459
|
-
notifiedKeys: resumeNotifiedKeys,
|
|
1460
|
-
...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
|
|
1461
|
-
});
|
|
1462
|
-
const vAppend = (type, data) => verifySink.append(type, data);
|
|
1463
|
-
const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
|
|
1464
|
-
const verifyInternals = {
|
|
1465
|
-
...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
|
|
1466
|
-
onForwardEvent: (e) => {
|
|
1467
|
-
fleetPub?.onForwardEvent(e);
|
|
1468
|
-
{
|
|
1469
|
-
const bg = e.bgAgentId;
|
|
1470
|
-
if (bg !== undefined)
|
|
1471
|
-
defaultSubagentTailBus.onForwardFrame(bg, verifyTailContentMode, e);
|
|
1472
|
-
}
|
|
1473
|
-
if (rsV && taskId)
|
|
1474
|
-
verifySink.onForwardEvent(e);
|
|
1475
|
-
},
|
|
1476
|
-
onStatusEvent: (e) => {
|
|
1477
|
-
deps.metrics?.inc("brain_retry_total", { phase: String(e.phase) });
|
|
1478
|
-
if (rsV && taskId) {
|
|
1479
|
-
void verifySink.appendStatus(e).catch((err) => deps.logger?.warn?.("status_event_append_failed", { route: "resume-verify", taskId, err: err instanceof Error ? err.message : String(err) }));
|
|
1480
|
-
}
|
|
1481
|
-
},
|
|
1482
|
-
onTaskNotification: (n, opts) => {
|
|
1483
|
-
if (n.task_type === "workflow")
|
|
1484
|
-
return;
|
|
1485
|
-
defaultSubagentTailBus.forgetHandle(n.task_id);
|
|
1486
|
-
if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
|
|
1487
|
-
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
1488
|
-
}
|
|
1489
|
-
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
1490
|
-
const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
|
|
1491
|
-
deps.logger?.info?.("task_notification_observed", { route: "resume-verify", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
|
|
1492
|
-
if (parked) {
|
|
1493
|
-
void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume-verify", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
|
|
1494
|
-
}
|
|
1495
|
-
else if (resumeLegLive) {
|
|
1496
|
-
const key = taskNotificationStreamKey(n);
|
|
1497
|
-
if (resumeNotifiedKeys.get(key) === undefined) {
|
|
1498
|
-
resumeNotifiedKeys.set(key, vAppend("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
},
|
|
1502
|
-
...(deps.subagentSteerRegistry && taskId
|
|
1503
|
-
? {
|
|
1504
|
-
onSubagentSpawn: (handle) => {
|
|
1505
|
-
subagentHandleEvictions.push(deps.subagentSteerRegistry.register(taskId, handle));
|
|
1506
|
-
},
|
|
1507
|
-
}
|
|
1508
|
-
: {}),
|
|
1509
|
-
};
|
|
1510
|
-
resumeLegLive = true;
|
|
1511
|
-
const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
|
|
1512
|
-
let safe = stripResumeToken(vr);
|
|
1513
|
-
const vrNested = nestedPauseOf(vr.terminal);
|
|
1514
|
-
const reParkStatus = nestedParkStatusOf(vr);
|
|
1515
|
-
const reSuspended = vrNested !== undefined;
|
|
1516
|
-
const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
|
|
1517
|
-
if (vrReopened)
|
|
1518
|
-
reopenConfirmedPark = true;
|
|
1519
|
-
if (taskId && deps.runStore) {
|
|
1520
|
-
const rs2 = deps.runStore;
|
|
1521
|
-
try {
|
|
1522
|
-
const ap = vAppend;
|
|
1523
|
-
await appendPromptManifest(ap, deps.promptManifests, taskId);
|
|
1524
|
-
if (reSuspended || vrReopened) {
|
|
1525
|
-
await appendModelUsageDelta(ap, deps.modelUsage, taskId);
|
|
1526
|
-
const vrParkCallId = vrNested !== undefined ? await parkToolCallId({ terminal: vrNested }, deps.checkpointStore) : undefined;
|
|
1527
|
-
await ap(reParkStatus ?? "suspended", { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
|
|
1528
|
-
}
|
|
1529
|
-
else {
|
|
1530
|
-
safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
|
|
1531
|
-
await ap("done", { result: safe });
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
catch (e) {
|
|
1535
|
-
if (!vrReopened)
|
|
1536
|
-
throw e;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
if (vrNested !== undefined)
|
|
1540
|
-
return { ...safe, terminal: vrNested };
|
|
1541
|
-
if (vr.verification?.verdict)
|
|
1542
|
-
deps.metrics?.inc("verifications_total", { verdict: vr.verification.verdict });
|
|
1543
|
-
return safe;
|
|
1544
|
-
}
|
|
1545
|
-
const rs = deps.runStore;
|
|
1546
|
-
const startSeq = rs && taskId ? await rs.maxSeq(taskId) : 0;
|
|
1547
|
-
let text = "";
|
|
1548
|
-
let reasoning = "";
|
|
1549
|
-
let reasoningEventId;
|
|
1550
|
-
const persistThinking = deps.config.traceThinking;
|
|
1551
|
-
const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
|
|
1552
|
-
const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
|
|
1553
|
-
unregisterEngineNotice = registerEngineNoticeLeg({
|
|
1554
|
-
sessionId,
|
|
1555
|
-
...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
|
|
1556
|
-
durable: (row) => void append("engine_notice", row).catch((err) => {
|
|
1557
|
-
recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
|
|
1558
|
-
deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
|
|
1559
|
-
}),
|
|
1560
|
-
});
|
|
1561
|
-
const anchorOwner = principal ?? null;
|
|
1562
|
-
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
1563
|
-
const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
|
|
1564
|
-
? async (eventId) => { const leaf = await getLeafId(sessionId); if (leaf)
|
|
1565
|
-
await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner); }
|
|
1566
|
-
: undefined;
|
|
1567
|
-
const captureUserMessageAnchor = deps.resumeAnchorStore && taskId
|
|
1568
|
-
? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
|
|
1569
|
-
: undefined;
|
|
1570
|
-
const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
|
|
1571
|
-
const flush = async () => {
|
|
1572
|
-
if (reasoning) {
|
|
1573
|
-
await append("reasoning", modelTextEventData({ text: reasoning, eventId: reasoningEventId }));
|
|
1574
|
-
reasoning = "";
|
|
1575
|
-
reasoningEventId = undefined;
|
|
1576
|
-
}
|
|
1577
|
-
if (text) {
|
|
1578
|
-
await append("text", modelTextEventData({ text, eventId: anchor.firstTextEventId }));
|
|
1579
|
-
text = "";
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
const driveResume = async () => {
|
|
1583
|
-
resumeLegLive = true;
|
|
1584
|
-
const tailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
|
|
1585
|
-
const stream = await legRunner.resumeStream(token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, {
|
|
1586
|
-
...(args.workflowParkedResume !== undefined ? { workflowParkedResume: args.workflowParkedResume } : {}),
|
|
1587
|
-
onForwardEvent: (e) => {
|
|
1588
|
-
fleetPub?.onForwardEvent(e);
|
|
1589
|
-
{
|
|
1590
|
-
const bg = e.bgAgentId;
|
|
1591
|
-
if (bg !== undefined)
|
|
1592
|
-
defaultSubagentTailBus.onForwardFrame(bg, tailContentMode, e);
|
|
1593
|
-
}
|
|
1594
|
-
if (!rs || !taskId)
|
|
1595
|
-
return;
|
|
1596
|
-
const t = e.type;
|
|
1597
|
-
const warnAppend = (kind) => (err) => deps.logger?.warn?.("forward_event_append_failed", { type: kind, taskId, err: err instanceof Error ? err.message : String(err) });
|
|
1598
|
-
if (t === "task_progress") {
|
|
1599
|
-
void append("task_progress", taskProgressEventData(e)).catch(warnAppend(t));
|
|
1600
|
-
}
|
|
1601
|
-
else if (t === "tool_start") {
|
|
1602
|
-
void append("tool_start", toolStartEventData(e)).catch(warnAppend(t));
|
|
1603
|
-
}
|
|
1604
|
-
else if (t === "tool_end") {
|
|
1605
|
-
void append("tool_end", toolEndEventData(e)).catch(warnAppend(t));
|
|
1606
|
-
}
|
|
1607
|
-
},
|
|
1608
|
-
onTaskNotification: (n, opts) => {
|
|
1609
|
-
if (n.task_type === "workflow")
|
|
1610
|
-
return;
|
|
1611
|
-
defaultSubagentTailBus.forgetHandle(n.task_id);
|
|
1612
|
-
if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
|
|
1613
|
-
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
1614
|
-
}
|
|
1615
|
-
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
1616
|
-
const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
|
|
1617
|
-
deps.logger?.info?.("task_notification_observed", { route: "resume", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
|
|
1618
|
-
if (parked) {
|
|
1619
|
-
void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
|
|
1620
|
-
}
|
|
1621
|
-
else if (resumeLegLive) {
|
|
1622
|
-
const key = taskNotificationStreamKey(n);
|
|
1623
|
-
if (resumeNotifiedKeys.get(key) === undefined) {
|
|
1624
|
-
resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
},
|
|
1628
|
-
...(deps.subagentSteerRegistry && taskId
|
|
1629
|
-
? {
|
|
1630
|
-
onSubagentSpawn: (handle) => {
|
|
1631
|
-
subagentHandleEvictions.push(deps.subagentSteerRegistry.register(taskId, handle));
|
|
1632
|
-
},
|
|
1633
|
-
}
|
|
1634
|
-
: {}),
|
|
1635
|
-
});
|
|
1636
|
-
resumeStreamRef = stream;
|
|
1637
|
-
if (taskId)
|
|
1638
|
-
steerableRuns.set(taskId, stream);
|
|
1639
|
-
if (!taskId || !rs)
|
|
1640
|
-
return await stream.result();
|
|
1641
|
-
onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
|
|
1642
|
-
for await (const ev of stream) {
|
|
1643
|
-
recordTurnActivity(taskId);
|
|
1644
|
-
fleetPub?.onEvent(ev);
|
|
1645
|
-
await appendPromptManifest(append, deps.promptManifests, taskId);
|
|
1646
|
-
switch (ev.type) {
|
|
1647
|
-
case "text_delta":
|
|
1648
|
-
text += ev.delta;
|
|
1649
|
-
anchor.onText(ev.eventId);
|
|
1650
|
-
break;
|
|
1651
|
-
case "reasoning_delta": {
|
|
1652
|
-
const rb = reasoningDeltaBytes(ev);
|
|
1653
|
-
if (persistThinking && rb !== undefined) {
|
|
1654
|
-
reasoning += rb;
|
|
1655
|
-
reasoningEventId ??= reasoningSegmentEventId(ev.eventId);
|
|
1656
|
-
}
|
|
1657
|
-
break;
|
|
1658
|
-
}
|
|
1659
|
-
case "tool_start":
|
|
1660
|
-
anchor.onTool();
|
|
1661
|
-
await flush();
|
|
1662
|
-
await append("tool_start", toolStartEventData(ev));
|
|
1663
|
-
break;
|
|
1664
|
-
case "tool_end":
|
|
1665
|
-
await flush();
|
|
1666
|
-
await append("tool_end", toolEndEventData(ev));
|
|
1667
|
-
break;
|
|
1668
|
-
case "status": {
|
|
1669
|
-
const st = ev;
|
|
1670
|
-
deps.metrics?.inc("brain_retry_total", { phase: String(st.phase) });
|
|
1671
|
-
await flush();
|
|
1672
|
-
await append("status", brainStatusEventData(st));
|
|
1673
|
-
break;
|
|
1674
|
-
}
|
|
1675
|
-
case "context_usage":
|
|
1676
|
-
await flush();
|
|
1677
|
-
await append("context_usage", contextUsageEventData(ev));
|
|
1678
|
-
break;
|
|
1679
|
-
case "compacted":
|
|
1680
|
-
await flush();
|
|
1681
|
-
await append("compacted", compactedEventData(ev));
|
|
1682
|
-
break;
|
|
1683
|
-
case "diagnostics":
|
|
1684
|
-
await flush();
|
|
1685
|
-
await append("diagnostics", diagnosticsEventData(ev));
|
|
1686
|
-
break;
|
|
1687
|
-
case "steering_injected":
|
|
1688
|
-
await flush();
|
|
1689
|
-
await append("steering_injected", steeringInjectedEventData(ev));
|
|
1690
|
-
break;
|
|
1691
|
-
case "compaction_outcome":
|
|
1692
|
-
await flush();
|
|
1693
|
-
await append("compaction_outcome", compactionOutcomeEventData(ev));
|
|
1694
|
-
break;
|
|
1695
|
-
case "workspace_changed":
|
|
1696
|
-
await flush();
|
|
1697
|
-
await append("workspace_changed", workspaceChangedEventData(ev));
|
|
1698
|
-
break;
|
|
1699
|
-
case "wiring_manifest":
|
|
1700
|
-
await flush();
|
|
1701
|
-
await append("wiring_manifest", wiringManifestEventData(ev));
|
|
1702
|
-
break;
|
|
1703
|
-
case "tool_roster_delta": {
|
|
1704
|
-
await flush();
|
|
1705
|
-
const rosterDelta = toolRosterDeltaEventData(ev);
|
|
1706
|
-
if (rosterDelta !== undefined)
|
|
1707
|
-
await append("tool_roster_delta", rosterDelta);
|
|
1708
|
-
break;
|
|
1709
|
-
}
|
|
1710
|
-
case "tool_disclosure": {
|
|
1711
|
-
await flush();
|
|
1712
|
-
const disclosure = toolDisclosureEventData(ev);
|
|
1713
|
-
if (disclosure !== undefined)
|
|
1714
|
-
await append("tool_disclosure", disclosure);
|
|
1715
|
-
break;
|
|
1716
|
-
}
|
|
1717
|
-
case "human_input":
|
|
1718
|
-
await flush();
|
|
1719
|
-
await append("human_input", humanInputEventData(ev));
|
|
1720
|
-
break;
|
|
1721
|
-
case "message_committed":
|
|
1722
|
-
await anchor.onMessageCommitted(ev.role, ev.entryId);
|
|
1723
|
-
break;
|
|
1724
|
-
case "task_progress":
|
|
1725
|
-
await append("task_progress", taskProgressEventData(ev));
|
|
1726
|
-
break;
|
|
1727
|
-
case "task_notification": {
|
|
1728
|
-
await flush();
|
|
1729
|
-
const tnr = ev.notification;
|
|
1730
|
-
const tnrKey = tnr?.task_id ? taskNotificationStreamKey({ task_type: tnr.task_type, task_id: tnr.task_id, status: String(tnr.status), seq: tnr.seq }) : undefined;
|
|
1731
|
-
const tnrPrior = tnrKey !== undefined ? resumeNotifiedKeys.get(tnrKey) : undefined;
|
|
1732
|
-
if (tnrPrior === undefined || !(await tnrPrior)) {
|
|
1733
|
-
const write = append("task_notification", taskNotificationEventData(ev));
|
|
1734
|
-
if (tnrKey !== undefined)
|
|
1735
|
-
resumeNotifiedKeys.set(tnrKey, write.then(() => true, () => false));
|
|
1736
|
-
await write;
|
|
1737
|
-
}
|
|
1738
|
-
break;
|
|
1739
|
-
}
|
|
1740
|
-
case "turn_end": {
|
|
1741
|
-
await flush();
|
|
1742
|
-
await append("turn_end", turnEndEventData(ev));
|
|
1743
|
-
await appendModelUsageDelta(append, deps.modelUsage, taskId);
|
|
1744
|
-
await anchor.onTurnEnd();
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
await flush();
|
|
1750
|
-
const r = await stream.result();
|
|
1751
|
-
const rPark = r.terminal.kind === "paused" ? terminalProjection(r.terminal).status : undefined;
|
|
1752
|
-
if (rPark === "suspended") {
|
|
1753
|
-
const parkCallId = await parkToolCallId(r, deps.checkpointStore);
|
|
1754
|
-
await append("suspended", { gate: r.terminal.kind === "paused" ? r.terminal.gate : null, ...(parkCallId !== undefined ? { toolCallId: parkCallId } : {}) });
|
|
1755
|
-
await appendModelUsageDelta(append, deps.modelUsage, taskId);
|
|
1756
|
-
return r;
|
|
1757
|
-
}
|
|
1758
|
-
if (rPark === "needs_review") {
|
|
1759
|
-
await append("needs_review", { gate: r.terminal.kind === "paused" ? r.terminal.gate : null });
|
|
1760
|
-
await appendModelUsageDelta(append, deps.modelUsage, taskId);
|
|
1761
|
-
return r;
|
|
1762
|
-
}
|
|
1763
|
-
if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
|
|
1764
|
-
reopenConfirmedPark = true;
|
|
1765
|
-
try {
|
|
1766
|
-
await append("suspended", { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
|
|
1767
|
-
await appendModelUsageDelta(append, deps.modelUsage, taskId);
|
|
1768
|
-
}
|
|
1769
|
-
catch {
|
|
1770
|
-
}
|
|
1771
|
-
return r;
|
|
1772
|
-
}
|
|
1773
|
-
const safe = await attachModelUsage(stripResumeToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
|
|
1774
|
-
await append("done", { result: safe });
|
|
1775
|
-
if (taskConfig.suggestNextPrompts && safe.terminal.kind === "completed") {
|
|
1776
|
-
const suggestions = await stream.suggestions().catch(() => []);
|
|
1777
|
-
if (suggestions.length)
|
|
1778
|
-
await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
|
|
1779
|
-
}
|
|
1780
|
-
return safe;
|
|
1781
|
-
};
|
|
1782
|
-
const approvalEmit = (frame) => {
|
|
1783
|
-
const { type, ...rest } = frame;
|
|
1784
|
-
return append(type, rest);
|
|
1785
|
-
};
|
|
1786
|
-
const withApproval = () => deps.toolApproval && taskId && approvalLeg.active && !approvalLeg.windowZero
|
|
1787
|
-
? deps.toolApproval.runWithContext({
|
|
1788
|
-
taskId,
|
|
1789
|
-
owner: principal ?? null,
|
|
1790
|
-
emit: approvalEmit,
|
|
1791
|
-
abortSignal: cancelCtrl.signal,
|
|
1792
|
-
sessionId,
|
|
1793
|
-
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
1794
|
-
emitRevoke: (f) => approvalEmit(f),
|
|
1795
|
-
legKey,
|
|
1796
|
-
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
1797
|
-
}, driveResume)
|
|
1798
|
-
: driveResume();
|
|
1799
|
-
const withElicitation = () => deps.elicitation && taskId
|
|
1800
|
-
? deps.elicitation.runWithContext({ taskId, owner: principal ?? null, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); }, abortSignal: cancelCtrl.signal }, withApproval)
|
|
1801
|
-
: withApproval();
|
|
1802
|
-
return await (deps.sendUserFile && taskId
|
|
1803
|
-
? deps.sendUserFile.runWithContext({ taskId, emit: async (frame) => { const { type, ...rest } = frame; await append(type, rest); } }, withElicitation)
|
|
1804
|
-
: withElicitation());
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
catch (e) {
|
|
1808
|
-
if (e instanceof CheckpointError) {
|
|
1809
|
-
if (e.code === "checkpoint.resume_aborted") {
|
|
1810
|
-
const cs2 = deps.checkpointStore;
|
|
1811
|
-
if (cs2) {
|
|
1812
|
-
try {
|
|
1813
|
-
const live = await cs2.get(token);
|
|
1814
|
-
if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
|
|
1815
|
-
deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
catch (cancelSettleErr) {
|
|
1819
|
-
deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
|
|
1820
|
-
taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
if (claimedRow && taskId && deps.runStore) {
|
|
1825
|
-
const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: e.message });
|
|
1826
|
-
await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch(() => undefined);
|
|
1827
|
-
}
|
|
1828
|
-
settleFleet("failed");
|
|
1829
|
-
return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
|
|
1830
|
-
}
|
|
1831
|
-
const retriable = e.code === "checkpoint.invalid_outcome" ||
|
|
1832
|
-
e.code === "checkpoint.reopen_revote" ||
|
|
1833
|
-
e.code === "checkpoint.reopened_concurrently" ||
|
|
1834
|
-
e.code === "resume.parent_constraint_missing" ||
|
|
1835
|
-
e.code === "resume.parent_constraint_mismatch" ||
|
|
1836
|
-
e.code === "resume.constraint_rejected" ||
|
|
1837
|
-
e.code === "resume.constraint_unprojectable" ||
|
|
1838
|
-
e.code === "resume.usage_window_exhausted" ||
|
|
1839
|
-
e.code === "resume.principal_mismatch" ||
|
|
1840
|
-
e.code === "resume.placement_mismatch" ||
|
|
1841
|
-
e.code === "resume.preflight_rejected" ||
|
|
1842
|
-
e.code.startsWith("wake.") ||
|
|
1843
|
-
(e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
|
|
1844
|
-
if (claimedRow && taskId && deps.runStore) {
|
|
1845
|
-
if (retriable && freshRunClaimed) {
|
|
1846
|
-
const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0, usageMissing: true } };
|
|
1847
|
-
await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch((err) => {
|
|
1848
|
-
deps.logger?.warn?.("wake_synthetic_run_release_failed", { taskId, sessionId, errorCode: e.code, error: err instanceof Error ? err.message : String(err), note: "the synthetic run row for this refused fresh-run resume could not be settled — its task_active claim is still held and the next attempt on this session will 409 conflict.session_active_run until reapStale collects it (runStaleSec)" });
|
|
1849
|
-
});
|
|
1850
|
-
settleFleet("failed");
|
|
1851
|
-
}
|
|
1852
|
-
else if (retriable) {
|
|
1853
|
-
await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
|
|
1854
|
-
settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
|
|
1855
|
-
}
|
|
1856
|
-
else {
|
|
1857
|
-
await deps.runStore.setTerminal(taskId, "failed", null, e.message).catch(() => undefined);
|
|
1858
|
-
settleFleet("failed");
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
const status = e.code === "checkpoint.not_found" ? 404 : 409;
|
|
1862
|
-
return {
|
|
1863
|
-
status,
|
|
1864
|
-
body: {
|
|
1865
|
-
...(taskId !== undefined ? { taskId } : {}),
|
|
1866
|
-
error: e.message,
|
|
1867
|
-
errorCode: e.code === "checkpoint.invalid_outcome"
|
|
1868
|
-
? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
|
|
1869
|
-
: e.code,
|
|
1870
|
-
...(e.detail?.field ? { field: e.detail.field } : {}),
|
|
1871
|
-
...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
|
|
1872
|
-
? { retryAfterSec: Math.max(1, Math.ceil(e.detail.retryAfterMs / 1000)) }
|
|
1873
|
-
: {}),
|
|
1874
|
-
...(retriable ? { retriable: true } : {}),
|
|
1875
|
-
},
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
if (cancelCtrl.signal.aborted) {
|
|
1879
|
-
if (taskId && deps.runStore) {
|
|
1880
|
-
const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user" });
|
|
1881
|
-
await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
|
|
1882
|
-
}
|
|
1883
|
-
settleFleet("failed");
|
|
1884
|
-
return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
|
|
1885
|
-
}
|
|
1886
|
-
if (claimedRow && taskId && deps.runStore) {
|
|
1887
|
-
await deps.runStore.setTerminal(taskId, "failed", null, errorText(e)).catch(() => undefined);
|
|
1888
|
-
}
|
|
1889
|
-
settleFleet("failed");
|
|
1890
|
-
throw e;
|
|
1891
|
-
}
|
|
1892
|
-
finally {
|
|
1893
|
-
unregisterEngineNotice?.();
|
|
1894
|
-
if (taskId) {
|
|
1895
|
-
if (inflightRuns.get(taskId) === cancelCtrl)
|
|
1896
|
-
inflightRuns.delete(taskId);
|
|
1897
|
-
if (preemptableRuns.get(taskId) === preemptCtrl)
|
|
1898
|
-
preemptableRuns.delete(taskId);
|
|
1899
|
-
if (resumeStreamRef !== undefined && steerableRuns.get(taskId) === resumeStreamRef)
|
|
1900
|
-
steerableRuns.delete(taskId);
|
|
1901
|
-
for (const evict of subagentHandleEvictions)
|
|
1902
|
-
evict();
|
|
1903
|
-
deps.modelUsage?.clear(taskId);
|
|
1904
|
-
deps.promptManifests?.clear(taskId, resumeManifestLease);
|
|
1905
|
-
}
|
|
1906
|
-
if (resumeHeartbeat)
|
|
1907
|
-
clearInterval(resumeHeartbeat);
|
|
1908
|
-
resumeLegLive = false;
|
|
1909
|
-
}
|
|
1910
|
-
evictIfConflict(deps.runner, sessionId, result);
|
|
1911
|
-
const checkpointReopened = reopenConfirmedPark;
|
|
1912
|
-
const resumePlane = terminalProjection(result.terminal);
|
|
1913
|
-
if (taskId && deps.runStore) {
|
|
1914
|
-
if (resumePlane.status === "suspended" || checkpointReopened)
|
|
1915
|
-
await deps.runStore.setSuspended(taskId);
|
|
1916
|
-
else if (resumePlane.status === "needs_review")
|
|
1917
|
-
await deps.runStore.setNeedsReview(taskId);
|
|
1918
|
-
else
|
|
1919
|
-
await deps.runStore.setTerminal(taskId, resumePlane.status, stripResumeToken(result), resumePlane.errorMessage ?? null);
|
|
1920
|
-
}
|
|
1921
|
-
settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(result));
|
|
1922
|
-
return {
|
|
1923
|
-
status: 200,
|
|
1924
|
-
body: {
|
|
1925
|
-
taskId, sessionId, status: resumePlane.status,
|
|
1926
|
-
bindingEnforced: true,
|
|
1927
|
-
...(resumePlane.errorCode ? { errorCode: resumePlane.errorCode } : {}),
|
|
1928
|
-
...(resumePlane.errorMessage ? { errorMessage: redactErrorMessage(resumePlane.errorMessage) ?? "" } : {}),
|
|
1929
|
-
...(checkpointReopened ? { retriable: true } : {}),
|
|
1930
|
-
},
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
async function mayDiscloseRowError(cs, token, decider) {
|
|
1934
|
-
if (decider === undefined || decider.explicitOperator)
|
|
1935
|
-
return true;
|
|
1936
|
-
let scope;
|
|
1937
|
-
try {
|
|
1938
|
-
scope = await cs.peekScopeByToken(token);
|
|
1939
|
-
}
|
|
1940
|
-
catch (e) {
|
|
1941
|
-
deps.logger?.warn?.("checkpoint_owner_probe_unreadable", { note: "peekScopeByToken failed — row-error disclosure withheld (fail-closed)", err: e instanceof Error ? e.message : String(e) });
|
|
1942
|
-
return false;
|
|
1943
|
-
}
|
|
1944
|
-
if (scope === undefined)
|
|
1945
|
-
return false;
|
|
1946
|
-
const owner = decodeCheckpointScope(scope ?? CHECKPOINT_PUBLIC_SCOPE);
|
|
1947
|
-
return owner === undefined || owner === decider.principal;
|
|
1948
|
-
}
|
|
1949
|
-
async function resumePreempted(sessionId, req, acceptEarly, decider) {
|
|
1950
|
-
const cs = deps.checkpointStore;
|
|
1951
|
-
const RESUMABLE_GATE_KIND = "resource_limit";
|
|
1952
|
-
const preemptBinding = { gateKinds: [RESUMABLE_GATE_KIND] };
|
|
1953
|
-
const token = (await cs.findPendingTokenBySession(sessionId, undefined, preemptBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
1954
|
-
const noSuspension = { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
|
|
1955
|
-
if (!token)
|
|
1956
|
-
return noSuspension;
|
|
1957
|
-
if (decider !== undefined && !decider.explicitOperator) {
|
|
1958
|
-
const peeked = await cs
|
|
1959
|
-
.peekPendingScope(sessionId, preemptBinding)
|
|
1960
|
-
.then((sc) => (sc === undefined ? cs.peekPendingScope(sessionId) : sc))
|
|
1961
|
-
.catch(() => undefined);
|
|
1962
|
-
const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
|
|
1963
|
-
if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
|
|
1964
|
-
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" });
|
|
1965
|
-
return noSuspension;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
let cp;
|
|
1969
|
-
try {
|
|
1970
|
-
cp = await cs.get(token);
|
|
1971
|
-
}
|
|
1972
|
-
catch (e) {
|
|
1973
|
-
if (!(await mayDiscloseRowError(cs, token, decider))) {
|
|
1974
|
-
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" });
|
|
1975
|
-
return noSuspension;
|
|
1976
|
-
}
|
|
1977
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
1978
|
-
}
|
|
1979
|
-
if (!cp) {
|
|
1980
|
-
if (!(await mayDiscloseRowError(cs, token, decider)))
|
|
1981
|
-
return noSuspension;
|
|
1982
|
-
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1983
|
-
}
|
|
1984
|
-
if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
|
|
1985
|
-
deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1986
|
-
return noSuspension;
|
|
1987
|
-
}
|
|
1988
|
-
const gateKind = cp.gate?.kind;
|
|
1989
|
-
if (gateKind !== RESUMABLE_GATE_KIND) {
|
|
1990
|
-
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" } };
|
|
1991
|
-
}
|
|
1992
|
-
const ctx = await cs.getCtx(sessionId).catch(() => null);
|
|
1993
|
-
if (!ctx)
|
|
1994
|
-
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1995
|
-
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1996
|
-
const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
|
|
1997
|
-
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1998
|
-
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1999
|
-
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, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
2000
|
-
}
|
|
2001
|
-
async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
|
|
2002
|
-
const cs = deps.checkpointStore;
|
|
2003
|
-
if (!deps.runStore) {
|
|
2004
|
-
return { status: 501, body: { error: "delivering a workflow decision on the host's next turn requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
|
|
2005
|
-
}
|
|
2006
|
-
const ctx = await cs.getCtx(sessionId).catch(() => null);
|
|
2007
|
-
if (!ctx) {
|
|
2008
|
-
return {
|
|
2009
|
-
status: 409,
|
|
2010
|
-
body: {
|
|
2011
|
-
error: "the workflow's originating session no longer has the rebuild inputs for a new turn: a session's resume context is garbage-collected once it is stale AND that session itself has no pending checkpoint and no active run (REAP_RUN_STALE_SEC, default 120s) — a workflow CHILD's park does not hold the HOST session's row open. Raise REAP_RUN_STALE_SEC to cover how long approvals may sit before someone decides them, or resume the workflow run yourself with the runId below",
|
|
2012
|
-
errorCode: "conflict.resume_context_unavailable",
|
|
2013
|
-
runId,
|
|
2014
|
-
},
|
|
2015
|
-
};
|
|
2016
|
-
}
|
|
2017
|
-
const windowDenial = await usageWindowDenialFor(principal || GLOBAL_USAGE_KEY);
|
|
2018
|
-
if (windowDenial)
|
|
2019
|
-
return { status: windowDenial.status, body: windowDenial.body };
|
|
2020
|
-
const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
|
|
2021
|
-
let spec;
|
|
2022
|
-
let engineNotices = [];
|
|
2023
|
-
try {
|
|
2024
|
-
({ spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth));
|
|
2025
|
-
}
|
|
2026
|
-
catch (e) {
|
|
2027
|
-
if (e instanceof HttpError) {
|
|
2028
|
-
return { status: 409, body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) } };
|
|
2029
|
-
}
|
|
2030
|
-
throw e;
|
|
2031
|
-
}
|
|
2032
|
-
const hostSpec = { ...spec, objective, sessionId };
|
|
2033
|
-
const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
|
|
2034
|
-
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
2035
|
-
fleetScope: resumeFleetScope(req, auth),
|
|
2036
|
-
elicitOwner: principal ?? null,
|
|
2037
|
-
source: "workflow_parked_decide",
|
|
2038
|
-
trustedInternals,
|
|
2039
|
-
});
|
|
2040
|
-
if (out.status !== 202)
|
|
2041
|
-
return { status: out.status, body: out.body };
|
|
2042
|
-
const minted = out.body;
|
|
2043
|
-
return acceptedResumingReceipt(minted.taskId, minted.sessionId);
|
|
2044
|
-
}
|
|
2045
|
-
async function resumeWake(sessionId, message, caller, req, acceptEarly, workflowParkedResume, ownerLeaseAdmittedFor) {
|
|
2046
|
-
const cs = deps.checkpointStore;
|
|
2047
|
-
const wakeBinding = { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" };
|
|
2048
|
-
const token = (await cs.findPendingTokenBySession(sessionId, undefined, wakeBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
2049
|
-
if (!token)
|
|
2050
|
-
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
2051
|
-
const operator = explicitOperatorOk(caller, deps.config.operatorPrincipals);
|
|
2052
|
-
if (!operator) {
|
|
2053
|
-
const scope = await cs.peekPendingScope(sessionId, wakeBinding).then((s) => (s === undefined ? cs.peekPendingScope(sessionId) : s)).catch(() => undefined);
|
|
2054
|
-
const decodedScope = scope != null ? decodeCheckpointScope(scope) : undefined;
|
|
2055
|
-
if (decodedScope !== undefined && decodedScope !== caller) {
|
|
2056
|
-
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
let cp;
|
|
2060
|
-
try {
|
|
2061
|
-
cp = await cs.get(token);
|
|
2062
|
-
}
|
|
2063
|
-
catch (e) {
|
|
2064
|
-
if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
|
|
2065
|
-
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" });
|
|
2066
|
-
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
2067
|
-
}
|
|
2068
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
2069
|
-
}
|
|
2070
|
-
if (!cp) {
|
|
2071
|
-
if (!(await mayDiscloseRowError(cs, token, { principal: caller, explicitOperator: operator }))) {
|
|
2072
|
-
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
2073
|
-
}
|
|
2074
|
-
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
2075
|
-
}
|
|
2076
|
-
if (!operator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== caller) {
|
|
2077
|
-
return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
|
|
2078
|
-
}
|
|
2079
|
-
const gateKind = cp.gate?.kind;
|
|
2080
|
-
if (gateKind !== "task_done") {
|
|
2081
|
-
return { status: 409, body: { error: `cannot wake: this checkpoint awaits a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
|
|
2082
|
-
}
|
|
2083
|
-
const trusted = operator;
|
|
2084
|
-
if (message !== undefined) {
|
|
2085
|
-
try {
|
|
2086
|
-
validatePendingSteer({ text: message, trusted });
|
|
2087
|
-
}
|
|
2088
|
-
catch (e) {
|
|
2089
|
-
if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
|
|
2090
|
-
return { status: 422, body: { errorCode: "steering.invalid_content", error: e.message } };
|
|
2091
|
-
}
|
|
2092
|
-
throw e;
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
const ctx = await cs.getCtx(sessionId).catch(() => null);
|
|
2096
|
-
if (!ctx)
|
|
2097
|
-
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
2098
|
-
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
2099
|
-
const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
|
|
2100
|
-
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
2101
|
-
return driveResumeIntoRunLog({
|
|
2102
|
-
runner: runnerFor(spec),
|
|
2103
|
-
token,
|
|
2104
|
-
sessionId: cp.sessionId,
|
|
2105
|
-
principal: auth.principal,
|
|
2106
|
-
fleetScope: resumeFleetScope(req, auth),
|
|
2107
|
-
taskConfig,
|
|
2108
|
-
resumeObjective,
|
|
2109
|
-
outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
|
|
2110
|
-
verifyRounds: undefined,
|
|
2111
|
-
mintFreshRun: { source: "wake" },
|
|
2112
|
-
...(engineNotices.length > 0 ? { engineNotices } : {}),
|
|
2113
|
-
...(acceptEarly === true ? { acceptEarly: true } : {}),
|
|
2114
|
-
...(req !== undefined ? { callerInitiated: true } : {}),
|
|
2115
|
-
...(workflowParkedResume !== undefined ? { workflowParkedResume } : {}),
|
|
2116
|
-
...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
|
|
2117
|
-
});
|
|
2118
|
-
}
|
|
2119
|
-
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
|
|
2120
|
-
const cs = deps.checkpointStore;
|
|
2121
|
-
const log404 = (r) => deps.logger?.info?.("decide_404", {
|
|
2122
|
-
reason: r,
|
|
2123
|
-
...(log?.taskId !== undefined ? { taskId: log.taskId } : { sessionId }),
|
|
2124
|
-
...(log?.principalPresent !== undefined ? { principalPresent: log.principalPresent } : {}),
|
|
2125
|
-
});
|
|
2126
|
-
const PLAN_REVIEW_GATE_KIND = "plan_review";
|
|
2127
|
-
const planReviewBinding = { gateKinds: [PLAN_REVIEW_GATE_KIND] };
|
|
2128
|
-
const token = (await cs.findPendingTokenBySession(sessionId, undefined, planReviewBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
2129
|
-
const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
|
|
2130
|
-
if (!token) {
|
|
2131
|
-
log404("no-pending-checkpoint");
|
|
2132
|
-
return noPending;
|
|
2133
|
-
}
|
|
2134
|
-
if (decider !== undefined && !decider.explicitOperator) {
|
|
2135
|
-
const peeked = await cs
|
|
2136
|
-
.peekPendingScope(sessionId, planReviewBinding)
|
|
2137
|
-
.then((sc) => (sc === undefined ? cs.peekPendingScope(sessionId) : sc))
|
|
2138
|
-
.catch(() => undefined);
|
|
2139
|
-
const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
|
|
2140
|
-
if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
|
|
2141
|
-
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" });
|
|
2142
|
-
return noPending;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
let cp;
|
|
2146
|
-
try {
|
|
2147
|
-
cp = await cs.get(token);
|
|
2148
|
-
}
|
|
2149
|
-
catch (e) {
|
|
2150
|
-
if (!(await mayDiscloseRowError(cs, token, decider))) {
|
|
2151
|
-
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" });
|
|
2152
|
-
return noPending;
|
|
2153
|
-
}
|
|
2154
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
2155
|
-
}
|
|
2156
|
-
if (!cp) {
|
|
2157
|
-
log404("checkpoint-missing");
|
|
2158
|
-
if (!(await mayDiscloseRowError(cs, token, decider)))
|
|
2159
|
-
return noPending;
|
|
2160
|
-
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
2161
|
-
}
|
|
2162
|
-
if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
|
|
2163
|
-
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
2164
|
-
return noPending;
|
|
2165
|
-
}
|
|
2166
|
-
const gateKind = cp.gate?.kind;
|
|
2167
|
-
if (gateKind !== PLAN_REVIEW_GATE_KIND) {
|
|
2168
|
-
return {
|
|
2169
|
-
status: 409,
|
|
2170
|
-
body: {
|
|
2171
|
-
error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a plan_review — a tool approval must be decided via POST /v1/approvals/:id/decide`,
|
|
2172
|
-
errorCode: "gate_not_plan_review",
|
|
2173
|
-
},
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
const ctx = await cs.getCtx(sessionId).catch(() => null);
|
|
2177
|
-
if (!ctx)
|
|
2178
|
-
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
2179
|
-
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
2180
|
-
const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
|
|
2181
|
-
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
2182
|
-
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
2183
|
-
const outcome = {
|
|
2184
|
-
gate: "plan_review",
|
|
2185
|
-
decision,
|
|
2186
|
-
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
2187
|
-
...(reason ? { reason } : {}),
|
|
2188
|
-
};
|
|
2189
|
-
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
2190
|
-
}
|
|
2191
119
|
function recordTaskResult(result) {
|
|
2192
120
|
const plane = terminalProjection(result.terminal);
|
|
2193
121
|
deps.metrics?.inc("tasks_total", { status: plane.status });
|
|
@@ -2212,500 +140,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2212
140
|
deps.planCacheProbe?.record(principal, objective, result.terminal.kind === "completed");
|
|
2213
141
|
evictIfConflict(deps.runner, sessionId, result);
|
|
2214
142
|
}
|
|
2215
|
-
function quotaExceeded(req, res) {
|
|
2216
|
-
if (!deps.costQuota)
|
|
2217
|
-
return false;
|
|
2218
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
2219
|
-
if (!principal)
|
|
2220
|
-
return false;
|
|
2221
|
-
const d = deps.costQuota.check(principal);
|
|
2222
|
-
if (d.allowed)
|
|
2223
|
-
return false;
|
|
2224
|
-
deps.metrics?.inc("cost_quota_rejected_total");
|
|
2225
|
-
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
2226
|
-
sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
|
|
2227
|
-
return true;
|
|
2228
|
-
}
|
|
2229
|
-
function ownerCostQuotaDenied(principal) {
|
|
2230
|
-
if (!deps.costQuota || !principal)
|
|
2231
|
-
return undefined;
|
|
2232
|
-
const d = deps.costQuota.check(principal);
|
|
2233
|
-
if (d.allowed)
|
|
2234
|
-
return undefined;
|
|
2235
|
-
deps.metrics?.inc("cost_quota_rejected_total");
|
|
2236
|
-
return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
|
|
2237
|
-
}
|
|
2238
|
-
async function ownerFleetLeaseDenied(owner) {
|
|
2239
|
-
if (!deps.fleetLease || !owner)
|
|
2240
|
-
return undefined;
|
|
2241
|
-
const adm = await deps.fleetLease.admit(owner);
|
|
2242
|
-
if (adm.ok)
|
|
2243
|
-
return undefined;
|
|
2244
|
-
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
2245
|
-
return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
|
|
2246
|
-
}
|
|
2247
|
-
async function ownerLeaseDenied(res, owner) {
|
|
2248
|
-
if (!deps.fleetLease || !owner)
|
|
2249
|
-
return false;
|
|
2250
|
-
const adm = await deps.fleetLease.admit(owner);
|
|
2251
|
-
if (adm.ok)
|
|
2252
|
-
return false;
|
|
2253
|
-
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
2254
|
-
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
2255
|
-
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
2256
|
-
return true;
|
|
2257
|
-
}
|
|
2258
|
-
function ownerQuotaDenied(res, owner) {
|
|
2259
|
-
const denial = ownerCostQuotaDenied(owner);
|
|
2260
|
-
if (!denial)
|
|
2261
|
-
return false;
|
|
2262
|
-
const { errorCode, error, ...extras } = denial.body;
|
|
2263
|
-
res.setHeader("retry-after", String(extras.retryAfterSec));
|
|
2264
|
-
sendError(res, denial.status, errorCode, error, extras);
|
|
2265
|
-
return true;
|
|
2266
|
-
}
|
|
2267
|
-
async function leaseDenied(req, res) {
|
|
2268
|
-
if (!deps.fleetLease)
|
|
2269
|
-
return false;
|
|
2270
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
2271
|
-
if (!principal)
|
|
2272
|
-
return false;
|
|
2273
|
-
const adm = await deps.fleetLease.admit(principal);
|
|
2274
|
-
if (adm.ok)
|
|
2275
|
-
return false;
|
|
2276
|
-
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
2277
|
-
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
2278
|
-
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
2279
|
-
return true;
|
|
2280
|
-
}
|
|
2281
|
-
async function usageWindowDenialFor(key) {
|
|
2282
|
-
const store = deps.usageWindowStore;
|
|
2283
|
-
const windows = deps.config.usageWindows;
|
|
2284
|
-
if (!store || !windows || windows.length === 0)
|
|
2285
|
-
return undefined;
|
|
2286
|
-
let retryAfterMs;
|
|
2287
|
-
try {
|
|
2288
|
-
retryAfterMs = usageRetryAfterMs(await store.read(key, windows, Date.now()));
|
|
2289
|
-
}
|
|
2290
|
-
catch (e) {
|
|
2291
|
-
deps.logger?.warn?.("usage_window_admission_read_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
2292
|
-
return undefined;
|
|
2293
|
-
}
|
|
2294
|
-
if (retryAfterMs === undefined)
|
|
2295
|
-
return undefined;
|
|
2296
|
-
deps.metrics?.inc("usage_window_rejected_total");
|
|
2297
|
-
deps.logger?.info?.("usage_window_exhausted", { hasPrincipal: key !== GLOBAL_USAGE_KEY, retryAfterMs });
|
|
2298
|
-
const retryAfterSec = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
2299
|
-
return { status: 429, body: { error: "usage window exhausted (deployment token governance)", errorCode: "usage.window_exhausted", retryAfterSec }, retryAfterSec };
|
|
2300
|
-
}
|
|
2301
|
-
async function usageWindowDenied(req, res) {
|
|
2302
|
-
const denial = await usageWindowDenialFor(gatedPrincipal(req, deps.config) || GLOBAL_USAGE_KEY);
|
|
2303
|
-
if (!denial)
|
|
2304
|
-
return false;
|
|
2305
|
-
res.setHeader("retry-after", String(denial.retryAfterSec));
|
|
2306
|
-
sendError(res, denial.status, denial.body.errorCode, denial.body.error, { retryAfterSec: denial.retryAfterSec });
|
|
2307
|
-
return true;
|
|
2308
|
-
}
|
|
2309
|
-
function rateLimited(req, res) {
|
|
2310
|
-
if (!deps.rateLimiter)
|
|
2311
|
-
return false;
|
|
2312
|
-
const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? req.socket.remoteAddress ?? "anon";
|
|
2313
|
-
const d = deps.rateLimiter.check(key);
|
|
2314
|
-
if (d.allowed)
|
|
2315
|
-
return false;
|
|
2316
|
-
deps.metrics?.inc("rate_limited_total");
|
|
2317
|
-
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
2318
|
-
sendError(res, 429, "limit.rate_exceeded", "rate limit exceeded", { retryAfterSec: d.retryAfterSec });
|
|
2319
|
-
return true;
|
|
2320
|
-
}
|
|
2321
|
-
function safeDecode(seg) {
|
|
2322
|
-
try {
|
|
2323
|
-
return decodeURIComponent(seg);
|
|
2324
|
-
}
|
|
2325
|
-
catch {
|
|
2326
|
-
return null;
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
function isFleetWide(req) {
|
|
2330
|
-
const { authToken: at, traceToken: tt } = deps.config;
|
|
2331
|
-
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
2332
|
-
return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
|
|
2333
|
-
}
|
|
2334
|
-
function sessionOwnerScope(req) {
|
|
2335
|
-
const fleetWide = isFleetWide(req);
|
|
2336
|
-
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
2337
|
-
return { fleetWide, gateOwner };
|
|
2338
|
-
}
|
|
2339
|
-
function sessionOwnerScopeForWrite(req) {
|
|
2340
|
-
const { authToken: at, traceToken: tt } = deps.config;
|
|
2341
|
-
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
2342
|
-
const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
|
|
2343
|
-
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
2344
|
-
return { fleetWide, gateOwner };
|
|
2345
|
-
}
|
|
2346
|
-
function runOwnerOk(req, res, owner, notFoundForm = "run") {
|
|
2347
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
2348
|
-
if (deps.config.requirePrincipal && !principal) {
|
|
2349
|
-
sendPrincipalRefusal(res, req, deps.config);
|
|
2350
|
-
return false;
|
|
2351
|
-
}
|
|
2352
|
-
if (!runOwnerReadable(principal, owner, deps.config)) {
|
|
2353
|
-
if (notFoundForm === "session")
|
|
2354
|
-
sendError(res, 404, "not_found.session", "session not found");
|
|
2355
|
-
else
|
|
2356
|
-
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
2357
|
-
return false;
|
|
2358
|
-
}
|
|
2359
|
-
return true;
|
|
2360
|
-
}
|
|
2361
|
-
const sessionAssertAbsentWarned = new Set();
|
|
2362
|
-
function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
|
|
2363
|
-
if (!run.sessionId)
|
|
2364
|
-
return true;
|
|
2365
|
-
const asserted = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
2366
|
-
if (asserted === null) {
|
|
2367
|
-
if (!sessionAssertAbsentWarned.has(face)) {
|
|
2368
|
-
sessionAssertAbsentWarned.add(face);
|
|
2369
|
-
console.warn(JSON.stringify({ ts: new Date().toISOString(), level: "warn", msg: "run_face_session_assert_absent", face, note: "caller sent no ?session= on a session-bound run — allowed in the accept phase; the enforcement train will 404 this ([1498]⑥/[1499])" }));
|
|
2370
|
-
}
|
|
2371
|
-
return true;
|
|
2372
|
-
}
|
|
2373
|
-
if (asserted === run.sessionId)
|
|
2374
|
-
return true;
|
|
2375
|
-
if (explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals))
|
|
2376
|
-
return true;
|
|
2377
|
-
sendError(res, 404, "not_found.run", notFoundError);
|
|
2378
|
-
return false;
|
|
2379
|
-
}
|
|
2380
|
-
async function readJson(req) {
|
|
2381
|
-
const chunks = [];
|
|
2382
|
-
let size = 0;
|
|
2383
|
-
for await (const c of req) {
|
|
2384
|
-
size += c.length;
|
|
2385
|
-
if (size > MAX_BODY)
|
|
2386
|
-
throw new HttpError(413, "request body too large");
|
|
2387
|
-
chunks.push(c);
|
|
2388
|
-
}
|
|
2389
|
-
if (chunks.length === 0)
|
|
2390
|
-
return {};
|
|
2391
|
-
try {
|
|
2392
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
|
|
2393
|
-
}
|
|
2394
|
-
catch {
|
|
2395
|
-
throw new HttpError(400, "invalid JSON body");
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
async function readRawBody(req, max) {
|
|
2399
|
-
const chunks = [];
|
|
2400
|
-
let size = 0;
|
|
2401
|
-
for await (const c of req) {
|
|
2402
|
-
size += c.length;
|
|
2403
|
-
if (size > max)
|
|
2404
|
-
throw new HttpError(413, "blob too large");
|
|
2405
|
-
chunks.push(c);
|
|
2406
|
-
}
|
|
2407
|
-
return Buffer.concat(chunks);
|
|
2408
|
-
}
|
|
2409
|
-
const denySweepClockGuard = createWallClockJumpGuard({
|
|
2410
|
-
slaMs: deps.config.approvalTimeoutSec > 0 ? deps.config.approvalTimeoutSec * 1000 : undefined,
|
|
2411
|
-
onJump: ({ jumpMs, graceMs }) => deps.logger?.warn?.("deny_sweep_wall_clock_jump", {
|
|
2412
|
-
jumpMs,
|
|
2413
|
-
graceMs,
|
|
2414
|
-
note: jumpMs > 0
|
|
2415
|
-
? "the wall clock stepped FORWARD relative to the monotonic clock; the approval SLA deny-sweep stands down for graceMs of MONOTONIC time so no pending gate is denied earlier than one full SLA of REAL time ([4232]①/[4235]). The checkpoint terminal_at_ms backstop is unaffected — nothing can hang forever."
|
|
2416
|
-
: "the wall clock stepped BACKWARD relative to the monotonic clock; the deny-sweep needs no grace for this direction (a receding deadline only DELAYS a deny) but the step itself is an operational fact worth recording.",
|
|
2417
|
-
}),
|
|
2418
|
-
onSuppressed: ({ remainingMs }) => deps.logger?.info?.("deny_sweep_clock_grace", { remainingMs }),
|
|
2419
|
-
onGraceCapped: ({ totalGraceMs }) => deps.logger?.warn?.("deny_sweep_clock_grace_capped", {
|
|
2420
|
-
totalGraceMs,
|
|
2421
|
-
note: "the wall clock kept stepping forward; the approval SLA deny-sweep has stood down for the maximum cumulative grace and now RESUMES on a demonstrably unstable clock — a fail-closed leg that never runs is worse than one running on a bad clock. Fix NTP on this host.",
|
|
2422
|
-
}),
|
|
2423
|
-
});
|
|
2424
|
-
async function denyExpiredApprovals(now) {
|
|
2425
|
-
const cs = deps.checkpointStore;
|
|
2426
|
-
if (!cs)
|
|
2427
|
-
return;
|
|
2428
|
-
if (!denySweepClockGuard.admit(now))
|
|
2429
|
-
return;
|
|
2430
|
-
const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
|
|
2431
|
-
const isParkedOwned = async (sessionId) => {
|
|
2432
|
-
if (deps.backgroundAgentStore === undefined && deps.workflowAgentSessionIndex === undefined)
|
|
2433
|
-
return false;
|
|
2434
|
-
try {
|
|
2435
|
-
const token = await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS });
|
|
2436
|
-
if (!token)
|
|
2437
|
-
return false;
|
|
2438
|
-
const cp = await cs.get(token);
|
|
2439
|
-
if (!cp)
|
|
2440
|
-
return false;
|
|
2441
|
-
return (await findParkOrigin({
|
|
2442
|
-
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
2443
|
-
...(deps.workflowAgentSessionIndex !== undefined && deps.workflowRunStore !== undefined
|
|
2444
|
-
? { workflowIndex: deps.workflowAgentSessionIndex, workflowRunStore: deps.workflowRunStore }
|
|
2445
|
-
: {}),
|
|
2446
|
-
}, { token, scope: cp.scope, sessionId: cp.sessionId })) !== undefined;
|
|
2447
|
-
}
|
|
2448
|
-
catch {
|
|
2449
|
-
return false;
|
|
2450
|
-
}
|
|
2451
|
-
};
|
|
2452
|
-
let parkedSkipped = 0;
|
|
2453
|
-
for (const { sessionId } of expired) {
|
|
2454
|
-
if (await isParkedOwned(sessionId)) {
|
|
2455
|
-
parkedSkipped += 1;
|
|
2456
|
-
continue;
|
|
2457
|
-
}
|
|
2458
|
-
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
|
|
2459
|
-
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) }));
|
|
2460
|
-
}
|
|
2461
|
-
if (parkedSkipped > 0)
|
|
2462
|
-
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
2463
|
-
}
|
|
2464
|
-
async function redeemParkedAsk(req) {
|
|
2465
|
-
const cs = deps.checkpointStore;
|
|
2466
|
-
if (!cs) {
|
|
2467
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
2468
|
-
}
|
|
2469
|
-
if (!hasServiceAuth(deps.config) && !deps.config.allowUnauthedWrites) {
|
|
2470
|
-
return { status: 503, body: { error: "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions", errorCode: "auth.service_token_required" } };
|
|
2471
|
-
}
|
|
2472
|
-
if (deps.drainState?.draining) {
|
|
2473
|
-
return {
|
|
2474
|
-
status: 503,
|
|
2475
|
-
body: {
|
|
2476
|
-
error: "draining",
|
|
2477
|
-
errorCode: "draining",
|
|
2478
|
-
message: "this instance is draining for shutdown/upgrade — retry against the replacement instance",
|
|
2479
|
-
...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}),
|
|
2480
|
-
},
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
if (deps.modelReady && !deps.modelReady()) {
|
|
2484
|
-
return {
|
|
2485
|
-
status: 503,
|
|
2486
|
-
body: {
|
|
2487
|
-
error: "model_roster_pending",
|
|
2488
|
-
errorCode: "state.model_roster_pending",
|
|
2489
|
-
message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly",
|
|
2490
|
-
},
|
|
2491
|
-
};
|
|
2492
|
-
}
|
|
2493
|
-
if (deps.costQuota && req.httpReq) {
|
|
2494
|
-
const quotaPrincipal = gatedPrincipal(req.httpReq, deps.config);
|
|
2495
|
-
if (quotaPrincipal !== undefined) {
|
|
2496
|
-
const d = deps.costQuota.check(quotaPrincipal);
|
|
2497
|
-
if (!d.allowed) {
|
|
2498
|
-
deps.metrics?.inc("cost_quota_rejected_total");
|
|
2499
|
-
return {
|
|
2500
|
-
status: 429,
|
|
2501
|
-
body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec },
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
let cp;
|
|
2507
|
-
try {
|
|
2508
|
-
cp = await cs.get(req.gateToken);
|
|
2509
|
-
}
|
|
2510
|
-
catch (e) {
|
|
2511
|
-
if (!(await mayDiscloseRowError(cs, req.gateToken, { principal: req.principal, explicitOperator: false }))) {
|
|
2512
|
-
deps.logger?.warn?.("parked_late_decision_unreadable_row_withheld", { askId: req.askId });
|
|
2513
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
2514
|
-
}
|
|
2515
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
2516
|
-
}
|
|
2517
|
-
if (!cp) {
|
|
2518
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
2519
|
-
}
|
|
2520
|
-
const cpPrincipal = decodeCheckpointScope(cp.scope);
|
|
2521
|
-
if (cpPrincipal !== undefined && cpPrincipal !== req.principal) {
|
|
2522
|
-
deps.logger?.warn?.("parked_late_decision_scope_mismatch", { askId: req.askId });
|
|
2523
|
-
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
2524
|
-
}
|
|
2525
|
-
const binding = {
|
|
2526
|
-
checkpointToken: req.gateToken,
|
|
2527
|
-
...(req.gateBoundCallId !== null ? { boundCallId: req.gateBoundCallId } : {}),
|
|
2528
|
-
...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
|
|
2529
|
-
...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
|
|
2530
|
-
};
|
|
2531
|
-
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, personDecision(req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
|
|
2532
|
-
return { status: out.status, body: out.body };
|
|
2533
|
-
}
|
|
2534
143
|
const isRunLiveOnThisReplica = (taskId) => inflightRuns.has(taskId) || steerableRuns.has(taskId);
|
|
2535
144
|
return Object.assign(server, { denyExpiredApprovals, isRunLiveOnThisReplica, redeemParkedAsk });
|
|
2536
145
|
}
|
|
2537
|
-
const WORKER_SWAP_REDEEMABLE = {
|
|
2538
|
-
version_newer: true,
|
|
2539
|
-
env_factory_missing: true,
|
|
2540
|
-
governed_unwired: true,
|
|
2541
|
-
origin_missing: true,
|
|
2542
|
-
seat_retired: true,
|
|
2543
|
-
ledger_event_unknown: true,
|
|
2544
|
-
real_approval_damaged: false,
|
|
2545
|
-
real_approval_forged: false,
|
|
2546
|
-
read_face_damaged: false,
|
|
2547
|
-
read_face_forged: false,
|
|
2548
|
-
write_face_damaged: false,
|
|
2549
|
-
constraint_chain_missing: false,
|
|
2550
|
-
};
|
|
2551
|
-
function checkpointRowRedeemableElsewhere(reason) {
|
|
2552
|
-
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
2553
|
-
}
|
|
2554
|
-
export function isMethodDispatchedSubmitPath(url) {
|
|
2555
|
-
return url === "/v1/a2a";
|
|
2556
|
-
}
|
|
2557
|
-
export function hasServiceAuth(config) {
|
|
2558
|
-
return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
|
|
2559
|
-
}
|
|
2560
|
-
export function isBillableSubmitPath(url) {
|
|
2561
|
-
return (url === "/v1/side-query" ||
|
|
2562
|
-
url === "/v1/tasks" ||
|
|
2563
|
-
url === "/v1/tasks/stream" ||
|
|
2564
|
-
url === "/v1/runs" ||
|
|
2565
|
-
url === "/v1/leader" ||
|
|
2566
|
-
url === "/v1/a2a" ||
|
|
2567
|
-
url === "/v1/admin/memory/consolidation/run" ||
|
|
2568
|
-
ASSISTANT_RESUME_RE.test(url) ||
|
|
2569
|
-
ASSISTANT_PLAN_REVIEW_RE.test(url) ||
|
|
2570
|
-
RUN_SUBAGENT_RESUME_RE.test(url) ||
|
|
2571
|
-
SESSION_WAKE_RE.test(url) ||
|
|
2572
|
-
url.startsWith("/v1/approvals"));
|
|
2573
|
-
}
|
|
2574
|
-
export function isCredentialGatedRewrite(method, url) {
|
|
2575
|
-
const m = method.toUpperCase();
|
|
2576
|
-
if (m === "POST") {
|
|
2577
|
-
return (url === "/v1/adoption" ||
|
|
2578
|
-
url.startsWith("/v1/rules/cc-import/") ||
|
|
2579
|
-
url.startsWith("/v1/rules/local-import/") ||
|
|
2580
|
-
url === "/v1/memory/import" ||
|
|
2581
|
-
url === "/v1/memory/export" ||
|
|
2582
|
-
url === "/v1/memory/erase" ||
|
|
2583
|
-
url.startsWith("/v1/memory/origin/entries/") ||
|
|
2584
|
-
url.startsWith("/v1/devices/"));
|
|
2585
|
-
}
|
|
2586
|
-
if (m === "PUT")
|
|
2587
|
-
return url === "/v1/admin/memory-optout" || url.startsWith("/v1/admin/memory-optout/");
|
|
2588
|
-
if (m === "DELETE")
|
|
2589
|
-
return url === "/v1/rules" || url.startsWith("/v1/admin/memory-optout/");
|
|
2590
|
-
if (m === "GET") {
|
|
2591
|
-
return (/^\/v1\/memory\/entries\/[^/]+\/provenance$/.test(url) ||
|
|
2592
|
-
url === "/v1/memory/origin/external" ||
|
|
2593
|
-
url === "/v1/memory/origin/clearances" ||
|
|
2594
|
-
url === "/v1/admin/memory-optout" ||
|
|
2595
|
-
url === "/v1/devices" ||
|
|
2596
|
-
/^\/v1\/devices\/sessions\/[^/]+$/.test(url));
|
|
2597
|
-
}
|
|
2598
|
-
return false;
|
|
2599
|
-
}
|
|
2600
|
-
const ROUTE_LABEL_PATTERNS = [
|
|
2601
|
-
[/^\/v1\/runs\/[^/]+\/steer$/, "/v1/runs/:id/steer"],
|
|
2602
|
-
[/^\/v1\/runs\/[^/]+\/memory\/capture-optout$/, "/v1/runs/:id/memory/capture-optout"],
|
|
2603
|
-
[/^\/v1\/runs\/[^/]+\/interrupt$/, "/v1/runs/:id/interrupt"],
|
|
2604
|
-
[/^\/v1\/runs\/[^/]+\/detach$/, "/v1/runs/:id/detach"],
|
|
2605
|
-
[/^\/v1\/runs\/[^/]+\/cancel$/, "/v1/runs/:id/cancel"],
|
|
2606
|
-
[/^\/v1\/runs\/[^/]+\/compact$/, "/v1/runs/:id/compact"],
|
|
2607
|
-
[/^\/v1\/runs\/[^/]+\/events$/, "/v1/runs/:id/events"],
|
|
2608
|
-
[/^\/v1\/runs\/[^/]+\/subagents\/[^/]+\/(steer|resume|output|stream)$/, "/v1/runs/:id/subagents/:sub/*"],
|
|
2609
|
-
[/^\/v1\/runs\/[^/]+\/tasks\/[^/]+\/(output|stop)$/, "/v1/runs/:id/tasks/:tid/*"],
|
|
2610
|
-
[/^\/v1\/runs\/[^/]+$/, "/v1/runs/:id"],
|
|
2611
|
-
[/^\/v1\/images\/bakes\/[^/]+\/events$/, "/v1/images/bakes/:id/events"],
|
|
2612
|
-
[/^\/v1\/images\/bakes\/[^/]+\/cancel$/, "/v1/images/bakes/:id/cancel"],
|
|
2613
|
-
[/^\/v1\/images\/bakes\/[^/]+\/claim$/, "/v1/images/bakes/:id/claim"],
|
|
2614
|
-
[/^\/v1\/images\/bakes\/[^/]+\/ingest$/, "/v1/images/bakes/:id/ingest"],
|
|
2615
|
-
[/^\/v1\/images\/bakes\/[^/]+$/, "/v1/images/bakes/:id"],
|
|
2616
|
-
[/^\/v1\/images\/digests\/[^/]+$/, "/v1/images/digests/:digest"],
|
|
2617
|
-
[/^\/v1\/images\/[^/]+$/, "/v1/images/:profile"],
|
|
2618
|
-
[/^\/v1\/approvals\/[^/]+\/decide$/, "/v1/approvals/:id/decide"],
|
|
2619
|
-
[/^\/v1\/approvals\/[^/]+\/exemptions(\/[^/]+)?$/, "/v1/approvals/:id/exemptions"],
|
|
2620
|
-
[/^\/v1\/sessions\/[^/]+\/(wake|fork|init|notify|events|head|mcp|a2a|policy|settings)$/, "/v1/sessions/:id/$verb"],
|
|
2621
|
-
[/^\/v1\/admin\/memory-optout\/[^/]+$/, "/v1/admin/memory-optout/:principal"],
|
|
2622
|
-
[/^\/v1\/sessions\/[^/]+\/memory\/erase$/, "/v1/sessions/:id/memory/erase"],
|
|
2623
|
-
[/^\/v1\/sessions\/[^/]+\/memory-status$/, "/v1/sessions/:id/memory-status"],
|
|
2624
|
-
[/^\/v1\/sessions\/[^/]+\/sync\/(manifest|entries|plan)$/, "/v1/sessions/:id/sync/$verb"],
|
|
2625
|
-
[/^\/v1\/sessions\/[^/]+\/sync\/import(\/[^/]+\/entries)?$/, "/v1/sessions/:id/sync/import"],
|
|
2626
|
-
[/^\/v1\/sessions\/[^/]+\/sync\/blobs(\/[^/]+)?$/, "/v1/sessions/:id/sync/blobs"],
|
|
2627
|
-
[/^\/v1\/sessions\/[^/]+\/sync\/history\/blobs\/[^/]+$/, "/v1/sessions/:id/sync/history/blobs/:hash"],
|
|
2628
|
-
[/^\/v1\/sessions\/[^/]+\/workspace(\/[^/]+(\/(tree|file|archive))?)?$/, "/v1/sessions/:id/workspace"],
|
|
2629
|
-
[/^\/v1\/sessions\/[^/]+$/, "/v1/sessions/:id"],
|
|
2630
|
-
[/^\/v1\/assistant\/tasks\/[^/]+\/preempt$/, "/v1/assistant/tasks/:id/preempt"],
|
|
2631
|
-
[/^\/v1\/assistant\/tasks\/[^/]+\/resume$/, "/v1/assistant/tasks/:id/resume"],
|
|
2632
|
-
[/^\/v1\/assistant\/tasks\/[^/]+\/plan_review$/, "/v1/assistant/tasks/:id/plan_review"],
|
|
2633
|
-
[/^\/v1\/elicitations\/[^/]+\/respond$/, "/v1/elicitations/:id/respond"],
|
|
2634
|
-
[/^\/v1\/questions\/[^/]+\/respond$/, "/v1/questions/:id/respond"],
|
|
2635
|
-
[/^\/v1\/tool-approvals\/[^/]+\/respond$/, "/v1/tool-approvals/:id/respond"],
|
|
2636
|
-
[/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
|
|
2637
|
-
[/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
|
|
2638
|
-
[/^\/v1\/workflows\/[^/]+$/, "/v1/workflows/:id"],
|
|
2639
|
-
[/^\/v1\/tasks\/[^/]+\/asks\/[^/]+\/decision$/, "/v1/tasks/:id/asks/:askId/decision"],
|
|
2640
|
-
[/^\/v1\/tasks\/[^/]+\/tool-results\/[^/]+$/, "/v1/tasks/:id/tool-results/:ref"],
|
|
2641
|
-
[/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
|
|
2642
|
-
[/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
|
|
2643
|
-
[/^\/v1\/devices\/sessions\/[^/]+\/rebind$/, "/v1/devices/sessions/:id/rebind"],
|
|
2644
|
-
[/^\/v1\/devices\/sessions\/[^/]+$/, "/v1/devices/sessions/:id"],
|
|
2645
|
-
[/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
|
|
2646
|
-
[/^\/v1\/capabilities\/scenarios\/[^/]+$/, "/v1/capabilities/scenarios/:name"],
|
|
2647
|
-
[/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
|
|
2648
|
-
[/^\/v1\/ops\/retention\/holds\/[^/]*$/, "/v1/ops/retention/holds/:domain"],
|
|
2649
|
-
[/^\/v1\/memory\/sync\/[^/]+$/, "/v1/memory/sync/:scope"],
|
|
2650
|
-
[/^\/v1\/memory\/entries\/[^/]+\/provenance$/, "/v1/memory/entries/:entryId/provenance"],
|
|
2651
|
-
[/^\/v1\/memory\/origin\/entries\/[^/]+\/clear$/, "/v1/memory/origin/entries/:entryId/clear"],
|
|
2652
|
-
[/^\/v1\/usage\/(summary|series|breakdown)$/, "/v1/usage/$sub"],
|
|
2653
|
-
[/^\/v1\/shared-memory\/stores\/[^/]+\/documents$/, "/v1/shared-memory/stores/:store/documents"],
|
|
2654
|
-
[/^\/v1\/shared-memory\/stores\/[^/]+\/document$/, "/v1/shared-memory/stores/:store/document"],
|
|
2655
|
-
];
|
|
2656
|
-
const ROUTE_LABEL_LITERALS = new Set([
|
|
2657
|
-
"/health", "/metrics", "/metrics/summary", "/metrics/plan-cache",
|
|
2658
|
-
"/v1/models", "/v1/tasks", "/v1/tasks/stream", "/v1/tasks/source-summary",
|
|
2659
|
-
"/v1/runs", "/v1/sessions", "/v1/approvals", "/v1/approvals/stream",
|
|
2660
|
-
"/v1/assistant/inbox", "/v1/assistant/tasks", "/v1/usage", "/v1/policy", "/v1/capabilities",
|
|
2661
|
-
"/v1/images", "/v1/images/bakes", "/v1/images/bakes/claim", "/v1/images/select", "/v1/images/register",
|
|
2662
|
-
"/v1/workflows", "/v1/attachments", "/v1/leader", "/v1/outcomes", "/v1/side-query",
|
|
2663
|
-
"/v1/devices",
|
|
2664
|
-
"/v1/fleet/stream",
|
|
2665
|
-
"/v1/memory/export", "/v1/sendfile-links",
|
|
2666
|
-
"/v1/memory/import",
|
|
2667
|
-
"/v1/memory/erase",
|
|
2668
|
-
"/v1/memory/origin/external", "/v1/memory/origin/clearances",
|
|
2669
|
-
"/v1/shared-memory/stores",
|
|
2670
|
-
"/v1/diagnostics/wiring",
|
|
2671
|
-
"/v1/adoption",
|
|
2672
|
-
"/v1/admin/drain",
|
|
2673
|
-
"/v1/admin/config/refresh",
|
|
2674
|
-
"/v1/config/catalog",
|
|
2675
|
-
"/v1/admin/memory-optout",
|
|
2676
|
-
"/v1/admin/memory/consolidation/run", "/v1/admin/memory/consolidation",
|
|
2677
|
-
"/v1/rules/cc-import/prepare", "/v1/rules/cc-import/redeem",
|
|
2678
|
-
"/v1/rules/local-import/prepare", "/v1/rules/local-import/redeem",
|
|
2679
|
-
"/v1/rules",
|
|
2680
|
-
"/.well-known/agent-card.json", "/v1/a2a",
|
|
2681
|
-
"/v1/agents/roster",
|
|
2682
|
-
"/v1/ops/retention/audit",
|
|
2683
|
-
]);
|
|
2684
|
-
export function routeLabel(_method, url) {
|
|
2685
|
-
if (ROUTE_LABEL_LITERALS.has(url))
|
|
2686
|
-
return url;
|
|
2687
|
-
for (const [re, label] of ROUTE_LABEL_PATTERNS)
|
|
2688
|
-
if (re.test(url))
|
|
2689
|
-
return label;
|
|
2690
|
-
return "other";
|
|
2691
|
-
}
|
|
2692
|
-
function cors(res, req, origins, principalHeader) {
|
|
2693
|
-
if (!origins || origins.length === 0)
|
|
2694
|
-
return;
|
|
2695
|
-
let allow;
|
|
2696
|
-
if (origins.length === 1) {
|
|
2697
|
-
allow = origins[0];
|
|
2698
|
-
}
|
|
2699
|
-
else {
|
|
2700
|
-
res.setHeader("vary", "Origin");
|
|
2701
|
-
const reqOrigin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
|
|
2702
|
-
allow = reqOrigin !== undefined && origins.includes(reqOrigin) ? reqOrigin : undefined;
|
|
2703
|
-
}
|
|
2704
|
-
if (allow === undefined)
|
|
2705
|
-
return;
|
|
2706
|
-
res.setHeader("access-control-allow-origin", allow);
|
|
2707
|
-
res.setHeader("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2708
|
-
res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
|
|
2709
|
-
res.setHeader("access-control-expose-headers", "x-task-id, retry-after");
|
|
2710
|
-
}
|
|
2711
146
|
//# sourceMappingURL=server.js.map
|