patchwarden 1.5.1 → 1.6.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/PatchWarden.cmd +32 -2
- package/README.en.md +152 -25
- package/README.md +120 -23
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentStore.js +42 -34
- package/dist/config.d.ts +15 -0
- package/dist/config.js +66 -13
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +6 -0
- package/dist/control/middleware/auth.js +14 -5
- package/dist/control/middleware/static.js +16 -3
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.js +17 -7
- package/dist/control/routes/evidence.js +1 -1
- package/dist/control/routes/lineage.js +1 -1
- package/dist/control/routes/process.d.ts +7 -0
- package/dist/control/routes/process.js +329 -42
- package/dist/control/routes/sessions.d.ts +9 -1
- package/dist/control/routes/sessions.js +84 -21
- package/dist/control/routes/status.d.ts +46 -1
- package/dist/control/routes/status.js +72 -15
- package/dist/control/routes/taskActions.js +24 -60
- package/dist/control/routes/tasks.d.ts +4 -0
- package/dist/control/routes/tasks.js +61 -62
- package/dist/control/routes/workspace.js +50 -23
- package/dist/control/runtime.d.ts +3 -2
- package/dist/control/runtime.js +63 -30
- package/dist/control/server.js +36 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +42 -29
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +62 -52
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.js +5 -12
- package/dist/goal/goalStatus.d.ts +2 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.js +97 -96
- package/dist/goal/subgoalSync.js +11 -9
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +111 -32
- package/dist/logging.js +5 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +13 -5
- package/dist/runner/changeCapture.js +243 -61
- package/dist/runner/postTaskCleanup.js +48 -19
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +115 -87
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +50 -36
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +256 -48
- package/dist/security/contentRedaction.js +2 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +24 -23
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.js +32 -31
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
- package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
- package/dist/tools/dispatch/directDispatch.js +11 -10
- package/dist/tools/dispatch/goalDispatch.js +5 -4
- package/dist/tools/dispatch/releaseDispatch.js +3 -2
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +7 -11
- package/dist/tools/registry.js +17 -1178
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
- package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +808 -143
- package/docs/architecture.md +15 -55
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +4 -4
- package/docs/desktop-app.md +175 -0
- package/docs/evidence-pack-schema.md +3 -3
- package/docs/open-source-application.md +17 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +39 -29
- package/docs/task-safe-review-workflow.md +5 -5
- package/docs/threat-model.md +19 -1
- package/examples/config.example.json +4 -2
- package/package.json +27 -10
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +91 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +10 -10
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +27 -3
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +26 -12
- package/src/assessments/assessmentStore.ts +52 -40
- package/src/config.ts +70 -13
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +22 -4
- package/src/control/middleware/static.ts +16 -3
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +14 -6
- package/src/control/routes/evidence.ts +1 -1
- package/src/control/routes/lineage.ts +1 -1
- package/src/control/routes/process.ts +341 -40
- package/src/control/routes/sessions.ts +88 -25
- package/src/control/routes/status.ts +97 -17
- package/src/control/routes/taskActions.ts +24 -58
- package/src/control/routes/tasks.ts +66 -58
- package/src/control/routes/workspace.ts +56 -25
- package/src/control/runtime.ts +69 -33
- package/src/control/server.ts +36 -342
- package/src/control/shared.ts +77 -11
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +72 -44
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +73 -59
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +5 -13
- package/src/goal/goalStatus.ts +19 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +12 -12
- package/src/goal/subgoalSync.ts +11 -8
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +113 -30
- package/src/logging.ts +10 -3
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +291 -57
- package/src/runner/postTaskCleanup.ts +41 -17
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +138 -91
- package/src/runner/simpleProcess.ts +66 -31
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +281 -51
- package/src/security/contentRedaction.ts +2 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +25 -24
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +36 -31
- package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
- package/src/tools/dispatch/directDispatch.ts +11 -10
- package/src/tools/dispatch/goalDispatch.ts +5 -4
- package/src/tools/dispatch/releaseDispatch.ts +3 -2
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +17 -1261
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +12 -0
- package/ui/pages/dashboard.html +90 -147
- package/ui/pages/direct-sessions.html +123 -26
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +12 -0
- package/ui/pages/tasks.html +54 -8
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/apply-patch.test.d.ts +0 -1
- package/dist/test/unit/apply-patch.test.js +0 -225
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/create-task.test.d.ts +0 -1
- package/dist/test/unit/create-task.test.js +0 -197
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -328
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -224
- package/dist/test/unit/get-task-status.test.d.ts +0 -1
- package/dist/test/unit/get-task-status.test.js +0 -174
- package/dist/test/unit/get-task-summary.test.d.ts +0 -1
- package/dist/test/unit/get-task-summary.test.js +0 -146
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-report.test.d.ts +0 -1
- package/dist/test/unit/goal-report.test.js +0 -159
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -192
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -133
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
- package/dist/test/unit/spec-kit-import.test.js +0 -341
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/wait-for-task.test.d.ts +0 -1
- package/dist/test/unit/wait-for-task.test.js +0 -144
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/execution-plan-2026-07-09.md +0 -315
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/docs/roadmap-execution-and-acceptance.md +0 -365
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/apply-patch.test.ts +0 -293
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/create-task.test.ts +0 -255
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -467
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -251
- package/src/test/unit/get-task-status.test.ts +0 -203
- package/src/test/unit/get-task-summary.test.ts +0 -173
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-report.test.ts +0 -189
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -245
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -180
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/spec-kit-import.test.ts +0 -429
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/wait-for-task.test.ts +0 -176
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
|
@@ -18,37 +18,53 @@
|
|
|
18
18
|
* 所有文件系统函数都接受可选的 workspaceRoot 参数用于测试;
|
|
19
19
|
* 默认从 getConfig().workspaceRoot 读取。
|
|
20
20
|
*/
|
|
21
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
22
|
-
import { join } from "node:path";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
|
|
22
|
+
import { join, normalize } from "node:path";
|
|
23
23
|
import { execFileSync } from "node:child_process";
|
|
24
24
|
import { randomBytes } from "node:crypto";
|
|
25
25
|
import { getConfig } from "../config.js";
|
|
26
|
-
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
26
|
+
import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
27
27
|
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
28
28
|
import { PatchWardenError } from "../errors.js";
|
|
29
|
+
import { buildGitEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
|
|
30
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
31
|
+
import { withFileLockSync } from "../utils/lockedJsonFile.js";
|
|
29
32
|
// ── 常量 ──────────────────────────────────────────────────────────
|
|
30
33
|
/** worktree 根目录名,放在 workspaceRoot 下。 */
|
|
31
34
|
export const WorktreeDir = "_workspacetrees";
|
|
32
35
|
/** 归档目录名(discard 后保留状态),位于 .patchwarden 下,始终为安全路径。 */
|
|
33
36
|
const WORKTREE_ARCHIVE_DIR = ".patchwarden/worktree-archive";
|
|
37
|
+
const WORKTREE_LOCK_DIR = ".patchwarden/worktree-locks";
|
|
34
38
|
const GIT_TIMEOUT_MS = 30000;
|
|
35
39
|
const GIT_BRANCH_TIMEOUT_MS = 15000;
|
|
40
|
+
const WORKTREE_ID_MAX_LENGTH = 32;
|
|
41
|
+
const WORKTREE_ID_PATTERN = /^wt_[a-z0-9]{1,16}_[a-f0-9]{12}$/;
|
|
36
42
|
// ── 辅助:解析 workspaceRoot ──────────────────────────────────────
|
|
37
43
|
function resolveWorkspaceRoot(workspaceRoot) {
|
|
38
44
|
return workspaceRoot ?? getConfig().workspaceRoot;
|
|
39
45
|
}
|
|
46
|
+
/** Accept only IDs emitted by generateWorktreeId(). */
|
|
47
|
+
export function assertValidWorktreeId(worktreeId) {
|
|
48
|
+
if (worktreeId.length > WORKTREE_ID_MAX_LENGTH ||
|
|
49
|
+
!WORKTREE_ID_PATTERN.test(worktreeId)) {
|
|
50
|
+
throw new PatchWardenError("invalid_worktree_id", "Invalid worktree id. Expected the wt_<base36 timestamp>_<12 hex chars> format.", "Use the worktree_id returned by create_subgoal_task.", true, { worktree_id_length: worktreeId.length });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
40
53
|
// ── 目录路径解析 ──────────────────────────────────────────────────
|
|
41
54
|
/**
|
|
42
55
|
* 返回 `<workspaceRoot>/_workspacetrees` 目录路径。不自动创建目录。
|
|
43
56
|
*/
|
|
44
57
|
export function getWorktreesDir(workspaceRoot) {
|
|
45
|
-
|
|
58
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
59
|
+
return guardPath(join(wsRoot, WorktreeDir), wsRoot, WorktreeDir);
|
|
46
60
|
}
|
|
47
61
|
/**
|
|
48
62
|
* 返回 `<getWorktreesDir()>/<worktreeId>` 路径。
|
|
49
63
|
*/
|
|
50
64
|
export function getWorktreeDir(worktreeId, workspaceRoot) {
|
|
51
|
-
|
|
65
|
+
assertValidWorktreeId(worktreeId);
|
|
66
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
67
|
+
return guardPath(join(getWorktreesDir(wsRoot), worktreeId), wsRoot, WorktreeDir);
|
|
52
68
|
}
|
|
53
69
|
// ── ID 与 branch 生成 ─────────────────────────────────────────────
|
|
54
70
|
/**
|
|
@@ -70,21 +86,76 @@ function sanitizeBranchSegment(value) {
|
|
|
70
86
|
const cleaned = value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
71
87
|
return cleaned === "" ? "x" : cleaned;
|
|
72
88
|
}
|
|
89
|
+
function samePath(left, right) {
|
|
90
|
+
const normalizedLeft = normalize(left);
|
|
91
|
+
const normalizedRight = normalize(right);
|
|
92
|
+
return process.platform === "win32"
|
|
93
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
94
|
+
: normalizedLeft === normalizedRight;
|
|
95
|
+
}
|
|
96
|
+
function withWorktreeRepositoryLock(workspaceRoot, action) {
|
|
97
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
98
|
+
const lockDir = guardPath(join(wsRoot, WORKTREE_LOCK_DIR), wsRoot, WORKTREE_LOCK_DIR);
|
|
99
|
+
mkdirSync(lockDir, { recursive: true });
|
|
100
|
+
return withFileLockSync(join(lockDir, "repository-mutation"), action, {
|
|
101
|
+
waitMs: 0,
|
|
102
|
+
busyError: () => new PatchWardenError("worktree_busy", "Another managed worktree operation is already changing this repository.", "Retry after the active create, merge, or discard operation finishes.", true),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function invalidWorktreeStatus(worktreeId, message) {
|
|
106
|
+
throw new PatchWardenError("invalid_worktree_status", `Invalid worktree status for "${worktreeId}": ${message}`, "Do not edit worktree_status.json manually; recreate the isolated worktree.", true, { worktree_id: worktreeId });
|
|
107
|
+
}
|
|
108
|
+
function validateWorktreeStatus(value, worktreeId, workspaceRoot, expectedPath) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
110
|
+
return invalidWorktreeStatus(worktreeId, "expected a JSON object");
|
|
111
|
+
}
|
|
112
|
+
const candidate = value;
|
|
113
|
+
const statusValues = new Set(["active", "merged", "discarded"]);
|
|
114
|
+
if (typeof candidate.worktree_id !== "string" ||
|
|
115
|
+
typeof candidate.goal_id !== "string" ||
|
|
116
|
+
typeof candidate.subgoal_id !== "string" ||
|
|
117
|
+
typeof candidate.path !== "string" ||
|
|
118
|
+
typeof candidate.created_at !== "string" ||
|
|
119
|
+
typeof candidate.status !== "string" ||
|
|
120
|
+
!statusValues.has(candidate.status) ||
|
|
121
|
+
typeof candidate.branch !== "string") {
|
|
122
|
+
return invalidWorktreeStatus(worktreeId, "missing or invalid required fields");
|
|
123
|
+
}
|
|
124
|
+
if (candidate.worktree_id !== worktreeId) {
|
|
125
|
+
return invalidWorktreeStatus(worktreeId, "worktree_id does not match its directory");
|
|
126
|
+
}
|
|
127
|
+
let guardedStatusPath;
|
|
128
|
+
try {
|
|
129
|
+
guardedStatusPath = guardPath(candidate.path, workspaceRoot, WorktreeDir);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return invalidWorktreeStatus(worktreeId, "path is outside the managed worktree root");
|
|
133
|
+
}
|
|
134
|
+
if (!samePath(guardedStatusPath, expectedPath)) {
|
|
135
|
+
return invalidWorktreeStatus(worktreeId, "path does not match its directory");
|
|
136
|
+
}
|
|
137
|
+
const expectedBranch = `pw-${sanitizeBranchSegment(candidate.goal_id)}-${sanitizeBranchSegment(candidate.subgoal_id)}`;
|
|
138
|
+
if (candidate.branch !== expectedBranch) {
|
|
139
|
+
return invalidWorktreeStatus(worktreeId, "branch does not match goal and subgoal metadata");
|
|
140
|
+
}
|
|
141
|
+
if ((candidate.merged_at !== undefined && typeof candidate.merged_at !== "string") ||
|
|
142
|
+
(candidate.discarded_at !== undefined && typeof candidate.discarded_at !== "string")) {
|
|
143
|
+
return invalidWorktreeStatus(worktreeId, "invalid lifecycle timestamp");
|
|
144
|
+
}
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
73
147
|
// ── 原子写 ────────────────────────────────────────────────────────
|
|
74
|
-
/**
|
|
75
|
-
* 原子写入 status 文件:先写到 `.tmp` 文件,再 renameSync。
|
|
76
|
-
* 参考 goalStore.ts writeGoalStatus 的模式。
|
|
77
|
-
*/
|
|
78
148
|
function writeStatusAtomic(statusFilePath, status) {
|
|
79
|
-
|
|
80
|
-
writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
|
|
81
|
-
renameSync(tmpPath, statusFilePath);
|
|
149
|
+
atomicWriteJsonFileSync(statusFilePath, status);
|
|
82
150
|
}
|
|
83
151
|
function runGit(args, cwd, timeoutMs) {
|
|
84
|
-
|
|
152
|
+
const env = buildGitEnvironment(cwd);
|
|
153
|
+
execFileSync(resolveTrustedExecutable("git", cwd, { pathValue: env.PATH }), args, {
|
|
85
154
|
cwd,
|
|
86
155
|
timeout: timeoutMs,
|
|
87
156
|
stdio: ["pipe", "pipe", "pipe"],
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
env,
|
|
88
159
|
});
|
|
89
160
|
}
|
|
90
161
|
function gitErrorMessage(err) {
|
|
@@ -114,62 +185,64 @@ function gitErrorMessage(err) {
|
|
|
114
185
|
* @returns { worktreeId, worktreePath, branch }
|
|
115
186
|
*/
|
|
116
187
|
export function createWorktree(goalId, subgoalId, workspaceRoot) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
catch (err) {
|
|
146
|
-
// 清理半成品
|
|
147
|
-
if (worktreeCreated) {
|
|
148
|
-
try {
|
|
149
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
150
|
-
}
|
|
151
|
-
catch { /* ignore */ }
|
|
152
|
-
// rmSync 可能无法清除 git 的 worktree 元数据,再尝试 git worktree remove
|
|
153
|
-
try {
|
|
154
|
-
runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_BRANCH_TIMEOUT_MS);
|
|
155
|
-
}
|
|
156
|
-
catch { /* ignore */ }
|
|
188
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
189
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
190
|
+
const worktreeId = generateWorktreeId();
|
|
191
|
+
const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
|
|
192
|
+
const branch = `pw-${sanitizeBranchSegment(goalId)}-${sanitizeBranchSegment(subgoalId)}`;
|
|
193
|
+
// 安全:校验 worktreePath 在 workspaceRoot 内且非敏感路径
|
|
194
|
+
guardWorkspacePath(worktreePath, wsRoot);
|
|
195
|
+
guardSensitivePath(worktreePath);
|
|
196
|
+
let worktreeCreated = false;
|
|
197
|
+
let branchCreated = false;
|
|
198
|
+
try {
|
|
199
|
+
runGit(["worktree", "add", "-b", branch, worktreePath], wsRoot, GIT_TIMEOUT_MS);
|
|
200
|
+
worktreeCreated = true;
|
|
201
|
+
branchCreated = true;
|
|
202
|
+
// 原子写入 worktree_status.json(worktreePath 是新目录,无旧 status,直接 tmp + rename)
|
|
203
|
+
const statusFilePath = join(worktreePath, "worktree_status.json");
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
const status = {
|
|
206
|
+
worktree_id: worktreeId,
|
|
207
|
+
goal_id: goalId,
|
|
208
|
+
subgoal_id: subgoalId,
|
|
209
|
+
path: worktreePath,
|
|
210
|
+
created_at: now,
|
|
211
|
+
status: "active",
|
|
212
|
+
branch,
|
|
213
|
+
};
|
|
214
|
+
writeStatusAtomic(statusFilePath, status);
|
|
215
|
+
return { worktreeId, worktreePath, branch };
|
|
157
216
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
217
|
+
catch (err) {
|
|
218
|
+
// 清理半成品
|
|
219
|
+
if (worktreeCreated) {
|
|
220
|
+
try {
|
|
221
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
catch { /* ignore */ }
|
|
224
|
+
// rmSync 可能无法清除 git 的 worktree 元数据,再尝试 git worktree remove
|
|
225
|
+
try {
|
|
226
|
+
runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_BRANCH_TIMEOUT_MS);
|
|
227
|
+
}
|
|
228
|
+
catch { /* ignore */ }
|
|
161
229
|
}
|
|
162
|
-
|
|
230
|
+
if (branchCreated) {
|
|
231
|
+
try {
|
|
232
|
+
runGit(["branch", "-D", branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore — branch 可能未创建或已随 worktree remove 清理 */ }
|
|
235
|
+
}
|
|
236
|
+
if (err instanceof PatchWardenError)
|
|
237
|
+
throw err;
|
|
238
|
+
throw new PatchWardenError("worktree_create_failed", `Failed to create worktree for goal "${goalId}" / subgoal "${subgoalId}": ${gitErrorMessage(err)}`, "Ensure workspaceRoot is a git repository with at least one commit, and the worktree path is writable.", true, {
|
|
239
|
+
goal_id: goalId,
|
|
240
|
+
subgoal_id: subgoalId,
|
|
241
|
+
branch,
|
|
242
|
+
worktree_path: worktreePath,
|
|
243
|
+
});
|
|
163
244
|
}
|
|
164
|
-
|
|
165
|
-
throw err;
|
|
166
|
-
throw new PatchWardenError("worktree_create_failed", `Failed to create worktree for goal "${goalId}" / subgoal "${subgoalId}": ${gitErrorMessage(err)}`, "Ensure workspaceRoot is a git repository with at least one commit, and the worktree path is writable.", true, {
|
|
167
|
-
goal_id: goalId,
|
|
168
|
-
subgoal_id: subgoalId,
|
|
169
|
-
branch,
|
|
170
|
-
worktree_path: worktreePath,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
245
|
+
});
|
|
173
246
|
}
|
|
174
247
|
/**
|
|
175
248
|
* 读取 worktree_status.json。不存在返回 null。
|
|
@@ -183,13 +256,14 @@ export function readWorktreeStatus(worktreeId, workspaceRoot) {
|
|
|
183
256
|
const statusFilePath = join(worktreePath, "worktree_status.json");
|
|
184
257
|
if (!existsSync(statusFilePath))
|
|
185
258
|
return null;
|
|
259
|
+
let parsed;
|
|
186
260
|
try {
|
|
187
|
-
|
|
188
|
-
return JSON.parse(raw);
|
|
261
|
+
parsed = JSON.parse(readFileSync(statusFilePath, "utf-8"));
|
|
189
262
|
}
|
|
190
263
|
catch {
|
|
191
|
-
return
|
|
264
|
+
return invalidWorktreeStatus(worktreeId, "file is not valid JSON");
|
|
192
265
|
}
|
|
266
|
+
return validateWorktreeStatus(parsed, worktreeId, wsRoot, worktreePath);
|
|
193
267
|
}
|
|
194
268
|
/**
|
|
195
269
|
* 将 worktree 的 branch 合并回主工作区(workspaceRoot)。
|
|
@@ -203,32 +277,34 @@ export function readWorktreeStatus(worktreeId, workspaceRoot) {
|
|
|
203
277
|
* (保留供人工排查冲突)。
|
|
204
278
|
*/
|
|
205
279
|
export function mergeWorktree(worktreeId, workspaceRoot) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
280
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
281
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
282
|
+
const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
|
|
283
|
+
guardWorkspacePath(worktreePath, wsRoot);
|
|
284
|
+
guardSensitivePath(worktreePath);
|
|
285
|
+
const status = readWorktreeStatus(worktreeId, workspaceRoot);
|
|
286
|
+
if (!status) {
|
|
287
|
+
throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before merging.", true, { worktree_id: worktreeId });
|
|
288
|
+
}
|
|
289
|
+
if (status.status !== "active") {
|
|
290
|
+
throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be merged.", true, { worktree_id: worktreeId, current_status: status.status });
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
runGit(["merge", status.branch], wsRoot, GIT_TIMEOUT_MS);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
// 合并失败:不删 worktree,保留供人工排查
|
|
297
|
+
throw new PatchWardenError("worktree_merge_failed", `Failed to merge worktree branch "${status.branch}" into workspace: ${gitErrorMessage(err)}`, "Resolve merge conflicts manually in the main workspace, then retry or discard the worktree.", true, { worktree_id: worktreeId, branch: status.branch });
|
|
298
|
+
}
|
|
299
|
+
const updatedStatus = {
|
|
300
|
+
...status,
|
|
301
|
+
status: "merged",
|
|
302
|
+
merged_at: new Date().toISOString(),
|
|
303
|
+
};
|
|
304
|
+
const statusFilePath = join(worktreePath, "worktree_status.json");
|
|
305
|
+
writeStatusAtomic(statusFilePath, updatedStatus);
|
|
306
|
+
return { status: "merged" };
|
|
307
|
+
});
|
|
232
308
|
}
|
|
233
309
|
/**
|
|
234
310
|
* 丢弃 worktree:移除 worktree 目录与临时 branch,归档最终状态。
|
|
@@ -245,48 +321,45 @@ export function mergeWorktree(worktreeId, workspaceRoot) {
|
|
|
245
321
|
* 移除失败抛 PatchWardenError("worktree_discard_failed")。
|
|
246
322
|
*/
|
|
247
323
|
export function discardWorktree(worktreeId, workspaceRoot) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
// git remove 失败时再尝试直接删目录(best effort),然后抛错
|
|
324
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
325
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
326
|
+
const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
|
|
327
|
+
guardWorkspacePath(worktreePath, wsRoot);
|
|
328
|
+
guardSensitivePath(worktreePath);
|
|
329
|
+
// 在 remove 之前读取 status(status 文件位于 worktree 目录内)
|
|
330
|
+
const status = readWorktreeStatus(worktreeId, workspaceRoot);
|
|
331
|
+
if (!status) {
|
|
332
|
+
throw new PatchWardenError("worktree_not_found", `Worktree "${worktreeId}" not found or has no worktree_status.json`, "Ensure the worktree id was created via createWorktree before discarding.", true, { worktree_id: worktreeId });
|
|
333
|
+
}
|
|
334
|
+
if (status.status !== "active") {
|
|
335
|
+
throw new PatchWardenError("invalid_worktree_state", `Worktree "${worktreeId}" is not active (current status: "${status.status}")`, "Only active worktrees can be discarded.", true, { worktree_id: worktreeId, current_status: status.status });
|
|
336
|
+
}
|
|
265
337
|
try {
|
|
266
|
-
|
|
338
|
+
runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
throw new PatchWardenError("worktree_discard_failed", `Failed to remove worktree "${worktreeId}": ${gitErrorMessage(err)}`, "Remove the worktree directory manually and run `git worktree prune`.", true, { worktree_id: worktreeId, worktree_path: worktreePath });
|
|
342
|
+
}
|
|
343
|
+
// 删除临时 branch(best effort — 可能已随 worktree remove 清理)
|
|
344
|
+
try {
|
|
345
|
+
runGit(["branch", "-D", status.branch], wsRoot, GIT_BRANCH_TIMEOUT_MS);
|
|
267
346
|
}
|
|
268
347
|
catch { /* ignore */ }
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
mkdirSync(archiveDir, { recursive: true });
|
|
287
|
-
}
|
|
288
|
-
catch { /* ignore */ }
|
|
289
|
-
const archiveFilePath = join(archiveDir, `${worktreeId}.json`);
|
|
290
|
-
writeStatusAtomic(archiveFilePath, archivedStatus);
|
|
291
|
-
return { status: "discarded" };
|
|
348
|
+
// 归档最终状态到 .patchwarden/worktree-archive/(worktree 目录可能已消失)
|
|
349
|
+
const archivedStatus = {
|
|
350
|
+
...status,
|
|
351
|
+
status: "discarded",
|
|
352
|
+
discarded_at: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
const archiveDir = join(wsRoot, WORKTREE_ARCHIVE_DIR);
|
|
355
|
+
guardWorkspacePath(archiveDir, wsRoot);
|
|
356
|
+
// The archive path is workspace-confined and contains only managed status metadata.
|
|
357
|
+
try {
|
|
358
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
359
|
+
}
|
|
360
|
+
catch { /* ignore */ }
|
|
361
|
+
const archiveFilePath = join(archiveDir, `${worktreeId}.json`);
|
|
362
|
+
writeStatusAtomic(archiveFilePath, archivedStatus);
|
|
363
|
+
return { status: "discarded" };
|
|
364
|
+
});
|
|
292
365
|
}
|
package/dist/httpServer.js
CHANGED
|
@@ -15,16 +15,21 @@
|
|
|
15
15
|
* or: npm run start:http
|
|
16
16
|
*/
|
|
17
17
|
import { createServer } from "node:http";
|
|
18
|
-
import { existsSync, readFileSync
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { join, resolve } from "node:path";
|
|
20
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
21
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
22
22
|
import { loadConfig, getTasksDir } from "./config.js";
|
|
23
23
|
import { registerTools } from "./tools/registry.js";
|
|
24
|
-
import { healthCheck } from "./tools/healthCheck.js";
|
|
24
|
+
import { healthCheck } from "./tools/diagnostics/healthCheck.js";
|
|
25
25
|
import { getToolCatalogSnapshot } from "./tools/registry.js";
|
|
26
26
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
27
27
|
import { logger } from "./logging.js";
|
|
28
|
+
import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
|
|
29
|
+
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
30
|
+
import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
|
|
31
|
+
import { mutateTaskStatus } from "./runner/taskStatusStore.js";
|
|
32
|
+
import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
|
|
28
33
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
29
34
|
const config = loadConfig();
|
|
30
35
|
const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
|
|
@@ -38,6 +43,16 @@ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed
|
|
|
38
43
|
const httpCfg = config.http || {};
|
|
39
44
|
const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
|
|
40
45
|
const ownerToken = process.env[ownerTokenEnv] || "";
|
|
46
|
+
const MAX_ADMIN_BODY_BYTES = 64 * 1024;
|
|
47
|
+
const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
|
|
48
|
+
const REVIEWABLE_TASK_STATUSES = new Set([
|
|
49
|
+
"done_by_agent",
|
|
50
|
+
"done",
|
|
51
|
+
"accepted",
|
|
52
|
+
"rejected",
|
|
53
|
+
"needs_fix",
|
|
54
|
+
"blocked",
|
|
55
|
+
]);
|
|
41
56
|
if (ownerToken) {
|
|
42
57
|
logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
|
|
43
58
|
}
|
|
@@ -47,13 +62,13 @@ else {
|
|
|
47
62
|
function checkOwnerToken(req) {
|
|
48
63
|
if (!ownerToken)
|
|
49
64
|
return true; // no token configured — allow all
|
|
50
|
-
const authHeader = req.headers["authorization"]
|
|
51
|
-
const customHeader = req.headers["x-patchwarden-token"]
|
|
65
|
+
const authHeader = firstHeaderValue(req.headers["authorization"]);
|
|
66
|
+
const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
|
|
52
67
|
if (authHeader.startsWith("Bearer ")) {
|
|
53
|
-
return authHeader.slice(7)
|
|
68
|
+
return timingSafeStringEqual(authHeader.slice(7), ownerToken);
|
|
54
69
|
}
|
|
55
|
-
if (
|
|
56
|
-
return customHeader
|
|
70
|
+
if (customHeader.length > 0) {
|
|
71
|
+
return timingSafeStringEqual(customHeader, ownerToken);
|
|
57
72
|
}
|
|
58
73
|
return false;
|
|
59
74
|
}
|
|
@@ -69,12 +84,23 @@ function handleAcceptance(taskId, status, body) {
|
|
|
69
84
|
throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
|
|
70
85
|
}
|
|
71
86
|
let notes = "";
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
if (body.trim()) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(body);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw httpError(400, "Acceptance body must be valid JSON.");
|
|
94
|
+
}
|
|
95
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
96
|
+
throw httpError(400, "Acceptance body must be a JSON object.");
|
|
97
|
+
}
|
|
98
|
+
const record = parsed;
|
|
99
|
+
const rawNotes = record.notes ?? record.reason ?? "";
|
|
100
|
+
if (typeof rawNotes !== "string") {
|
|
101
|
+
throw httpError(400, "Acceptance notes or reason must be a string.");
|
|
102
|
+
}
|
|
103
|
+
notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
|
|
78
104
|
}
|
|
79
105
|
const acceptance = {
|
|
80
106
|
status,
|
|
@@ -82,22 +108,67 @@ function handleAcceptance(taskId, status, body) {
|
|
|
82
108
|
reviewer: "human",
|
|
83
109
|
notes,
|
|
84
110
|
};
|
|
85
|
-
|
|
86
|
-
//
|
|
111
|
+
// Commit the review artifact and status annotation while holding the same
|
|
112
|
+
// status lock used by the runner, cancel, and audit paths.
|
|
87
113
|
const statusFile = join(taskDir, "status.json");
|
|
88
|
-
if (existsSync(statusFile)) {
|
|
89
|
-
|
|
90
|
-
const current = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
91
|
-
current.acceptance_status = status;
|
|
92
|
-
current.acceptance_reviewed_at = acceptance.reviewed_at;
|
|
93
|
-
writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// status.json update is best-effort
|
|
97
|
-
}
|
|
114
|
+
if (!existsSync(statusFile)) {
|
|
115
|
+
throw httpError(409, `Task "${taskId}" has no status.json to review.`);
|
|
98
116
|
}
|
|
117
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
118
|
+
if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
|
|
119
|
+
throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
|
|
120
|
+
}
|
|
121
|
+
atomicWriteJsonFileSync(filePath, acceptance);
|
|
122
|
+
const next = {
|
|
123
|
+
...current,
|
|
124
|
+
acceptance_status: status,
|
|
125
|
+
acceptance_reviewed_at: acceptance.reviewed_at,
|
|
126
|
+
updated_at: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
return { next, result: next };
|
|
129
|
+
});
|
|
99
130
|
return acceptance;
|
|
100
131
|
}
|
|
132
|
+
function httpError(statusCode, message) {
|
|
133
|
+
return Object.assign(new Error(message), { statusCode });
|
|
134
|
+
}
|
|
135
|
+
function readBoundedRequestBody(req, maxBytes) {
|
|
136
|
+
const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
|
|
137
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
138
|
+
req.resume();
|
|
139
|
+
return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
140
|
+
}
|
|
141
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let total = 0;
|
|
144
|
+
let settled = false;
|
|
145
|
+
req.on("data", (chunk) => {
|
|
146
|
+
if (settled)
|
|
147
|
+
return;
|
|
148
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
149
|
+
total += buffer.length;
|
|
150
|
+
if (total > maxBytes) {
|
|
151
|
+
settled = true;
|
|
152
|
+
req.resume();
|
|
153
|
+
rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
chunks.push(buffer);
|
|
157
|
+
});
|
|
158
|
+
req.on("end", () => {
|
|
159
|
+
if (!settled) {
|
|
160
|
+
settled = true;
|
|
161
|
+
resolveBody(Buffer.concat(chunks).toString("utf-8"));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
req.on("error", (error) => {
|
|
165
|
+
if (!settled) {
|
|
166
|
+
settled = true;
|
|
167
|
+
rejectBody(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
101
172
|
function readAcceptance(taskId) {
|
|
102
173
|
const filePath = getAcceptancePath(taskId);
|
|
103
174
|
if (!existsSync(filePath)) {
|
|
@@ -164,6 +235,15 @@ function parseAdminUrl(url) {
|
|
|
164
235
|
}
|
|
165
236
|
// ── HTTP server ───────────────────────────────────────────────────
|
|
166
237
|
const httpServer = createServer(async (req, res) => {
|
|
238
|
+
if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
|
|
239
|
+
res.writeHead(421, {
|
|
240
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
241
|
+
"Cache-Control": "no-store",
|
|
242
|
+
"X-Content-Type-Options": "nosniff",
|
|
243
|
+
});
|
|
244
|
+
res.end(JSON.stringify({ error: "Untrusted Host header" }));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
167
247
|
// Health check endpoints
|
|
168
248
|
if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
|
|
169
249
|
const health = healthCheck(getToolCatalogSnapshot());
|
|
@@ -188,11 +268,7 @@ const httpServer = createServer(async (req, res) => {
|
|
|
188
268
|
return;
|
|
189
269
|
}
|
|
190
270
|
if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
|
|
191
|
-
const
|
|
192
|
-
for await (const chunk of req) {
|
|
193
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
194
|
-
}
|
|
195
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
271
|
+
const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
|
|
196
272
|
const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
|
|
197
273
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
198
274
|
res.end(JSON.stringify(acceptance, null, 2));
|
|
@@ -202,9 +278,12 @@ const httpServer = createServer(async (req, res) => {
|
|
|
202
278
|
res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
|
|
203
279
|
}
|
|
204
280
|
catch (err) {
|
|
205
|
-
const statusCode = err
|
|
281
|
+
const statusCode = err && typeof err === "object" && "statusCode" in err
|
|
282
|
+
? Number(err.statusCode) || 500
|
|
283
|
+
: 500;
|
|
284
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
206
285
|
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
207
|
-
res.end(JSON.stringify({ error:
|
|
286
|
+
res.end(JSON.stringify({ error: message }));
|
|
208
287
|
}
|
|
209
288
|
return;
|
|
210
289
|
}
|