patchwarden 1.5.0 → 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 +194 -26
- package/README.md +160 -22
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +24 -15
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +44 -36
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +98 -11
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +16 -0
- package/dist/control/middleware/auth.js +17 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +78 -0
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +287 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +12 -0
- package/dist/control/routes/process.js +487 -0
- package/dist/control/routes/sessions.d.ts +30 -0
- package/dist/control/routes/sessions.js +287 -0
- package/dist/control/routes/status.d.ts +51 -0
- package/dist/control/routes/status.js +307 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +197 -0
- package/dist/control/routes/tasks.d.ts +24 -0
- package/dist/control/routes/tasks.js +309 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +220 -0
- package/dist/control/runtime.d.ts +92 -0
- package/dist/control/runtime.js +425 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +150 -0
- package/dist/control/shared.d.ts +59 -0
- package/dist/control/shared.js +341 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +72 -37
- 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.d.ts +18 -1
- package/dist/doctor.js +614 -373
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +197 -0
- package/dist/goal/goalStatus.d.ts +8 -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.d.ts +63 -0
- package/dist/goal/specKitImport.js +221 -0
- package/dist/goal/subgoalSync.js +13 -10
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +126 -44
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +13 -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 +14 -6
- package/dist/runner/changeCapture.js +287 -81
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +60 -7
- 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 +339 -287
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +52 -38
- 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 +265 -54
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +24 -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 +280 -273
- 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} +28 -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} +8 -63
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
- 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} +14 -10
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
- 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.d.ts → direct/createDirectSession.d.ts} +1 -1
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
- 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.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +283 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +116 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +92 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +46 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- 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} +2 -2
- 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 +10 -14
- package/dist/tools/registry.js +47 -1651
- 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} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
- 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.d.ts → tasks/evidencePack.d.ts} +6 -0
- package/dist/tools/tasks/evidencePack.js +387 -0
- 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/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
- 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} +59 -31
- 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 +1648 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +23 -0
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/desktop-app.md +175 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +168 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +82 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +97 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/examples/config.example.json +4 -2
- package/package.json +28 -11
- 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 +447 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +39 -33
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +30 -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 +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/scripts/generate-demo-gif.py +320 -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 +29 -15
- package/src/assessments/assessmentStore.ts +55 -43
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +105 -11
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +35 -0
- package/src/control/middleware/static.ts +84 -0
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +329 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +505 -0
- package/src/control/routes/sessions.ts +314 -0
- package/src/control/routes/status.ts +405 -0
- package/src/control/routes/taskActions.ts +214 -0
- package/src/control/routes/tasks.ts +331 -0
- package/src/control/routes/workspace.ts +234 -0
- package/src/control/runtime.ts +508 -0
- package/src/control/server.ts +165 -0
- package/src/control/shared.ts +360 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +100 -51
- 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 +766 -492
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +263 -0
- package/src/goal/goalStatus.ts +25 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +15 -10
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +130 -44
- package/src/index.ts +7 -4
- package/src/logging.ts +20 -4
- 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 +342 -80
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +55 -7
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +441 -292
- package/src/runner/simpleProcess.ts +68 -33
- 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 +290 -57
- package/src/security/contentRedaction.ts +31 -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 +276 -273
- 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} +29 -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} +196 -89
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
- 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} +29 -19
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
- 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 +379 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
- package/src/tools/dispatch/directDispatch.ts +168 -0
- package/src/tools/dispatch/goalDispatch.ts +128 -0
- package/src/tools/dispatch/releaseDispatch.ts +66 -0
- package/src/tools/dispatch/types.ts +24 -0
- 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} +43 -28
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +54 -1858
- 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} +56 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/tasks/evidencePack.ts +504 -0
- 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} +70 -35
- 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 +204 -3
- package/ui/pages/dashboard.html +937 -79
- package/ui/pages/direct-sessions.html +609 -60
- 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 +468 -438
- package/ui/pages/tasks.html +648 -65
- 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/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/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 -213
- 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 -130
- 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-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 -190
- 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 -109
- 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/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/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/evidencePack.js +0 -168
- 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/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/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/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/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -297
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -142
- 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-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 -243
- 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 -150
- 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/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/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/evidencePack.ts +0 -205
- 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/{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/{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/{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,14 +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";
|
|
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.
|
|
28
44
|
|
|
29
45
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
30
46
|
|
|
@@ -35,37 +51,175 @@ const wsRoot = resolveWorkspaceRoot(config);
|
|
|
35
51
|
|
|
36
52
|
const POLL_INTERVAL_MS = 4000;
|
|
37
53
|
const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
|
|
54
|
+
const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
|
|
38
55
|
const WATCHER_STARTED_AT = new Date().toISOString();
|
|
39
56
|
const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
|
|
40
57
|
const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
|
|
41
58
|
? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
|
|
42
59
|
: null;
|
|
43
|
-
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
44
|
-
|
|
45
|
-
console.error(`[watcher] Workspace: ${wsRoot}`);
|
|
46
|
-
console.error(`[watcher] Tasks: ${tasksDir}`);
|
|
47
|
-
console.error(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
48
|
-
console.error(`[watcher] Press Ctrl+C to stop`);
|
|
49
|
-
|
|
50
60
|
// Track executed tasks to prevent re-execution
|
|
51
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
|
+
}
|
|
52
211
|
|
|
53
212
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
54
213
|
|
|
55
214
|
async function tick() {
|
|
56
215
|
try {
|
|
57
|
-
|
|
58
|
-
writeFileSync(heartbeatTemporary, JSON.stringify({
|
|
59
|
-
status: "running",
|
|
60
|
-
pid: process.pid,
|
|
61
|
-
started_at: WATCHER_STARTED_AT,
|
|
62
|
-
last_heartbeat_at: new Date().toISOString(),
|
|
63
|
-
instance_id: WATCHER_INSTANCE_ID,
|
|
64
|
-
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
65
|
-
}, null, 2), "utf-8");
|
|
66
|
-
renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
|
|
216
|
+
writeWatcherHeartbeat("running");
|
|
67
217
|
// Ensure tasks directory exists
|
|
68
|
-
if (!existsSync(tasksDir))
|
|
218
|
+
if (!existsSync(tasksDir)) {
|
|
219
|
+
consecutiveTickFailures = 0;
|
|
220
|
+
writeWatcherHeartbeat("running");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
69
223
|
|
|
70
224
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
71
225
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
@@ -81,11 +235,19 @@ async function tick() {
|
|
|
81
235
|
|
|
82
236
|
if (!existsSync(statusFile)) continue;
|
|
83
237
|
|
|
84
|
-
let statusData:
|
|
238
|
+
let statusData: Record<string, unknown>;
|
|
85
239
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
251
|
}
|
|
90
252
|
|
|
91
253
|
if (statusData.status !== "pending") continue;
|
|
@@ -93,13 +255,18 @@ async function tick() {
|
|
|
93
255
|
// ── Pre-flight safety checks ──
|
|
94
256
|
try {
|
|
95
257
|
// Check repo_path
|
|
96
|
-
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);
|
|
97
264
|
|
|
98
265
|
// Check agent
|
|
99
|
-
guardAgentCommand(statusData.agent, config);
|
|
266
|
+
guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
|
|
100
267
|
|
|
101
268
|
// Check test_command
|
|
102
|
-
if (statusData.test_command) {
|
|
269
|
+
if (typeof statusData.test_command === "string" && statusData.test_command) {
|
|
103
270
|
guardTestCommand(statusData.test_command, config, resolvedRepoPath);
|
|
104
271
|
}
|
|
105
272
|
if (Array.isArray(statusData.verify_commands)) {
|
|
@@ -109,60 +276,126 @@ async function tick() {
|
|
|
109
276
|
}
|
|
110
277
|
|
|
111
278
|
// Assessment freshness revalidation
|
|
112
|
-
if (statusData.assessment_id) {
|
|
113
|
-
const preExecSnapshot = captureRepoSnapshot(resolvedRepoPath);
|
|
114
|
-
const validation = validateAssessmentFreshness(
|
|
279
|
+
if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
|
|
280
|
+
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
281
|
+
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
115
282
|
if (!validation.valid) {
|
|
116
283
|
throw new Error(`assessment validation failed: ${validation.failure_reason}`);
|
|
117
284
|
}
|
|
118
285
|
}
|
|
119
286
|
} catch (err) {
|
|
120
|
-
const errMsg =
|
|
121
|
-
|
|
287
|
+
const errMsg = redactSensitiveContent(
|
|
288
|
+
`[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
289
|
+
).content;
|
|
290
|
+
logger.error(errMsg);
|
|
122
291
|
|
|
123
292
|
// Write error and mark as failed so it doesn't get re-picked
|
|
124
293
|
try {
|
|
125
|
-
|
|
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
|
+
});
|
|
305
|
+
} catch (err) {
|
|
306
|
+
logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
|
|
307
|
+
}
|
|
132
308
|
executedTasks.add(taskId);
|
|
133
309
|
continue;
|
|
134
310
|
}
|
|
135
311
|
|
|
136
312
|
// ── Execute ──
|
|
137
|
-
|
|
313
|
+
logger.info(`[watcher] Executing: ${taskId}`);
|
|
138
314
|
executedTasks.add(taskId);
|
|
139
315
|
|
|
140
316
|
try {
|
|
317
|
+
const { runTask } = await import("./runTask.js");
|
|
141
318
|
const result = await runTask(taskId);
|
|
142
|
-
|
|
319
|
+
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
143
320
|
} catch (err) {
|
|
144
|
-
|
|
321
|
+
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
322
|
}
|
|
146
323
|
}
|
|
324
|
+
consecutiveTickFailures = 0;
|
|
325
|
+
writeWatcherHeartbeat("running");
|
|
147
326
|
} catch (err) {
|
|
148
|
-
|
|
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
|
+
}
|
|
149
337
|
}
|
|
150
338
|
}
|
|
151
339
|
|
|
152
|
-
|
|
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`);
|
|
153
369
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
379
|
|
|
157
|
-
|
|
158
|
-
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);
|
|
159
385
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
@@ -12,6 +12,7 @@ export interface StructuredRedactionResult<T> {
|
|
|
12
12
|
|
|
13
13
|
interface RedactionRule {
|
|
14
14
|
category: string;
|
|
15
|
+
reason: string;
|
|
15
16
|
pattern: RegExp;
|
|
16
17
|
replace: string;
|
|
17
18
|
}
|
|
@@ -19,31 +20,59 @@ interface RedactionRule {
|
|
|
19
20
|
const RULES: RedactionRule[] = [
|
|
20
21
|
{
|
|
21
22
|
category: "private_key",
|
|
23
|
+
reason: "matched private key block",
|
|
22
24
|
pattern: /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi,
|
|
23
25
|
replace: "[REDACTED PRIVATE KEY]",
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
category: "bearer_token",
|
|
29
|
+
reason: "matched bearer token",
|
|
27
30
|
pattern: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}={0,2}/gi,
|
|
28
31
|
replace: "Bearer [REDACTED]",
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
34
|
category: "npm_token",
|
|
35
|
+
reason: "matched npm auth token",
|
|
32
36
|
pattern: /(\b_?authToken\s*=\s*)[^\s\r\n]+/gi,
|
|
33
37
|
replace: "$1[REDACTED]",
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
category: "credential_assignment",
|
|
37
|
-
|
|
41
|
+
reason: "matched credential assignment",
|
|
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,
|
|
38
43
|
replace: "$1$2[REDACTED]$4",
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
category: "known_token_format",
|
|
42
|
-
|
|
47
|
+
reason: "matched known token format",
|
|
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,
|
|
43
49
|
replace: "[REDACTED TOKEN]",
|
|
44
50
|
},
|
|
45
51
|
];
|
|
46
52
|
|
|
53
|
+
export interface RedactionCategoryCount {
|
|
54
|
+
category: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
count: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function countRedactionsByCategory(input: string): RedactionCategoryCount[] {
|
|
60
|
+
const counts = new Map<string, number>();
|
|
61
|
+
for (const rule of RULES) {
|
|
62
|
+
rule.pattern.lastIndex = 0;
|
|
63
|
+
const matches = input.match(rule.pattern);
|
|
64
|
+
if (matches && matches.length > 0) {
|
|
65
|
+
counts.set(rule.category, (counts.get(rule.category) || 0) + matches.length);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const result: RedactionCategoryCount[] = [];
|
|
69
|
+
for (const rule of RULES) {
|
|
70
|
+
const count = counts.get(rule.category);
|
|
71
|
+
if (count) result.push({ category: rule.category, reason: rule.reason, count });
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
export function redactSensitiveContent(input: string): RedactionResult {
|
|
48
77
|
let content = input;
|
|
49
78
|
const categories: string[] = [];
|
|
@@ -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
|
}
|