@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.10
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 +4 -1
- package/CHANGELOG.md +248 -95
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +23 -5
- package/dist/cli/program.js +21 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +455 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +2306 -61
- package/dist/executors/pi-executor.js +7 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1249 -224
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +39 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +50 -5
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +18 -1
- package/dist/task/contract/schema.js +4 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +199 -18
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +298 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- 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/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +298 -32
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- 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 +14 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +227 -7
- package/dist/worker/console/operator-actions.js +388 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +355 -3
- package/dist/worker/console/security.js +67 -13
- package/dist/worker/console/server.js +609 -164
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
- package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
- package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
- package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
- package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- 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/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- 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/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +344 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +90 -4
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +208 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- 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 -96
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- 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 +184 -1
- package/dist/worker/observe/static/styles.css +395 -0
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +14 -5
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/failures.js +5 -2
- 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 +78 -9
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +534 -178
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +371 -393
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +288 -14
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +272 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +984 -618
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +468 -34
- package/dist/workflows/dag/prompt.js +44 -19
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +81 -6
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +318 -8
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +321 -31
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +92 -32
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +9 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +45 -1
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +7 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +3 -4
- package/skills/frontend-implementation/references/design-spec.md +15 -10
- package/skills/frontend-implementation/references/node-contracts.md +23 -13
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/command-reference.md +12 -4
- package/skills/loop-agent/references/hybrid-dag.md +8 -1
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -3,10 +3,12 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { dispatchOperatorAction } from "./operator-actions.js";
|
|
5
5
|
import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
|
|
6
|
+
import { probePiReadiness } from "./pi-readiness.js";
|
|
6
7
|
import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
7
8
|
import { ROUTES } from "../observe/routes.js";
|
|
8
9
|
import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
|
|
9
10
|
import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
|
|
11
|
+
import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
|
|
10
12
|
export function sendJson(res, status, body, extraHeaders) {
|
|
11
13
|
const payload = JSON.stringify(body);
|
|
12
14
|
res.writeHead(status, {
|
|
@@ -38,6 +40,10 @@ function contentTypeFor(pathname) {
|
|
|
38
40
|
return "application/octet-stream";
|
|
39
41
|
}
|
|
40
42
|
async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
|
|
43
|
+
// Body pre-read by the multi-workspace dispatcher (workspaceId resolution).
|
|
44
|
+
const preRead = req.__preReadBody;
|
|
45
|
+
if (preRead !== undefined)
|
|
46
|
+
return preRead;
|
|
41
47
|
const chunks = [];
|
|
42
48
|
let total = 0;
|
|
43
49
|
for await (const chunk of req) {
|
|
@@ -67,6 +73,7 @@ export async function handleConsoleHealthRequest(_req, res, ctx) {
|
|
|
67
73
|
piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
|
|
68
74
|
inspectReady: ctx.inspect.ready,
|
|
69
75
|
routes: ROUTES,
|
|
76
|
+
consoleInstanceId: ctx.consoleInstanceId,
|
|
70
77
|
}));
|
|
71
78
|
}
|
|
72
79
|
catch {
|
|
@@ -77,6 +84,7 @@ export async function handleConsoleHealthRequest(_req, res, ctx) {
|
|
|
77
84
|
repoFingerprint: "unavailable",
|
|
78
85
|
packageName: "unknown",
|
|
79
86
|
packageVersion: "0.0.0",
|
|
87
|
+
consoleInstanceId: ctx.consoleInstanceId,
|
|
80
88
|
piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
|
|
81
89
|
inspect: {
|
|
82
90
|
ready: ctx.inspect.ready,
|
|
@@ -116,6 +124,7 @@ async function handleRecoveryCandidates(_req, res, ctx, url) {
|
|
|
116
124
|
}
|
|
117
125
|
async function handleSessionPing(req, res, ctx) {
|
|
118
126
|
const failure = evaluateMutationGate(req, {
|
|
127
|
+
cookieName: ctx.bootToken.cookieName,
|
|
119
128
|
bootToken: ctx.bootToken.value,
|
|
120
129
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
121
130
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
@@ -130,6 +139,16 @@ async function handleSessionPing(req, res, ctx) {
|
|
|
130
139
|
}
|
|
131
140
|
sendJson(res, 200, { ok: true, pong: true });
|
|
132
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Actions the inspect-scoped mutation token may dispatch. Deliberately
|
|
144
|
+
* minimal: the experimental prompt-override rerun tab needs plan preflight +
|
|
145
|
+
* execution only. Everything else (task creation, runDag, reconcile, chat,
|
|
146
|
+
* worker actions…) requires the general Console token.
|
|
147
|
+
*/
|
|
148
|
+
export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
|
|
149
|
+
"dagRerunPlan",
|
|
150
|
+
"dagRerun",
|
|
151
|
+
]);
|
|
133
152
|
async function handleCreateOperation(req, res, ctx) {
|
|
134
153
|
if (!ctx.operator) {
|
|
135
154
|
sendJson(res, 503, {
|
|
@@ -141,12 +160,31 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
141
160
|
});
|
|
142
161
|
return;
|
|
143
162
|
}
|
|
144
|
-
const
|
|
163
|
+
const generalGateContext = {
|
|
164
|
+
cookieName: ctx.bootToken.cookieName,
|
|
145
165
|
bootToken: ctx.bootToken.value,
|
|
146
166
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
147
167
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
148
168
|
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
149
|
-
}
|
|
169
|
+
};
|
|
170
|
+
let failure = evaluateMutationGate(req, generalGateContext);
|
|
171
|
+
let inspectScoped = false;
|
|
172
|
+
if (failure && ctx.inspectBootToken) {
|
|
173
|
+
// Scoped /inspect credentials: try the inspect gate before failing. The
|
|
174
|
+
// general gate already rejected, so passing here only ever yields the
|
|
175
|
+
// allowlisted actions below — never general write capability.
|
|
176
|
+
const scopedFailure = evaluateMutationGate(req, {
|
|
177
|
+
cookieName: ctx.inspectBootToken.cookieName,
|
|
178
|
+
bootToken: ctx.inspectBootToken.value,
|
|
179
|
+
confirmationToken: ctx.inspectBootToken.confirmationToken,
|
|
180
|
+
consoleOrigin: ctx.getConsoleOrigin(),
|
|
181
|
+
allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
|
|
182
|
+
});
|
|
183
|
+
if (!scopedFailure) {
|
|
184
|
+
inspectScoped = true;
|
|
185
|
+
failure = undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
150
188
|
if (failure) {
|
|
151
189
|
sendJson(res, failure.status, {
|
|
152
190
|
ok: false,
|
|
@@ -173,10 +211,21 @@ async function handleCreateOperation(req, res, ctx) {
|
|
|
173
211
|
? body.actionParams
|
|
174
212
|
: {};
|
|
175
213
|
const clientRequestId = typeof body.clientRequestId === "string" ? body.clientRequestId : undefined;
|
|
214
|
+
if (inspectScoped && !INSPECT_MUTATION_ALLOWED_ACTIONS.has(action)) {
|
|
215
|
+
sendJson(res, 403, {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: {
|
|
218
|
+
code: "ACTION_NOT_IN_INSPECT_SCOPE",
|
|
219
|
+
message: `inspect-scoped token only allows: ${[...INSPECT_MUTATION_ALLOWED_ACTIONS].join(", ")}`,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
176
224
|
const result = await dispatchOperatorAction(ctx.operator.actionContext, {
|
|
177
225
|
action,
|
|
178
226
|
actionParams,
|
|
179
227
|
clientRequestId,
|
|
228
|
+
source: "human-ui",
|
|
180
229
|
});
|
|
181
230
|
sendJson(res, result.status, result.body);
|
|
182
231
|
}
|
|
@@ -202,7 +251,35 @@ async function handleGetOperation(_req, res, ctx, operationId) {
|
|
|
202
251
|
});
|
|
203
252
|
return;
|
|
204
253
|
}
|
|
205
|
-
sendJson(res, 200, {
|
|
254
|
+
sendJson(res, 200, {
|
|
255
|
+
ok: true,
|
|
256
|
+
workspaceId: ctx.workspace?.workspaceId,
|
|
257
|
+
operation: { ...op, workspaceId: ctx.workspace?.workspaceId },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* GET /api/operator/v1/operations?workspaceId= — NEW per-workspace list route
|
|
262
|
+
* (multi-workspace foundation). Returns only the resolved Context's Operations
|
|
263
|
+
* and injects the owning workspaceId. operation-store.ts is unchanged.
|
|
264
|
+
*/
|
|
265
|
+
async function handleListOperations(_req, res, ctx) {
|
|
266
|
+
if (!ctx.operator) {
|
|
267
|
+
sendJson(res, 503, {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: {
|
|
270
|
+
code: "INTERNAL_ERROR",
|
|
271
|
+
message: "operator runtime unavailable",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const operations = await ctx.operator.operations.list();
|
|
277
|
+
sendJson(res, 200, {
|
|
278
|
+
ok: true,
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
workspaceId: ctx.workspace?.workspaceId,
|
|
281
|
+
ops: operations.map((op) => ({ ...op, workspaceId: ctx.workspace?.workspaceId })),
|
|
282
|
+
});
|
|
206
283
|
}
|
|
207
284
|
async function handleOperationEvents(req, res, ctx, operationId) {
|
|
208
285
|
if (!ctx.operator) {
|
|
@@ -410,6 +487,10 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
410
487
|
await handleCreateOperation(req, res, ctx);
|
|
411
488
|
return true;
|
|
412
489
|
}
|
|
490
|
+
if (method === "GET" && pathname === "/api/operator/v1/operations") {
|
|
491
|
+
await handleListOperations(req, res, ctx);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
413
494
|
if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
|
|
414
495
|
await handleNightAuxTickerGet(req, res, ctx);
|
|
415
496
|
return true;
|
|
@@ -428,6 +509,7 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
|
|
|
428
509
|
}
|
|
429
510
|
if (isMutationMethod(method)) {
|
|
430
511
|
const failure = evaluateMutationGate(req, {
|
|
512
|
+
cookieName: ctx.bootToken.cookieName,
|
|
431
513
|
bootToken: ctx.bootToken.value,
|
|
432
514
|
confirmationToken: ctx.bootToken.confirmationToken,
|
|
433
515
|
consoleOrigin: ctx.getConsoleOrigin(),
|
|
@@ -473,3 +555,273 @@ export async function handleConsoleRequest(req, res, ctx) {
|
|
|
473
555
|
}
|
|
474
556
|
return false;
|
|
475
557
|
}
|
|
558
|
+
export function createWorkspaceReadinessCache() {
|
|
559
|
+
const entries = new Map();
|
|
560
|
+
return {
|
|
561
|
+
get: (repoRoot) => entries.get(repoRoot),
|
|
562
|
+
set: (repoRoot, entry) => {
|
|
563
|
+
entries.set(repoRoot, entry);
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const defaultWorkspaceReadinessCache = createWorkspaceReadinessCache();
|
|
568
|
+
const READY_STATE_TTL_MS = 60_000;
|
|
569
|
+
const NON_READY_STATE_TTL_MS = 15_000;
|
|
570
|
+
function isCacheFresh(entry, now) {
|
|
571
|
+
const ttl = entry.state === "ready" ? READY_STATE_TTL_MS : NON_READY_STATE_TTL_MS;
|
|
572
|
+
return now - entry.probedAt < ttl;
|
|
573
|
+
}
|
|
574
|
+
/** Readiness placeholder for a never-probed workspace (AC-006): the list is
|
|
575
|
+
* still returned immediately with a pending marker, refreshed by the
|
|
576
|
+
* background probe. */
|
|
577
|
+
function pendingReadinessSummary(inspectReady) {
|
|
578
|
+
return {
|
|
579
|
+
state: "pending",
|
|
580
|
+
summary: `pi=pending; inspect=${inspectReady ? "ready" : "unavailable"}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Process-level Workspace Registry HTTP surface (2026-08-22 multi-workspace
|
|
585
|
+
* foundation):
|
|
586
|
+
* GET /api/operator/v1/workspaces — visible list
|
|
587
|
+
* POST /api/operator/v1/workspaces/pick — host native folder chooser
|
|
588
|
+
* POST /api/operator/v1/workspaces — body { "name": "<label>",
|
|
589
|
+
* "directoryPath": "<dir>" }
|
|
590
|
+
* DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
|
|
591
|
+
*
|
|
592
|
+
* POST/DELETE are mutations and pass the Console mutation gate. GET carries no
|
|
593
|
+
* workspace selection parameter.
|
|
594
|
+
*
|
|
595
|
+
* AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
|
|
596
|
+
* readiness cache answer immediately (first sight may be a pending
|
|
597
|
+
* placeholder); probes refresh the cache in the background for subsequent
|
|
598
|
+
* requests. A hanging probe can never delay the 200 list.
|
|
599
|
+
*/
|
|
600
|
+
export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
|
|
601
|
+
if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
|
|
602
|
+
const records = await registry.listVisible();
|
|
603
|
+
const inspectReady = readinessDeps.inspectReady;
|
|
604
|
+
const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
|
|
605
|
+
const workspaces = records.map((record) => {
|
|
606
|
+
const cached = readinessCache.get(record.repoRoot);
|
|
607
|
+
return {
|
|
608
|
+
workspaceId: record.workspaceId,
|
|
609
|
+
name: record.name,
|
|
610
|
+
displayPath: record.displayPath,
|
|
611
|
+
repoFingerprint: record.repoFingerprint,
|
|
612
|
+
createdAt: record.createdAt,
|
|
613
|
+
lastAccessedAt: record.lastAccessedAt,
|
|
614
|
+
readiness: cached
|
|
615
|
+
? {
|
|
616
|
+
state: cached.state,
|
|
617
|
+
summary: `pi=${cached.state}; inspect=${inspectReady ? "ready" : "unavailable"}`,
|
|
618
|
+
}
|
|
619
|
+
: pendingReadinessSummary(inspectReady),
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
// Respond FIRST, then refresh readiness in the background (AC-006).
|
|
623
|
+
sendJson(res, 200, {
|
|
624
|
+
ok: true,
|
|
625
|
+
schemaVersion: 1,
|
|
626
|
+
workspaces,
|
|
627
|
+
});
|
|
628
|
+
void Promise.all(records.map(async (record) => {
|
|
629
|
+
const cached = readinessCache.get(record.repoRoot);
|
|
630
|
+
if (cached && isCacheFresh(cached, Date.now()))
|
|
631
|
+
return;
|
|
632
|
+
let state;
|
|
633
|
+
try {
|
|
634
|
+
const report = await probePiReadiness({
|
|
635
|
+
override: deps.piReadinessProbe,
|
|
636
|
+
repoRoot: record.repoRoot,
|
|
637
|
+
});
|
|
638
|
+
state = report.state;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
state = "setup-required";
|
|
642
|
+
}
|
|
643
|
+
readinessCache.set(record.repoRoot, {
|
|
644
|
+
state,
|
|
645
|
+
probedAt: Date.now(),
|
|
646
|
+
});
|
|
647
|
+
})).catch(() => undefined);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (method === "POST" && pathname === "/api/operator/v1/workspaces/pick") {
|
|
651
|
+
const failure = evaluateMutationGate(req, {
|
|
652
|
+
cookieName: deps.bootToken.cookieName,
|
|
653
|
+
bootToken: deps.bootToken.value,
|
|
654
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
655
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
656
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
657
|
+
});
|
|
658
|
+
if (failure) {
|
|
659
|
+
sendJson(res, failure.status, {
|
|
660
|
+
ok: false,
|
|
661
|
+
error: { code: failure.code, message: failure.message },
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (deps.allowNonLoopbackAccess) {
|
|
666
|
+
sendJson(res, 409, {
|
|
667
|
+
ok: false,
|
|
668
|
+
error: {
|
|
669
|
+
code: "NATIVE_PICKER_UNAVAILABLE",
|
|
670
|
+
message: "原生目录选择仅在本机回环地址启动的 Console 中可用。",
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const abortController = new AbortController();
|
|
676
|
+
const abort = () => abortController.abort();
|
|
677
|
+
req.once("aborted", abort);
|
|
678
|
+
res.once("close", abort);
|
|
679
|
+
try {
|
|
680
|
+
const directoryPath = await (deps.pickDirectory ?? pickNativeWorkspaceDirectory)(abortController.signal);
|
|
681
|
+
if (!res.writableEnded && !abortController.signal.aborted) {
|
|
682
|
+
sendJson(res, 200, { ok: true, directoryPath });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (!res.writableEnded && !abortController.signal.aborted) {
|
|
687
|
+
sendJson(res, 500, {
|
|
688
|
+
ok: false,
|
|
689
|
+
error: {
|
|
690
|
+
code: "DIRECTORY_PICKER_FAILED",
|
|
691
|
+
message: error instanceof Error ? error.message : String(error),
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
req.off("aborted", abort);
|
|
698
|
+
res.off("close", abort);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (method === "POST" && pathname === "/api/operator/v1/workspaces") {
|
|
703
|
+
const failure = evaluateMutationGate(req, {
|
|
704
|
+
cookieName: deps.bootToken.cookieName,
|
|
705
|
+
bootToken: deps.bootToken.value,
|
|
706
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
707
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
708
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
709
|
+
});
|
|
710
|
+
if (failure) {
|
|
711
|
+
sendJson(res, failure.status, {
|
|
712
|
+
ok: false,
|
|
713
|
+
error: { code: failure.code, message: failure.message },
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
let body;
|
|
718
|
+
try {
|
|
719
|
+
body = (await readJsonBody(req));
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
sendJson(res, 400, {
|
|
723
|
+
ok: false,
|
|
724
|
+
error: {
|
|
725
|
+
code: "INVALID_INPUT",
|
|
726
|
+
message: error instanceof Error ? error.message : String(error),
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const directoryPath = typeof body.directoryPath === "string"
|
|
732
|
+
? body.directoryPath
|
|
733
|
+
: typeof body.path === "string"
|
|
734
|
+
? body.path
|
|
735
|
+
: "";
|
|
736
|
+
if (!directoryPath.trim()) {
|
|
737
|
+
sendJson(res, 400, {
|
|
738
|
+
ok: false,
|
|
739
|
+
error: {
|
|
740
|
+
code: "INVALID_INPUT",
|
|
741
|
+
message: "directoryPath (string) is required",
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (body.name !== undefined &&
|
|
747
|
+
(typeof body.name !== "string" || !body.name.trim())) {
|
|
748
|
+
sendJson(res, 400, {
|
|
749
|
+
ok: false,
|
|
750
|
+
error: {
|
|
751
|
+
code: "INVALID_INPUT",
|
|
752
|
+
message: "name must be a non-empty string when provided",
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const name = typeof body.name === "string" ? body.name : undefined;
|
|
758
|
+
const result = await registry.add(directoryPath, name ? { name } : {});
|
|
759
|
+
if (!result.ok) {
|
|
760
|
+
const status = result.code === "DUPLICATE"
|
|
761
|
+
? 409
|
|
762
|
+
: result.code === "INVALID_PATH"
|
|
763
|
+
? 400
|
|
764
|
+
: 400;
|
|
765
|
+
sendJson(res, status, {
|
|
766
|
+
ok: false,
|
|
767
|
+
error: {
|
|
768
|
+
code: result.code,
|
|
769
|
+
message: result.message,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const record = result.record;
|
|
775
|
+
sendJson(res, 201, {
|
|
776
|
+
ok: true,
|
|
777
|
+
workspace: {
|
|
778
|
+
workspaceId: record.workspaceId,
|
|
779
|
+
name: record.name,
|
|
780
|
+
displayPath: record.displayPath,
|
|
781
|
+
repoFingerprint: record.repoFingerprint,
|
|
782
|
+
createdAt: record.createdAt,
|
|
783
|
+
lastAccessedAt: record.lastAccessedAt,
|
|
784
|
+
readiness: {
|
|
785
|
+
state: "setup-required",
|
|
786
|
+
summary: `pi=unknown; inspect=${readinessDeps.inspectReady ? "ready" : "unavailable"}`,
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
|
|
793
|
+
if (method === "DELETE" && removeMatch) {
|
|
794
|
+
const failure = evaluateMutationGate(req, {
|
|
795
|
+
cookieName: deps.bootToken.cookieName,
|
|
796
|
+
bootToken: deps.bootToken.value,
|
|
797
|
+
confirmationToken: deps.bootToken.confirmationToken,
|
|
798
|
+
consoleOrigin: deps.getConsoleOrigin(),
|
|
799
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
800
|
+
});
|
|
801
|
+
if (failure) {
|
|
802
|
+
sendJson(res, failure.status, {
|
|
803
|
+
ok: false,
|
|
804
|
+
error: { code: failure.code, message: failure.message },
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const workspaceId = decodeURIComponent(removeMatch[1]);
|
|
809
|
+
const removed = await registry.remove(workspaceId);
|
|
810
|
+
if (!removed) {
|
|
811
|
+
sendJson(res, 404, {
|
|
812
|
+
ok: false,
|
|
813
|
+
error: {
|
|
814
|
+
code: "WORKSPACE_NOT_FOUND",
|
|
815
|
+
message: `unknown workspace: ${workspaceId}`,
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
sendJson(res, 200, { ok: true, workspaceId, removed: true });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sendJson(res, 404, {
|
|
824
|
+
ok: false,
|
|
825
|
+
error: { code: "NOT_FOUND", message: `unknown workspace route: ${pathname}` },
|
|
826
|
+
});
|
|
827
|
+
}
|
|
@@ -1,24 +1,78 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { isLoopbackHost } from "./loopback.js";
|
|
3
|
+
/** Prefix for instance-scoped Cookie names; not a lookup name. */
|
|
3
4
|
export const BOOT_CAPABILITY_COOKIE = "loop_console_capability";
|
|
4
5
|
/** Custom mutation confirmation header (design §9). JS-readable; not the HttpOnly boot cookie. */
|
|
5
6
|
export const CONFIRMATION_TOKEN_HEADER = "x-loop-console-confirmation";
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
function normalizeInstanceScope(scope) {
|
|
8
|
+
const repoFingerprint = scope.repoFingerprint.trim();
|
|
9
|
+
if (!repoFingerprint) {
|
|
10
|
+
throw new Error("console instance scope requires a non-empty repoFingerprint");
|
|
11
|
+
}
|
|
12
|
+
const bindHost = scope.bindHost.trim();
|
|
13
|
+
if (!bindHost) {
|
|
14
|
+
throw new Error("console instance scope requires a non-empty bindHost");
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(scope.configuredPort) || scope.configuredPort < 0) {
|
|
17
|
+
throw new Error(`console instance scope configuredPort must be an integer >= 0, got ${String(scope.configuredPort)}`);
|
|
18
|
+
}
|
|
19
|
+
const instanceNonce = scope.instanceNonce?.trim();
|
|
20
|
+
if (scope.configuredPort === 0) {
|
|
21
|
+
if (!instanceNonce) {
|
|
22
|
+
throw new Error("console instance scope requires a non-empty instanceNonce when configuredPort is 0");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
repoFingerprint,
|
|
26
|
+
bindHost,
|
|
27
|
+
configuredPort: 0,
|
|
28
|
+
instanceNonce,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
repoFingerprint,
|
|
33
|
+
bindHost,
|
|
34
|
+
configuredPort: scope.configuredPort,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Deterministic Cookie name for one Console instance; never includes repo path or token. */
|
|
38
|
+
export function deriveBootCapabilityCookieName(scope, variant) {
|
|
39
|
+
const normalized = normalizeInstanceScope(scope);
|
|
40
|
+
const digest = createHash("sha256")
|
|
41
|
+
.update("loop-console-capability-v1\0", "utf8")
|
|
42
|
+
.update(normalized.repoFingerprint, "utf8")
|
|
43
|
+
.update("\0", "utf8")
|
|
44
|
+
.update(normalized.bindHost, "utf8")
|
|
45
|
+
.update("\0", "utf8")
|
|
46
|
+
.update(String(normalized.configuredPort), "utf8")
|
|
47
|
+
.update("\0", "utf8")
|
|
48
|
+
.update(normalized.instanceNonce ?? "", "utf8")
|
|
49
|
+
.update("\0", "utf8")
|
|
50
|
+
.update(variant ?? "", "utf8")
|
|
51
|
+
.digest("hex")
|
|
52
|
+
.slice(0, 32);
|
|
53
|
+
return `${BOOT_CAPABILITY_COOKIE}_${digest}`;
|
|
54
|
+
}
|
|
55
|
+
/** Issue per-process unguessable boot capability + confirmation tokens.
|
|
56
|
+
* `variant` (e.g. "inspect") derives a separate cookie name so scoped
|
|
57
|
+
* credentials cannot be replayed against the general mutation surface. */
|
|
58
|
+
export function issueBootCapabilityToken(scope, options = {}) {
|
|
59
|
+
const cookieName = deriveBootCapabilityCookieName(scope, options.variant);
|
|
8
60
|
const value = randomBytes(32).toString("base64url");
|
|
9
61
|
const confirmationToken = randomBytes(32).toString("base64url");
|
|
10
62
|
const setCookieHeader = [
|
|
11
|
-
`${
|
|
63
|
+
`${cookieName}=${value}`,
|
|
12
64
|
"HttpOnly",
|
|
13
65
|
"SameSite=Strict",
|
|
14
66
|
"Path=/",
|
|
15
67
|
].join("; ");
|
|
16
|
-
return { value, setCookieHeader, confirmationToken };
|
|
68
|
+
return { cookieName, value, setCookieHeader, confirmationToken };
|
|
17
69
|
}
|
|
18
|
-
/** Inject confirmation token into HTML so the SPA can send the mutation
|
|
19
|
-
|
|
70
|
+
/** Inject confirmation token into HTML so the SPA can send the mutation
|
|
71
|
+
* header. `varName` separates the general Console token from the scoped
|
|
72
|
+
* inspect token on the read-only /inspect surface. */
|
|
73
|
+
export function injectConfirmationTokenScript(html, confirmationToken, varName = "__CONSOLE_CONFIRMATION_TOKEN__") {
|
|
20
74
|
const source = typeof html === "string" ? html : html.toString("utf8");
|
|
21
|
-
const script = `<script>window
|
|
75
|
+
const script = `<script>window.${varName}=${JSON.stringify(confirmationToken)};</script>`;
|
|
22
76
|
if (source.includes("</head>")) {
|
|
23
77
|
return Buffer.from(source.replace("</head>", `${script}</head>`), "utf8");
|
|
24
78
|
}
|
|
@@ -65,16 +119,16 @@ function safeEqualToken(a, b) {
|
|
|
65
119
|
function loopbackOriginMatches(origin, consoleOrigin) {
|
|
66
120
|
if (origin === consoleOrigin)
|
|
67
121
|
return true;
|
|
68
|
-
const
|
|
122
|
+
const parseLoopbackPort = (value) => {
|
|
69
123
|
const match = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i.exec(value);
|
|
70
|
-
return match ?
|
|
124
|
+
return match ? (match[2] ?? ":80") : null;
|
|
71
125
|
};
|
|
72
|
-
const originKey =
|
|
73
|
-
const consoleKey =
|
|
126
|
+
const originKey = parseLoopbackPort(origin);
|
|
127
|
+
const consoleKey = parseLoopbackPort(consoleOrigin);
|
|
74
128
|
return originKey !== null && consoleKey !== null && originKey === consoleKey;
|
|
75
129
|
}
|
|
76
130
|
export function evaluateMutationGate(req, ctx) {
|
|
77
|
-
const cookieToken = parseCookieHeader(req.headers.cookie,
|
|
131
|
+
const cookieToken = parseCookieHeader(req.headers.cookie, ctx.cookieName);
|
|
78
132
|
if (!cookieToken) {
|
|
79
133
|
return {
|
|
80
134
|
status: 403,
|