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