patchwarden 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +32 -2
- package/README.en.md +152 -25
- package/README.md +120 -23
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentStore.js +42 -34
- package/dist/config.d.ts +15 -0
- package/dist/config.js +66 -13
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +6 -0
- package/dist/control/middleware/auth.js +14 -5
- package/dist/control/middleware/static.js +16 -3
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.js +17 -7
- package/dist/control/routes/evidence.js +1 -1
- package/dist/control/routes/lineage.js +1 -1
- package/dist/control/routes/process.d.ts +7 -0
- package/dist/control/routes/process.js +329 -42
- package/dist/control/routes/sessions.d.ts +9 -1
- package/dist/control/routes/sessions.js +84 -21
- package/dist/control/routes/status.d.ts +46 -1
- package/dist/control/routes/status.js +72 -15
- package/dist/control/routes/taskActions.js +24 -60
- package/dist/control/routes/tasks.d.ts +4 -0
- package/dist/control/routes/tasks.js +61 -62
- package/dist/control/routes/workspace.js +50 -23
- package/dist/control/runtime.d.ts +3 -2
- package/dist/control/runtime.js +63 -30
- package/dist/control/server.js +36 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +42 -29
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +62 -52
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.js +5 -12
- package/dist/goal/goalStatus.d.ts +2 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.js +97 -96
- package/dist/goal/subgoalSync.js +11 -9
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +111 -32
- package/dist/logging.js +5 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +13 -5
- package/dist/runner/changeCapture.js +243 -61
- package/dist/runner/postTaskCleanup.js +48 -19
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +115 -87
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +50 -36
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +256 -48
- package/dist/security/contentRedaction.js +2 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +24 -23
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.js +32 -31
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
- package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
- package/dist/tools/dispatch/directDispatch.js +11 -10
- package/dist/tools/dispatch/goalDispatch.js +5 -4
- package/dist/tools/dispatch/releaseDispatch.js +3 -2
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +7 -11
- package/dist/tools/registry.js +17 -1178
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
- package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +808 -143
- package/docs/architecture.md +15 -55
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +4 -4
- package/docs/desktop-app.md +175 -0
- package/docs/evidence-pack-schema.md +3 -3
- package/docs/open-source-application.md +17 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +39 -29
- package/docs/task-safe-review-workflow.md +5 -5
- package/docs/threat-model.md +19 -1
- package/examples/config.example.json +4 -2
- package/package.json +27 -10
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +91 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +10 -10
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +27 -3
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +26 -12
- package/src/assessments/assessmentStore.ts +52 -40
- package/src/config.ts +70 -13
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +22 -4
- package/src/control/middleware/static.ts +16 -3
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +14 -6
- package/src/control/routes/evidence.ts +1 -1
- package/src/control/routes/lineage.ts +1 -1
- package/src/control/routes/process.ts +341 -40
- package/src/control/routes/sessions.ts +88 -25
- package/src/control/routes/status.ts +97 -17
- package/src/control/routes/taskActions.ts +24 -58
- package/src/control/routes/tasks.ts +66 -58
- package/src/control/routes/workspace.ts +56 -25
- package/src/control/runtime.ts +69 -33
- package/src/control/server.ts +36 -342
- package/src/control/shared.ts +77 -11
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +72 -44
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +73 -59
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +5 -13
- package/src/goal/goalStatus.ts +19 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +12 -12
- package/src/goal/subgoalSync.ts +11 -8
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +113 -30
- package/src/logging.ts +10 -3
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +291 -57
- package/src/runner/postTaskCleanup.ts +41 -17
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +138 -91
- package/src/runner/simpleProcess.ts +66 -31
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +281 -51
- package/src/security/contentRedaction.ts +2 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +25 -24
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +36 -31
- package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
- package/src/tools/dispatch/directDispatch.ts +11 -10
- package/src/tools/dispatch/goalDispatch.ts +5 -4
- package/src/tools/dispatch/releaseDispatch.ts +3 -2
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +17 -1261
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +12 -0
- package/ui/pages/dashboard.html +90 -147
- package/ui/pages/direct-sessions.html +123 -26
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +12 -0
- package/ui/pages/tasks.html +54 -8
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/apply-patch.test.d.ts +0 -1
- package/dist/test/unit/apply-patch.test.js +0 -225
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/create-task.test.d.ts +0 -1
- package/dist/test/unit/create-task.test.js +0 -197
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -328
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -224
- package/dist/test/unit/get-task-status.test.d.ts +0 -1
- package/dist/test/unit/get-task-status.test.js +0 -174
- package/dist/test/unit/get-task-summary.test.d.ts +0 -1
- package/dist/test/unit/get-task-summary.test.js +0 -146
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-report.test.d.ts +0 -1
- package/dist/test/unit/goal-report.test.js +0 -159
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -192
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -133
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
- package/dist/test/unit/spec-kit-import.test.js +0 -341
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/wait-for-task.test.d.ts +0 -1
- package/dist/test/unit/wait-for-task.test.js +0 -144
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/execution-plan-2026-07-09.md +0 -315
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/docs/roadmap-execution-and-acceptance.md +0 -365
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/apply-patch.test.ts +0 -293
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/create-task.test.ts +0 -255
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -467
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -251
- package/src/test/unit/get-task-status.test.ts +0 -203
- package/src/test/unit/get-task-summary.test.ts +0 -173
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-report.test.ts +0 -189
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -245
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -180
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/spec-kit-import.test.ts +0 -429
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/wait-for-task.test.ts +0 -176
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
copyFileSync,
|
|
4
4
|
existsSync,
|
|
5
5
|
mkdirSync,
|
|
6
6
|
readFileSync,
|
|
@@ -16,7 +16,8 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
|
|
17
17
|
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
18
18
|
const root = resolve(scriptDir, "..", "..");
|
|
19
|
-
const
|
|
19
|
+
const releaseRoot = resolve(root, "release");
|
|
20
|
+
const releaseDir = resolve(releaseRoot, "package");
|
|
20
21
|
const archivePath = resolve(root, "patchwarden-release.tar.gz");
|
|
21
22
|
|
|
22
23
|
// Read version from package.json for zip naming
|
|
@@ -31,11 +32,6 @@ const include = [
|
|
|
31
32
|
"src",
|
|
32
33
|
"ui",
|
|
33
34
|
"PatchWarden.cmd",
|
|
34
|
-
"PatchWarden-Control.cmd",
|
|
35
|
-
"PatchWarden-Control-Tray.cmd",
|
|
36
|
-
"PatchWarden-Desktop.cmd",
|
|
37
|
-
"Restart-PatchWarden-Control.cmd",
|
|
38
|
-
"Stop-PatchWarden.cmd",
|
|
39
35
|
".gitignore",
|
|
40
36
|
"LICENSE",
|
|
41
37
|
"README.md",
|
|
@@ -57,6 +53,8 @@ const forbidden = [
|
|
|
57
53
|
/(^|[\\/])\.env$/,
|
|
58
54
|
/\.log$/,
|
|
59
55
|
/^docs[\\/]optimization-proposal\.md$/i,
|
|
56
|
+
/^docs[\\/]archive(?:[\\/]|$)/i,
|
|
57
|
+
/^(?:dist|src)[\\/]test(?:[\\/]|$)/i,
|
|
60
58
|
/(^|[\\/])kill-patchwarden\.(cmd|ps1)$/i,
|
|
61
59
|
];
|
|
62
60
|
|
|
@@ -173,9 +171,15 @@ function readZipEntryNames(zipPath) {
|
|
|
173
171
|
return names;
|
|
174
172
|
}
|
|
175
173
|
|
|
174
|
+
const stageRelativeToRoot = toPosix(relative(root, releaseDir));
|
|
175
|
+
if (stageRelativeToRoot !== "release/package") {
|
|
176
|
+
throw new Error(`Refusing to clean non-isolated release staging path: ${stageRelativeToRoot}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
176
179
|
console.log("[pack-clean] Preparing clean release directory...");
|
|
177
180
|
rmSync(releaseDir, { recursive: true, force: true });
|
|
178
181
|
rmSync(archivePath, { force: true });
|
|
182
|
+
mkdirSync(releaseRoot, { recursive: true });
|
|
179
183
|
mkdirSync(releaseDir, { recursive: true });
|
|
180
184
|
|
|
181
185
|
for (const item of include) {
|
|
@@ -185,17 +189,26 @@ for (const item of include) {
|
|
|
185
189
|
console.log(` skip missing: ${item}`);
|
|
186
190
|
continue;
|
|
187
191
|
}
|
|
188
|
-
|
|
189
|
-
recursive: true,
|
|
190
|
-
filter(sourcePath) {
|
|
191
|
-
const rel = toPosix(relative(root, sourcePath));
|
|
192
|
-
return !isForbidden(rel);
|
|
193
|
-
},
|
|
194
|
-
});
|
|
192
|
+
copyAllowedEntry(source, target);
|
|
195
193
|
console.log(` copied: ${item}`);
|
|
196
194
|
}
|
|
197
195
|
|
|
196
|
+
// Keep an independent second boundary before the fail-closed validation. This
|
|
197
|
+
// protects package contents if a future copy implementation changes behavior.
|
|
198
|
+
pruneForbiddenEntries(releaseDir);
|
|
199
|
+
|
|
198
200
|
const releaseFiles = listFiles(releaseDir);
|
|
201
|
+
const requiredReleaseEntries = ["package.json", "README.md", "dist/index.js"];
|
|
202
|
+
const missingReleaseEntries = requiredReleaseEntries.filter(
|
|
203
|
+
(entry) => !existsSync(resolve(releaseDir, entry))
|
|
204
|
+
);
|
|
205
|
+
if (releaseFiles.length === 0 || missingReleaseEntries.length > 0) {
|
|
206
|
+
console.error("[pack-clean] Release directory is incomplete:");
|
|
207
|
+
for (const entry of missingReleaseEntries) {
|
|
208
|
+
console.error(` missing: ${entry}`);
|
|
209
|
+
}
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
199
212
|
const badReleaseEntries = releaseFiles.filter((file) =>
|
|
200
213
|
isForbidden(toPosix(relative(releaseDir, file)))
|
|
201
214
|
);
|
|
@@ -296,6 +309,38 @@ function listFiles(dir) {
|
|
|
296
309
|
return entries;
|
|
297
310
|
}
|
|
298
311
|
|
|
312
|
+
function copyAllowedEntry(source, target) {
|
|
313
|
+
const rel = toPosix(relative(root, source));
|
|
314
|
+
if (isForbidden(rel)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const stat = statSync(source);
|
|
319
|
+
if (stat.isDirectory()) {
|
|
320
|
+
mkdirSync(target, { recursive: true });
|
|
321
|
+
for (const name of readdirSync(source)) {
|
|
322
|
+
copyAllowedEntry(join(source, name), join(target, name));
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
copyFileSync(source, target);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function pruneForbiddenEntries(dir) {
|
|
331
|
+
for (const name of readdirSync(dir)) {
|
|
332
|
+
const fullPath = join(dir, name);
|
|
333
|
+
const rel = toPosix(relative(releaseDir, fullPath));
|
|
334
|
+
if (isForbidden(rel)) {
|
|
335
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (statSync(fullPath).isDirectory()) {
|
|
339
|
+
pruneForbiddenEntries(fullPath);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
299
344
|
function isForbidden(value) {
|
|
300
345
|
return forbidden.some((pattern) => pattern.test(value));
|
|
301
346
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve, relative, isAbsolute } from "node:path";
|
|
1
|
+
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
3
2
|
import { PatchWardenConfig } from "../config.js";
|
|
4
3
|
import { buildAgentInvocation, buildAssessmentPrompt } from "../runner/agentInvocation.js";
|
|
5
4
|
import { runSimpleProcessSync } from "../runner/simpleProcess.js";
|
|
6
5
|
import { captureRepoSnapshot, compareSnapshots, type RepoSnapshot, type ChangedFile } from "../runner/changeCapture.js";
|
|
7
6
|
import type { AgentAssessmentOutput, AgentAssessmentSummary } from "./assessmentStore.js";
|
|
7
|
+
import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
8
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
8
9
|
|
|
9
10
|
const ASSESSMENT_MARKER = "===ASSESSMENT_JSON===";
|
|
10
11
|
|
|
@@ -45,9 +46,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
// ── 1. Build prompt and write to file for {prompt_file} support ──
|
|
48
|
-
const
|
|
49
|
+
const safeGoal = redactSensitiveContent(input.goal).content.slice(0, 10_000);
|
|
50
|
+
const safePlan = redactSensitiveContent(input.planContent).content.slice(0, 1024 * 1024);
|
|
51
|
+
const assessmentPrompt = buildAssessmentPrompt(safeGoal, safePlan, input.repoPath);
|
|
49
52
|
const promptFilePath = resolve(input.assessmentDir, "agent-assessment-prompt.md");
|
|
50
|
-
|
|
53
|
+
atomicWriteFileSync(promptFilePath, assessmentPrompt);
|
|
51
54
|
logPaths.prompt = promptFilePath;
|
|
52
55
|
|
|
53
56
|
// ── 2. Before snapshot (repo-scoped) ──
|
|
@@ -89,6 +92,8 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
|
|
|
89
92
|
maxStderrBytes: Math.max(16384, Math.floor(input.maxOutputBytes / 4)),
|
|
90
93
|
stdoutPath: logPaths.stdout,
|
|
91
94
|
stderrPath: logPaths.stderr,
|
|
95
|
+
environmentVariableNames: invocation.environmentVariableNames,
|
|
96
|
+
blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
// ── 5. After snapshot + read-only violation check ──
|
|
@@ -123,11 +128,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
|
|
|
123
128
|
stderr_truncated: result.stderrTruncated,
|
|
124
129
|
log_paths: { ...logPaths, violation: resolve(input.assessmentDir, "agent-assessment-violation.json") },
|
|
125
130
|
};
|
|
126
|
-
|
|
131
|
+
atomicWriteJsonFileSync(summary.log_paths.violation!, {
|
|
127
132
|
violation: "read_only_violation",
|
|
128
133
|
files: violationFiles,
|
|
129
134
|
exit_code: result.exitCode,
|
|
130
|
-
}
|
|
135
|
+
});
|
|
131
136
|
writeSummary(logPaths.assessment, summary);
|
|
132
137
|
return summary;
|
|
133
138
|
}
|
|
@@ -253,12 +258,16 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
|
|
|
253
258
|
const jsonEnd = jsonText.lastIndexOf("}");
|
|
254
259
|
if (jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart) return { output: null, sanitized_reasons };
|
|
255
260
|
|
|
256
|
-
let
|
|
261
|
+
let parsedValue: unknown;
|
|
257
262
|
try {
|
|
258
|
-
|
|
263
|
+
parsedValue = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
|
|
259
264
|
} catch {
|
|
260
265
|
return { output: null, sanitized_reasons };
|
|
261
266
|
}
|
|
267
|
+
if (!parsedValue || typeof parsedValue !== "object" || Array.isArray(parsedValue)) {
|
|
268
|
+
return { output: null, sanitized_reasons };
|
|
269
|
+
}
|
|
270
|
+
const parsed = parsedValue as Record<string, unknown>;
|
|
262
271
|
|
|
263
272
|
// ── Strict validation ──
|
|
264
273
|
if (parsed.risk_level !== "low" && parsed.risk_level !== "medium" && parsed.risk_level !== "high") {
|
|
@@ -272,7 +281,9 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
|
|
|
272
281
|
|
|
273
282
|
if (!Array.isArray(parsed.reason_codes)) return { output: null, sanitized_reasons };
|
|
274
283
|
if (parsed.reason_codes.length > 50) return { output: null, sanitized_reasons };
|
|
275
|
-
const reasonCodes = parsed.reason_codes.filter((
|
|
284
|
+
const reasonCodes = parsed.reason_codes.filter((reason): reason is string =>
|
|
285
|
+
typeof reason === "string" && reason.length <= 100
|
|
286
|
+
);
|
|
276
287
|
if (reasonCodes.length !== parsed.reason_codes.length) {
|
|
277
288
|
sanitized_reasons.push("reason_codes_filtered");
|
|
278
289
|
}
|
|
@@ -287,7 +298,10 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
|
|
|
287
298
|
if (isAbsolute(p)) { pathsSanitized = true; continue; }
|
|
288
299
|
const resolved = resolve(repoPath, p);
|
|
289
300
|
const rel = relative(repoPath, resolved);
|
|
290
|
-
if (rel
|
|
301
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
302
|
+
pathsSanitized = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
291
305
|
affectedPaths.push(p);
|
|
292
306
|
}
|
|
293
307
|
if (pathsSanitized) {
|
|
@@ -297,7 +311,7 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
|
|
|
297
311
|
if (!Array.isArray(parsed.destructive_actions)) return { output: null, sanitized_reasons };
|
|
298
312
|
if (parsed.destructive_actions.length > 20) return { output: null, sanitized_reasons };
|
|
299
313
|
const destructiveActions = parsed.destructive_actions.filter(
|
|
300
|
-
(
|
|
314
|
+
(action): action is string => typeof action === "string" && action.length <= 200
|
|
301
315
|
);
|
|
302
316
|
|
|
303
317
|
if (typeof parsed.requires_user_confirm !== "boolean") return { output: null, sanitized_reasons };
|
|
@@ -320,5 +334,5 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
|
|
|
320
334
|
}
|
|
321
335
|
|
|
322
336
|
function writeSummary(path: string, summary: AgentAssessmentSummary): void {
|
|
323
|
-
|
|
337
|
+
atomicWriteJsonFileSync(path, summary);
|
|
324
338
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mkdirSync,
|
|
3
|
-
writeFileSync,
|
|
4
3
|
existsSync,
|
|
5
4
|
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
5
|
} from "node:fs";
|
|
8
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
7
|
import { resolve, join } from "node:path";
|
|
10
8
|
import { getAssessmentsDir, getConfig, getRepoAllowedTestCommands, PatchWardenConfig } from "../config.js";
|
|
11
9
|
import { guardPath } from "../security/pathGuard.js";
|
|
12
10
|
import { PatchWardenError } from "../errors.js";
|
|
11
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
12
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
13
|
+
import { withFileLockSync } from "../utils/lockedJsonFile.js";
|
|
13
14
|
import { TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
14
|
-
import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
|
|
15
|
+
import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
|
|
15
16
|
import { captureRepoSnapshot, type RepoSnapshot } from "../runner/changeCapture.js";
|
|
16
17
|
import type { RiskAssessmentResult } from "../security/riskEngine.js";
|
|
17
18
|
import type { TaskTemplateName, ChangePolicy } from "../tools/taskTemplates.js";
|
|
@@ -203,7 +204,7 @@ export function createAssessment(input: AssessmentCreateInput): AssessmentRecord
|
|
|
203
204
|
repo_path: input.repo_path,
|
|
204
205
|
resolved_repo_path: input.resolved_repo_path,
|
|
205
206
|
template: input.template || null,
|
|
206
|
-
goal: input.goal
|
|
207
|
+
goal: input.goal ? redactSensitiveContent(input.goal).content : null,
|
|
207
208
|
test_command: input.test_command || null,
|
|
208
209
|
verify_commands: input.verify_commands || [],
|
|
209
210
|
agent: input.agent,
|
|
@@ -216,7 +217,7 @@ export function createAssessment(input: AssessmentCreateInput): AssessmentRecord
|
|
|
216
217
|
agent_assessment_summary: input.agent_assessment_summary || null,
|
|
217
218
|
};
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
atomicWriteJsonFileSync(join(dir, "assessment.json"), record);
|
|
220
221
|
|
|
221
222
|
return record;
|
|
222
223
|
}
|
|
@@ -323,6 +324,10 @@ export async function confirmAssessment(assessmentId: string): Promise<Assessmen
|
|
|
323
324
|
);
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
const config = getConfig();
|
|
328
|
+
const assessmentFile = resolve(getAssessmentsDir(config), assessmentId, "assessment.json");
|
|
329
|
+
guardPath(assessmentFile, config.workspaceRoot, config.assessmentsDir);
|
|
330
|
+
|
|
326
331
|
const assessment = readAssessment(assessmentId);
|
|
327
332
|
if (assessment.decision !== "needs_confirm" || !assessment.requires_confirm) {
|
|
328
333
|
throw new PatchWardenError(
|
|
@@ -335,43 +340,50 @@ export async function confirmAssessment(assessmentId: string): Promise<Assessmen
|
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
const snapshot = await captureRepoSnapshot(assessment.resolved_repo_path);
|
|
338
|
-
const validation = validateAssessmentFreshness(assessmentId, snapshot, {
|
|
339
|
-
allow_unconfirmed: true,
|
|
340
|
-
// A standalone CLI has no active MCP profile snapshot. The execute path
|
|
341
|
-
// still performs the full tool-manifest check before creating the task.
|
|
342
|
-
skip_tool_manifest: true,
|
|
343
|
-
});
|
|
344
|
-
if (!validation.valid || !validation.assessment) {
|
|
345
|
-
throw new PatchWardenError(
|
|
346
|
-
validation.failure_reason || "assessment_validation_failed",
|
|
347
|
-
`Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`,
|
|
348
|
-
"Run create_task with execution_mode=assess_only again, then confirm the new assessment locally."
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
343
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
// Re-read and write while holding one cross-process lock. Concurrent local
|
|
345
|
+
// confirmers may compute snapshots in parallel, but they cannot replace one
|
|
346
|
+
// another's confirmation metadata from a stale assessment record.
|
|
347
|
+
return withFileLockSync(assessmentFile, () => {
|
|
348
|
+
const validation = validateAssessmentFreshness(assessmentId, snapshot, {
|
|
349
|
+
allow_unconfirmed: true,
|
|
350
|
+
// A standalone CLI has no active MCP profile snapshot. The execute path
|
|
351
|
+
// still performs the full tool-manifest check before creating the task.
|
|
352
|
+
skip_tool_manifest: true,
|
|
353
|
+
});
|
|
354
|
+
if (!validation.valid || !validation.assessment) {
|
|
355
|
+
throw new PatchWardenError(
|
|
356
|
+
validation.failure_reason || "assessment_validation_failed",
|
|
357
|
+
`Assessment "${assessmentId}" cannot be confirmed: ${validation.failure_reason || "validation_failed"}.`,
|
|
358
|
+
"Run create_task with execution_mode=assess_only again, then confirm the new assessment locally."
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (validation.assessment.decision !== "needs_confirm" || !validation.assessment.requires_confirm) {
|
|
362
|
+
throw new PatchWardenError(
|
|
363
|
+
"assessment_confirmation_not_allowed",
|
|
364
|
+
`Assessment "${assessmentId}" is no longer eligible for local confirmation.`,
|
|
365
|
+
"Run create_task with execution_mode=assess_only again."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
366
368
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
369
|
+
const confirmedAt = validation.assessment.confirmed_at || new Date().toISOString();
|
|
370
|
+
const confirmedRecord: AssessmentRecord = {
|
|
371
|
+
...validation.assessment,
|
|
372
|
+
confirmed: true,
|
|
373
|
+
confirmed_at: confirmedAt,
|
|
374
|
+
confirm_code: validation.assessment.confirm_code || randomBytes(8).toString("hex"),
|
|
375
|
+
};
|
|
376
|
+
atomicWriteJsonFileSync(assessmentFile, confirmedRecord);
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
assessment_id: assessmentId,
|
|
380
|
+
decision: "needs_confirm",
|
|
381
|
+
confirmed: true,
|
|
382
|
+
confirmed_at: confirmedAt,
|
|
383
|
+
expires_at: confirmedRecord.expires_at,
|
|
384
|
+
next_action: "Call create_task with only execution_mode=execute and this full assessment_id.",
|
|
385
|
+
};
|
|
386
|
+
});
|
|
375
387
|
}
|
|
376
388
|
|
|
377
389
|
export function computeWorkspaceFingerprint(snapshot: RepoSnapshot): string {
|
package/src/config.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { validateWorkspaceRoot } from "./security/workspaceRootGuard.js";
|
|
3
4
|
|
|
4
5
|
// ── Type definitions ──────────────────────────────────────────────
|
|
5
6
|
|
|
6
7
|
export interface AgentConfig {
|
|
7
8
|
command: string;
|
|
8
9
|
args: string[];
|
|
10
|
+
envAllowlist?: string[];
|
|
11
|
+
adapter?: string;
|
|
12
|
+
model?: string;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export interface PatchWardenConfig {
|
|
@@ -31,6 +35,12 @@ export interface PatchWardenConfig {
|
|
|
31
35
|
agentAssessmentMaxOutputBytes?: number;
|
|
32
36
|
agentAssessmentAgentName?: string;
|
|
33
37
|
enableDirectProfile?: boolean;
|
|
38
|
+
tunnelClientPath?: string;
|
|
39
|
+
tunnelProxy?: {
|
|
40
|
+
scope: "shared" | "separate";
|
|
41
|
+
core: { mode: "environment" | "none" | "manual"; url?: string };
|
|
42
|
+
direct: { mode: "environment" | "none" | "manual"; url?: string };
|
|
43
|
+
};
|
|
34
44
|
directSessionsDir: string;
|
|
35
45
|
directSessionTtlSeconds: number;
|
|
36
46
|
directMaxPatchBytes: number;
|
|
@@ -47,16 +57,7 @@ const DEFAULT_CONFIG: PatchWardenConfig = {
|
|
|
47
57
|
tasksDir: ".patchwarden/tasks",
|
|
48
58
|
assessmentsDir: ".patchwarden/assessments",
|
|
49
59
|
assessmentTtlSeconds: 3600,
|
|
50
|
-
agents: {
|
|
51
|
-
codex: {
|
|
52
|
-
command: "codex",
|
|
53
|
-
args: ["exec", "--cd", "{repo}", "{prompt}"],
|
|
54
|
-
},
|
|
55
|
-
opencode: {
|
|
56
|
-
command: "opencode",
|
|
57
|
-
args: ["run", "{prompt}"],
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
+
agents: {},
|
|
60
61
|
allowedTestCommands: [
|
|
61
62
|
"npm test",
|
|
62
63
|
"npm run test",
|
|
@@ -175,6 +176,11 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
175
176
|
if (!config.workspaceRoot || typeof config.workspaceRoot !== "string") {
|
|
176
177
|
throw new Error("workspaceRoot must be a non-empty string");
|
|
177
178
|
}
|
|
179
|
+
const workspaceValidation = validateWorkspaceRoot(config.workspaceRoot);
|
|
180
|
+
if (!workspaceValidation.ok) {
|
|
181
|
+
throw new Error(`Invalid workspaceRoot: ${workspaceValidation.reason} (${workspaceValidation.path})`);
|
|
182
|
+
}
|
|
183
|
+
config.workspaceRoot = workspaceValidation.path;
|
|
178
184
|
if (!config.plansDir || typeof config.plansDir !== "string") {
|
|
179
185
|
throw new Error("plansDir must be a non-empty string");
|
|
180
186
|
}
|
|
@@ -190,6 +196,20 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
190
196
|
if (!config.agents || typeof config.agents !== "object") {
|
|
191
197
|
throw new Error("agents must be an object");
|
|
192
198
|
}
|
|
199
|
+
const agents: Record<string, AgentConfig> = {};
|
|
200
|
+
for (const [agentName, agent] of Object.entries(config.agents)) {
|
|
201
|
+
if (!agent || typeof agent !== "object" || typeof agent.command !== "string" || !Array.isArray(agent.args)) {
|
|
202
|
+
throw new Error(`agents["${agentName}"] must define command and args`);
|
|
203
|
+
}
|
|
204
|
+
const envAllowlist = agent.envAllowlist ?? [];
|
|
205
|
+
if (!Array.isArray(envAllowlist) || envAllowlist.some((name) => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
|
|
206
|
+
throw new Error(`agents["${agentName}"].envAllowlist must contain only environment variable names`);
|
|
207
|
+
}
|
|
208
|
+
const blockedNames = new Set(["CONTROL_PLANE_API_KEY", "PATCHWARDEN_OWNER_TOKEN", config.http?.ownerTokenEnv?.toUpperCase()]);
|
|
209
|
+
const blocked = envAllowlist.find((name) => blockedNames.has(name.toUpperCase()));
|
|
210
|
+
if (blocked) throw new Error(`agents["${agentName}"].envAllowlist cannot include reserved variable "${blocked}"`);
|
|
211
|
+
agents[agentName] = { ...agent, args: [...agent.args], envAllowlist: [...new Set(envAllowlist)] };
|
|
212
|
+
}
|
|
193
213
|
if (!Array.isArray(config.allowedTestCommands)) {
|
|
194
214
|
throw new Error("allowedTestCommands must be an array");
|
|
195
215
|
}
|
|
@@ -263,8 +283,14 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
263
283
|
if (config.http.host !== undefined && typeof config.http.host !== "string") {
|
|
264
284
|
throw new Error("http.host must be a string");
|
|
265
285
|
}
|
|
266
|
-
if (
|
|
267
|
-
|
|
286
|
+
if (
|
|
287
|
+
config.http.ownerTokenEnv !== undefined
|
|
288
|
+
&& (
|
|
289
|
+
typeof config.http.ownerTokenEnv !== "string"
|
|
290
|
+
|| !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.http.ownerTokenEnv)
|
|
291
|
+
)
|
|
292
|
+
) {
|
|
293
|
+
throw new Error("http.ownerTokenEnv must be a valid environment variable name");
|
|
268
294
|
}
|
|
269
295
|
}
|
|
270
296
|
if (config.enableRunTaskTool !== undefined && typeof config.enableRunTaskTool !== "boolean") {
|
|
@@ -289,6 +315,36 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
289
315
|
if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
|
|
290
316
|
throw new Error("enableDirectProfile must be a boolean");
|
|
291
317
|
}
|
|
318
|
+
if (config.tunnelClientPath !== undefined) {
|
|
319
|
+
if (typeof config.tunnelClientPath !== "string" || !isAbsolute(config.tunnelClientPath)) {
|
|
320
|
+
throw new Error("tunnelClientPath must be an absolute path");
|
|
321
|
+
}
|
|
322
|
+
if (basename(config.tunnelClientPath).toLowerCase() !== "tunnel-client.exe") {
|
|
323
|
+
throw new Error("tunnelClientPath must point to tunnel-client.exe");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (config.tunnelProxy !== undefined) {
|
|
327
|
+
if (!config.tunnelProxy || typeof config.tunnelProxy !== "object" || !["shared", "separate"].includes(config.tunnelProxy.scope)) {
|
|
328
|
+
throw new Error("tunnelProxy.scope must be shared or separate");
|
|
329
|
+
}
|
|
330
|
+
for (const key of ["core", "direct"] as const) {
|
|
331
|
+
const endpoint = config.tunnelProxy[key];
|
|
332
|
+
if (!endpoint || typeof endpoint !== "object" || !["environment", "none", "manual"].includes(endpoint.mode)) {
|
|
333
|
+
throw new Error(`tunnelProxy.${key}.mode must be environment, none, or manual`);
|
|
334
|
+
}
|
|
335
|
+
if (endpoint.mode === "manual") {
|
|
336
|
+
if (typeof endpoint.url !== "string") throw new Error(`tunnelProxy.${key}.url is required in manual mode`);
|
|
337
|
+
let parsed: URL;
|
|
338
|
+
try { parsed = new URL(endpoint.url); } catch { throw new Error(`tunnelProxy.${key}.url must be a valid URL`); }
|
|
339
|
+
if (!["http:", "https:", "socks5:"].includes(parsed.protocol)) {
|
|
340
|
+
throw new Error(`tunnelProxy.${key}.url must use http, https, or socks5`);
|
|
341
|
+
}
|
|
342
|
+
if (parsed.username || parsed.password) throw new Error(`tunnelProxy.${key}.url must not contain credentials`);
|
|
343
|
+
} else if (endpoint.url !== undefined) {
|
|
344
|
+
throw new Error(`tunnelProxy.${key}.url is only allowed in manual mode`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
292
348
|
if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
|
|
293
349
|
throw new Error("directSessionsDir must be a non-empty string");
|
|
294
350
|
}
|
|
@@ -331,6 +387,7 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
331
387
|
|
|
332
388
|
return {
|
|
333
389
|
...config,
|
|
390
|
+
agents,
|
|
334
391
|
workspaceRoot: resolve(config.workspaceRoot),
|
|
335
392
|
allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
|
|
336
393
|
repoAllowedTestCommands,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { buildChildEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
|
|
4
|
+
|
|
5
|
+
const FILE_MANAGER_ENVIRONMENT = [
|
|
6
|
+
"DISPLAY",
|
|
7
|
+
"WAYLAND_DISPLAY",
|
|
8
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
9
|
+
"XDG_RUNTIME_DIR",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
/** Launch a trusted platform file manager without inheriting ambient secrets. */
|
|
13
|
+
export function launchFileManager(target: string, cwd: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const env = buildChildEnvironment({ cwd, allowedNames: FILE_MANAGER_ENVIRONMENT });
|
|
16
|
+
const requested = fileManagerCommand(env);
|
|
17
|
+
const command = resolveTrustedExecutable(requested, cwd, { pathValue: env.PATH });
|
|
18
|
+
const child = spawn(command, [target], {
|
|
19
|
+
cwd,
|
|
20
|
+
detached: true,
|
|
21
|
+
env,
|
|
22
|
+
stdio: "ignore",
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
});
|
|
25
|
+
child.on("error", () => { /* opening a convenience viewer is best effort */ });
|
|
26
|
+
child.unref();
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fileManagerCommand(env: NodeJS.ProcessEnv): string {
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
const windowsRoot = env.SystemRoot || env.WINDIR;
|
|
36
|
+
return windowsRoot ? join(windowsRoot, "explorer.exe") : "explorer.exe";
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === "darwin") return "open";
|
|
39
|
+
return "xdg-open";
|
|
40
|
+
}
|
|
@@ -7,11 +7,29 @@
|
|
|
7
7
|
* action must pass through `checkControlToken` first.
|
|
8
8
|
*/
|
|
9
9
|
import { type IncomingMessage } from "node:http";
|
|
10
|
+
import { firstHeaderValue, timingSafeStringEqual } from "../../security/secretComparison.js";
|
|
11
|
+
import { isTrustedLoopbackHostHeader } from "../../security/loopbackHost.js";
|
|
10
12
|
import { controlToken } from "../shared.js";
|
|
11
13
|
|
|
12
14
|
export function checkControlToken(req: IncomingMessage): boolean {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const provided = firstHeaderValue(req.headers["x-patchwarden-control-token"]);
|
|
16
|
+
return provided.length > 0 && timingSafeStringEqual(provided, controlToken);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reject non-loopback Host headers before exposing the browser-readable
|
|
21
|
+
* control token. Binding to 127.0.0.1 alone does not prevent DNS rebinding.
|
|
22
|
+
*/
|
|
23
|
+
export function isTrustedControlHost(
|
|
24
|
+
req: IncomingMessage,
|
|
25
|
+
expectedPort: number,
|
|
26
|
+
): boolean {
|
|
27
|
+
return isTrustedControlHostHeader(req.headers.host, expectedPort);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isTrustedControlHostHeader(
|
|
31
|
+
value: string | string[] | undefined,
|
|
32
|
+
expectedPort: number,
|
|
33
|
+
): boolean {
|
|
34
|
+
return isTrustedLoopbackHostHeader(value, expectedPort);
|
|
17
35
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* assets, css) with path-traversal protection, plus the inline SVG favicon.
|
|
6
6
|
* Content types are mapped from `CONTENT_TYPES` in shared.ts.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
9
|
import { extname, isAbsolute, relative, resolve } from "node:path";
|
|
10
10
|
import { type ServerResponse } from "node:http";
|
|
11
11
|
import {
|
|
@@ -51,9 +51,22 @@ export function serveStatic(res: ServerResponse, urlPath: string): void {
|
|
|
51
51
|
sendJson(res, 404, { error: "Not found" });
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// Resolve links before reading. Lexical `..` checks alone do not stop a
|
|
55
|
+
// symlink or junction inside `ui/` from pointing outside the bundle.
|
|
56
|
+
const realRoot = realpathSync(uiRoot);
|
|
57
|
+
const realCandidate = realpathSync(candidate);
|
|
58
|
+
const realRelative = relative(realRoot, realCandidate);
|
|
59
|
+
if (realRelative === "" || realRelative.startsWith("..") || isAbsolute(realRelative)) {
|
|
60
|
+
sendJson(res, 403, { error: "Forbidden" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!statSync(realCandidate).isFile()) {
|
|
64
|
+
sendJson(res, 404, { error: "Not found" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const ext = extname(realCandidate).toLowerCase();
|
|
55
68
|
const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
|
|
56
|
-
const content = readFileSync(
|
|
69
|
+
const content = readFileSync(realCandidate);
|
|
57
70
|
res.writeHead(200, { "Content-Type": contentType });
|
|
58
71
|
res.end(content);
|
|
59
72
|
} catch (err) {
|