@tea-agent/loop-agent 0.39.2 → 0.40.0-beta.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 +133 -92
- 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 +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -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/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -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 +10 -0
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- 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/native-directory-picker.js +13 -0
- 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 +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- 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/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- 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/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- 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/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- 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/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/init-hybrid.js +178 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- 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 +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -1,12 +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";
|
|
7
|
+
import { probePiReadiness } from "./pi-readiness.js";
|
|
6
8
|
import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
7
9
|
import { ROUTES } from "../observe/routes.js";
|
|
8
|
-
import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
10
|
+
import { evaluateProtectedDownloadGate, evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
9
11
|
import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
|
|
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 { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
|
|
10
20
|
export function sendJson(res, status, body, extraHeaders) {
|
|
11
21
|
const payload = JSON.stringify(body);
|
|
12
22
|
res.writeHead(status, {
|
|
@@ -38,6 +48,10 @@ function contentTypeFor(pathname) {
|
|
|
38
48
|
return "application/octet-stream";
|
|
39
49
|
}
|
|
40
50
|
async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
|
|
51
|
+
// Body pre-read by the multi-workspace dispatcher (workspaceId resolution).
|
|
52
|
+
const preRead = req.__preReadBody;
|
|
53
|
+
if (preRead !== undefined)
|
|
54
|
+
return preRead;
|
|
41
55
|
const chunks = [];
|
|
42
56
|
let total = 0;
|
|
43
57
|
for await (const chunk of req) {
|
|
@@ -142,7 +156,516 @@ async function handleSessionPing(req, res, ctx) {
|
|
|
142
156
|
export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
|
|
143
157
|
"dagRerunPlan",
|
|
144
158
|
"dagRerun",
|
|
159
|
+
"contextExport",
|
|
160
|
+
"artifactRevisionStage",
|
|
161
|
+
"artifactRevisionPlan",
|
|
162
|
+
"artifactRevisionExecute",
|
|
145
163
|
]);
|
|
164
|
+
function evaluateConsoleCapability(req, ctx) {
|
|
165
|
+
const common = {
|
|
166
|
+
consoleOrigin: ctx.getConsoleOrigin(),
|
|
167
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
168
|
+
};
|
|
169
|
+
const general = evaluateMutationGate(req, {
|
|
170
|
+
...common,
|
|
171
|
+
cookieName: ctx.bootToken.cookieName,
|
|
172
|
+
bootToken: ctx.bootToken.value,
|
|
173
|
+
confirmationToken: ctx.bootToken.confirmationToken,
|
|
174
|
+
});
|
|
175
|
+
if (!general || !ctx.inspectBootToken)
|
|
176
|
+
return general;
|
|
177
|
+
return evaluateMutationGate(req, {
|
|
178
|
+
...common,
|
|
179
|
+
cookieName: ctx.inspectBootToken.cookieName,
|
|
180
|
+
bootToken: ctx.inspectBootToken.value,
|
|
181
|
+
confirmationToken: ctx.inspectBootToken.confirmationToken,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function evaluateConsoleDownloadCapability(req, ctx) {
|
|
185
|
+
const common = {
|
|
186
|
+
consoleOrigin: ctx.getConsoleOrigin(),
|
|
187
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
188
|
+
};
|
|
189
|
+
const general = evaluateProtectedDownloadGate(req, {
|
|
190
|
+
...common,
|
|
191
|
+
cookieName: ctx.bootToken.cookieName,
|
|
192
|
+
bootToken: ctx.bootToken.value,
|
|
193
|
+
confirmationToken: ctx.bootToken.confirmationToken,
|
|
194
|
+
});
|
|
195
|
+
if (!general || !ctx.inspectBootToken)
|
|
196
|
+
return general;
|
|
197
|
+
return evaluateProtectedDownloadGate(req, {
|
|
198
|
+
...common,
|
|
199
|
+
cookieName: ctx.inspectBootToken.cookieName,
|
|
200
|
+
bootToken: ctx.inspectBootToken.value,
|
|
201
|
+
confirmationToken: ctx.inspectBootToken.confirmationToken,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function acceptContextExportOperation(input) {
|
|
205
|
+
const operator = input.ctx.operator;
|
|
206
|
+
const clientRequestId = input.clientRequestId?.trim();
|
|
207
|
+
if (!clientRequestId) {
|
|
208
|
+
return {
|
|
209
|
+
status: 400,
|
|
210
|
+
body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
|
|
214
|
+
const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
|
|
215
|
+
const attempt = Number(input.actionParams.attempt ?? 1);
|
|
216
|
+
const profile = input.actionParams.profile === "raw" ? "raw" : "redacted";
|
|
217
|
+
const target = ["generic", "pi-session", "pi-handoff", "readable"].includes(String(input.actionParams.target))
|
|
218
|
+
? input.actionParams.target
|
|
219
|
+
: "generic";
|
|
220
|
+
if (!runId || !nodeId || !Number.isInteger(attempt) || attempt < 1) {
|
|
221
|
+
return {
|
|
222
|
+
status: 400,
|
|
223
|
+
body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, nodeId and positive attempt are required" } },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (profile === "raw" && input.actionParams.rawAcknowledged !== true) {
|
|
227
|
+
return {
|
|
228
|
+
status: 400,
|
|
229
|
+
body: { ok: false, error: { code: "RAW_EXPORT_ACKNOWLEDGEMENT_REQUIRED", message: "raw export requires explicit acknowledgement" } },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const created = await operator.operations.create({
|
|
233
|
+
clientRequestId,
|
|
234
|
+
action: "contextExport",
|
|
235
|
+
actionParams: { runId, nodeId, attempt, profile, target, rawAcknowledged: profile === "raw" },
|
|
236
|
+
});
|
|
237
|
+
if (created.kind === "conflict") {
|
|
238
|
+
return {
|
|
239
|
+
status: 409,
|
|
240
|
+
body: { ok: false, error: { code: created.code, message: created.message } },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (created.kind === "created") {
|
|
244
|
+
void recordContextTransferEvent({
|
|
245
|
+
appData: operator.actionContext.appData,
|
|
246
|
+
event: "context_export.planned",
|
|
247
|
+
fields: {
|
|
248
|
+
runId,
|
|
249
|
+
nodeId,
|
|
250
|
+
profile,
|
|
251
|
+
target,
|
|
252
|
+
operationId: created.operation.operationId,
|
|
253
|
+
},
|
|
254
|
+
}).catch(() => undefined);
|
|
255
|
+
queueMicrotask(() => {
|
|
256
|
+
void (async () => {
|
|
257
|
+
const startedAt = Date.now();
|
|
258
|
+
await operator.operations.update(created.operation.operationId, {
|
|
259
|
+
state: "running",
|
|
260
|
+
startedAt: new Date().toISOString(),
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const appData = operator.actionContext.appData;
|
|
264
|
+
if (!appData.contextExports)
|
|
265
|
+
throw new Error("console context export root unavailable");
|
|
266
|
+
ensureSecureDir(appData.contextExports);
|
|
267
|
+
const incomingPath = path.join(appData.contextExports, `${randomBytes(16).toString("hex")}.incoming.zip`);
|
|
268
|
+
await rm(incomingPath, { force: true });
|
|
269
|
+
try {
|
|
270
|
+
const kernel = await runKernelCommand({
|
|
271
|
+
ctx: input.ctx,
|
|
272
|
+
args: [
|
|
273
|
+
"dag", "context", "export",
|
|
274
|
+
"--run-id", runId,
|
|
275
|
+
"--node-id", nodeId,
|
|
276
|
+
"--attempt", String(attempt),
|
|
277
|
+
"--target", target,
|
|
278
|
+
"--redaction", profile,
|
|
279
|
+
...(profile === "raw" ? ["--acknowledge-sensitive-data"] : []),
|
|
280
|
+
"--output", incomingPath,
|
|
281
|
+
"--json",
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
const payload = (kernel.json ?? {});
|
|
285
|
+
if (!kernel.ok || !payload.bundleId || !payload.bundleSha256) {
|
|
286
|
+
throw new Error(kernelFailureMessage(kernel));
|
|
287
|
+
}
|
|
288
|
+
const zipBytes = await readFile(incomingPath);
|
|
289
|
+
const exported = await createContextExport({
|
|
290
|
+
appData,
|
|
291
|
+
bundle: {
|
|
292
|
+
zipBytes,
|
|
293
|
+
manifest: {
|
|
294
|
+
bundleId: payload.bundleId,
|
|
295
|
+
bundleSha256: payload.bundleSha256,
|
|
296
|
+
profile,
|
|
297
|
+
target,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
await recordContextTransferDiagnostic({
|
|
302
|
+
appData: operator.actionContext.appData,
|
|
303
|
+
event: "context-bundle.generate",
|
|
304
|
+
dimensions: {
|
|
305
|
+
outcome: "succeeded",
|
|
306
|
+
target,
|
|
307
|
+
profile,
|
|
308
|
+
bytes: exported.bytes,
|
|
309
|
+
durationMs: Date.now() - startedAt,
|
|
310
|
+
},
|
|
311
|
+
}).catch(() => undefined);
|
|
312
|
+
await recordContextTransferEvent({
|
|
313
|
+
appData: operator.actionContext.appData,
|
|
314
|
+
event: "context_export.ready",
|
|
315
|
+
fields: {
|
|
316
|
+
runId,
|
|
317
|
+
nodeId,
|
|
318
|
+
exportId: exported.exportId,
|
|
319
|
+
profile,
|
|
320
|
+
target,
|
|
321
|
+
bytes: exported.bytes,
|
|
322
|
+
durationMs: Date.now() - startedAt,
|
|
323
|
+
operationId: created.operation.operationId,
|
|
324
|
+
},
|
|
325
|
+
}).catch(() => undefined);
|
|
326
|
+
await operator.operations.update(created.operation.operationId, {
|
|
327
|
+
state: "succeeded",
|
|
328
|
+
finishedAt: new Date().toISOString(),
|
|
329
|
+
result: {
|
|
330
|
+
ok: true,
|
|
331
|
+
exportId: exported.exportId,
|
|
332
|
+
bundleId: exported.bundleId,
|
|
333
|
+
bundleSha256: exported.bundleSha256,
|
|
334
|
+
bytes: exported.bytes,
|
|
335
|
+
expiresAt: exported.expiresAt,
|
|
336
|
+
downloadUrl: `/api/operator/v1/context-exports/${exported.exportId}/download`,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
await rm(incomingPath, { force: true }).catch(() => undefined);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
await recordContextTransferDiagnostic({
|
|
346
|
+
appData: operator.actionContext.appData,
|
|
347
|
+
event: "context-bundle.generate",
|
|
348
|
+
dimensions: {
|
|
349
|
+
outcome: "failed",
|
|
350
|
+
target,
|
|
351
|
+
profile,
|
|
352
|
+
reasonCode: "CONTEXT_EXPORT_FAILED",
|
|
353
|
+
durationMs: Date.now() - startedAt,
|
|
354
|
+
},
|
|
355
|
+
}).catch(() => undefined);
|
|
356
|
+
await recordContextTransferEvent({
|
|
357
|
+
appData: operator.actionContext.appData,
|
|
358
|
+
event: "context_export.failed",
|
|
359
|
+
fields: {
|
|
360
|
+
runId,
|
|
361
|
+
nodeId,
|
|
362
|
+
profile,
|
|
363
|
+
target,
|
|
364
|
+
reasonCode: "CONTEXT_EXPORT_FAILED",
|
|
365
|
+
durationMs: Date.now() - startedAt,
|
|
366
|
+
operationId: created.operation.operationId,
|
|
367
|
+
},
|
|
368
|
+
}).catch(() => undefined);
|
|
369
|
+
await operator.operations.update(created.operation.operationId, {
|
|
370
|
+
state: "failed",
|
|
371
|
+
finishedAt: new Date().toISOString(),
|
|
372
|
+
errorCode: "CONTEXT_EXPORT_FAILED",
|
|
373
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
status: 202,
|
|
381
|
+
body: {
|
|
382
|
+
operationId: created.operation.operationId,
|
|
383
|
+
state: created.operation.state,
|
|
384
|
+
clientRequestId: created.operation.clientRequestId,
|
|
385
|
+
action: created.operation.action,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async function runKernelCommand(input) {
|
|
390
|
+
const actionContext = input.ctx.operator.actionContext;
|
|
391
|
+
const run = actionContext.runCommand ??
|
|
392
|
+
((args, options) => actionContext.client.run(args, options));
|
|
393
|
+
return (await run(input.args, {
|
|
394
|
+
cwd: actionContext.repoRoot,
|
|
395
|
+
artifactName: `kernel-${input.args.filter((arg) => !arg.startsWith("--")).slice(0, 3).join("-").replace(/\s+/g, "-")}`,
|
|
396
|
+
expectJson: true,
|
|
397
|
+
env: { ...process.env, LOOP_CONSOLE_APP_DATA: actionContext.appData.root },
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
function kernelFailureMessage(result) {
|
|
401
|
+
const stderr = result.stderr.trim();
|
|
402
|
+
if (stderr) {
|
|
403
|
+
const lastLine = stderr.split("\n").filter(Boolean).pop() ?? stderr;
|
|
404
|
+
return lastLine.slice(0, 300);
|
|
405
|
+
}
|
|
406
|
+
const stdout = result.stdout.trim();
|
|
407
|
+
if (stdout)
|
|
408
|
+
return stdout.slice(0, 300);
|
|
409
|
+
return `kernel command failed with exit code ${result.exitCode}`;
|
|
410
|
+
}
|
|
411
|
+
function decodeBoundedBase64(value) {
|
|
412
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 14_000_000) {
|
|
413
|
+
throw new Error("candidateBase64 is required and exceeds no more than 10 MiB decoded");
|
|
414
|
+
}
|
|
415
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
416
|
+
throw new Error("candidateBase64 is invalid");
|
|
417
|
+
}
|
|
418
|
+
const bytes = Buffer.from(value, "base64");
|
|
419
|
+
if (bytes.byteLength > 10 * 1024 * 1024)
|
|
420
|
+
throw new Error("candidate exceeds 10 MiB");
|
|
421
|
+
return bytes;
|
|
422
|
+
}
|
|
423
|
+
function revisionProjection(revision) {
|
|
424
|
+
if (!revision)
|
|
425
|
+
return undefined;
|
|
426
|
+
return {
|
|
427
|
+
revisionId: revision.revisionId,
|
|
428
|
+
state: revision.state,
|
|
429
|
+
parent: revision.parent,
|
|
430
|
+
candidate: {
|
|
431
|
+
sha256: revision.candidate.sha256,
|
|
432
|
+
bytes: revision.candidate.bytes,
|
|
433
|
+
mediaType: revision.candidate.mediaType,
|
|
434
|
+
uploadedAt: revision.candidate.uploadedAt,
|
|
435
|
+
},
|
|
436
|
+
validation: revision.validation,
|
|
437
|
+
diff: {
|
|
438
|
+
kind: revision.diff.kind,
|
|
439
|
+
summary: revision.diff.summary,
|
|
440
|
+
truncated: revision.diff.truncated,
|
|
441
|
+
},
|
|
442
|
+
expiresAt: revision.expiresAt,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function acceptArtifactRevisionOperation(input) {
|
|
446
|
+
const operator = input.ctx.operator;
|
|
447
|
+
const clientRequestId = input.clientRequestId?.trim();
|
|
448
|
+
if (!clientRequestId) {
|
|
449
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "clientRequestId is required" } } };
|
|
450
|
+
}
|
|
451
|
+
let candidateBytes;
|
|
452
|
+
try {
|
|
453
|
+
if (input.action === "artifactRevisionStage") {
|
|
454
|
+
candidateBytes = decodeBoundedBase64(input.actionParams.candidateBase64);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } } };
|
|
459
|
+
}
|
|
460
|
+
const runId = typeof input.actionParams.runId === "string" ? input.actionParams.runId.trim() : "";
|
|
461
|
+
const nodeId = typeof input.actionParams.nodeId === "string" ? input.actionParams.nodeId.trim() : "";
|
|
462
|
+
const artifactId = typeof input.actionParams.artifactId === "string" ? input.actionParams.artifactId.trim() : "";
|
|
463
|
+
const revisionId = typeof input.actionParams.revisionId === "string" ? input.actionParams.revisionId.trim() : "";
|
|
464
|
+
const planHash = typeof input.actionParams.planHash === "string" ? input.actionParams.planHash.trim() : "";
|
|
465
|
+
const reason = typeof input.actionParams.reason === "string" ? input.actionParams.reason.trim() : "";
|
|
466
|
+
const mediaType = typeof input.actionParams.mediaType === "string" ? input.actionParams.mediaType.trim() : "";
|
|
467
|
+
if (!runId || !artifactId || (input.action === "artifactRevisionStage" && (!nodeId || !mediaType))) {
|
|
468
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "runId, artifactId, nodeId and mediaType are required" } } };
|
|
469
|
+
}
|
|
470
|
+
if (input.action !== "artifactRevisionStage" && !revisionId) {
|
|
471
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "revisionId is required" } } };
|
|
472
|
+
}
|
|
473
|
+
if (input.action === "artifactRevisionExecute" && (!planHash || !reason)) {
|
|
474
|
+
return { status: 400, body: { ok: false, error: { code: "INVALID_INPUT", message: "planHash and reason are required" } } };
|
|
475
|
+
}
|
|
476
|
+
let workerContinuation;
|
|
477
|
+
if (input.action !== "artifactRevisionStage") {
|
|
478
|
+
try {
|
|
479
|
+
workerContinuation = await resolveWorkerContinuationAuthority({
|
|
480
|
+
repoRoot: input.ctx.repoRoot,
|
|
481
|
+
parentDagRunId: runId,
|
|
482
|
+
revisionId,
|
|
483
|
+
allowActiveRevision: input.action === "artifactRevisionExecute",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
return {
|
|
489
|
+
status: 422,
|
|
490
|
+
body: {
|
|
491
|
+
ok: false,
|
|
492
|
+
error: {
|
|
493
|
+
code: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
494
|
+
message,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const safeActionParams = {
|
|
501
|
+
runId,
|
|
502
|
+
artifactId,
|
|
503
|
+
...(nodeId ? { nodeId } : {}),
|
|
504
|
+
...(revisionId ? { revisionId } : {}),
|
|
505
|
+
...(planHash ? { planHash } : {}),
|
|
506
|
+
...(reason ? { reason } : {}),
|
|
507
|
+
...(mediaType ? { mediaType } : {}),
|
|
508
|
+
...(candidateBytes
|
|
509
|
+
? { candidateSha256: createHash("sha256").update(candidateBytes).digest("hex"), candidateBytes: candidateBytes.byteLength }
|
|
510
|
+
: {}),
|
|
511
|
+
...(workerContinuation
|
|
512
|
+
? { workerContinuation: workerContinuation.authority }
|
|
513
|
+
: {}),
|
|
514
|
+
};
|
|
515
|
+
const appData = operator.actionContext.appData;
|
|
516
|
+
if (!appData.artifactRevisions) {
|
|
517
|
+
return { status: 503, body: { ok: false, error: { code: "INTERNAL_ERROR", message: "console artifact revision root unavailable" } } };
|
|
518
|
+
}
|
|
519
|
+
let cliArgs;
|
|
520
|
+
if (input.action === "artifactRevisionStage") {
|
|
521
|
+
const candidateSha256 = safeActionParams.candidateSha256;
|
|
522
|
+
cliArgs = [
|
|
523
|
+
"dag", "artifact", "revision", "stage",
|
|
524
|
+
"--run-id", runId,
|
|
525
|
+
"--node-id", nodeId,
|
|
526
|
+
"--artifact-id", artifactId,
|
|
527
|
+
"--candidate-upload", candidateSha256,
|
|
528
|
+
"--request-id", `console-${clientRequestId}`,
|
|
529
|
+
"--json",
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
else if (input.action === "artifactRevisionPlan") {
|
|
533
|
+
cliArgs = [
|
|
534
|
+
"dag", "rerun",
|
|
535
|
+
"--run-id", runId,
|
|
536
|
+
"--artifact-revision", revisionId,
|
|
537
|
+
"--plan", "--json",
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
cliArgs = [
|
|
542
|
+
"dag", "rerun",
|
|
543
|
+
"--run-id", runId,
|
|
544
|
+
"--artifact-revision", revisionId,
|
|
545
|
+
"--plan-hash", planHash,
|
|
546
|
+
"--request-id", `console-${clientRequestId}`,
|
|
547
|
+
"--reason", reason,
|
|
548
|
+
"--json",
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
if (workerContinuation) {
|
|
552
|
+
cliArgs.push("--worker-continuation", JSON.stringify(workerContinuation.authority));
|
|
553
|
+
}
|
|
554
|
+
let admittedOperationId;
|
|
555
|
+
const accepted = await acceptOperation(operator.actionContext, {
|
|
556
|
+
action: input.action,
|
|
557
|
+
actionParams: safeActionParams,
|
|
558
|
+
clientRequestId,
|
|
559
|
+
cliArgs,
|
|
560
|
+
...(input.action === "artifactRevisionStage" ||
|
|
561
|
+
(input.action === "artifactRevisionExecute" && workerContinuation)
|
|
562
|
+
? {
|
|
563
|
+
prepare: async () => {
|
|
564
|
+
if (input.action === "artifactRevisionStage") {
|
|
565
|
+
await writeArtifactRevisionUpload({
|
|
566
|
+
appData,
|
|
567
|
+
candidateSha256: safeActionParams.candidateSha256,
|
|
568
|
+
candidateBytes: candidateBytes,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
else if (workerContinuation && admittedOperationId) {
|
|
572
|
+
await operator.actionContext.operations.update(admittedOperationId, { dagRunId: workerContinuation.authority.childWorkerRunId });
|
|
573
|
+
await beginWorkerContinuation({
|
|
574
|
+
repoRoot: input.ctx.repoRoot,
|
|
575
|
+
resolved: workerContinuation,
|
|
576
|
+
operationId: admittedOperationId,
|
|
577
|
+
client: operator.actionContext.client,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
throw new Error("Worker continuation operation identity is unavailable");
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
prepareFailure: (error) => {
|
|
585
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
586
|
+
if (input.action === "artifactRevisionExecute") {
|
|
587
|
+
const conflict = message.includes("attempt-lease-conflict") || message.includes("state-conflict");
|
|
588
|
+
return {
|
|
589
|
+
status: conflict ? 409 : 422,
|
|
590
|
+
errorCode: conflict
|
|
591
|
+
? "WORKER_CONTINUATION_LEASE_CONFLICT"
|
|
592
|
+
: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
593
|
+
body: {
|
|
594
|
+
ok: false,
|
|
595
|
+
error: {
|
|
596
|
+
code: conflict
|
|
597
|
+
? "WORKER_CONTINUATION_LEASE_CONFLICT"
|
|
598
|
+
: "WORKER_CONTINUATION_AUTHORITY_MISMATCH",
|
|
599
|
+
message,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const stableCode = [
|
|
605
|
+
"ARTIFACT_REVISION_QUOTA_EXCEEDED",
|
|
606
|
+
"ARTIFACT_REVISION_DRIFT",
|
|
607
|
+
"ARTIFACT_REVISION_PATH_INVALID",
|
|
608
|
+
].find((code) => message.startsWith(code));
|
|
609
|
+
void recordContextTransferEvent({
|
|
610
|
+
appData,
|
|
611
|
+
event: "artifact_revision.rejected",
|
|
612
|
+
fields: {
|
|
613
|
+
runId,
|
|
614
|
+
nodeId,
|
|
615
|
+
reasonCode: stableCode ?? "INVALID_INPUT",
|
|
616
|
+
},
|
|
617
|
+
}).catch(() => undefined);
|
|
618
|
+
return {
|
|
619
|
+
status: stableCode ? 422 : 400,
|
|
620
|
+
errorCode: stableCode ?? "INVALID_INPUT",
|
|
621
|
+
body: { ok: false, error: { code: stableCode ?? "INVALID_INPUT", message } },
|
|
622
|
+
};
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
: {}),
|
|
626
|
+
onAdmitted: (operation) => {
|
|
627
|
+
admittedOperationId = operation.operationId;
|
|
628
|
+
if (input.action === "artifactRevisionStage") {
|
|
629
|
+
return recordContextTransferEvent({
|
|
630
|
+
appData,
|
|
631
|
+
event: "artifact_revision.upload_started",
|
|
632
|
+
fields: {
|
|
633
|
+
runId,
|
|
634
|
+
nodeId,
|
|
635
|
+
operationId: operation.operationId,
|
|
636
|
+
},
|
|
637
|
+
}).catch(() => undefined);
|
|
638
|
+
}
|
|
639
|
+
if (input.action === "artifactRevisionExecute") {
|
|
640
|
+
return recordContextTransferEvent({
|
|
641
|
+
appData,
|
|
642
|
+
event: "artifact_rerun.execute_requested",
|
|
643
|
+
fields: {
|
|
644
|
+
runId,
|
|
645
|
+
revisionId,
|
|
646
|
+
operationId: operation.operationId,
|
|
647
|
+
},
|
|
648
|
+
}).catch(() => undefined);
|
|
649
|
+
}
|
|
650
|
+
return undefined;
|
|
651
|
+
},
|
|
652
|
+
...(input.action === "artifactRevisionExecute" && workerContinuation
|
|
653
|
+
? {
|
|
654
|
+
beforeTerminal: (operation, state, info) => settleWorkerContinuation({
|
|
655
|
+
repoRoot: input.ctx.repoRoot,
|
|
656
|
+
resolved: workerContinuation,
|
|
657
|
+
operation,
|
|
658
|
+
state,
|
|
659
|
+
info,
|
|
660
|
+
}),
|
|
661
|
+
}
|
|
662
|
+
: {}),
|
|
663
|
+
});
|
|
664
|
+
if (accepted.kind === "error") {
|
|
665
|
+
return { status: accepted.status, body: accepted.body };
|
|
666
|
+
}
|
|
667
|
+
return { status: accepted.status, body: accepted.body };
|
|
668
|
+
}
|
|
146
669
|
async function handleCreateOperation(req, res, ctx) {
|
|
147
670
|
if (!ctx.operator) {
|
|
148
671
|
sendJson(res, 503, {
|
|
@@ -188,7 +711,7 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
188
711
|
}
|
|
189
712
|
let body;
|
|
190
713
|
try {
|
|
191
|
-
body = (await readJsonBody(req));
|
|
714
|
+
body = (await readJsonBody(req, 15 * 1024 * 1024));
|
|
192
715
|
}
|
|
193
716
|
catch (error) {
|
|
194
717
|
sendJson(res, 400, {
|
|
@@ -215,6 +738,27 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
215
738
|
});
|
|
216
739
|
return;
|
|
217
740
|
}
|
|
741
|
+
if (action === "contextExport") {
|
|
742
|
+
const accepted = await acceptContextExportOperation({
|
|
743
|
+
ctx,
|
|
744
|
+
actionParams,
|
|
745
|
+
clientRequestId,
|
|
746
|
+
});
|
|
747
|
+
sendJson(res, accepted.status, accepted.body);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (action === "artifactRevisionStage" ||
|
|
751
|
+
action === "artifactRevisionPlan" ||
|
|
752
|
+
action === "artifactRevisionExecute") {
|
|
753
|
+
const accepted = await acceptArtifactRevisionOperation({
|
|
754
|
+
ctx,
|
|
755
|
+
action,
|
|
756
|
+
actionParams,
|
|
757
|
+
clientRequestId,
|
|
758
|
+
});
|
|
759
|
+
sendJson(res, accepted.status, accepted.body);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
218
762
|
const result = await dispatchOperatorAction(ctx.operator.actionContext, {
|
|
219
763
|
action,
|
|
220
764
|
actionParams,
|
|
@@ -245,7 +789,35 @@ async function handleGetOperation(_req, res, ctx, operationId) {
|
|
|
245
789
|
});
|
|
246
790
|
return;
|
|
247
791
|
}
|
|
248
|
-
sendJson(res, 200, {
|
|
792
|
+
sendJson(res, 200, {
|
|
793
|
+
ok: true,
|
|
794
|
+
workspaceId: ctx.workspace?.workspaceId,
|
|
795
|
+
operation: { ...op, workspaceId: ctx.workspace?.workspaceId },
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* GET /api/operator/v1/operations?workspaceId= — NEW per-workspace list route
|
|
800
|
+
* (multi-workspace foundation). Returns only the resolved Context's Operations
|
|
801
|
+
* and injects the owning workspaceId. operation-store.ts is unchanged.
|
|
802
|
+
*/
|
|
803
|
+
async function handleListOperations(_req, res, ctx) {
|
|
804
|
+
if (!ctx.operator) {
|
|
805
|
+
sendJson(res, 503, {
|
|
806
|
+
ok: false,
|
|
807
|
+
error: {
|
|
808
|
+
code: "INTERNAL_ERROR",
|
|
809
|
+
message: "operator runtime unavailable",
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const operations = await ctx.operator.operations.list();
|
|
815
|
+
sendJson(res, 200, {
|
|
816
|
+
ok: true,
|
|
817
|
+
schemaVersion: 1,
|
|
818
|
+
workspaceId: ctx.workspace?.workspaceId,
|
|
819
|
+
ops: operations.map((op) => ({ ...op, workspaceId: ctx.workspace?.workspaceId })),
|
|
820
|
+
});
|
|
249
821
|
}
|
|
250
822
|
async function handleOperationEvents(req, res, ctx, operationId) {
|
|
251
823
|
if (!ctx.operator) {
|
|
@@ -330,6 +902,34 @@ async function handleOperationEvents(req, res, ctx, operationId) {
|
|
|
330
902
|
* Path-escape-safe static file serve for Console SPA.
|
|
331
903
|
* Sets boot capability cookie on HTML responses.
|
|
332
904
|
*/
|
|
905
|
+
/**
|
|
906
|
+
* Inject the general Console confirmation token into HTML, and additionally
|
|
907
|
+
* the scoped inspect mutation token when the experimental prompt-override
|
|
908
|
+
* switch is enabled and inspect credentials are available. The standalone
|
|
909
|
+
* /inspect/ document canonicalizes into the Console SPA (/#/inspect...), so
|
|
910
|
+
* the SPA must carry the scoped inspect token for the export/rerun tab to be
|
|
911
|
+
* rendered by the mounted inspect workspace.
|
|
912
|
+
*/
|
|
913
|
+
function injectScopedInspectTokens(raw, ctx) {
|
|
914
|
+
let content = injectConfirmationTokenScript(raw, ctx.bootToken.confirmationToken);
|
|
915
|
+
if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
|
|
916
|
+
content = injectConfirmationTokenScript(content, ctx.inspectBootToken.confirmationToken, "__INSPECT_MUTATION_TOKEN__");
|
|
917
|
+
}
|
|
918
|
+
return content;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* HTML response Set-Cookie headers for the Console SPA. The general boot
|
|
922
|
+
* cookie is always set; when the experimental prompt-override switch is
|
|
923
|
+
* enabled the scoped inspect cookie is set alongside so the hosted inspect
|
|
924
|
+
* workspace (mounted inside the SPA) can authenticate its allowlisted
|
|
925
|
+
* mutation requests.
|
|
926
|
+
*/
|
|
927
|
+
function scopedSetCookieHeaders(ctx) {
|
|
928
|
+
if (isDagRerunPromptOverrideEnabled() && ctx.inspectBootToken) {
|
|
929
|
+
return [ctx.bootToken.setCookieHeader, ctx.inspectBootToken.setCookieHeader];
|
|
930
|
+
}
|
|
931
|
+
return ctx.bootToken.setCookieHeader;
|
|
932
|
+
}
|
|
333
933
|
export async function serveConsoleStatic(req, res, ctx) {
|
|
334
934
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
335
935
|
let pathname = decodeURIComponent(url.pathname);
|
|
@@ -348,11 +948,14 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
348
948
|
// SPA fallback for non-file routes
|
|
349
949
|
const indexPath = path.join(resolvedStatic, "index.html");
|
|
350
950
|
if (existsSync(indexPath) && !pathname.includes(".")) {
|
|
351
|
-
const html =
|
|
951
|
+
const html = injectScopedInspectTokens(await readFile(indexPath), ctx);
|
|
352
952
|
res.writeHead(200, {
|
|
353
953
|
"Content-Type": "text/html; charset=utf-8",
|
|
354
954
|
"Content-Length": html.byteLength,
|
|
355
|
-
|
|
955
|
+
// Boot capability tokens are baked into the document; a cached
|
|
956
|
+
// page from a previous Console process makes every mutation 403.
|
|
957
|
+
"Cache-Control": "no-store",
|
|
958
|
+
"Set-Cookie": scopedSetCookieHeaders(ctx),
|
|
356
959
|
});
|
|
357
960
|
res.end(html);
|
|
358
961
|
return;
|
|
@@ -363,14 +966,15 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
363
966
|
const raw = await readFile(resolvedFile);
|
|
364
967
|
const contentType = contentTypeFor(pathname);
|
|
365
968
|
const content = contentType.startsWith("text/html")
|
|
366
|
-
?
|
|
969
|
+
? injectScopedInspectTokens(raw, ctx)
|
|
367
970
|
: raw;
|
|
368
971
|
const headers = {
|
|
369
972
|
"Content-Type": contentType,
|
|
370
973
|
"Content-Length": content.byteLength,
|
|
371
974
|
};
|
|
372
975
|
if (contentType.startsWith("text/html")) {
|
|
373
|
-
headers["
|
|
976
|
+
headers["Cache-Control"] = "no-store";
|
|
977
|
+
headers["Set-Cookie"] = scopedSetCookieHeaders(ctx);
|
|
374
978
|
}
|
|
375
979
|
res.writeHead(200, headers);
|
|
376
980
|
res.end(content);
|
|
@@ -453,6 +1057,51 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
453
1057
|
await handleCreateOperation(req, res, ctx);
|
|
454
1058
|
return true;
|
|
455
1059
|
}
|
|
1060
|
+
if (method === "GET" && pathname === "/api/operator/v1/operations") {
|
|
1061
|
+
await handleListOperations(req, res, ctx);
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
const contextDownloadMatch = pathname.match(/^\/api\/operator\/v1\/context-exports\/([a-f0-9]{32})\/download$/);
|
|
1065
|
+
if (method === "GET" && contextDownloadMatch) {
|
|
1066
|
+
if (!ctx.operator) {
|
|
1067
|
+
sendJson(res, 503, { ok: false, error: { code: "INTERNAL_ERROR", message: "operator runtime unavailable" } });
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
const failure = evaluateConsoleDownloadCapability(req, ctx);
|
|
1071
|
+
if (failure) {
|
|
1072
|
+
sendJson(res, failure.status, { ok: false, error: { code: failure.code, message: failure.message } });
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
const exportId = contextDownloadMatch[1];
|
|
1077
|
+
const leased = await acquireContextExportLease({
|
|
1078
|
+
appData: ctx.operator.actionContext.appData,
|
|
1079
|
+
exportId,
|
|
1080
|
+
});
|
|
1081
|
+
res.writeHead(200, {
|
|
1082
|
+
"Content-Type": "application/zip",
|
|
1083
|
+
"Content-Length": leased.bytes.byteLength,
|
|
1084
|
+
"Content-Disposition": `attachment; filename="context-${leased.record.bundleId}.zip"`,
|
|
1085
|
+
"Cache-Control": "no-store",
|
|
1086
|
+
"X-Content-Type-Options": "nosniff",
|
|
1087
|
+
});
|
|
1088
|
+
res.end(leased.bytes);
|
|
1089
|
+
await releaseContextExportLease({
|
|
1090
|
+
appData: ctx.operator.actionContext.appData,
|
|
1091
|
+
exportId,
|
|
1092
|
+
leaseId: leased.leaseId,
|
|
1093
|
+
}).catch(() => undefined);
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
if (res.headersSent)
|
|
1097
|
+
return true;
|
|
1098
|
+
sendJson(res, 404, {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
error: { code: "CONTEXT_EXPORT_UNAVAILABLE", message: error instanceof Error ? error.message : String(error) },
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
456
1105
|
if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
|
|
457
1106
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
458
1107
|
return true;
|
|
@@ -517,3 +1166,273 @@ export async function handleConsoleRequest(req, res, ctx) {
|
|
|
517
1166
|
}
|
|
518
1167
|
return false;
|
|
519
1168
|
}
|
|
1169
|
+
export function createWorkspaceReadinessCache() {
|
|
1170
|
+
const entries = new Map();
|
|
1171
|
+
return {
|
|
1172
|
+
get: (repoRoot) => entries.get(repoRoot),
|
|
1173
|
+
set: (repoRoot, entry) => {
|
|
1174
|
+
entries.set(repoRoot, entry);
|
|
1175
|
+
},
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const defaultWorkspaceReadinessCache = createWorkspaceReadinessCache();
|
|
1179
|
+
const READY_STATE_TTL_MS = 60_000;
|
|
1180
|
+
const NON_READY_STATE_TTL_MS = 15_000;
|
|
1181
|
+
function isCacheFresh(entry, now) {
|
|
1182
|
+
const ttl = entry.state === "ready" ? READY_STATE_TTL_MS : NON_READY_STATE_TTL_MS;
|
|
1183
|
+
return now - entry.probedAt < ttl;
|
|
1184
|
+
}
|
|
1185
|
+
/** Readiness placeholder for a never-probed workspace (AC-006): the list is
|
|
1186
|
+
* still returned immediately with a pending marker, refreshed by the
|
|
1187
|
+
* background probe. */
|
|
1188
|
+
function pendingReadinessSummary(inspectReady) {
|
|
1189
|
+
return {
|
|
1190
|
+
state: "pending",
|
|
1191
|
+
summary: `pi=pending; inspect=${inspectReady ? "ready" : "unavailable"}`,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Process-level Workspace Registry HTTP surface (2026-08-22 multi-workspace
|
|
1196
|
+
* foundation):
|
|
1197
|
+
* GET /api/operator/v1/workspaces — visible list
|
|
1198
|
+
* POST /api/operator/v1/workspaces/pick — host native folder chooser
|
|
1199
|
+
* POST /api/operator/v1/workspaces — body { "name": "<label>",
|
|
1200
|
+
* "directoryPath": "<dir>" }
|
|
1201
|
+
* DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
|
|
1202
|
+
*
|
|
1203
|
+
* POST/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
1204
|
+
* workspace selection parameter.
|
|
1205
|
+
*
|
|
1206
|
+
* AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
|
|
1207
|
+
* readiness cache answer immediately (first sight may be a pending
|
|
1208
|
+
* placeholder); probes refresh the cache in the background for subsequent
|
|
1209
|
+
* requests. A hanging probe can never delay the 200 list.
|
|
1210
|
+
*/
|
|
1211
|
+
export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
|
|
1212
|
+
if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
|
|
1213
|
+
const records = await registry.listVisible();
|
|
1214
|
+
const inspectReady = readinessDeps.inspectReady;
|
|
1215
|
+
const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
|
|
1216
|
+
const workspaces = records.map((record) => {
|
|
1217
|
+
const cached = readinessCache.get(record.repoRoot);
|
|
1218
|
+
return {
|
|
1219
|
+
workspaceId: record.workspaceId,
|
|
1220
|
+
name: record.name,
|
|
1221
|
+
displayPath: record.displayPath,
|
|
1222
|
+
repoFingerprint: record.repoFingerprint,
|
|
1223
|
+
createdAt: record.createdAt,
|
|
1224
|
+
lastAccessedAt: record.lastAccessedAt,
|
|
1225
|
+
readiness: cached
|
|
1226
|
+
? {
|
|
1227
|
+
state: cached.state,
|
|
1228
|
+
summary: `pi=${cached.state}; inspect=${inspectReady ? "ready" : "unavailable"}`,
|
|
1229
|
+
}
|
|
1230
|
+
: pendingReadinessSummary(inspectReady),
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
// Respond FIRST, then refresh readiness in the background (AC-006).
|
|
1234
|
+
sendJson(res, 200, {
|
|
1235
|
+
ok: true,
|
|
1236
|
+
schemaVersion: 1,
|
|
1237
|
+
workspaces,
|
|
1238
|
+
});
|
|
1239
|
+
void Promise.all(records.map(async (record) => {
|
|
1240
|
+
const cached = readinessCache.get(record.repoRoot);
|
|
1241
|
+
if (cached && isCacheFresh(cached, Date.now()))
|
|
1242
|
+
return;
|
|
1243
|
+
let state;
|
|
1244
|
+
try {
|
|
1245
|
+
const report = await probePiReadiness({
|
|
1246
|
+
override: deps.piReadinessProbe,
|
|
1247
|
+
repoRoot: record.repoRoot,
|
|
1248
|
+
});
|
|
1249
|
+
state = report.state;
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
state = "setup-required";
|
|
1253
|
+
}
|
|
1254
|
+
readinessCache.set(record.repoRoot, {
|
|
1255
|
+
state,
|
|
1256
|
+
probedAt: Date.now(),
|
|
1257
|
+
});
|
|
1258
|
+
})).catch(() => undefined);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (method === "POST" && pathname === "/api/operator/v1/workspaces/pick") {
|
|
1262
|
+
const failure = evaluateMutationGate(req, {
|
|
1263
|
+
cookieName: deps.bootToken.cookieName,
|
|
1264
|
+
bootToken: deps.bootToken.value,
|
|
1265
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
1266
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
1267
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
1268
|
+
});
|
|
1269
|
+
if (failure) {
|
|
1270
|
+
sendJson(res, failure.status, {
|
|
1271
|
+
ok: false,
|
|
1272
|
+
error: { code: failure.code, message: failure.message },
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (deps.allowNonLoopbackAccess) {
|
|
1277
|
+
sendJson(res, 409, {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
error: {
|
|
1280
|
+
code: "NATIVE_PICKER_UNAVAILABLE",
|
|
1281
|
+
message: "原生目录选择仅在本机回环地址启动的 Console 中可用。",
|
|
1282
|
+
},
|
|
1283
|
+
});
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const abortController = new AbortController();
|
|
1287
|
+
const abort = () => abortController.abort();
|
|
1288
|
+
req.once("aborted", abort);
|
|
1289
|
+
res.once("close", abort);
|
|
1290
|
+
try {
|
|
1291
|
+
const directoryPath = await (deps.pickDirectory ?? pickNativeWorkspaceDirectory)(abortController.signal);
|
|
1292
|
+
if (!res.writableEnded && !abortController.signal.aborted) {
|
|
1293
|
+
sendJson(res, 200, { ok: true, directoryPath });
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
if (!res.writableEnded && !abortController.signal.aborted) {
|
|
1298
|
+
sendJson(res, 500, {
|
|
1299
|
+
ok: false,
|
|
1300
|
+
error: {
|
|
1301
|
+
code: "DIRECTORY_PICKER_FAILED",
|
|
1302
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
finally {
|
|
1308
|
+
req.off("aborted", abort);
|
|
1309
|
+
res.off("close", abort);
|
|
1310
|
+
}
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (method === "POST" && pathname === "/api/operator/v1/workspaces") {
|
|
1314
|
+
const failure = evaluateMutationGate(req, {
|
|
1315
|
+
cookieName: deps.bootToken.cookieName,
|
|
1316
|
+
bootToken: deps.bootToken.value,
|
|
1317
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
1318
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
1319
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
1320
|
+
});
|
|
1321
|
+
if (failure) {
|
|
1322
|
+
sendJson(res, failure.status, {
|
|
1323
|
+
ok: false,
|
|
1324
|
+
error: { code: failure.code, message: failure.message },
|
|
1325
|
+
});
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
let body;
|
|
1329
|
+
try {
|
|
1330
|
+
body = (await readJsonBody(req));
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
sendJson(res, 400, {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
error: {
|
|
1336
|
+
code: "INVALID_INPUT",
|
|
1337
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const directoryPath = typeof body.directoryPath === "string"
|
|
1343
|
+
? body.directoryPath
|
|
1344
|
+
: typeof body.path === "string"
|
|
1345
|
+
? body.path
|
|
1346
|
+
: "";
|
|
1347
|
+
if (!directoryPath.trim()) {
|
|
1348
|
+
sendJson(res, 400, {
|
|
1349
|
+
ok: false,
|
|
1350
|
+
error: {
|
|
1351
|
+
code: "INVALID_INPUT",
|
|
1352
|
+
message: "directoryPath (string) is required",
|
|
1353
|
+
},
|
|
1354
|
+
});
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (body.name !== undefined &&
|
|
1358
|
+
(typeof body.name !== "string" || !body.name.trim())) {
|
|
1359
|
+
sendJson(res, 400, {
|
|
1360
|
+
ok: false,
|
|
1361
|
+
error: {
|
|
1362
|
+
code: "INVALID_INPUT",
|
|
1363
|
+
message: "name must be a non-empty string when provided",
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const name = typeof body.name === "string" ? body.name : undefined;
|
|
1369
|
+
const result = await registry.add(directoryPath, name ? { name } : {});
|
|
1370
|
+
if (!result.ok) {
|
|
1371
|
+
const status = result.code === "DUPLICATE"
|
|
1372
|
+
? 409
|
|
1373
|
+
: result.code === "INVALID_PATH"
|
|
1374
|
+
? 400
|
|
1375
|
+
: 400;
|
|
1376
|
+
sendJson(res, status, {
|
|
1377
|
+
ok: false,
|
|
1378
|
+
error: {
|
|
1379
|
+
code: result.code,
|
|
1380
|
+
message: result.message,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const record = result.record;
|
|
1386
|
+
sendJson(res, 201, {
|
|
1387
|
+
ok: true,
|
|
1388
|
+
workspace: {
|
|
1389
|
+
workspaceId: record.workspaceId,
|
|
1390
|
+
name: record.name,
|
|
1391
|
+
displayPath: record.displayPath,
|
|
1392
|
+
repoFingerprint: record.repoFingerprint,
|
|
1393
|
+
createdAt: record.createdAt,
|
|
1394
|
+
lastAccessedAt: record.lastAccessedAt,
|
|
1395
|
+
readiness: {
|
|
1396
|
+
state: "setup-required",
|
|
1397
|
+
summary: `pi=unknown; inspect=${readinessDeps.inspectReady ? "ready" : "unavailable"}`,
|
|
1398
|
+
},
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
|
|
1404
|
+
if (method === "DELETE" && removeMatch) {
|
|
1405
|
+
const failure = evaluateMutationGate(req, {
|
|
1406
|
+
cookieName: deps.bootToken.cookieName,
|
|
1407
|
+
bootToken: deps.bootToken.value,
|
|
1408
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
1409
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
1410
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
1411
|
+
});
|
|
1412
|
+
if (failure) {
|
|
1413
|
+
sendJson(res, failure.status, {
|
|
1414
|
+
ok: false,
|
|
1415
|
+
error: { code: failure.code, message: failure.message },
|
|
1416
|
+
});
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const workspaceId = decodeURIComponent(removeMatch[1]);
|
|
1420
|
+
const removed = await registry.remove(workspaceId);
|
|
1421
|
+
if (!removed) {
|
|
1422
|
+
sendJson(res, 404, {
|
|
1423
|
+
ok: false,
|
|
1424
|
+
error: {
|
|
1425
|
+
code: "WORKSPACE_NOT_FOUND",
|
|
1426
|
+
message: `unknown workspace: ${workspaceId}`,
|
|
1427
|
+
},
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
sendJson(res, 200, { ok: true, workspaceId, removed: true });
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
sendJson(res, 404, {
|
|
1435
|
+
ok: false,
|
|
1436
|
+
error: { code: "NOT_FOUND", message: `unknown workspace route: ${pathname}` },
|
|
1437
|
+
});
|
|
1438
|
+
}
|