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