@tea-agent/loop-agent 0.40.0-next.9 → 0.41.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/AGENTS.md +1 -1
- package/CHANGELOG.md +130 -3
- package/README.md +2 -2
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +21 -5
- package/dist/cli/program.js +61 -4
- package/dist/commands/client-recovery.js +8 -36
- 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/commands/init-model-catalog.js +22 -0
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/executors/dag-pi-executor.js +39 -12
- package/dist/executors/pi-sdk-executor.js +36 -5
- package/dist/executors/shell-executor.js +64 -17
- package/dist/governance/checks.js +1 -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 +23 -1
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -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/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +84 -13
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +285 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +487 -30
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +28 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +32 -3
- 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 +21 -3
- 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 +124 -8
- package/dist/worker/console/operator-selection.js +6 -2
- 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.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +179 -0
- package/dist/worker/console/routes.js +864 -13
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +177 -44
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-DSVcg1YO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-BtXhIqui.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-B-nh-ilJ.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BIUSVdZI.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-Cmw3L4qM.js} +1 -1
- package/dist/worker/console/static/assets/channel-BhZFNefx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGpMMjGm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BNBkEpEJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-ubln3eaU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-_XcBTt1i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Ln30Pgrh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-ESH3lhSx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DAJt-qXB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-Cx10LIcH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CENeOxoJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-DXqK-D38.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Sbv-NJni.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Sbv-NJni.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-CE4dmvGY.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CRacN1zK.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cneukjrt.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DPFboRoL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DIAQgcOP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-VrYMKxuy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-CLHvBghn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-DVCWBif8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-BTMmbBzJ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BUZsKJ56.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-BsBZ5NXC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-D7fH_kDa.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-Bza5OohY.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DpkoGR_Y.js} +1 -1
- package/dist/worker/console/static/assets/index-Ius3vxhf.css +1 -0
- package/dist/worker/console/static/assets/index-yASzcbBr.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BYm3g6LT.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C2RS_H4v.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4slKpCo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-B7orRNpI.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-D1BeBxLQ.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-0TtMP8k4.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-XL5EbCtT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CwF7uas5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-A_Jo-72f.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CxOa4gKL.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-DoAoswIH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-C1Lc1MlD.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-DFXOkq0U.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-B0ufUCuW.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-iPbW55g3.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CHiXzEKP.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BtMgzo_H.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-LyFs7TpX.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cb-OVd9Y.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-tNkJnb6z.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-DFm7Oo8o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-tpblTp9i.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-BCDLWR_D.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/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
- package/dist/worker/console/workspace-context.js +56 -5
- package/dist/worker/console/workspace-registry.js +335 -62
- 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/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +7 -2
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1108 -135
- package/dist/worker/observe/static/views/dag-inspector.js +1257 -59
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +210 -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/backend-test-case-coverage-analysis.js +162 -8
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -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 +151 -4
- 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 +8 -7
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/agent-dag.schema.json +32 -0
- package/docs/templates/backend-test-dag.json +6 -5
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/task.yaml +1 -1
- package/package.json +5 -2
- package/skills/loop-agent/references/command-reference.md +21 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -1,13 +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";
|
|
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, } from "./pi-plugins.js";
|
|
11
20
|
import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
|
|
12
21
|
export function sendJson(res, status, body, extraHeaders) {
|
|
13
22
|
const payload = JSON.stringify(body);
|
|
@@ -148,7 +157,516 @@ async function handleSessionPing(req, res, ctx) {
|
|
|
148
157
|
export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
|
|
149
158
|
"dagRerunPlan",
|
|
150
159
|
"dagRerun",
|
|
160
|
+
"contextExport",
|
|
161
|
+
"artifactRevisionStage",
|
|
162
|
+
"artifactRevisionPlan",
|
|
163
|
+
"artifactRevisionExecute",
|
|
151
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
|
+
}
|
|
152
670
|
async function handleCreateOperation(req, res, ctx) {
|
|
153
671
|
if (!ctx.operator) {
|
|
154
672
|
sendJson(res, 503, {
|
|
@@ -194,7 +712,7 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
194
712
|
}
|
|
195
713
|
let body;
|
|
196
714
|
try {
|
|
197
|
-
body = (await readJsonBody(req));
|
|
715
|
+
body = (await readJsonBody(req, 15 * 1024 * 1024));
|
|
198
716
|
}
|
|
199
717
|
catch (error) {
|
|
200
718
|
sendJson(res, 400, {
|
|
@@ -221,6 +739,27 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
221
739
|
});
|
|
222
740
|
return;
|
|
223
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
|
+
}
|
|
224
763
|
const result = await dispatchOperatorAction(ctx.operator.actionContext, {
|
|
225
764
|
action,
|
|
226
765
|
actionParams,
|
|
@@ -364,6 +903,34 @@ async function handleOperationEvents(req, res, ctx, operationId) {
|
|
|
364
903
|
* Path-escape-safe static file serve for Console SPA.
|
|
365
904
|
* Sets boot capability cookie on HTML responses.
|
|
366
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
|
+
}
|
|
367
934
|
export async function serveConsoleStatic(req, res, ctx) {
|
|
368
935
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
369
936
|
let pathname = decodeURIComponent(url.pathname);
|
|
@@ -382,11 +949,14 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
382
949
|
// SPA fallback for non-file routes
|
|
383
950
|
const indexPath = path.join(resolvedStatic, "index.html");
|
|
384
951
|
if (existsSync(indexPath) && !pathname.includes(".")) {
|
|
385
|
-
const html =
|
|
952
|
+
const html = injectScopedInspectTokens(await readFile(indexPath), ctx);
|
|
386
953
|
res.writeHead(200, {
|
|
387
954
|
"Content-Type": "text/html; charset=utf-8",
|
|
388
955
|
"Content-Length": html.byteLength,
|
|
389
|
-
|
|
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),
|
|
390
960
|
});
|
|
391
961
|
res.end(html);
|
|
392
962
|
return;
|
|
@@ -397,14 +967,15 @@ export async function serveConsoleStatic(req, res, ctx) {
|
|
|
397
967
|
const raw = await readFile(resolvedFile);
|
|
398
968
|
const contentType = contentTypeFor(pathname);
|
|
399
969
|
const content = contentType.startsWith("text/html")
|
|
400
|
-
?
|
|
970
|
+
? injectScopedInspectTokens(raw, ctx)
|
|
401
971
|
: raw;
|
|
402
972
|
const headers = {
|
|
403
973
|
"Content-Type": contentType,
|
|
404
974
|
"Content-Length": content.byteLength,
|
|
405
975
|
};
|
|
406
976
|
if (contentType.startsWith("text/html")) {
|
|
407
|
-
headers["
|
|
977
|
+
headers["Cache-Control"] = "no-store";
|
|
978
|
+
headers["Set-Cookie"] = scopedSetCookieHeaders(ctx);
|
|
408
979
|
}
|
|
409
980
|
res.writeHead(200, headers);
|
|
410
981
|
res.end(content);
|
|
@@ -462,6 +1033,95 @@ async function handleNightAuxTickerPost(req, res, ctx) {
|
|
|
462
1033
|
const status = ticker.setEnabled(body.enabled);
|
|
463
1034
|
sendJson(res, 200, { ok: true, ticker: status });
|
|
464
1035
|
}
|
|
1036
|
+
async function handlePiPluginInstall(req, res) {
|
|
1037
|
+
let body;
|
|
1038
|
+
try {
|
|
1039
|
+
body = (await readJsonBody(req));
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
sendJson(res, 400, {
|
|
1043
|
+
ok: false,
|
|
1044
|
+
error: {
|
|
1045
|
+
code: "INVALID_INPUT",
|
|
1046
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1052
|
+
if (!name) {
|
|
1053
|
+
sendJson(res, 400, {
|
|
1054
|
+
ok: false,
|
|
1055
|
+
error: { code: "INVALID_INPUT", message: "missing package name" },
|
|
1056
|
+
});
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const version = typeof body.version === "string" ? body.version.trim() : undefined;
|
|
1060
|
+
try {
|
|
1061
|
+
const result = await installPiPlugin(name, version);
|
|
1062
|
+
if (!result.ok) {
|
|
1063
|
+
sendJson(res, 400, {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
error: { code: "INSTALL_FAILED", message: result.error },
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
sendJson(res, 200, { ok: true });
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
sendJson(res, 500, {
|
|
1073
|
+
ok: false,
|
|
1074
|
+
error: {
|
|
1075
|
+
code: "INTERNAL_ERROR",
|
|
1076
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function handlePiPluginUninstall(req, res) {
|
|
1082
|
+
let body;
|
|
1083
|
+
try {
|
|
1084
|
+
body = (await readJsonBody(req));
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
sendJson(res, 400, {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
error: {
|
|
1090
|
+
code: "INVALID_INPUT",
|
|
1091
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1097
|
+
if (!name) {
|
|
1098
|
+
sendJson(res, 400, {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
error: { code: "INVALID_INPUT", message: "missing package name" },
|
|
1101
|
+
});
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const result = await uninstallPiPlugin(name);
|
|
1106
|
+
if (!result.ok) {
|
|
1107
|
+
sendJson(res, 400, {
|
|
1108
|
+
ok: false,
|
|
1109
|
+
error: { code: "UNINSTALL_FAILED", message: result.error },
|
|
1110
|
+
});
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
sendJson(res, 200, { ok: true });
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
sendJson(res, 500, {
|
|
1117
|
+
ok: false,
|
|
1118
|
+
error: {
|
|
1119
|
+
code: "INTERNAL_ERROR",
|
|
1120
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1121
|
+
},
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
465
1125
|
export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
466
1126
|
const method = (req.method ?? "GET").toUpperCase();
|
|
467
1127
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -491,10 +1151,79 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
491
1151
|
await handleListOperations(req, res, ctx);
|
|
492
1152
|
return true;
|
|
493
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
|
+
}
|
|
494
1195
|
if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
|
|
495
1196
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
496
1197
|
return true;
|
|
497
1198
|
}
|
|
1199
|
+
if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
|
|
1200
|
+
sendJson(res, 200, { ok: true, data: listPiPlugins() });
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
if (method === "GET" &&
|
|
1204
|
+
pathname === "/api/operator/v1/pi-plugins/versions") {
|
|
1205
|
+
const name = url.searchParams.get("name")?.trim() ?? "";
|
|
1206
|
+
if (!name) {
|
|
1207
|
+
sendJson(res, 400, {
|
|
1208
|
+
ok: false,
|
|
1209
|
+
error: { code: "INVALID_INPUT", message: "missing name" },
|
|
1210
|
+
});
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
const result = await listPiPluginVersions(name);
|
|
1214
|
+
if (!result.ok) {
|
|
1215
|
+
sendJson(res, 400, {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
error: { code: "FETCH_FAILED", message: result.error },
|
|
1218
|
+
});
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
sendJson(res, 200, {
|
|
1222
|
+
ok: true,
|
|
1223
|
+
data: { versions: result.versions, latest: result.latest },
|
|
1224
|
+
});
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
498
1227
|
const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
|
|
499
1228
|
if (opMatch) {
|
|
500
1229
|
const operationId = decodeURIComponent(opMatch[1]);
|
|
@@ -526,6 +1255,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
526
1255
|
await handleNightAuxTickerPost(req, res, ctx);
|
|
527
1256
|
return true;
|
|
528
1257
|
}
|
|
1258
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
|
|
1259
|
+
await handlePiPluginInstall(req, res);
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
|
|
1263
|
+
await handlePiPluginUninstall(req, res);
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
529
1266
|
// Allow other operator POSTs only under known routes; unknown → 404
|
|
530
1267
|
sendJson(res, 404, {
|
|
531
1268
|
ok: false,
|
|
@@ -587,9 +1324,11 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
587
1324
|
* POST /api/operator/v1/workspaces/pick — host native folder chooser
|
|
588
1325
|
* POST /api/operator/v1/workspaces — body { "name": "<label>",
|
|
589
1326
|
* "directoryPath": "<dir>" }
|
|
1327
|
+
* PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
|
|
1328
|
+
* "<id>" | null }
|
|
590
1329
|
* DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
|
|
591
1330
|
*
|
|
592
|
-
* POST/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
1331
|
+
* POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
593
1332
|
* workspace selection parameter.
|
|
594
1333
|
*
|
|
595
1334
|
* AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
|
|
@@ -598,8 +1337,53 @@ function pendingReadinessSummary(inspectReady) {
|
|
|
598
1337
|
* requests. A hanging probe can never delay the 200 list.
|
|
599
1338
|
*/
|
|
600
1339
|
export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
|
|
1340
|
+
if (method === "GET" &&
|
|
1341
|
+
pathname === "/api/operator/v1/workspaces/events") {
|
|
1342
|
+
res.writeHead(200, {
|
|
1343
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1344
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1345
|
+
Connection: "keep-alive",
|
|
1346
|
+
"X-Accel-Buffering": "no",
|
|
1347
|
+
});
|
|
1348
|
+
const sendRevision = (event, revision) => {
|
|
1349
|
+
if (res.destroyed || res.writableEnded)
|
|
1350
|
+
return;
|
|
1351
|
+
res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
|
|
1352
|
+
};
|
|
1353
|
+
let baselineSent = false;
|
|
1354
|
+
let pendingRevision = 0;
|
|
1355
|
+
const unsubscribe = registry.subscribe((revision) => {
|
|
1356
|
+
if (!baselineSent)
|
|
1357
|
+
pendingRevision = Math.max(pendingRevision, revision);
|
|
1358
|
+
else
|
|
1359
|
+
sendRevision("workspace_registry_changed", revision);
|
|
1360
|
+
});
|
|
1361
|
+
const initial = await registry.snapshotVisible();
|
|
1362
|
+
sendRevision("workspace_registry_ready", initial.revision);
|
|
1363
|
+
baselineSent = true;
|
|
1364
|
+
if (pendingRevision > initial.revision) {
|
|
1365
|
+
sendRevision("workspace_registry_changed", pendingRevision);
|
|
1366
|
+
}
|
|
1367
|
+
const heartbeat = setInterval(() => {
|
|
1368
|
+
if (!res.destroyed && !res.writableEnded)
|
|
1369
|
+
res.write(": keepalive\n\n");
|
|
1370
|
+
}, 15_000);
|
|
1371
|
+
heartbeat.unref();
|
|
1372
|
+
let closed = false;
|
|
1373
|
+
const close = () => {
|
|
1374
|
+
if (closed)
|
|
1375
|
+
return;
|
|
1376
|
+
closed = true;
|
|
1377
|
+
clearInterval(heartbeat);
|
|
1378
|
+
unsubscribe();
|
|
1379
|
+
};
|
|
1380
|
+
req.once("aborted", close);
|
|
1381
|
+
res.once("close", close);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
601
1384
|
if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
|
|
602
|
-
const
|
|
1385
|
+
const snapshot = await registry.snapshotVisible();
|
|
1386
|
+
const records = snapshot.workspaces;
|
|
603
1387
|
const inspectReady = readinessDeps.inspectReady;
|
|
604
1388
|
const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
|
|
605
1389
|
const workspaces = records.map((record) => {
|
|
@@ -611,6 +1395,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
611
1395
|
repoFingerprint: record.repoFingerprint,
|
|
612
1396
|
createdAt: record.createdAt,
|
|
613
1397
|
lastAccessedAt: record.lastAccessedAt,
|
|
1398
|
+
sessionIds: record.sessionIds,
|
|
614
1399
|
readiness: cached
|
|
615
1400
|
? {
|
|
616
1401
|
state: cached.state,
|
|
@@ -622,7 +1407,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
622
1407
|
// Respond FIRST, then refresh readiness in the background (AC-006).
|
|
623
1408
|
sendJson(res, 200, {
|
|
624
1409
|
ok: true,
|
|
625
|
-
schemaVersion:
|
|
1410
|
+
schemaVersion: 2,
|
|
1411
|
+
revision: snapshot.revision,
|
|
626
1412
|
workspaces,
|
|
627
1413
|
});
|
|
628
1414
|
void Promise.all(records.map(async (record) => {
|
|
@@ -789,8 +1575,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
789
1575
|
});
|
|
790
1576
|
return;
|
|
791
1577
|
}
|
|
792
|
-
const
|
|
793
|
-
if (method === "
|
|
1578
|
+
const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
|
|
1579
|
+
if (method === "PATCH" && workspaceMatch) {
|
|
1580
|
+
const failure = evaluateMutationGate(req, {
|
|
1581
|
+
cookieName: deps.bootToken.cookieName,
|
|
1582
|
+
bootToken: deps.bootToken.value,
|
|
1583
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
1584
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
1585
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
1586
|
+
});
|
|
1587
|
+
if (failure) {
|
|
1588
|
+
sendJson(res, failure.status, {
|
|
1589
|
+
ok: false,
|
|
1590
|
+
error: { code: failure.code, message: failure.message },
|
|
1591
|
+
});
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
let body;
|
|
1595
|
+
try {
|
|
1596
|
+
body = (await readJsonBody(req));
|
|
1597
|
+
}
|
|
1598
|
+
catch (error) {
|
|
1599
|
+
sendJson(res, 400, {
|
|
1600
|
+
ok: false,
|
|
1601
|
+
error: {
|
|
1602
|
+
code: "INVALID_INPUT",
|
|
1603
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (body.beforeWorkspaceId !== null &&
|
|
1609
|
+
typeof body.beforeWorkspaceId !== "string") {
|
|
1610
|
+
sendJson(res, 400, {
|
|
1611
|
+
ok: false,
|
|
1612
|
+
error: {
|
|
1613
|
+
code: "INVALID_INPUT",
|
|
1614
|
+
message: "beforeWorkspaceId must be a workspace id or null",
|
|
1615
|
+
},
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
1620
|
+
const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
|
|
1621
|
+
? body.beforeWorkspaceId
|
|
1622
|
+
: null;
|
|
1623
|
+
const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
|
|
1624
|
+
if (!reordered.ok) {
|
|
1625
|
+
sendJson(res, 404, {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
error: {
|
|
1628
|
+
code: reordered.code,
|
|
1629
|
+
message: reordered.code === "ANCHOR_NOT_FOUND"
|
|
1630
|
+
? `unknown workspace anchor: ${beforeWorkspaceId}`
|
|
1631
|
+
: `unknown workspace: ${workspaceId}`,
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
sendJson(res, 200, {
|
|
1637
|
+
ok: true,
|
|
1638
|
+
workspaceId,
|
|
1639
|
+
beforeWorkspaceId,
|
|
1640
|
+
changed: reordered.changed,
|
|
1641
|
+
});
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (method === "DELETE" && workspaceMatch) {
|
|
794
1645
|
const failure = evaluateMutationGate(req, {
|
|
795
1646
|
cookieName: deps.bootToken.cookieName,
|
|
796
1647
|
bootToken: deps.bootToken.value,
|
|
@@ -805,7 +1656,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
|
|
|
805
1656
|
});
|
|
806
1657
|
return;
|
|
807
1658
|
}
|
|
808
|
-
const workspaceId = decodeURIComponent(
|
|
1659
|
+
const workspaceId = decodeURIComponent(workspaceMatch[1]);
|
|
809
1660
|
const removed = await registry.remove(workspaceId);
|
|
810
1661
|
if (!removed) {
|
|
811
1662
|
sendJson(res, 404, {
|