@tea-agent/loop-agent 0.41.0 → 0.42.0-next.1
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/CHANGELOG.md +102 -14
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +39 -12
- package/dist/executors/pi-sdk-executor.js +3 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +2 -1
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +2 -2
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +1 -1
- package/dist/worker/console/chat/session-store.js +1 -1
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.js +57 -0
- package/dist/worker/console/routes.js +637 -8
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +6 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-D6PvaVd-.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
- package/dist/worker/console/static/assets/{index-Bih9w9h9.js → index-xwu9GxEc.js} +80 -80
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
- package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-CfUiDDB3.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-BQe6fpqj.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/workspace-context.js +28 -2
- package/dist/worker/console/workspace-registry.js +2 -2
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1037 -126
- package/dist/worker/observe/static/views/dag-inspector.js +1215 -58
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +146 -0
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +555 -1
- package/dist/workflows/dag/rerun-run.js +601 -15
- package/dist/workflows/dag/runner.js +20 -1
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +183 -7
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/agent-dag.schema.json +32 -0
- package/docs/templates/product-line/task.yaml +1 -1
- package/package.json +3 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
- package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, rm } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { dispatchOperatorAction } from "./operator-actions.js";
|
|
5
6
|
import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
|
|
6
7
|
import { probePiReadiness } from "./pi-readiness.js";
|
|
7
8
|
import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
8
9
|
import { ROUTES } from "../observe/routes.js";
|
|
9
|
-
import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
10
|
+
import { evaluateProtectedDownloadGate, evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
10
11
|
import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
|
|
11
|
-
import {
|
|
12
|
+
import { writeArtifactRevisionUpload } from "../../infrastructure/console/artifact-revision-store.js";
|
|
13
|
+
import { ensureSecureDir } from "../../infrastructure/console/app-data.js";
|
|
14
|
+
import { acquireContextExportLease, createContextExport, releaseContextExportLease, } from "../../infrastructure/console/context-export-store.js";
|
|
15
|
+
import { acceptOperation } from "./operator-actions.js";
|
|
16
|
+
import { recordContextTransferDiagnostic, recordContextTransferEvent, } from "./context-transfer-diagnostics.js";
|
|
17
|
+
import { isDagRerunPromptOverrideEnabled } from "../../shared/dag-prompt-override.js";
|
|
18
|
+
import { beginWorkerContinuation, resolveWorkerContinuationAuthority, settleWorkerContinuation, } from "../continuation/worker-continuation.js";
|
|
19
|
+
import { installPiPlugin, listPiPluginVersions, listPiPlugins, uninstallPiPlugin, checkPiPluginUpdates, } from "./pi-plugins.js";
|
|
12
20
|
import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
|
|
13
21
|
export function sendJson(res, status, body, extraHeaders) {
|
|
14
22
|
const payload = JSON.stringify(body);
|
|
@@ -149,7 +157,516 @@ async function handleSessionPing(req, res, ctx) {
|
|
|
149
157
|
export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
|
|
150
158
|
"dagRerunPlan",
|
|
151
159
|
"dagRerun",
|
|
160
|
+
"contextExport",
|
|
161
|
+
"artifactRevisionStage",
|
|
162
|
+
"artifactRevisionPlan",
|
|
163
|
+
"artifactRevisionExecute",
|
|
152
164
|
]);
|
|
165
|
+
function evaluateConsoleCapability(req, ctx) {
|
|
166
|
+
const common = {
|
|
167
|
+
consoleOrigin: ctx.getConsoleOrigin(),
|
|
168
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
169
|
+
};
|
|
170
|
+
const general = evaluateMutationGate(req, {
|
|
171
|
+
...common,
|
|
172
|
+
cookieName: ctx.bootToken.cookieName,
|
|
173
|
+
bootToken: ctx.bootToken.value,
|
|
174
|
+
confirmationToken: ctx.bootToken.confirmationToken,
|
|
175
|
+
});
|
|
176
|
+
if (!general || !ctx.inspectBootToken)
|
|
177
|
+
return general;
|
|
178
|
+
return evaluateMutationGate(req, {
|
|
179
|
+
...common,
|
|
180
|
+
cookieName: ctx.inspectBootToken.cookieName,
|
|
181
|
+
bootToken: ctx.inspectBootToken.value,
|
|
182
|
+
confirmationToken: ctx.inspectBootToken.confirmationToken,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function evaluateConsoleDownloadCapability(req, ctx) {
|
|
186
|
+
const common = {
|
|
187
|
+
consoleOrigin: ctx.getConsoleOrigin(),
|
|
188
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
189
|
+
};
|
|
190
|
+
const general = evaluateProtectedDownloadGate(req, {
|
|
191
|
+
...common,
|
|
192
|
+
cookieName: ctx.bootToken.cookieName,
|
|
193
|
+
bootToken: ctx.bootToken.value,
|
|
194
|
+
confirmationToken: ctx.bootToken.confirmationToken,
|
|
195
|
+
});
|
|
196
|
+
if (!general || !ctx.inspectBootToken)
|
|
197
|
+
return general;
|
|
198
|
+
return evaluateProtectedDownloadGate(req, {
|
|
199
|
+
...common,
|
|
200
|
+
cookieName: ctx.inspectBootToken.cookieName,
|
|
201
|
+
bootToken: ctx.inspectBootToken.value,
|
|
202
|
+
confirmationToken: ctx.inspectBootToken.confirmationToken,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function acceptContextExportOperation(input) {
|
|
206
|
+
const operator = input.ctx.operator;
|
|
207
|
+
const clientRequestId = input.clientRequestId?.trim();
|
|
208
|
+
if (!clientRequestId) {
|
|
209
|
+
return {
|
|
210
|
+
status: 400,
|
|
211
|
+
body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
|
|
215
|
+
const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
|
|
216
|
+
const attempt = Number(input.actionParams.attempt ?? 1);
|
|
217
|
+
const profile = input.actionParams.profile === "raw" ? "raw" : "redacted";
|
|
218
|
+
const target = ["generic", "pi-session", "pi-handoff", "readable"].includes(String(input.actionParams.target))
|
|
219
|
+
? input.actionParams.target
|
|
220
|
+
: "generic";
|
|
221
|
+
if (!runId || !nodeId || !Number.isInteger(attempt) || attempt < 1) {
|
|
222
|
+
return {
|
|
223
|
+
status: 400,
|
|
224
|
+
body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, nodeId and positive attempt are required" } },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (profile === "raw" && input.actionParams.rawAcknowledged !== true) {
|
|
228
|
+
return {
|
|
229
|
+
status: 400,
|
|
230
|
+
body: { ok: false, error: { code: "RAW_EXPORT_ACKNOWLEDGEMENT_REQUIRED", message: "raw export requires explicit acknowledgement" } },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const created = await operator.operations.create({
|
|
234
|
+
clientRequestId,
|
|
235
|
+
action: "contextExport",
|
|
236
|
+
actionParams: { runId, nodeId, attempt, profile, target, rawAcknowledged: profile === "raw" },
|
|
237
|
+
});
|
|
238
|
+
if (created.kind === "conflict") {
|
|
239
|
+
return {
|
|
240
|
+
status: 409,
|
|
241
|
+
body: { ok: false, error: { code: created.code, message: created.message } },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (created.kind === "created") {
|
|
245
|
+
void recordContextTransferEvent({
|
|
246
|
+
appData: operator.actionContext.appData,
|
|
247
|
+
event: "context_export.planned",
|
|
248
|
+
fields: {
|
|
249
|
+
runId,
|
|
250
|
+
nodeId,
|
|
251
|
+
profile,
|
|
252
|
+
target,
|
|
253
|
+
operationId: created.operation.operationId,
|
|
254
|
+
},
|
|
255
|
+
}).catch(() => undefined);
|
|
256
|
+
queueMicrotask(() => {
|
|
257
|
+
void (async () => {
|
|
258
|
+
const startedAt = Date.now();
|
|
259
|
+
await operator.operations.update(created.operation.operationId, {
|
|
260
|
+
state: "running",
|
|
261
|
+
startedAt: new Date().toISOString(),
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
const appData = operator.actionContext.appData;
|
|
265
|
+
if (!appData.contextExports)
|
|
266
|
+
throw new Error("console context export root unavailable");
|
|
267
|
+
ensureSecureDir(appData.contextExports);
|
|
268
|
+
const incomingPath = path.join(appData.contextExports, `${randomBytes(16).toString("hex")}.incoming.zip`);
|
|
269
|
+
await rm(incomingPath, { force: true });
|
|
270
|
+
try {
|
|
271
|
+
const kernel = await runKernelCommand({
|
|
272
|
+
ctx: input.ctx,
|
|
273
|
+
args: [
|
|
274
|
+
"dag", "context", "export",
|
|
275
|
+
"--run-id", runId,
|
|
276
|
+
"--node-id", nodeId,
|
|
277
|
+
"--attempt", String(attempt),
|
|
278
|
+
"--target", target,
|
|
279
|
+
"--redaction", profile,
|
|
280
|
+
...(profile === "raw" ? ["--acknowledge-sensitive-data"] : []),
|
|
281
|
+
"--output", incomingPath,
|
|
282
|
+
"--json",
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
const payload = (kernel.json ?? {});
|
|
286
|
+
if (!kernel.ok || !payload.bundleId || !payload.bundleSha256) {
|
|
287
|
+
throw new Error(kernelFailureMessage(kernel));
|
|
288
|
+
}
|
|
289
|
+
const zipBytes = await readFile(incomingPath);
|
|
290
|
+
const exported = await createContextExport({
|
|
291
|
+
appData,
|
|
292
|
+
bundle: {
|
|
293
|
+
zipBytes,
|
|
294
|
+
manifest: {
|
|
295
|
+
bundleId: payload.bundleId,
|
|
296
|
+
bundleSha256: payload.bundleSha256,
|
|
297
|
+
profile,
|
|
298
|
+
target,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
await recordContextTransferDiagnostic({
|
|
303
|
+
appData: operator.actionContext.appData,
|
|
304
|
+
event: "context-bundle.generate",
|
|
305
|
+
dimensions: {
|
|
306
|
+
outcome: "succeeded",
|
|
307
|
+
target,
|
|
308
|
+
profile,
|
|
309
|
+
bytes: exported.bytes,
|
|
310
|
+
durationMs: Date.now() - startedAt,
|
|
311
|
+
},
|
|
312
|
+
}).catch(() => undefined);
|
|
313
|
+
await recordContextTransferEvent({
|
|
314
|
+
appData: operator.actionContext.appData,
|
|
315
|
+
event: "context_export.ready",
|
|
316
|
+
fields: {
|
|
317
|
+
runId,
|
|
318
|
+
nodeId,
|
|
319
|
+
exportId: exported.exportId,
|
|
320
|
+
profile,
|
|
321
|
+
target,
|
|
322
|
+
bytes: exported.bytes,
|
|
323
|
+
durationMs: Date.now() - startedAt,
|
|
324
|
+
operationId: created.operation.operationId,
|
|
325
|
+
},
|
|
326
|
+
}).catch(() => undefined);
|
|
327
|
+
await operator.operations.update(created.operation.operationId, {
|
|
328
|
+
state: "succeeded",
|
|
329
|
+
finishedAt: new Date().toISOString(),
|
|
330
|
+
result: {
|
|
331
|
+
ok: true,
|
|
332
|
+
exportId: exported.exportId,
|
|
333
|
+
bundleId: exported.bundleId,
|
|
334
|
+
bundleSha256: exported.bundleSha256,
|
|
335
|
+
bytes: exported.bytes,
|
|
336
|
+
expiresAt: exported.expiresAt,
|
|
337
|
+
downloadUrl: `/api/operator/v1/context-exports/${exported.exportId}/download`,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
await rm(incomingPath, { force: true }).catch(() => undefined);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
await recordContextTransferDiagnostic({
|
|
347
|
+
appData: operator.actionContext.appData,
|
|
348
|
+
event: "context-bundle.generate",
|
|
349
|
+
dimensions: {
|
|
350
|
+
outcome: "failed",
|
|
351
|
+
target,
|
|
352
|
+
profile,
|
|
353
|
+
reasonCode: "CONTEXT_EXPORT_FAILED",
|
|
354
|
+
durationMs: Date.now() - startedAt,
|
|
355
|
+
},
|
|
356
|
+
}).catch(() => undefined);
|
|
357
|
+
await recordContextTransferEvent({
|
|
358
|
+
appData: operator.actionContext.appData,
|
|
359
|
+
event: "context_export.failed",
|
|
360
|
+
fields: {
|
|
361
|
+
runId,
|
|
362
|
+
nodeId,
|
|
363
|
+
profile,
|
|
364
|
+
target,
|
|
365
|
+
reasonCode: "CONTEXT_EXPORT_FAILED",
|
|
366
|
+
durationMs: Date.now() - startedAt,
|
|
367
|
+
operationId: created.operation.operationId,
|
|
368
|
+
},
|
|
369
|
+
}).catch(() => undefined);
|
|
370
|
+
await operator.operations.update(created.operation.operationId, {
|
|
371
|
+
state: "failed",
|
|
372
|
+
finishedAt: new Date().toISOString(),
|
|
373
|
+
errorCode: "CONTEXT_EXPORT_FAILED",
|
|
374
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
})();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
status: 202,
|
|
382
|
+
body: {
|
|
383
|
+
operationId: created.operation.operationId,
|
|
384
|
+
state: created.operation.state,
|
|
385
|
+
clientRequestId: created.operation.clientRequestId,
|
|
386
|
+
action: created.operation.action,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async function runKernelCommand(input) {
|
|
391
|
+
const actionContext = input.ctx.operator.actionContext;
|
|
392
|
+
const run = actionContext.runCommand ??
|
|
393
|
+
((args, options) => actionContext.client.run(args, options));
|
|
394
|
+
return (await run(input.args, {
|
|
395
|
+
cwd: actionContext.repoRoot,
|
|
396
|
+
artifactName: `kernel-${input.args.filter((arg) => !arg.startsWith("--")).slice(0, 3).join("-").replace(/\s+/g, "-")}`,
|
|
397
|
+
expectJson: true,
|
|
398
|
+
env: { ...process.env, LOOP_CONSOLE_APP_DATA: actionContext.appData.root },
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
function kernelFailureMessage(result) {
|
|
402
|
+
const stderr = result.stderr.trim();
|
|
403
|
+
if (stderr) {
|
|
404
|
+
const lastLine = stderr.split("\n").filter(Boolean).pop() ?? stderr;
|
|
405
|
+
return lastLine.slice(0, 300);
|
|
406
|
+
}
|
|
407
|
+
const stdout = result.stdout.trim();
|
|
408
|
+
if (stdout)
|
|
409
|
+
return stdout.slice(0, 300);
|
|
410
|
+
return `kernel command failed with exit code ${result.exitCode}`;
|
|
411
|
+
}
|
|
412
|
+
function decodeBoundedBase64(value) {
|
|
413
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 14_000_000) {
|
|
414
|
+
throw new Error("candidateBase64 is required and exceeds no more than 10 MiB decoded");
|
|
415
|
+
}
|
|
416
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
417
|
+
throw new Error("candidateBase64 is invalid");
|
|
418
|
+
}
|
|
419
|
+
const bytes = Buffer.from(value, "base64");
|
|
420
|
+
if (bytes.byteLength > 10 * 1024 * 1024)
|
|
421
|
+
throw new Error("candidate exceeds 10 MiB");
|
|
422
|
+
return bytes;
|
|
423
|
+
}
|
|
424
|
+
function revisionProjection(revision) {
|
|
425
|
+
if (!revision)
|
|
426
|
+
return undefined;
|
|
427
|
+
return {
|
|
428
|
+
revisionId: revision.revisionId,
|
|
429
|
+
state: revision.state,
|
|
430
|
+
parent: revision.parent,
|
|
431
|
+
candidate: {
|
|
432
|
+
sha256: revision.candidate.sha256,
|
|
433
|
+
bytes: revision.candidate.bytes,
|
|
434
|
+
mediaType: revision.candidate.mediaType,
|
|
435
|
+
uploadedAt: revision.candidate.uploadedAt,
|
|
436
|
+
},
|
|
437
|
+
validation: revision.validation,
|
|
438
|
+
diff: {
|
|
439
|
+
kind: revision.diff.kind,
|
|
440
|
+
summary: revision.diff.summary,
|
|
441
|
+
truncated: revision.diff.truncated,
|
|
442
|
+
},
|
|
443
|
+
expiresAt: revision.expiresAt,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async function acceptArtifactRevisionOperation(input) {
|
|
447
|
+
const operator = input.ctx.operator;
|
|
448
|
+
const clientRequestId = input.clientRequestId?.trim();
|
|
449
|
+
if (!clientRequestId) {
|
|
450
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } } };
|
|
451
|
+
}
|
|
452
|
+
let candidateBytes;
|
|
453
|
+
try {
|
|
454
|
+
if (input.action === "artifactRevisionStage") {
|
|
455
|
+
candidateBytes = decodeBoundedBase64(input.actionParams.candidateBase64);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } } };
|
|
460
|
+
}
|
|
461
|
+
const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
|
|
462
|
+
const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
|
|
463
|
+
const artifactId = typeof input.actionParams.artifactId === "string" ? input.actionParams.artifactId.trim() : "";
|
|
464
|
+
const revisionId = typeof input.actionParams.revisionId === "string" ? input.actionParams.revisionId.trim() : "";
|
|
465
|
+
const planHash = typeof input.actionParams.planHash === "string" ? input.actionParams.planHash.trim() : "";
|
|
466
|
+
const reason = typeof input.actionParams.reason === "string" ? input.actionParams.reason.trim() : "";
|
|
467
|
+
const mediaType = typeof input.actionParams.mediaType === "string" ? input.actionParams.mediaType.trim() : "";
|
|
468
|
+
if (!runId || !artifactId || (input.action === "artifactRevisionStage" && (!nodeId || !mediaType))) {
|
|
469
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, artifactId, nodeId and mediaType are required" } } };
|
|
470
|
+
}
|
|
471
|
+
if (input.action !== "artifactRevisionStage" && !revisionId) {
|
|
472
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "revisionId is required" } } };
|
|
473
|
+
}
|
|
474
|
+
if (input.action === "artifactRevisionExecute" && (!planHash || !reason)) {
|
|
475
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "planHash and reason are required" } } };
|
|
476
|
+
}
|
|
477
|
+
let workerContinuation;
|
|
478
|
+
if (input.action !== "artifactRevisionStage") {
|
|
479
|
+
try {
|
|
480
|
+
workerContinuation = await resolveWorkerContinuationAuthority({
|
|
481
|
+
repoRoot: input.ctx.repoRoot,
|
|
482
|
+
parentDagRunId: runId,
|
|
483
|
+
revisionId,
|
|
484
|
+
allowActiveRevision: input.action === "artifactRevisionExecute",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
489
|
+
return {
|
|
490
|
+
status: 422,
|
|
491
|
+
body: {
|
|
492
|
+
ok: false,
|
|
493
|
+
error: {
|
|
494
|
+
code: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
495
|
+
message,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const safeActionParams = {
|
|
502
|
+
runId,
|
|
503
|
+
artifactId,
|
|
504
|
+
...(nodeId ? { nodeId } : {}),
|
|
505
|
+
...(revisionId ? { revisionId } : {}),
|
|
506
|
+
...(planHash ? { planHash } : {}),
|
|
507
|
+
...(reason ? { reason } : {}),
|
|
508
|
+
...(mediaType ? { mediaType } : {}),
|
|
509
|
+
...(candidateBytes
|
|
510
|
+
? { candidateSha256: createHash("sha256").update(candidateBytes).digest("hex"), candidateBytes: candidateBytes.byteLength }
|
|
511
|
+
: {}),
|
|
512
|
+
...(workerContinuation
|
|
513
|
+
? { workerContinuation: workerContinuation.authority }
|
|
514
|
+
: {}),
|
|
515
|
+
};
|
|
516
|
+
const appData = operator.actionContext.appData;
|
|
517
|
+
if (!appData.artifactRevisions) {
|
|
518
|
+
return { status: 503, body: { ok: false, error: { code: "INTERNAL_ERROR", message: "console artifact revision root unavailable" } } };
|
|
519
|
+
}
|
|
520
|
+
let cliArgs;
|
|
521
|
+
if (input.action === "artifactRevisionStage") {
|
|
522
|
+
const candidateSha256 = safeActionParams.candidateSha256;
|
|
523
|
+
cliArgs = [
|
|
524
|
+
"dag", "artifact", "revision", "stage",
|
|
525
|
+
"--run-id", runId,
|
|
526
|
+
"--node-id", nodeId,
|
|
527
|
+
"--artifact-id", artifactId,
|
|
528
|
+
"--candidate-upload", candidateSha256,
|
|
529
|
+
"--request-id", `console-${clientRequestId}`,
|
|
530
|
+
"--json",
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
else if (input.action === "artifactRevisionPlan") {
|
|
534
|
+
cliArgs = [
|
|
535
|
+
"dag", "rerun",
|
|
536
|
+
"--run-id", runId,
|
|
537
|
+
"--artifact-revision", revisionId,
|
|
538
|
+
"--plan", "--json",
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
cliArgs = [
|
|
543
|
+
"dag", "rerun",
|
|
544
|
+
"--run-id", runId,
|
|
545
|
+
"--artifact-revision", revisionId,
|
|
546
|
+
"--plan-hash", planHash,
|
|
547
|
+
"--request-id", `console-${clientRequestId}`,
|
|
548
|
+
"--reason", reason,
|
|
549
|
+
"--json",
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
if (workerContinuation) {
|
|
553
|
+
cliArgs.push("--worker-continuation", JSON.stringify(workerContinuation.authority));
|
|
554
|
+
}
|
|
555
|
+
let admittedOperationId;
|
|
556
|
+
const accepted = await acceptOperation(operator.actionContext, {
|
|
557
|
+
action: input.action,
|
|
558
|
+
actionParams: safeActionParams,
|
|
559
|
+
clientRequestId,
|
|
560
|
+
cliArgs,
|
|
561
|
+
...(input.action === "artifactRevisionStage" ||
|
|
562
|
+
(input.action === "artifactRevisionExecute" && workerContinuation)
|
|
563
|
+
? {
|
|
564
|
+
prepare: async () => {
|
|
565
|
+
if (input.action === "artifactRevisionStage") {
|
|
566
|
+
await writeArtifactRevisionUpload({
|
|
567
|
+
appData,
|
|
568
|
+
candidateSha256: safeActionParams.candidateSha256,
|
|
569
|
+
candidateBytes: candidateBytes,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
else if (workerContinuation && admittedOperationId) {
|
|
573
|
+
await operator.actionContext.operations.update(admittedOperationId, { dagRunId: workerContinuation.authority.childWorkerRunId });
|
|
574
|
+
await beginWorkerContinuation({
|
|
575
|
+
repoRoot: input.ctx.repoRoot,
|
|
576
|
+
resolved: workerContinuation,
|
|
577
|
+
operationId: admittedOperationId,
|
|
578
|
+
client: operator.actionContext.client,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
throw new Error("Worker continuation operation identity is unavailable");
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
prepareFailure: (error) => {
|
|
586
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
587
|
+
if (input.action === "artifactRevisionExecute") {
|
|
588
|
+
const conflict = message.includes("attempt-lease-conflict") || message.includes("state-conflict");
|
|
589
|
+
return {
|
|
590
|
+
status: conflict ? 409 : 422,
|
|
591
|
+
errorCode: conflict
|
|
592
|
+
? "WORKER_CONTINUATION_LEASE_CONFLICT"
|
|
593
|
+
: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
594
|
+
body: {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: {
|
|
597
|
+
code: conflict
|
|
598
|
+
? "WORKER_CONTINUATION_LEASE_CONFLICT"
|
|
599
|
+
: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
600
|
+
message,
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const stableCode = [
|
|
606
|
+
"ARTIFACT_REVISION_QUOTA_EXCEEDED",
|
|
607
|
+
"ARTIFACT_REVISION_DRIFT",
|
|
608
|
+
"ARTIFACT_REVISION_PATH_INVALID",
|
|
609
|
+
].find((code) => message.startsWith(code));
|
|
610
|
+
void recordContextTransferEvent({
|
|
611
|
+
appData,
|
|
612
|
+
event: "artifact_revision.rejected",
|
|
613
|
+
fields: {
|
|
614
|
+
runId,
|
|
615
|
+
nodeId,
|
|
616
|
+
reasonCode: stableCode ?? "INVALID_INPUT",
|
|
617
|
+
},
|
|
618
|
+
}).catch(() => undefined);
|
|
619
|
+
return {
|
|
620
|
+
status: stableCode ? 422 : 400,
|
|
621
|
+
errorCode: stableCode ?? "INVALID_INPUT",
|
|
622
|
+
body: { ok: false, error: { code: stableCode ?? "INVALID_INPUT", message } },
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
}
|
|
626
|
+
: {}),
|
|
627
|
+
onAdmitted: (operation) => {
|
|
628
|
+
admittedOperationId = operation.operationId;
|
|
629
|
+
if (input.action === "artifactRevisionStage") {
|
|
630
|
+
return recordContextTransferEvent({
|
|
631
|
+
appData,
|
|
632
|
+
event: "artifact_revision.upload_started",
|
|
633
|
+
fields: {
|
|
634
|
+
runId,
|
|
635
|
+
nodeId,
|
|
636
|
+
operationId: operation.operationId,
|
|
637
|
+
},
|
|
638
|
+
}).catch(() => undefined);
|
|
639
|
+
}
|
|
640
|
+
if (input.action === "artifactRevisionExecute") {
|
|
641
|
+
return recordContextTransferEvent({
|
|
642
|
+
appData,
|
|
643
|
+
event: "artifact_rerun.execute_requested",
|
|
644
|
+
fields: {
|
|
645
|
+
runId,
|
|
646
|
+
revisionId,
|
|
647
|
+
operationId: operation.operationId,
|
|
648
|
+
},
|
|
649
|
+
}).catch(() => undefined);
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
},
|
|
653
|
+
...(input.action === "artifactRevisionExecute" && workerContinuation
|
|
654
|
+
? {
|
|
655
|
+
beforeTerminal: (operation, state, info) => settleWorkerContinuation({
|
|
656
|
+
repoRoot: input.ctx.repoRoot,
|
|
657
|
+
resolved: workerContinuation,
|
|
658
|
+
operation,
|
|
659
|
+
state,
|
|
660
|
+
info,
|
|
661
|
+
}),
|
|
662
|
+
}
|
|
663
|
+
: {}),
|
|
664
|
+
});
|
|
665
|
+
if (accepted.kind === "error") {
|
|
666
|
+
return { status: accepted.status, body: accepted.body };
|
|
667
|
+
}
|
|
668
|
+
return { status: accepted.status, body: accepted.body };
|
|
669
|
+
}
|
|
153
670
|
async function handleCreateOperation(req, res, ctx) {
|
|
154
671
|
if (!ctx.operator) {
|
|
155
672
|
sendJson(res, 503, {
|
|
@@ -195,7 +712,7 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
195
712
|
}
|
|
196
713
|
let body;
|
|
197
714
|
try {
|
|
198
|
-
body = (await readJsonBody(req));
|
|
715
|
+
body = (await readJsonBody(req, 15 * 1024 * 1024));
|
|
199
716
|
}
|
|
200
717
|
catch (error) {
|
|
201
718
|
sendJson(res, 400, {
|
|
@@ -222,6 +739,27 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
222
739
|
});
|
|
223
740
|
return;
|
|
224
741
|
}
|
|
742
|
+
if (action === "contextExport") {
|
|
743
|
+
const accepted = await acceptContextExportOperation({
|
|
744
|
+
ctx,
|
|
745
|
+
actionParams,
|
|
746
|
+
clientRequestId,
|
|
747
|
+
});
|
|
748
|
+
sendJson(res, accepted.status, accepted.body);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (action === "artifactRevisionStage" ||
|
|
752
|
+
action === "artifactRevisionPlan" ||
|
|
753
|
+
action === "artifactRevisionExecute") {
|
|
754
|
+
const accepted = await acceptArtifactRevisionOperation({
|
|
755
|
+
ctx,
|
|
756
|
+
action,
|
|
757
|
+
actionParams,
|
|
758
|
+
clientRequestId,
|
|
759
|
+
});
|
|
760
|
+
sendJson(res, accepted.status, accepted.body);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
225
763
|
const result = await dispatchOperatorAction(ctx.operator.actionContext, {
|
|
226
764
|
action,
|
|
227
765
|
actionParams,
|
|
@@ -365,6 +903,34 @@ async function handleOperationEvents(req, res, ctx, operationId) {
|
|
|
365
903
|
* Path-escape-safe static file serve for Console SPA.
|
|
366
904
|
* Sets boot capability cookie on HTML responses.
|
|
367
905
|
*/
|
|
906
|
+
/**
|
|
907
|
+
* Inject the general Console confirmation token into HTML, and additionally
|
|
908
|
+
* the scoped inspect mutation token when the experimental prompt-override
|
|
909
|
+
* switch is enabled and inspect credentials are available. The standalone
|
|
910
|
+
* /inspect/ document canonicalizes into the Console SPA (/#/inspect...), so
|
|
911
|
+
* the SPA must carry the scoped inspect token for the export/rerun tab to be
|
|
912
|
+
* rendered by the mounted inspect workspace.
|
|
913
|
+
*/
|
|
914
|
+
function injectScopedInspectTokens(raw, ctx) {
|
|
915
|
+
let content = injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken);
|
|
916
|
+
if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
|
|
917
|
+
content = injectConfirmationTokenScript(content, ctx.inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__");
|
|
918
|
+
}
|
|
919
|
+
return content;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* HTML response Set-Cookie headers for the Console SPA. The general boot
|
|
923
|
+
* cookie is always set; when the experimental prompt-override switch is
|
|
924
|
+
* enabled the scoped inspect cookie is set alongside so the hosted inspect
|
|
925
|
+
* workspace (mounted inside the SPA) can authenticate its allowlisted
|
|
926
|
+
* mutation requests.
|
|
927
|
+
*/
|
|
928
|
+
function scopedSetCookieHeaders(ctx) {
|
|
929
|
+
if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
|
|
930
|
+
return [ctx.bootToken.setCookieHeader, ctx.inspectBootToken.setCookieHeader];
|
|
931
|
+
}
|
|
932
|
+
return ctx.bootToken.setCookieHeader;
|
|
933
|
+
}
|
|
368
934
|
export async function serveConsoleStatic(req, res, ctx) {
|
|
369
935
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
370
936
|
let pathname = decodeURIComponent(url.pathname);
|
|
@@ -383,11 +949,14 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
383
949
|
// SPA fallback for non-file routes
|
|
384
950
|
const indexPath = path.join(resolvedStatic, "index.html");
|
|
385
951
|
if (existsSync(indexPath) && !pathname.includes(".")) {
|
|
386
|
-
const html =
|
|
952
|
+
const html = injectScopedInspectTokens(await readFile(indexPath), ctx);
|
|
387
953
|
res.writeHead(200, {
|
|
388
954
|
"Content-Type": "text/html; charset=utf-8",
|
|
389
955
|
"Content-Length": html.byteLength,
|
|
390
|
-
|
|
956
|
+
// Boot capability tokens are baked into the document; a cached
|
|
957
|
+
// page from a previous Console process makes every mutation 403.
|
|
958
|
+
"Cache-Control": "no-store",
|
|
959
|
+
"Set-Cookie": scopedSetCookieHeaders(ctx),
|
|
391
960
|
});
|
|
392
961
|
res.end(html);
|
|
393
962
|
return;
|
|
@@ -398,14 +967,15 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
398
967
|
const raw = await readFile(resolvedFile);
|
|
399
968
|
const contentType = contentTypeFor(pathname);
|
|
400
969
|
const content = contentType.startsWith("text/html")
|
|
401
|
-
?
|
|
970
|
+
? injectScopedInspectTokens(raw, ctx)
|
|
402
971
|
: raw;
|
|
403
972
|
const headers = {
|
|
404
973
|
"Content-Type": contentType,
|
|
405
974
|
"Content-Length": content.byteLength,
|
|
406
975
|
};
|
|
407
976
|
if (contentType.startsWith("text/html")) {
|
|
408
|
-
headers["
|
|
977
|
+
headers["Cache-Control"] = "no-store";
|
|
978
|
+
headers["Set-Cookie"] = scopedSetCookieHeaders(ctx);
|
|
409
979
|
}
|
|
410
980
|
res.writeHead(200, headers);
|
|
411
981
|
res.end(content);
|
|
@@ -581,6 +1151,47 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
581
1151
|
await handleListOperations(req, res, ctx);
|
|
582
1152
|
return true;
|
|
583
1153
|
}
|
|
1154
|
+
const contextDownloadMatch = pathname.match(/^\/api\/operator\/v1\/context-exports\/([a-f0-9]{32})\/download$/);
|
|
1155
|
+
if (method === "GET" && contextDownloadMatch) {
|
|
1156
|
+
if (!ctx.operator) {
|
|
1157
|
+
sendJson(res, 503, { ok: false, error: { code: "INTERNAL_ERROR", message: "operator runtime unavailable" } });
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
const failure = evaluateConsoleDownloadCapability(req, ctx);
|
|
1161
|
+
if (failure) {
|
|
1162
|
+
sendJson(res, failure.status, { ok: false, error: { code: failure.code, message: failure.message } });
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
const exportId = contextDownloadMatch[1];
|
|
1167
|
+
const leased = await acquireContextExportLease({
|
|
1168
|
+
appData: ctx.operator.actionContext.appData,
|
|
1169
|
+
exportId,
|
|
1170
|
+
});
|
|
1171
|
+
res.writeHead(200, {
|
|
1172
|
+
"Content-Type": "application/zip",
|
|
1173
|
+
"Content-Length": leased.bytes.byteLength,
|
|
1174
|
+
"Content-Disposition": `attachment; filename="context-${leased.record.bundleId}.zip"`,
|
|
1175
|
+
"Cache-Control": "no-store",
|
|
1176
|
+
"X-Content-Type-Options": "nosniff",
|
|
1177
|
+
});
|
|
1178
|
+
res.end(leased.bytes);
|
|
1179
|
+
await releaseContextExportLease({
|
|
1180
|
+
appData: ctx.operator.actionContext.appData,
|
|
1181
|
+
exportId,
|
|
1182
|
+
leaseId: leased.leaseId,
|
|
1183
|
+
}).catch(() => undefined);
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
if (res.headersSent)
|
|
1187
|
+
return true;
|
|
1188
|
+
sendJson(res, 404, {
|
|
1189
|
+
ok: false,
|
|
1190
|
+
error: { code: "CONTEXT_EXPORT_UNAVAILABLE", message: error instanceof Error ? error.message : String(error) },
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
584
1195
|
if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
|
|
585
1196
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
586
1197
|
return true;
|
|
@@ -613,6 +1224,24 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
613
1224
|
});
|
|
614
1225
|
return true;
|
|
615
1226
|
}
|
|
1227
|
+
if (method === "GET" && pathname === "/api/operator/v1/pi-plugins/updates") {
|
|
1228
|
+
try {
|
|
1229
|
+
const view = await checkPiPluginUpdates({
|
|
1230
|
+
force: url.searchParams.get("force") === "1",
|
|
1231
|
+
});
|
|
1232
|
+
sendJson(res, 200, { ok: true, data: view });
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
sendJson(res, 500, {
|
|
1236
|
+
ok: false,
|
|
1237
|
+
error: {
|
|
1238
|
+
code: "INTERNAL_ERROR",
|
|
1239
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
616
1245
|
const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
|
|
617
1246
|
if (opMatch) {
|
|
618
1247
|
const operationId = decodeURIComponent(opMatch[1]);
|