patchwarden 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +32 -2
- package/README.en.md +152 -25
- package/README.md +120 -23
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentStore.js +42 -34
- package/dist/config.d.ts +15 -0
- package/dist/config.js +66 -13
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +6 -0
- package/dist/control/middleware/auth.js +14 -5
- package/dist/control/middleware/static.js +16 -3
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.js +17 -7
- package/dist/control/routes/evidence.js +1 -1
- package/dist/control/routes/lineage.js +1 -1
- package/dist/control/routes/process.d.ts +7 -0
- package/dist/control/routes/process.js +329 -42
- package/dist/control/routes/sessions.d.ts +9 -1
- package/dist/control/routes/sessions.js +84 -21
- package/dist/control/routes/status.d.ts +46 -1
- package/dist/control/routes/status.js +72 -15
- package/dist/control/routes/taskActions.js +24 -60
- package/dist/control/routes/tasks.d.ts +4 -0
- package/dist/control/routes/tasks.js +61 -62
- package/dist/control/routes/workspace.js +50 -23
- package/dist/control/runtime.d.ts +3 -2
- package/dist/control/runtime.js +63 -30
- package/dist/control/server.js +36 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +42 -29
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +62 -52
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.js +5 -12
- package/dist/goal/goalStatus.d.ts +2 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.js +97 -96
- package/dist/goal/subgoalSync.js +11 -9
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +111 -32
- package/dist/logging.js +5 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +13 -5
- package/dist/runner/changeCapture.js +243 -61
- package/dist/runner/postTaskCleanup.js +48 -19
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +115 -87
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +50 -36
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +256 -48
- package/dist/security/contentRedaction.js +2 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +24 -23
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.js +32 -31
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
- package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
- package/dist/tools/dispatch/directDispatch.js +11 -10
- package/dist/tools/dispatch/goalDispatch.js +5 -4
- package/dist/tools/dispatch/releaseDispatch.js +3 -2
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +7 -11
- package/dist/tools/registry.js +17 -1178
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
- package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +808 -143
- package/docs/architecture.md +15 -55
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +4 -4
- package/docs/desktop-app.md +175 -0
- package/docs/evidence-pack-schema.md +3 -3
- package/docs/open-source-application.md +17 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +39 -29
- package/docs/task-safe-review-workflow.md +5 -5
- package/docs/threat-model.md +19 -1
- package/examples/config.example.json +4 -2
- package/package.json +27 -10
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +91 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +10 -10
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +27 -3
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +26 -12
- package/src/assessments/assessmentStore.ts +52 -40
- package/src/config.ts +70 -13
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +22 -4
- package/src/control/middleware/static.ts +16 -3
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +14 -6
- package/src/control/routes/evidence.ts +1 -1
- package/src/control/routes/lineage.ts +1 -1
- package/src/control/routes/process.ts +341 -40
- package/src/control/routes/sessions.ts +88 -25
- package/src/control/routes/status.ts +97 -17
- package/src/control/routes/taskActions.ts +24 -58
- package/src/control/routes/tasks.ts +66 -58
- package/src/control/routes/workspace.ts +56 -25
- package/src/control/runtime.ts +69 -33
- package/src/control/server.ts +36 -342
- package/src/control/shared.ts +77 -11
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +72 -44
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +73 -59
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +5 -13
- package/src/goal/goalStatus.ts +19 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +12 -12
- package/src/goal/subgoalSync.ts +11 -8
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +113 -30
- package/src/logging.ts +10 -3
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +291 -57
- package/src/runner/postTaskCleanup.ts +41 -17
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +138 -91
- package/src/runner/simpleProcess.ts +66 -31
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +281 -51
- package/src/security/contentRedaction.ts +2 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +25 -24
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +36 -31
- package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
- package/src/tools/dispatch/directDispatch.ts +11 -10
- package/src/tools/dispatch/goalDispatch.ts +5 -4
- package/src/tools/dispatch/releaseDispatch.ts +3 -2
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +17 -1261
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +12 -0
- package/ui/pages/dashboard.html +90 -147
- package/ui/pages/direct-sessions.html +123 -26
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +12 -0
- package/ui/pages/tasks.html +54 -8
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/apply-patch.test.d.ts +0 -1
- package/dist/test/unit/apply-patch.test.js +0 -225
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/create-task.test.d.ts +0 -1
- package/dist/test/unit/create-task.test.js +0 -197
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -328
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -224
- package/dist/test/unit/get-task-status.test.d.ts +0 -1
- package/dist/test/unit/get-task-status.test.js +0 -174
- package/dist/test/unit/get-task-summary.test.d.ts +0 -1
- package/dist/test/unit/get-task-summary.test.js +0 -146
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-report.test.d.ts +0 -1
- package/dist/test/unit/goal-report.test.js +0 -159
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -192
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -133
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
- package/dist/test/unit/spec-kit-import.test.js +0 -341
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/wait-for-task.test.d.ts +0 -1
- package/dist/test/unit/wait-for-task.test.js +0 -144
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/execution-plan-2026-07-09.md +0 -315
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/docs/roadmap-execution-and-acceptance.md +0 -365
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/apply-patch.test.ts +0 -293
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/create-task.test.ts +0 -255
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -467
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -251
- package/src/test/unit/get-task-status.test.ts +0 -203
- package/src/test/unit/get-task-summary.test.ts +0 -173
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-report.test.ts +0 -189
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -245
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -180
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/spec-kit-import.test.ts +0 -429
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/wait-for-task.test.ts +0 -176
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
package/src/runner/watch.ts
CHANGED
|
@@ -17,15 +17,30 @@
|
|
|
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
|
+
// Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
|
|
42
|
+
// This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
|
|
43
|
+
// avoids eagerly pulling in the full tools/dispatch graph at module load.
|
|
29
44
|
|
|
30
45
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
31
46
|
|
|
@@ -36,37 +51,175 @@ const wsRoot = resolveWorkspaceRoot(config);
|
|
|
36
51
|
|
|
37
52
|
const POLL_INTERVAL_MS = 4000;
|
|
38
53
|
const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
|
|
54
|
+
const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
|
|
39
55
|
const WATCHER_STARTED_AT = new Date().toISOString();
|
|
40
56
|
const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
|
|
41
57
|
const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
|
|
42
58
|
? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
|
|
43
59
|
: 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
60
|
// Track executed tasks to prevent re-execution
|
|
52
61
|
const executedTasks = new Set<string>();
|
|
62
|
+
let consecutiveTickFailures = 0;
|
|
63
|
+
|
|
64
|
+
// ── Single-instance lock ─────────────────────────────────────────
|
|
65
|
+
// watch.ts is a long-running poller; without a lock, two watchers started
|
|
66
|
+
// against the same workspace would race on the same pending tasks. The lock
|
|
67
|
+
// is best-effort: it detects a live PID via process.kill(pid, 0) and refuses
|
|
68
|
+
// to start when one is found. A dead PID (crashed watcher) is taken over.
|
|
69
|
+
|
|
70
|
+
export class WatcherAlreadyRunningError extends Error {
|
|
71
|
+
constructor(message: string) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "WatcherAlreadyRunningError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type WatcherLockFile = {
|
|
78
|
+
pid?: number;
|
|
79
|
+
instance_id?: string;
|
|
80
|
+
started_at?: string;
|
|
81
|
+
launcher_pid?: number | null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function errorCode(error: unknown): string | null {
|
|
85
|
+
return error && typeof error === "object" && "code" in error
|
|
86
|
+
? String((error as NodeJS.ErrnoException).code || "")
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readWatcherLock(lockFilePath: string): WatcherLockFile | null {
|
|
91
|
+
try {
|
|
92
|
+
const parsed: unknown = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
93
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
94
|
+
? parsed as WatcherLockFile
|
|
95
|
+
: null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function processIsAlive(pid: number): boolean {
|
|
102
|
+
try {
|
|
103
|
+
process.kill(pid, 0);
|
|
104
|
+
return true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// EPERM means the process exists but the current user cannot signal it.
|
|
107
|
+
return errorCode(error) === "EPERM";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runningWatcherError(lockData: WatcherLockFile | null): WatcherAlreadyRunningError {
|
|
112
|
+
return new WatcherAlreadyRunningError(
|
|
113
|
+
`[watcher] Already running (pid=${lockData?.pid ?? "unknown"}, instance=${lockData?.instance_id ?? "unknown"}). Exiting.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createWatcherLockExclusive(lockFilePath: string, content: string): void {
|
|
118
|
+
const descriptor = openSync(lockFilePath, "wx", 0o600);
|
|
119
|
+
let writeError: unknown = null;
|
|
120
|
+
try {
|
|
121
|
+
writeFileSync(descriptor, content, "utf-8");
|
|
122
|
+
} catch (error) {
|
|
123
|
+
writeError = error;
|
|
124
|
+
} finally {
|
|
125
|
+
closeSync(descriptor);
|
|
126
|
+
}
|
|
127
|
+
if (writeError) {
|
|
128
|
+
try { unlinkSync(lockFilePath); } catch { /* best effort after a partial write */ }
|
|
129
|
+
throw writeError;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
|
|
135
|
+
* recorded PID is still alive (per `process.kill(pid, 0)`), throws
|
|
136
|
+
* {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
|
|
137
|
+
*
|
|
138
|
+
* Exported for unit testing; `lockFilePath` defaults to the module-level
|
|
139
|
+
* {@link WATCHER_LOCK_FILE} but tests pass a temp path.
|
|
140
|
+
*/
|
|
141
|
+
export function acquireWatcherLock(lockFilePath: string = WATCHER_LOCK_FILE): void {
|
|
142
|
+
const lockContent = JSON.stringify({
|
|
143
|
+
pid: process.pid,
|
|
144
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
145
|
+
started_at: WATCHER_STARTED_AT,
|
|
146
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
147
|
+
}, null, 2);
|
|
148
|
+
try {
|
|
149
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
150
|
+
return;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existing = readWatcherLock(lockFilePath);
|
|
156
|
+
if (typeof existing?.pid === "number" && processIsAlive(existing.pid)) {
|
|
157
|
+
throw runningWatcherError(existing);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
unlinkSync(lockFilePath);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (errorCode(error) === "EEXIST") {
|
|
170
|
+
throw runningWatcherError(readWatcherLock(lockFilePath));
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function releaseWatcherLock(
|
|
177
|
+
lockFilePath: string = WATCHER_LOCK_FILE,
|
|
178
|
+
instanceId: string = WATCHER_INSTANCE_ID,
|
|
179
|
+
pid: number = process.pid,
|
|
180
|
+
): boolean {
|
|
181
|
+
const existing = readWatcherLock(lockFilePath);
|
|
182
|
+
if (existing?.pid !== pid || existing.instance_id !== instanceId) return false;
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(lockFilePath);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (errorCode(error) === "ENOENT") return false;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createNonOverlappingRunner(
|
|
193
|
+
operation: () => Promise<void>,
|
|
194
|
+
onSkipped: () => void = () => undefined,
|
|
195
|
+
): () => Promise<boolean> {
|
|
196
|
+
let running = false;
|
|
197
|
+
return async () => {
|
|
198
|
+
if (running) {
|
|
199
|
+
onSkipped();
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
running = true;
|
|
203
|
+
try {
|
|
204
|
+
await operation();
|
|
205
|
+
return true;
|
|
206
|
+
} finally {
|
|
207
|
+
running = false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
53
211
|
|
|
54
212
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
55
213
|
|
|
56
214
|
async function tick() {
|
|
57
215
|
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);
|
|
216
|
+
writeWatcherHeartbeat("running");
|
|
68
217
|
// Ensure tasks directory exists
|
|
69
|
-
if (!existsSync(tasksDir))
|
|
218
|
+
if (!existsSync(tasksDir)) {
|
|
219
|
+
consecutiveTickFailures = 0;
|
|
220
|
+
writeWatcherHeartbeat("running");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
70
223
|
|
|
71
224
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
72
225
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
@@ -82,11 +235,19 @@ async function tick() {
|
|
|
82
235
|
|
|
83
236
|
if (!existsSync(statusFile)) continue;
|
|
84
237
|
|
|
85
|
-
let statusData:
|
|
238
|
+
let statusData: Record<string, unknown>;
|
|
86
239
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
240
|
+
const parsed: unknown = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
242
|
+
throw new Error("status.json must contain an object");
|
|
243
|
+
}
|
|
244
|
+
statusData = parsed as Record<string, unknown>;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.warn("[watcher] Skipping unreadable task status", {
|
|
247
|
+
task_id: taskId,
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
90
251
|
}
|
|
91
252
|
|
|
92
253
|
if (statusData.status !== "pending") continue;
|
|
@@ -94,13 +255,18 @@ async function tick() {
|
|
|
94
255
|
// ── Pre-flight safety checks ──
|
|
95
256
|
try {
|
|
96
257
|
// Check repo_path
|
|
97
|
-
const
|
|
258
|
+
const repoPath = typeof statusData.resolved_repo_path === "string"
|
|
259
|
+
? statusData.resolved_repo_path
|
|
260
|
+
: typeof statusData.repo_path === "string"
|
|
261
|
+
? statusData.repo_path
|
|
262
|
+
: wsRoot;
|
|
263
|
+
const resolvedRepoPath = guardWorkspacePath(repoPath, wsRoot);
|
|
98
264
|
|
|
99
265
|
// Check agent
|
|
100
|
-
guardAgentCommand(statusData.agent, config);
|
|
266
|
+
guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
|
|
101
267
|
|
|
102
268
|
// Check test_command
|
|
103
|
-
if (statusData.test_command) {
|
|
269
|
+
if (typeof statusData.test_command === "string" && statusData.test_command) {
|
|
104
270
|
guardTestCommand(statusData.test_command, config, resolvedRepoPath);
|
|
105
271
|
}
|
|
106
272
|
if (Array.isArray(statusData.verify_commands)) {
|
|
@@ -110,25 +276,32 @@ async function tick() {
|
|
|
110
276
|
}
|
|
111
277
|
|
|
112
278
|
// Assessment freshness revalidation
|
|
113
|
-
if (statusData.assessment_id) {
|
|
279
|
+
if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
|
|
114
280
|
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
115
|
-
const validation = validateAssessmentFreshness(
|
|
281
|
+
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
116
282
|
if (!validation.valid) {
|
|
117
283
|
throw new Error(`assessment validation failed: ${validation.failure_reason}`);
|
|
118
284
|
}
|
|
119
285
|
}
|
|
120
286
|
} catch (err) {
|
|
121
|
-
const errMsg =
|
|
287
|
+
const errMsg = redactSensitiveContent(
|
|
288
|
+
`[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
289
|
+
).content;
|
|
122
290
|
logger.error(errMsg);
|
|
123
291
|
|
|
124
292
|
// Write error and mark as failed so it doesn't get re-picked
|
|
125
293
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
294
|
+
atomicWriteFileSync(join(taskDir, "error.log"), errMsg);
|
|
295
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
296
|
+
if (current.status !== "pending") return { result: undefined };
|
|
297
|
+
const next = {
|
|
298
|
+
...current,
|
|
299
|
+
status: "failed",
|
|
300
|
+
error: errMsg,
|
|
301
|
+
updated_at: new Date().toISOString(),
|
|
302
|
+
};
|
|
303
|
+
return { next, result: undefined };
|
|
304
|
+
});
|
|
132
305
|
} catch (err) {
|
|
133
306
|
logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
|
|
134
307
|
}
|
|
@@ -141,31 +314,88 @@ async function tick() {
|
|
|
141
314
|
executedTasks.add(taskId);
|
|
142
315
|
|
|
143
316
|
try {
|
|
317
|
+
const { runTask } = await import("./runTask.js");
|
|
144
318
|
const result = await runTask(taskId);
|
|
145
319
|
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
146
320
|
} catch (err) {
|
|
147
321
|
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
322
|
}
|
|
149
323
|
}
|
|
324
|
+
consecutiveTickFailures = 0;
|
|
325
|
+
writeWatcherHeartbeat("running");
|
|
150
326
|
} catch (err) {
|
|
151
|
-
|
|
327
|
+
consecutiveTickFailures += 1;
|
|
328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
+
logger.error(`[watcher] Tick error (${consecutiveTickFailures} consecutive): ${message}`);
|
|
330
|
+
try {
|
|
331
|
+
writeWatcherHeartbeat(consecutiveTickFailures >= 3 ? "degraded" : "running", message);
|
|
332
|
+
} catch (heartbeatError) {
|
|
333
|
+
logger.error("[watcher] Failed to persist degraded heartbeat", {
|
|
334
|
+
error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
152
337
|
}
|
|
153
338
|
}
|
|
154
339
|
|
|
155
|
-
|
|
340
|
+
function writeWatcherHeartbeat(status: "running" | "degraded", lastError?: string): void {
|
|
341
|
+
atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
|
|
342
|
+
status,
|
|
343
|
+
pid: process.pid,
|
|
344
|
+
started_at: WATCHER_STARTED_AT,
|
|
345
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
346
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
347
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
348
|
+
consecutive_failures: consecutiveTickFailures,
|
|
349
|
+
last_error: lastError ? lastError.slice(0, 500) : null,
|
|
350
|
+
}, null, 2));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Start (only when executed as a script) ─────────────────────────
|
|
354
|
+
// The bootstrap below has process-wide side effects (mkdir, signal handlers,
|
|
355
|
+
// polling interval, lock acquisition). Guarding it with a main-module check
|
|
356
|
+
// keeps `import { acquireWatcherLock }` safe for unit tests — without it,
|
|
357
|
+
// importing this module would start polling the real tasks dir and register
|
|
358
|
+
// SIGINT/SIGTERM handlers that interfere with the test runner.
|
|
359
|
+
|
|
360
|
+
const isMainModule = !!process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
361
|
+
|
|
362
|
+
if (isMainModule) {
|
|
363
|
+
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
364
|
+
|
|
365
|
+
logger.info(`[watcher] Workspace: ${wsRoot}`);
|
|
366
|
+
logger.info(`[watcher] Tasks: ${tasksDir}`);
|
|
367
|
+
logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
368
|
+
logger.info(`[watcher] Press Ctrl+C to stop`);
|
|
156
369
|
|
|
157
|
-
|
|
158
|
-
|
|
370
|
+
try {
|
|
371
|
+
acquireWatcherLock(WATCHER_LOCK_FILE);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err instanceof WatcherAlreadyRunningError) {
|
|
374
|
+
logger.error(err.message);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
159
379
|
|
|
160
|
-
|
|
161
|
-
tick()
|
|
380
|
+
logger.info("[watcher] Started");
|
|
381
|
+
const scheduledTick = createNonOverlappingRunner(tick, () => {
|
|
382
|
+
logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
|
|
383
|
+
});
|
|
384
|
+
setInterval(() => { void scheduledTick(); }, POLL_INTERVAL_MS);
|
|
162
385
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
386
|
+
// Run first tick immediately
|
|
387
|
+
void scheduledTick();
|
|
388
|
+
|
|
389
|
+
// Graceful shutdown
|
|
390
|
+
function shutdown(): void {
|
|
391
|
+
logger.info("[watcher] Stopped");
|
|
392
|
+
try { releaseWatcherLock(WATCHER_LOCK_FILE); } catch (error) {
|
|
393
|
+
logger.warn("[watcher] Failed to release watcher lock", {
|
|
394
|
+
error: error instanceof Error ? error.message : String(error),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
process.once("SIGINT", shutdown);
|
|
400
|
+
process.once("SIGTERM", shutdown);
|
|
401
|
+
}
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve, join, dirname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { PatchWardenError } from "../errors.js";
|
|
4
|
+
import { isPathChildOf, isSamePath } from "../utils/platform.js";
|
|
4
5
|
|
|
5
6
|
const CRITICAL_RUNTIME_DIRS = ["dist", "src", "scripts", "release"];
|
|
6
7
|
|
|
@@ -10,19 +11,21 @@ const CRITICAL_RUNTIME_DIRS = ["dist", "src", "scripts", "release"];
|
|
|
10
11
|
* and direct-session flows share the same protection.
|
|
11
12
|
*/
|
|
12
13
|
export function guardRuntimeSelfModification(resolvedRepoPath: string): void {
|
|
13
|
-
|
|
14
|
+
// runtimeGuard lives at <package-root>/{src,dist}/security/runtimeGuard.*.
|
|
15
|
+
const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
|
+
const isRuntimeRoot = isSamePath(resolvedRepoPath, runtimeRoot);
|
|
14
17
|
if (
|
|
15
|
-
|
|
16
|
-
resolvedRepoPath
|
|
18
|
+
isRuntimeRoot ||
|
|
19
|
+
isPathChildOf(resolvedRepoPath, runtimeRoot)
|
|
17
20
|
) {
|
|
18
21
|
const isCritical = CRITICAL_RUNTIME_DIRS.some((dir) => {
|
|
19
22
|
const full = join(runtimeRoot, dir);
|
|
20
23
|
return (
|
|
21
|
-
resolvedRepoPath
|
|
22
|
-
resolvedRepoPath
|
|
24
|
+
isSamePath(resolvedRepoPath, full) ||
|
|
25
|
+
isPathChildOf(resolvedRepoPath, full)
|
|
23
26
|
);
|
|
24
27
|
});
|
|
25
|
-
if (
|
|
28
|
+
if (isRuntimeRoot || isCritical) {
|
|
26
29
|
throw new PatchWardenError(
|
|
27
30
|
"runtime_self_modification_blocked",
|
|
28
31
|
`repo_path points to the active PatchWarden runtime or its critical subdirectories.`,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Compare secret text without content-dependent byte comparison. */
|
|
4
|
+
export function timingSafeStringEqual(actual: string, expected: string): boolean {
|
|
5
|
+
const actualBytes = Buffer.from(actual, "utf-8");
|
|
6
|
+
const expectedBytes = Buffer.from(expected, "utf-8");
|
|
7
|
+
return actualBytes.length === expectedBytes.length
|
|
8
|
+
&& timingSafeEqual(actualBytes, expectedBytes);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function firstHeaderValue(value: string | string[] | undefined): string {
|
|
12
|
+
return Array.isArray(value) ? (value[0] || "") : (value || "");
|
|
13
|
+
}
|