patchwarden 1.5.1 → 1.6.2
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 +158 -26
- package/README.md +124 -24
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
- package/dist/assessments/assessmentDiagnostics.js +132 -0
- package/dist/assessments/assessmentStore.d.ts +39 -0
- package/dist/assessments/assessmentStore.js +207 -45
- package/dist/assessments/securitySnapshot.d.ts +50 -0
- package/dist/assessments/securitySnapshot.js +158 -0
- 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 +60 -1
- package/dist/control/routes/status.js +94 -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 +65 -40
- package/dist/control/server.js +37 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
- package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
- 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 +128 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +65 -57
- 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 +2 -1
- package/dist/runner/runTask.js +162 -93
- 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} +3 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +35 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +320 -49
- 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 +27 -26
- 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} +2 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
- 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 +86 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
- 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} +11 -6
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
- 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} +5 -4
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
- package/dist/tools/tasks/reconcileTasks.js +496 -0
- 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} +63 -41
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/tasks/taskStates.d.ts +6 -0
- package/dist/tools/tasks/taskStates.js +52 -0
- package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
- 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/workspace/listAgents.d.ts +23 -0
- package/dist/tools/workspace/listAgents.js +56 -0
- 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 +67 -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 +16 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +35 -39
- 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 +102 -10
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +15 -12
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +58 -4
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +38 -1
- 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 +191 -33
- package/scripts/e2e/demo-runtime.mjs +153 -0
- 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/assessmentDiagnostics.ts +172 -0
- package/src/assessments/assessmentStore.ts +288 -51
- package/src/assessments/securitySnapshot.ts +231 -0
- 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 +134 -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 +71 -43
- package/src/control/server.ts +37 -342
- package/src/control/shared.ts +77 -11
- package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
- 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 +213 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +76 -64
- 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 +197 -97
- package/src/runner/simpleProcess.ts +66 -31
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
- package/src/runner/taskStatusStore.ts +69 -0
- package/src/runner/watch.ts +348 -52
- 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 +27 -26
- 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} +136 -39
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
- 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 +110 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
- 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} +13 -6
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
- 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/tasks/taskStates.ts +54 -0
- package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
- 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/workspace/listAgents.ts +75 -0
- 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 +74 -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 +94 -147
- package/ui/pages/direct-sessions.html +123 -26
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +134 -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 +254 -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/listAgents.d.ts +0 -12
- package/dist/tools/listAgents.js +0 -42
- package/dist/tools/reconcileTasks.js +0 -352
- 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/listAgents.ts +0 -54
- 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/{waitForTask.d.ts → tasks/waitForTask.d.ts} +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/dist/runner/watch.js
CHANGED
|
@@ -16,15 +16,23 @@
|
|
|
16
16
|
* Run: node dist/runner/watch.js
|
|
17
17
|
* or: npm run watch
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
20
20
|
import { resolve, join, dirname } from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
21
22
|
import { loadConfig, getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
|
|
22
23
|
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
23
24
|
import { guardAgentCommand, guardTestCommand } from "../security/commandGuard.js";
|
|
24
25
|
import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
|
|
25
26
|
import { captureRepoSnapshot } from "./changeCapture.js";
|
|
26
|
-
import {
|
|
27
|
+
import { mutateTaskStatus } from "./taskStatusStore.js";
|
|
27
28
|
import { logger } from "../logging.js";
|
|
29
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
30
|
+
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
31
|
+
import { reconcileTasks } from "../tools/tasks/reconcileTasks.js";
|
|
32
|
+
import { buildAssessmentValidationFailureDiagnostic, recordAssessmentValidationFailure, } from "../assessments/assessmentDiagnostics.js";
|
|
33
|
+
// Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
|
|
34
|
+
// This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
|
|
35
|
+
// avoids eagerly pulling in the full tools/dispatch graph at module load.
|
|
28
36
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
29
37
|
loadConfig();
|
|
30
38
|
const config = getConfig();
|
|
@@ -32,34 +40,168 @@ const tasksDir = getTasksDir(config);
|
|
|
32
40
|
const wsRoot = resolveWorkspaceRoot(config);
|
|
33
41
|
const POLL_INTERVAL_MS = 4000;
|
|
34
42
|
const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
|
|
43
|
+
const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
|
|
35
44
|
const WATCHER_STARTED_AT = new Date().toISOString();
|
|
36
45
|
const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
|
|
37
46
|
const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
|
|
38
47
|
? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
|
|
39
48
|
: null;
|
|
40
|
-
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
41
|
-
logger.info(`[watcher] Workspace: ${wsRoot}`);
|
|
42
|
-
logger.info(`[watcher] Tasks: ${tasksDir}`);
|
|
43
|
-
logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
44
|
-
logger.info(`[watcher] Press Ctrl+C to stop`);
|
|
45
49
|
// Track executed tasks to prevent re-execution
|
|
46
50
|
const executedTasks = new Set();
|
|
51
|
+
let consecutiveTickFailures = 0;
|
|
52
|
+
let activeTaskId = null;
|
|
53
|
+
let activeTaskPhase = null;
|
|
54
|
+
let tickRunning = false;
|
|
55
|
+
let tickStartedAt = null;
|
|
56
|
+
let lastTickCompletedAt = null;
|
|
57
|
+
// ── Single-instance lock ─────────────────────────────────────────
|
|
58
|
+
// watch.ts is a long-running poller; without a lock, two watchers started
|
|
59
|
+
// against the same workspace would race on the same pending tasks. The lock
|
|
60
|
+
// is best-effort: it detects a live PID via process.kill(pid, 0) and refuses
|
|
61
|
+
// to start when one is found. A dead PID (crashed watcher) is taken over.
|
|
62
|
+
export class WatcherAlreadyRunningError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "WatcherAlreadyRunningError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function errorCode(error) {
|
|
69
|
+
return error && typeof error === "object" && "code" in error
|
|
70
|
+
? String(error.code || "")
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
function readWatcherLock(lockFilePath) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
76
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
77
|
+
? parsed
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function processIsAlive(pid) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// EPERM means the process exists but the current user cannot signal it.
|
|
91
|
+
return errorCode(error) === "EPERM";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function runningWatcherError(lockData) {
|
|
95
|
+
return new WatcherAlreadyRunningError(`[watcher] Already running (pid=${lockData?.pid ?? "unknown"}, instance=${lockData?.instance_id ?? "unknown"}). Exiting.`);
|
|
96
|
+
}
|
|
97
|
+
function createWatcherLockExclusive(lockFilePath, content) {
|
|
98
|
+
const descriptor = openSync(lockFilePath, "wx", 0o600);
|
|
99
|
+
let writeError = null;
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(descriptor, content, "utf-8");
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
writeError = error;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
closeSync(descriptor);
|
|
108
|
+
}
|
|
109
|
+
if (writeError) {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(lockFilePath);
|
|
112
|
+
}
|
|
113
|
+
catch { /* best effort after a partial write */ }
|
|
114
|
+
throw writeError;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
|
|
119
|
+
* recorded PID is still alive (per `process.kill(pid, 0)`), throws
|
|
120
|
+
* {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
|
|
121
|
+
*
|
|
122
|
+
* Exported for unit testing; `lockFilePath` defaults to the module-level
|
|
123
|
+
* {@link WATCHER_LOCK_FILE} but tests pass a temp path.
|
|
124
|
+
*/
|
|
125
|
+
export function acquireWatcherLock(lockFilePath = WATCHER_LOCK_FILE) {
|
|
126
|
+
const lockContent = JSON.stringify({
|
|
127
|
+
pid: process.pid,
|
|
128
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
129
|
+
started_at: WATCHER_STARTED_AT,
|
|
130
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
131
|
+
}, null, 2);
|
|
132
|
+
try {
|
|
133
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (errorCode(error) !== "EEXIST")
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const existing = readWatcherLock(lockFilePath);
|
|
141
|
+
if (typeof existing?.pid === "number" && processIsAlive(existing.pid)) {
|
|
142
|
+
throw runningWatcherError(existing);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(lockFilePath);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (errorCode(error) !== "ENOENT")
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (errorCode(error) === "EEXIST") {
|
|
156
|
+
throw runningWatcherError(readWatcherLock(lockFilePath));
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function releaseWatcherLock(lockFilePath = WATCHER_LOCK_FILE, instanceId = WATCHER_INSTANCE_ID, pid = process.pid) {
|
|
162
|
+
const existing = readWatcherLock(lockFilePath);
|
|
163
|
+
if (existing?.pid !== pid || existing.instance_id !== instanceId)
|
|
164
|
+
return false;
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(lockFilePath);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (errorCode(error) === "ENOENT")
|
|
171
|
+
return false;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function createNonOverlappingRunner(operation, onSkipped = () => undefined) {
|
|
176
|
+
let running = false;
|
|
177
|
+
return async () => {
|
|
178
|
+
if (running) {
|
|
179
|
+
onSkipped();
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
running = true;
|
|
183
|
+
try {
|
|
184
|
+
await operation();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
running = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
47
192
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
48
193
|
async function tick() {
|
|
194
|
+
tickRunning = true;
|
|
195
|
+
tickStartedAt = new Date().toISOString();
|
|
49
196
|
try {
|
|
50
|
-
|
|
51
|
-
writeFileSync(heartbeatTemporary, JSON.stringify({
|
|
52
|
-
status: "running",
|
|
53
|
-
pid: process.pid,
|
|
54
|
-
started_at: WATCHER_STARTED_AT,
|
|
55
|
-
last_heartbeat_at: new Date().toISOString(),
|
|
56
|
-
instance_id: WATCHER_INSTANCE_ID,
|
|
57
|
-
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
58
|
-
}, null, 2), "utf-8");
|
|
59
|
-
renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
|
|
197
|
+
writeWatcherHeartbeat("running");
|
|
60
198
|
// Ensure tasks directory exists
|
|
61
|
-
if (!existsSync(tasksDir))
|
|
199
|
+
if (!existsSync(tasksDir)) {
|
|
200
|
+
consecutiveTickFailures = 0;
|
|
201
|
+
writeWatcherHeartbeat("running");
|
|
62
202
|
return;
|
|
203
|
+
}
|
|
204
|
+
reconcileInterruptedTasks();
|
|
63
205
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
64
206
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
65
207
|
for (const entry of taskDirs) {
|
|
@@ -73,21 +215,34 @@ async function tick() {
|
|
|
73
215
|
continue;
|
|
74
216
|
let statusData;
|
|
75
217
|
try {
|
|
76
|
-
|
|
218
|
+
const parsed = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
219
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
220
|
+
throw new Error("status.json must contain an object");
|
|
221
|
+
}
|
|
222
|
+
statusData = parsed;
|
|
77
223
|
}
|
|
78
|
-
catch {
|
|
79
|
-
|
|
224
|
+
catch (error) {
|
|
225
|
+
logger.warn("[watcher] Skipping unreadable task status", {
|
|
226
|
+
task_id: taskId,
|
|
227
|
+
error: error instanceof Error ? error.message : String(error),
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
80
230
|
}
|
|
81
231
|
if (statusData.status !== "pending")
|
|
82
232
|
continue;
|
|
83
233
|
// ── Pre-flight safety checks ──
|
|
84
234
|
try {
|
|
85
235
|
// Check repo_path
|
|
86
|
-
const
|
|
236
|
+
const repoPath = typeof statusData.resolved_repo_path === "string"
|
|
237
|
+
? statusData.resolved_repo_path
|
|
238
|
+
: typeof statusData.repo_path === "string"
|
|
239
|
+
? statusData.repo_path
|
|
240
|
+
: wsRoot;
|
|
241
|
+
const resolvedRepoPath = guardWorkspacePath(repoPath, wsRoot);
|
|
87
242
|
// Check agent
|
|
88
|
-
guardAgentCommand(statusData.agent, config);
|
|
243
|
+
guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
|
|
89
244
|
// Check test_command
|
|
90
|
-
if (statusData.test_command) {
|
|
245
|
+
if (typeof statusData.test_command === "string" && statusData.test_command) {
|
|
91
246
|
guardTestCommand(statusData.test_command, config, resolvedRepoPath);
|
|
92
247
|
}
|
|
93
248
|
if (Array.isArray(statusData.verify_commands)) {
|
|
@@ -96,25 +251,33 @@ async function tick() {
|
|
|
96
251
|
}
|
|
97
252
|
}
|
|
98
253
|
// Assessment freshness revalidation
|
|
99
|
-
if (statusData.assessment_id) {
|
|
254
|
+
if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
|
|
100
255
|
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
101
|
-
const validation = validateAssessmentFreshness(
|
|
256
|
+
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
102
257
|
if (!validation.valid) {
|
|
103
|
-
|
|
258
|
+
recordAssessmentValidationFailure(validation);
|
|
259
|
+
const diagnostic = buildAssessmentValidationFailureDiagnostic(validation);
|
|
260
|
+
throw new Error(`assessment validation failed: ${JSON.stringify(diagnostic)}`);
|
|
104
261
|
}
|
|
105
262
|
}
|
|
106
263
|
}
|
|
107
264
|
catch (err) {
|
|
108
|
-
const errMsg = `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
|
|
265
|
+
const errMsg = redactSensitiveContent(`[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`).content;
|
|
109
266
|
logger.error(errMsg);
|
|
110
267
|
// Write error and mark as failed so it doesn't get re-picked
|
|
111
268
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
269
|
+
atomicWriteFileSync(join(taskDir, "error.log"), errMsg);
|
|
270
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
271
|
+
if (current.status !== "pending")
|
|
272
|
+
return { result: undefined };
|
|
273
|
+
const next = {
|
|
274
|
+
...current,
|
|
275
|
+
status: "failed",
|
|
276
|
+
error: errMsg,
|
|
277
|
+
updated_at: new Date().toISOString(),
|
|
278
|
+
};
|
|
279
|
+
return { next, result: undefined };
|
|
280
|
+
});
|
|
118
281
|
}
|
|
119
282
|
catch (err) {
|
|
120
283
|
logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -125,30 +288,138 @@ async function tick() {
|
|
|
125
288
|
// ── Execute ──
|
|
126
289
|
logger.info(`[watcher] Executing: ${taskId}`);
|
|
127
290
|
executedTasks.add(taskId);
|
|
291
|
+
activeTaskId = taskId;
|
|
292
|
+
activeTaskPhase = "executing_agent";
|
|
128
293
|
try {
|
|
294
|
+
const { runTask } = await import("./runTask.js");
|
|
129
295
|
const result = await runTask(taskId);
|
|
296
|
+
activeTaskPhase = result.status;
|
|
130
297
|
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
131
298
|
}
|
|
132
299
|
catch (err) {
|
|
300
|
+
activeTaskPhase = "error";
|
|
133
301
|
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
302
|
}
|
|
303
|
+
finally {
|
|
304
|
+
activeTaskId = null;
|
|
305
|
+
activeTaskPhase = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
consecutiveTickFailures = 0;
|
|
309
|
+
writeWatcherHeartbeat("running");
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
consecutiveTickFailures += 1;
|
|
313
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
314
|
+
logger.error(`[watcher] Tick error (${consecutiveTickFailures} consecutive): ${message}`);
|
|
315
|
+
try {
|
|
316
|
+
writeWatcherHeartbeat(consecutiveTickFailures >= 3 ? "degraded" : "running", message);
|
|
317
|
+
}
|
|
318
|
+
catch (heartbeatError) {
|
|
319
|
+
logger.error("[watcher] Failed to persist degraded heartbeat", {
|
|
320
|
+
error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
tickRunning = false;
|
|
326
|
+
lastTickCompletedAt = new Date().toISOString();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function reconcileInterruptedTasks() {
|
|
330
|
+
const result = reconcileTasks({
|
|
331
|
+
mode: "safe_fix",
|
|
332
|
+
max_age_minutes: Math.max(config.watcherStaleSeconds / 60, 1 / 60),
|
|
333
|
+
include_done_candidates: true,
|
|
334
|
+
}, config);
|
|
335
|
+
if (result.reconciled > 0) {
|
|
336
|
+
logger.warn("[watcher] Reconciled tasks left by a previous runner", {
|
|
337
|
+
reconciled: result.reconciled,
|
|
338
|
+
task_ids: result.reports
|
|
339
|
+
.filter((report) => report.action_taken !== "left_unchanged")
|
|
340
|
+
.map((report) => report.task_id)
|
|
341
|
+
.slice(0, 20),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function writeWatcherHeartbeat(status, lastError) {
|
|
346
|
+
atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
|
|
347
|
+
status,
|
|
348
|
+
pid: process.pid,
|
|
349
|
+
started_at: WATCHER_STARTED_AT,
|
|
350
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
351
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
352
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
353
|
+
consecutive_failures: consecutiveTickFailures,
|
|
354
|
+
last_error: lastError ? lastError.slice(0, 500) : null,
|
|
355
|
+
heartbeat_source: "independent_interval",
|
|
356
|
+
tick_running: tickRunning,
|
|
357
|
+
tick_started_at: tickStartedAt,
|
|
358
|
+
last_tick_completed_at: lastTickCompletedAt,
|
|
359
|
+
active_task_id: activeTaskId,
|
|
360
|
+
active_task_phase: activeTaskPhase,
|
|
361
|
+
}, null, 2));
|
|
362
|
+
}
|
|
363
|
+
function startWatcherHeartbeatLoop() {
|
|
364
|
+
const intervalMs = Math.min(2000, Math.max(1000, Math.floor(config.watcherStaleSeconds * 1000 / 3)));
|
|
365
|
+
const timer = setInterval(() => {
|
|
366
|
+
try {
|
|
367
|
+
writeWatcherHeartbeat("running");
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
logger.warn("[watcher] Independent heartbeat write failed", {
|
|
371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
372
|
+
});
|
|
135
373
|
}
|
|
374
|
+
}, intervalMs);
|
|
375
|
+
timer.unref?.();
|
|
376
|
+
return timer;
|
|
377
|
+
}
|
|
378
|
+
// ── Start (only when executed as a script) ─────────────────────────
|
|
379
|
+
// The bootstrap below has process-wide side effects (mkdir, signal handlers,
|
|
380
|
+
// polling interval, lock acquisition). Guarding it with a main-module check
|
|
381
|
+
// keeps `import { acquireWatcherLock }` safe for unit tests — without it,
|
|
382
|
+
// importing this module would start polling the real tasks dir and register
|
|
383
|
+
// SIGINT/SIGTERM handlers that interfere with the test runner.
|
|
384
|
+
const isMainModule = !!process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
385
|
+
if (isMainModule) {
|
|
386
|
+
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
387
|
+
logger.info(`[watcher] Workspace: ${wsRoot}`);
|
|
388
|
+
logger.info(`[watcher] Tasks: ${tasksDir}`);
|
|
389
|
+
logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
390
|
+
logger.info(`[watcher] Press Ctrl+C to stop`);
|
|
391
|
+
try {
|
|
392
|
+
acquireWatcherLock(WATCHER_LOCK_FILE);
|
|
136
393
|
}
|
|
137
394
|
catch (err) {
|
|
138
|
-
|
|
395
|
+
if (err instanceof WatcherAlreadyRunningError) {
|
|
396
|
+
logger.error(err.message);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
logger.info("[watcher] Started");
|
|
402
|
+
const heartbeatTimer = startWatcherHeartbeatLoop();
|
|
403
|
+
const scheduledTick = createNonOverlappingRunner(tick, () => {
|
|
404
|
+
logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
|
|
405
|
+
});
|
|
406
|
+
setInterval(() => { void scheduledTick(); }, POLL_INTERVAL_MS);
|
|
407
|
+
// Run first tick immediately
|
|
408
|
+
void scheduledTick();
|
|
409
|
+
// Graceful shutdown
|
|
410
|
+
function shutdown() {
|
|
411
|
+
logger.info("[watcher] Stopped");
|
|
412
|
+
clearInterval(heartbeatTimer);
|
|
413
|
+
try {
|
|
414
|
+
releaseWatcherLock(WATCHER_LOCK_FILE);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
logger.warn("[watcher] Failed to release watcher lock", {
|
|
418
|
+
error: error instanceof Error ? error.message : String(error),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
process.exit(0);
|
|
139
422
|
}
|
|
423
|
+
process.once("SIGINT", shutdown);
|
|
424
|
+
process.once("SIGTERM", shutdown);
|
|
140
425
|
}
|
|
141
|
-
// ── Start ─────────────────────────────────────────────────────────
|
|
142
|
-
logger.info("[watcher] Started");
|
|
143
|
-
setInterval(tick, POLL_INTERVAL_MS);
|
|
144
|
-
// Run first tick immediately
|
|
145
|
-
tick();
|
|
146
|
-
// Graceful shutdown
|
|
147
|
-
process.on("SIGINT", () => {
|
|
148
|
-
logger.info("[watcher] Stopped");
|
|
149
|
-
process.exit(0);
|
|
150
|
-
});
|
|
151
|
-
process.on("SIGTERM", () => {
|
|
152
|
-
logger.info("[watcher] Stopped");
|
|
153
|
-
process.exit(0);
|
|
154
|
-
});
|
|
@@ -20,13 +20,13 @@ const RULES = [
|
|
|
20
20
|
{
|
|
21
21
|
category: "credential_assignment",
|
|
22
22
|
reason: "matched credential assignment",
|
|
23
|
-
pattern: /\b((?:access[_ -]?token|api[_ -]?key|secret|password|credential|token)\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
|
|
23
|
+
pattern: /\b((?:(?:[A-Za-z][A-Za-z0-9]*[_ -])*(?:access[_ -]?token|refresh[_ -]?token|api[_ -]?key|secret(?:[_ -]?access)?[_ -]?key|client[_ -]?secret|private[_ -]?key|secret|password|credential|token))\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
|
|
24
24
|
replace: "$1$2[REDACTED]$4",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
category: "known_token_format",
|
|
28
28
|
reason: "matched known token format",
|
|
29
|
-
pattern: /\b(?:sk|
|
|
29
|
+
pattern: /\b(?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}|sk_[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_-]{16,}|github_pat_[A-Za-z0-9_-]{16,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|(?:hf|npm)_[A-Za-z0-9_-]{16,}|(?:sk|rk)_live_[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
|
|
30
30
|
replace: "[REDACTED TOKEN]",
|
|
31
31
|
},
|
|
32
32
|
];
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - 不持久化到磁盘(内存 Map,进程内共享,单例)
|
|
10
10
|
* - token 单次使用语义(consumeToken 后即删除)
|
|
11
11
|
*/
|
|
12
|
-
import type { ToolRisk } from "../tools/toolRegistry.js";
|
|
13
|
-
import type { ToolProfile } from "../tools/toolCatalog.js";
|
|
12
|
+
import type { ToolRisk } from "../tools/catalog/toolRegistry.js";
|
|
13
|
+
import type { ToolProfile } from "../tools/catalog/toolCatalog.js";
|
|
14
14
|
export interface DiscoveryTokenRecord {
|
|
15
15
|
token: string;
|
|
16
16
|
toolName: string;
|
|
@@ -31,6 +31,7 @@ export interface IssueTokenInput {
|
|
|
31
31
|
allowedScope?: string[];
|
|
32
32
|
ttlMs?: number;
|
|
33
33
|
}
|
|
34
|
+
export declare const MAX_ACTIVE_DISCOVERY_TOKENS = 1024;
|
|
34
35
|
/**
|
|
35
36
|
* 生成并保存 discovery token,返回 token id。
|
|
36
37
|
* token id 格式:dst_{YYYYMMDD}_{randomHex12}
|
|
@@ -13,6 +13,7 @@ import { randomBytes } from "node:crypto";
|
|
|
13
13
|
import { PatchWardenError } from "../errors.js";
|
|
14
14
|
// ── 常量 ──────────────────────────────────────────────────────────
|
|
15
15
|
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
|
|
16
|
+
export const MAX_ACTIVE_DISCOVERY_TOKENS = 1024;
|
|
16
17
|
// ── 模块级 token store(单例,进程内共享) ───────────────────────
|
|
17
18
|
const tokenStore = new Map();
|
|
18
19
|
// ── 内部工具 ──────────────────────────────────────────────────────
|
|
@@ -27,15 +28,29 @@ function generateTokenId() {
|
|
|
27
28
|
const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
|
|
28
29
|
return `dst_${datePart}_${randomPart}`;
|
|
29
30
|
}
|
|
31
|
+
function pruneTokenStore(nowMs) {
|
|
32
|
+
for (const [token, record] of tokenStore) {
|
|
33
|
+
if (Date.parse(record.expiresAt) <= nowMs)
|
|
34
|
+
tokenStore.delete(token);
|
|
35
|
+
}
|
|
36
|
+
while (tokenStore.size >= MAX_ACTIVE_DISCOVERY_TOKENS) {
|
|
37
|
+
const oldest = tokenStore.keys().next().value;
|
|
38
|
+
if (!oldest)
|
|
39
|
+
break;
|
|
40
|
+
tokenStore.delete(oldest);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
30
43
|
// ── 公共 API ──────────────────────────────────────────────────────
|
|
31
44
|
/**
|
|
32
45
|
* 生成并保存 discovery token,返回 token id。
|
|
33
46
|
* token id 格式:dst_{YYYYMMDD}_{randomHex12}
|
|
34
47
|
*/
|
|
35
48
|
export function issueToken(input) {
|
|
36
|
-
const
|
|
49
|
+
const nowMs = Date.now();
|
|
50
|
+
pruneTokenStore(nowMs);
|
|
51
|
+
const now = new Date(nowMs);
|
|
37
52
|
const issuedAt = now.toISOString();
|
|
38
|
-
const expiresAt = new Date(
|
|
53
|
+
const expiresAt = new Date(nowMs + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
|
|
39
54
|
const token = generateTokenId();
|
|
40
55
|
const record = {
|
|
41
56
|
token,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { firstHeaderValue } from "./secretComparison.js";
|
|
2
|
+
const LOOPBACK_HOST_RE = /^(127\.0\.0\.1|localhost)(?::([0-9]{1,5}))?$/i;
|
|
3
|
+
/** Validate an HTTP Host header for a service bound to IPv4 loopback. */
|
|
4
|
+
export function isTrustedLoopbackHostHeader(value, expectedPort) {
|
|
5
|
+
const raw = firstHeaderValue(value).trim();
|
|
6
|
+
const match = LOOPBACK_HOST_RE.exec(raw);
|
|
7
|
+
if (!match)
|
|
8
|
+
return false;
|
|
9
|
+
if (match[2] === undefined)
|
|
10
|
+
return true;
|
|
11
|
+
const declaredPort = Number(match[2]);
|
|
12
|
+
return Number.isInteger(declaredPort)
|
|
13
|
+
&& declaredPort >= 1
|
|
14
|
+
&& declaredPort <= 65_535
|
|
15
|
+
&& declaredPort === expectedPort;
|
|
16
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, normalize, parse, resolve, sep, } from "node:path";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
3
|
import { PatchWardenError } from "../errors.js";
|
|
4
4
|
export function guardPath(requestedPath, workspaceRoot, allowedPrefix) {
|
|
5
5
|
const ws = normalize(resolve(workspaceRoot));
|
|
@@ -55,7 +55,7 @@ function realPathOrSelf(value) {
|
|
|
55
55
|
return normalize(value);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function realPathOrExistingPrefix(candidate) {
|
|
58
|
+
function realPathOrExistingPrefix(candidate, seenLinks = new Set()) {
|
|
59
59
|
const normalized = normalize(candidate);
|
|
60
60
|
const root = parse(normalized).root;
|
|
61
61
|
const suffix = [];
|
|
@@ -64,7 +64,33 @@ function realPathOrExistingPrefix(candidate) {
|
|
|
64
64
|
try {
|
|
65
65
|
return resolve(realpathSync(current), ...suffix);
|
|
66
66
|
}
|
|
67
|
-
catch {
|
|
67
|
+
catch (error) {
|
|
68
|
+
try {
|
|
69
|
+
const metadata = lstatSync(current);
|
|
70
|
+
if (metadata.isSymbolicLink()) {
|
|
71
|
+
const linkKey = process.platform === "win32" ? current.toLowerCase() : current;
|
|
72
|
+
if (seenLinks.has(linkKey)) {
|
|
73
|
+
throw new PatchWardenError("workspace_path_escape", `Symbolic link cycle detected while resolving "${candidate}"`, "Remove the cyclic link and use a path inside the configured workspace.", true, { path: candidate, operation: "path_access", safe_alternative: "Use a non-link path inside workspaceRoot." });
|
|
74
|
+
}
|
|
75
|
+
const nextSeen = new Set(seenLinks);
|
|
76
|
+
nextSeen.add(linkKey);
|
|
77
|
+
const linkTarget = readlinkSync(current);
|
|
78
|
+
const resolvedTarget = isAbsolute(linkTarget)
|
|
79
|
+
? resolve(linkTarget)
|
|
80
|
+
: resolve(dirname(current), linkTarget);
|
|
81
|
+
return resolve(realPathOrExistingPrefix(resolvedTarget, nextSeen), ...suffix);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (linkError) {
|
|
85
|
+
if (linkError instanceof PatchWardenError)
|
|
86
|
+
throw linkError;
|
|
87
|
+
const linkCode = linkError.code;
|
|
88
|
+
if (linkCode !== "ENOENT" && linkCode !== "ENOTDIR")
|
|
89
|
+
throw linkError;
|
|
90
|
+
}
|
|
91
|
+
const code = error.code;
|
|
92
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
93
|
+
throw error;
|
|
68
94
|
suffix.unshift(basename(current));
|
|
69
95
|
const parent = dirname(current);
|
|
70
96
|
if (parent === current)
|
|
@@ -72,24 +98,13 @@ function realPathOrExistingPrefix(candidate) {
|
|
|
72
98
|
current = parent;
|
|
73
99
|
}
|
|
74
100
|
}
|
|
75
|
-
|
|
76
|
-
return resolve(realpathSync(root || current), ...suffix);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return normalized;
|
|
80
|
-
}
|
|
101
|
+
return resolve(realpathSync(root || current), ...suffix);
|
|
81
102
|
}
|
|
82
103
|
function assertInside(candidate, root, requestedPath, message) {
|
|
83
104
|
const normalizedRoot = normalize(root);
|
|
84
105
|
const normalizedCandidate = normalize(candidate);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: normalizedRoot;
|
|
88
|
-
const checkCandidate = process.platform === "win32"
|
|
89
|
-
? normalizedCandidate.toLowerCase()
|
|
90
|
-
: normalizedCandidate;
|
|
91
|
-
if (checkCandidate !== checkRoot &&
|
|
92
|
-
!checkCandidate.startsWith(checkRoot + sep)) {
|
|
106
|
+
if (normalizedCandidate !== normalizedRoot &&
|
|
107
|
+
!normalizedCandidate.startsWith(normalizedRoot + sep)) {
|
|
93
108
|
if (message.startsWith("Path escapes workspace")) {
|
|
94
109
|
throw new PatchWardenError("workspace_path_escape", `Path escapes workspace: "${requestedPath}" resolves to "${normalizedCandidate}" which is outside "${normalizedRoot}"`, "Use a path inside the configured workspace and allowed prefix.", true, { path: requestedPath, operation: "path_access", safe_alternative: "Read a path inside the configured workspace and allowed prefix." });
|
|
95
110
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PatchWardenError } from "../errors.js";
|
|
2
|
+
import { redactSensitiveContent } from "./contentRedaction.js";
|
|
2
3
|
const PLAN_RULES = [
|
|
3
4
|
{
|
|
4
5
|
id: "plan_secret_access",
|
|
@@ -35,7 +36,7 @@ export function guardPlanContent(title, content) {
|
|
|
35
36
|
throw new PatchWardenError(rule.id, `save_plan blocked content in category "${rule.category}".`, rule.suggestion, true, {
|
|
36
37
|
operation: "save_plan",
|
|
37
38
|
matched_category: rule.category,
|
|
38
|
-
matched_text: match[0].slice(0, 160),
|
|
39
|
+
matched_text: redactSensitiveContent(match[0]).content.slice(0, 160),
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
}
|