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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relative } from "node:path";
|
|
2
2
|
import { existsSync, statSync, openSync, readSync, closeSync } from "node:fs";
|
|
3
3
|
import { PatchWardenError } from "../errors.js";
|
|
4
|
-
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
4
|
+
import { hasWindowsAlternateDataStream, isSensitivePath, } from "../security/sensitiveGuard.js";
|
|
5
|
+
import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
5
6
|
import { getConfig } from "../config.js";
|
|
6
7
|
// ── Session state guards ───────────────────────────────────────────
|
|
7
8
|
export function guardDirectSessionActive(session) {
|
|
@@ -23,34 +24,34 @@ export function guardDirectSessionFinalized(session) {
|
|
|
23
24
|
* inside both the repo and the workspace root.
|
|
24
25
|
*/
|
|
25
26
|
export function guardDirectPath(filePath, resolvedRepoPath, workspaceRoot) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
if (hasWindowsAlternateDataStream(filePath)) {
|
|
28
|
+
throw new PatchWardenError("windows_ads_path_blocked", `Path "${filePath}" contains a Windows alternate data stream suffix.`, "Use a normal relative file path without a colon or NTFS stream name.", true, { path: filePath, operation: "direct_path_access" });
|
|
29
|
+
}
|
|
30
|
+
// Resolve the repo first so a repo-level symlink/junction cannot escape the
|
|
31
|
+
// workspace. guardPath then resolves the candidate or its nearest existing
|
|
32
|
+
// parent, closing intermediate symlink/junction escapes for new files too.
|
|
33
|
+
try {
|
|
34
|
+
const guardedRepo = guardWorkspacePath(resolvedRepoPath, workspaceRoot);
|
|
35
|
+
return guardPath(filePath, guardedRepo);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof PatchWardenError && error.reason === "workspace_path_escape") {
|
|
39
|
+
throw new PatchWardenError("path_outside_repo", `Path "${filePath}" resolves outside the session repo.`, "Use a relative path inside the session's repo_path.", true, { path: filePath, operation: "direct_path_access" });
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
39
42
|
}
|
|
40
|
-
return resolved;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Guard a read path: must be inside repo, not sensitive, not binary.
|
|
44
46
|
*/
|
|
45
47
|
export function guardDirectReadPath(filePath, resolvedRepoPath, workspaceRoot) {
|
|
46
48
|
const resolved = guardDirectPath(filePath, resolvedRepoPath, workspaceRoot);
|
|
47
|
-
const
|
|
49
|
+
const policyPaths = directPolicyPaths(filePath, resolved, resolvedRepoPath, workspaceRoot);
|
|
48
50
|
// Block internal PatchWarden paths (sessions, tasks, plans, assessments)
|
|
49
|
-
if (
|
|
50
|
-
normalized.includes("/.patchwarden/")) {
|
|
51
|
+
if (policyPaths.some((path) => hasPathSegment(path, ".patchwarden"))) {
|
|
51
52
|
throw new PatchWardenError("internal_patchwarden_path_blocked", `Access denied: "${filePath}" is inside the internal .patchwarden directory.`, "Internal PatchWarden files cannot be accessed through Direct mode.", true, { path: filePath, operation: "direct_read" });
|
|
52
53
|
}
|
|
53
|
-
if (isSensitivePath(
|
|
54
|
+
if (policyPaths.some((path) => isSensitivePath(path))) {
|
|
54
55
|
throw new PatchWardenError("sensitive_path_blocked", `Access denied: "${filePath}" matches a sensitive file pattern.`, "Read only non-sensitive files within the session repo.", true, { path: filePath, operation: "direct_read" });
|
|
55
56
|
}
|
|
56
57
|
if (existsSync(resolved) && statSync(resolved).isFile() && isBinaryFile(resolved)) {
|
|
@@ -64,25 +65,24 @@ export function guardDirectReadPath(filePath, resolvedRepoPath, workspaceRoot) {
|
|
|
64
65
|
*/
|
|
65
66
|
export function guardDirectWritePath(filePath, resolvedRepoPath, workspaceRoot) {
|
|
66
67
|
const resolved = guardDirectPath(filePath, resolvedRepoPath, workspaceRoot);
|
|
67
|
-
const
|
|
68
|
+
const policyPaths = directPolicyPaths(filePath, resolved, resolvedRepoPath, workspaceRoot);
|
|
68
69
|
// Block internal PatchWarden paths
|
|
69
|
-
if (
|
|
70
|
-
normalized.includes("/.patchwarden/")) {
|
|
70
|
+
if (policyPaths.some((path) => hasPathSegment(path, ".patchwarden"))) {
|
|
71
71
|
throw new PatchWardenError("internal_patchwarden_path_blocked", `Modification denied: "${filePath}" is inside the internal .patchwarden directory.`, "Internal PatchWarden files cannot be modified through Direct mode.", true, { path: filePath, operation: "direct_write" });
|
|
72
72
|
}
|
|
73
|
-
if (isSensitivePath(
|
|
73
|
+
if (policyPaths.some((path) => isSensitivePath(path))) {
|
|
74
74
|
throw new PatchWardenError("sensitive_path_blocked", `Modification denied: "${filePath}" matches a sensitive file pattern.`, "Do not modify .env, credentials, keys, or other sensitive files.", true, { path: filePath, operation: "direct_write" });
|
|
75
75
|
}
|
|
76
76
|
// Block node_modules
|
|
77
|
-
if (
|
|
77
|
+
if (policyPaths.some((path) => hasPathSegment(path, "node_modules"))) {
|
|
78
78
|
throw new PatchWardenError("blocked_artifact_path", `Modification denied: "${filePath}" is inside node_modules.`, "Do not modify dependencies directly. Use package management tools instead.", true, { path: filePath, operation: "direct_write" });
|
|
79
79
|
}
|
|
80
80
|
// Block release/
|
|
81
|
-
if (
|
|
81
|
+
if (policyPaths.some((path) => hasPathSegment(path, "release"))) {
|
|
82
82
|
throw new PatchWardenError("blocked_artifact_path", `Modification denied: "${filePath}" is inside the release directory.`, "Do not manually modify release artifacts.", true, { path: filePath, operation: "direct_write" });
|
|
83
83
|
}
|
|
84
84
|
// Block dist/
|
|
85
|
-
if (
|
|
85
|
+
if (policyPaths.some((path) => hasPathSegment(path, "dist"))) {
|
|
86
86
|
throw new PatchWardenError("blocked_artifact_path", `Modification denied: "${filePath}" is inside the dist directory.`, "Do not manually modify build output. Modify source files instead.", true, { path: filePath, operation: "direct_write" });
|
|
87
87
|
}
|
|
88
88
|
if (existsSync(resolved) && statSync(resolved).isFile() && isBinaryFile(resolved)) {
|
|
@@ -90,6 +90,20 @@ export function guardDirectWritePath(filePath, resolvedRepoPath, workspaceRoot)
|
|
|
90
90
|
}
|
|
91
91
|
return resolved;
|
|
92
92
|
}
|
|
93
|
+
function directPolicyPaths(requestedPath, resolvedPath, resolvedRepoPath, workspaceRoot) {
|
|
94
|
+
const guardedRepo = guardWorkspacePath(resolvedRepoPath, workspaceRoot);
|
|
95
|
+
return [
|
|
96
|
+
requestedPath.replace(/\\/g, "/"),
|
|
97
|
+
relative(guardedRepo, resolvedPath).replace(/\\/g, "/"),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
function hasPathSegment(filePath, expectedSegment) {
|
|
101
|
+
const expected = expectedSegment.toLowerCase();
|
|
102
|
+
return filePath
|
|
103
|
+
.split("/")
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.some((segment) => segment.toLowerCase() === expected);
|
|
106
|
+
}
|
|
93
107
|
// ── Size guards ────────────────────────────────────────────────────
|
|
94
108
|
export function guardDirectPatchSize(patchBytes) {
|
|
95
109
|
const config = getConfig();
|
|
@@ -97,8 +111,7 @@ export function guardDirectPatchSize(patchBytes) {
|
|
|
97
111
|
throw new PatchWardenError("patch_too_large", `Patch size ${patchBytes} bytes exceeds maximum ${config.directMaxPatchBytes} bytes.`, "Split the patch into smaller operations or reduce the patch size.", true, { patch_bytes: patchBytes, max_bytes: config.directMaxPatchBytes });
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
|
-
export function guardDirectFileSize(fileBytes) {
|
|
101
|
-
const config = getConfig();
|
|
114
|
+
export function guardDirectFileSize(fileBytes, config = getConfig()) {
|
|
102
115
|
if (fileBytes > config.directMaxFileBytes) {
|
|
103
116
|
throw new PatchWardenError("file_too_large", `File size ${fileBytes} bytes exceeds maximum ${config.directMaxFileBytes} bytes.`, "Use search_workspace to find specific content, or split the read into smaller portions.", true, { file_bytes: fileBytes, max_bytes: config.directMaxFileBytes });
|
|
104
117
|
}
|
|
@@ -113,23 +126,45 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
113
126
|
".bin", ".dat", ".pak", ".pfx", ".p12", ".class", ".jar", ".war",
|
|
114
127
|
".wasm", ".node",
|
|
115
128
|
]);
|
|
129
|
+
/**
|
|
130
|
+
* Maximum bytes to scan for null-byte detection when the extension is not
|
|
131
|
+
* a known binary type. 1 MB catches binary content embedded deep in files
|
|
132
|
+
* (e.g., null bytes at offset 8200) without reading excessively large files.
|
|
133
|
+
*/
|
|
134
|
+
const BINARY_SCAN_LIMIT = 1_048_576; // 1 MB
|
|
135
|
+
const BINARY_SCAN_CHUNK = 65536; // 64 KB per read
|
|
116
136
|
export function isBinaryFile(filePath) {
|
|
117
137
|
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
118
138
|
for (const ext of BINARY_EXTENSIONS) {
|
|
119
139
|
if (normalized.endsWith(ext))
|
|
120
140
|
return true;
|
|
121
141
|
}
|
|
122
|
-
//
|
|
142
|
+
// Scan file content for null bytes in chunks up to BINARY_SCAN_LIMIT.
|
|
143
|
+
// Previously only the first 8 KB were checked, allowing null bytes at
|
|
144
|
+
// offset 8193+ to bypass detection.
|
|
123
145
|
try {
|
|
146
|
+
const stat = statSync(filePath);
|
|
147
|
+
const scanSize = Math.min(stat.size, BINARY_SCAN_LIMIT);
|
|
124
148
|
const fd = openSync(filePath, "r");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
try {
|
|
150
|
+
const chunk = Buffer.alloc(BINARY_SCAN_CHUNK);
|
|
151
|
+
let scanned = 0;
|
|
152
|
+
while (scanned < scanSize) {
|
|
153
|
+
const toRead = Math.min(BINARY_SCAN_CHUNK, scanSize - scanned);
|
|
154
|
+
const bytesRead = readSync(fd, chunk, 0, toRead, scanned);
|
|
155
|
+
if (bytesRead === 0)
|
|
156
|
+
break;
|
|
157
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
158
|
+
if (chunk[i] === 0)
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
scanned += bytesRead;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
closeSync(fd);
|
|
131
167
|
}
|
|
132
|
-
return false;
|
|
133
168
|
}
|
|
134
169
|
catch {
|
|
135
170
|
return false;
|
|
@@ -13,5 +13,10 @@ export interface ApplyPatchResult {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function computeFileSha256(filePath: string): string;
|
|
15
15
|
export declare function computeContentSha256(content: string): string;
|
|
16
|
+
export interface ApplyPatchOptions {
|
|
17
|
+
expectedSha256?: string;
|
|
18
|
+
maxFileBytes?: number;
|
|
19
|
+
revalidatePath?: () => string;
|
|
20
|
+
}
|
|
16
21
|
export declare function validateExpectedSha256(filePath: string, expectedSha256: string): string;
|
|
17
|
-
export declare function applyPatchOperations(filePath: string, operations: PatchOperation[]): ApplyPatchResult;
|
|
22
|
+
export declare function applyPatchOperations(filePath: string, operations: PatchOperation[], options?: ApplyPatchOptions): ApplyPatchResult;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
3
|
import { PatchWardenError } from "../errors.js";
|
|
4
|
+
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
5
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
5
6
|
// ── Hash helpers ───────────────────────────────────────────────────
|
|
6
7
|
export function computeFileSha256(filePath) {
|
|
7
8
|
const content = readFileSync(filePath);
|
|
@@ -21,12 +22,36 @@ export function validateExpectedSha256(filePath, expectedSha256) {
|
|
|
21
22
|
return currentHash;
|
|
22
23
|
}
|
|
23
24
|
// ── Patch application ──────────────────────────────────────────────
|
|
24
|
-
export function applyPatchOperations(filePath, operations) {
|
|
25
|
+
export function applyPatchOperations(filePath, operations, options = {}) {
|
|
25
26
|
if (!existsSync(filePath)) {
|
|
26
27
|
throw new PatchWardenError("patch_anchor_not_found", `File does not exist: "${filePath}".`, "Ensure the file exists before applying a patch. Direct mode does not support creating new files.", true, { path: filePath });
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
let descriptor = null;
|
|
30
|
+
let beforeBytes;
|
|
31
|
+
let existingMode;
|
|
32
|
+
try {
|
|
33
|
+
descriptor = openSync(filePath, "r");
|
|
34
|
+
const metadata = fstatSync(descriptor);
|
|
35
|
+
if (!metadata.isFile()) {
|
|
36
|
+
throw new PatchWardenError("direct_target_not_file", `Patch target is not a regular file: "${filePath}".`, "Choose an existing text file inside the Direct session repository.");
|
|
37
|
+
}
|
|
38
|
+
guardFileByteLimit(metadata.size, options.maxFileBytes);
|
|
39
|
+
existingMode = metadata.mode;
|
|
40
|
+
beforeBytes = readFileSync(descriptor);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (descriptor !== null)
|
|
44
|
+
closeSync(descriptor);
|
|
45
|
+
}
|
|
46
|
+
guardFileByteLimit(beforeBytes.length, options.maxFileBytes);
|
|
47
|
+
const beforeSha256 = createHash("sha256").update(beforeBytes).digest("hex");
|
|
48
|
+
if (options.expectedSha256 && beforeSha256 !== options.expectedSha256) {
|
|
49
|
+
throw fileHashMismatch(options.expectedSha256, beforeSha256);
|
|
50
|
+
}
|
|
51
|
+
const beforeContent = beforeBytes.toString("utf-8");
|
|
52
|
+
if (!Buffer.from(beforeContent, "utf-8").equals(beforeBytes)) {
|
|
53
|
+
throw new PatchWardenError("unsupported_text_encoding", `File "${filePath}" is not valid UTF-8 text.`, "Use a normal task or convert the file to UTF-8 before Direct editing.", true, { path: filePath });
|
|
54
|
+
}
|
|
30
55
|
let content = beforeContent;
|
|
31
56
|
let operationsApplied = 0;
|
|
32
57
|
for (const op of operations) {
|
|
@@ -34,10 +59,19 @@ export function applyPatchOperations(filePath, operations) {
|
|
|
34
59
|
operationsApplied++;
|
|
35
60
|
}
|
|
36
61
|
const afterSha256 = computeContentSha256(content);
|
|
62
|
+
guardFileByteLimit(Buffer.byteLength(content, "utf-8"), options.maxFileBytes);
|
|
63
|
+
const sensitive = redactSensitiveContent(content);
|
|
64
|
+
if (sensitive.redacted) {
|
|
65
|
+
throw new PatchWardenError("sensitive_content_blocked", `Patch result contains credential-like content (${sensitive.redaction_categories.join(", ")}).`, "Remove the sensitive value and retry with placeholders or environment-variable references.", true, { path: filePath, redaction_categories: sensitive.redaction_categories });
|
|
66
|
+
}
|
|
37
67
|
const bytesChanged = Math.abs(Buffer.byteLength(content, "utf-8") - Buffer.byteLength(beforeContent, "utf-8"));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
if (options.revalidatePath && !samePath(options.revalidatePath(), filePath)) {
|
|
69
|
+
throw new PatchWardenError("direct_path_changed_during_write", `Patch target path changed while preparing "${filePath}".`, "Retry only after concurrent filesystem changes stop.", true, { path: filePath, operation: "apply_patch" });
|
|
70
|
+
}
|
|
71
|
+
const currentSha256 = computeFileSha256(filePath);
|
|
72
|
+
if (currentSha256 !== beforeSha256)
|
|
73
|
+
throw fileHashMismatch(beforeSha256, currentSha256);
|
|
74
|
+
atomicWriteFileSync(filePath, content, { mode: existingMode });
|
|
41
75
|
return {
|
|
42
76
|
before_sha256: beforeSha256,
|
|
43
77
|
after_sha256: afterSha256,
|
|
@@ -45,6 +79,19 @@ export function applyPatchOperations(filePath, operations) {
|
|
|
45
79
|
bytes_changed: bytesChanged,
|
|
46
80
|
};
|
|
47
81
|
}
|
|
82
|
+
function guardFileByteLimit(fileBytes, maxFileBytes) {
|
|
83
|
+
if (maxFileBytes === undefined || fileBytes <= maxFileBytes)
|
|
84
|
+
return;
|
|
85
|
+
throw new PatchWardenError("file_too_large", `File size ${fileBytes} bytes exceeds maximum ${maxFileBytes} bytes.`, "Use a normal task for larger files.", true, { file_bytes: fileBytes, max_bytes: maxFileBytes });
|
|
86
|
+
}
|
|
87
|
+
function fileHashMismatch(expectedSha256, actualSha256) {
|
|
88
|
+
return new PatchWardenError("file_hash_mismatch", `File hash mismatch. Expected "${expectedSha256}" but got "${actualSha256}".`, "Re-read the file to get the current sha256, then retry the patch.", true, { expected_sha256: expectedSha256, actual_sha256: actualSha256 });
|
|
89
|
+
}
|
|
90
|
+
function samePath(left, right) {
|
|
91
|
+
return process.platform === "win32"
|
|
92
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
93
|
+
: left === right;
|
|
94
|
+
}
|
|
48
95
|
function applySingleOperation(content, op) {
|
|
49
96
|
switch (op.type) {
|
|
50
97
|
case "replace_exact":
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { PatchWardenConfig } from "../config.js";
|
|
1
2
|
import { type RepoSnapshot, type ChangeArtifacts } from "../runner/changeCapture.js";
|
|
2
3
|
export interface DirectSessionOperation {
|
|
3
4
|
index: number;
|
|
4
5
|
timestamp: string;
|
|
5
6
|
path: string;
|
|
6
|
-
|
|
7
|
+
operation_type?: "patch" | "sync";
|
|
8
|
+
source_path?: string;
|
|
9
|
+
before_sha256: string | null;
|
|
7
10
|
after_sha256: string;
|
|
8
11
|
operations_applied: number;
|
|
9
12
|
bytes_changed: number;
|
|
@@ -48,12 +51,19 @@ export interface DirectSessionCreateInput {
|
|
|
48
51
|
snapshot: RepoSnapshot;
|
|
49
52
|
}
|
|
50
53
|
export declare function generateDirectSessionId(): string;
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Direct session IDs are directory names. Keep one shared, bounded grammar so
|
|
56
|
+
* MCP tools and Control Center routes cannot disagree about traversal input.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isValidDirectSessionId(sessionId: string): boolean;
|
|
59
|
+
export declare function createDirectSessionDir(sessionId: string, config?: PatchWardenConfig): string;
|
|
60
|
+
export declare function getDirectSessionDir(sessionId: string, config?: PatchWardenConfig): string;
|
|
53
61
|
export declare function createDirectSession(input: DirectSessionCreateInput): DirectSessionRecord;
|
|
54
|
-
export declare function readDirectSession(sessionId: string): DirectSessionRecord;
|
|
62
|
+
export declare function readDirectSession(sessionId: string, config?: PatchWardenConfig): DirectSessionRecord;
|
|
55
63
|
export declare function updateDirectSession(sessionId: string, updates: Partial<DirectSessionRecord>): DirectSessionRecord;
|
|
56
|
-
export declare function appendDirectSessionOperation(sessionId: string, operation: DirectSessionOperation): DirectSessionRecord;
|
|
64
|
+
export declare function appendDirectSessionOperation(sessionId: string, operation: DirectSessionOperation, config?: PatchWardenConfig): DirectSessionRecord;
|
|
65
|
+
export declare function withDirectSessionMutationLock<R>(sessionId: string, action: () => R, config?: PatchWardenConfig): R;
|
|
66
|
+
export declare function withDirectSessionMutationLockAsync<R>(sessionId: string, action: () => Promise<R>, config?: PatchWardenConfig): Promise<R>;
|
|
57
67
|
export declare function appendDirectSessionVerificationRun(sessionId: string, run: DirectSessionVerificationRun): DirectSessionRecord;
|
|
58
68
|
export interface DirectSessionValidationResult {
|
|
59
69
|
valid: boolean;
|
|
@@ -61,5 +71,5 @@ export interface DirectSessionValidationResult {
|
|
|
61
71
|
session: DirectSessionRecord | null;
|
|
62
72
|
}
|
|
63
73
|
export declare function validateDirectSessionFreshness(sessionId: string): DirectSessionValidationResult;
|
|
64
|
-
export declare function finalizeDirectSessionRecord(sessionId: string,
|
|
74
|
+
export declare function finalizeDirectSessionRecord(sessionId: string, changeArtifacts: ChangeArtifacts): DirectSessionRecord;
|
|
65
75
|
export declare function computeWorkspaceFingerprint(snapshot: RepoSnapshot): string;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { mkdirSync,
|
|
1
|
+
import { mkdirSync, existsSync, } from "node:fs";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { resolve, join } from "node:path";
|
|
4
4
|
import { getDirectSessionsDir, getConfig, getRepoDirectAllowedCommands, } from "../config.js";
|
|
5
5
|
import { guardPath } from "../security/pathGuard.js";
|
|
6
6
|
import { PatchWardenError } from "../errors.js";
|
|
7
7
|
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
8
|
-
import {
|
|
8
|
+
import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
9
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
10
|
+
import { mutateLockedJsonFileSync, readJsonObjectFileSync, withFileLock, withFileLockSync, } from "../utils/lockedJsonFile.js";
|
|
11
|
+
import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
|
|
9
12
|
// ── ID generation ──────────────────────────────────────────────────
|
|
10
13
|
export function generateDirectSessionId() {
|
|
11
14
|
const timestamp = new Date()
|
|
@@ -16,21 +19,32 @@ export function generateDirectSessionId() {
|
|
|
16
19
|
const randomHex = randomBytes(16).toString("hex");
|
|
17
20
|
return `direct_${timestamp}_${randomHex}`;
|
|
18
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Direct session IDs are directory names. Keep one shared, bounded grammar so
|
|
24
|
+
* MCP tools and Control Center routes cannot disagree about traversal input.
|
|
25
|
+
*/
|
|
26
|
+
export function isValidDirectSessionId(sessionId) {
|
|
27
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(sessionId);
|
|
28
|
+
}
|
|
29
|
+
function guardDirectSessionId(sessionId) {
|
|
30
|
+
if (!isValidDirectSessionId(sessionId)) {
|
|
31
|
+
throw new PatchWardenError("invalid_session_id", `Direct session ID "${sessionId}" is invalid.`, "Use the session_id returned by create_direct_session.", true, { session_id: sessionId });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
19
34
|
// ── Directory management ───────────────────────────────────────────
|
|
20
|
-
export function createDirectSessionDir(sessionId) {
|
|
21
|
-
|
|
35
|
+
export function createDirectSessionDir(sessionId, config = getConfig()) {
|
|
36
|
+
guardDirectSessionId(sessionId);
|
|
22
37
|
const sessionsDir = getDirectSessionsDir(config);
|
|
23
38
|
guardPath(sessionsDir, config.workspaceRoot, config.directSessionsDir);
|
|
24
39
|
mkdirSync(sessionsDir, { recursive: true });
|
|
25
|
-
const dir =
|
|
26
|
-
guardPath(dir, config.workspaceRoot, config.directSessionsDir);
|
|
40
|
+
const dir = getDirectSessionDir(sessionId, config);
|
|
27
41
|
mkdirSync(dir, { recursive: true });
|
|
28
42
|
return dir;
|
|
29
43
|
}
|
|
30
|
-
export function getDirectSessionDir(sessionId) {
|
|
31
|
-
|
|
44
|
+
export function getDirectSessionDir(sessionId, config = getConfig()) {
|
|
45
|
+
guardDirectSessionId(sessionId);
|
|
32
46
|
const sessionsDir = getDirectSessionsDir(config);
|
|
33
|
-
return resolve(sessionsDir, sessionId);
|
|
47
|
+
return guardPath(resolve(sessionsDir, sessionId), config.workspaceRoot, config.directSessionsDir);
|
|
34
48
|
}
|
|
35
49
|
// ── CRUD ───────────────────────────────────────────────────────────
|
|
36
50
|
export function createDirectSession(input) {
|
|
@@ -48,7 +62,11 @@ export function createDirectSession(input) {
|
|
|
48
62
|
const expiresAt = new Date(now.getTime() + config.directSessionTtlSeconds * 1000).toISOString();
|
|
49
63
|
const record = {
|
|
50
64
|
session_id: sessionId,
|
|
51
|
-
title: input.title || ""
|
|
65
|
+
title: redactSensitiveContent(input.title || "")
|
|
66
|
+
.content
|
|
67
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
68
|
+
.trim()
|
|
69
|
+
.slice(0, 500),
|
|
52
70
|
repo_path: input.repo_path,
|
|
53
71
|
resolved_repo_path: input.resolved_repo_path,
|
|
54
72
|
created_at: now.toISOString(),
|
|
@@ -66,36 +84,55 @@ export function createDirectSession(input) {
|
|
|
66
84
|
audited: false,
|
|
67
85
|
change_artifacts: null,
|
|
68
86
|
};
|
|
69
|
-
|
|
87
|
+
atomicWriteJsonFileSync(join(dir, "session.json"), record);
|
|
70
88
|
return record;
|
|
71
89
|
}
|
|
72
|
-
export function readDirectSession(sessionId) {
|
|
73
|
-
const
|
|
74
|
-
const sessionsDir = getDirectSessionsDir(config);
|
|
75
|
-
const dir = resolve(sessionsDir, sessionId);
|
|
76
|
-
const file = join(dir, "session.json");
|
|
77
|
-
guardPath(file, config.workspaceRoot, config.directSessionsDir);
|
|
90
|
+
export function readDirectSession(sessionId, config = getConfig()) {
|
|
91
|
+
const file = getDirectSessionFile(sessionId, config);
|
|
78
92
|
if (!existsSync(file)) {
|
|
79
93
|
throw new PatchWardenError("session_not_found", `Direct session "${sessionId}" not found.`, "Call create_direct_session first to get a valid session_id.", true, { session_id: sessionId });
|
|
80
94
|
}
|
|
81
|
-
|
|
95
|
+
let value;
|
|
96
|
+
try {
|
|
97
|
+
value = readJsonObjectFileSync(file);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw invalidDirectSessionRecord(sessionId, "session.json is not valid JSON");
|
|
101
|
+
}
|
|
102
|
+
return validateDirectSessionRecord(sessionId, value);
|
|
82
103
|
}
|
|
83
104
|
export function updateDirectSession(sessionId, updates) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
return mutateDirectSessionRecord(sessionId, (session) => ({
|
|
106
|
+
...session,
|
|
107
|
+
...updates,
|
|
108
|
+
session_id: session.session_id,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
export function appendDirectSessionOperation(sessionId, operation, config = getConfig()) {
|
|
112
|
+
return mutateDirectSessionRecord(sessionId, (session) => ({
|
|
113
|
+
...session,
|
|
114
|
+
operations: [
|
|
115
|
+
...session.operations,
|
|
116
|
+
{
|
|
117
|
+
...operation,
|
|
118
|
+
index: session.operations.reduce((max, entry) => Math.max(max, entry.index), -1) + 1,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
}), config);
|
|
89
122
|
}
|
|
90
|
-
export function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
export function withDirectSessionMutationLock(sessionId, action, config = getConfig()) {
|
|
124
|
+
const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
|
|
125
|
+
return withFileLockSync(lockTarget, action, directSessionBusyOptions(sessionId));
|
|
126
|
+
}
|
|
127
|
+
export function withDirectSessionMutationLockAsync(sessionId, action, config = getConfig()) {
|
|
128
|
+
const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
|
|
129
|
+
return withFileLock(lockTarget, action, directSessionBusyOptions(sessionId));
|
|
94
130
|
}
|
|
95
131
|
export function appendDirectSessionVerificationRun(sessionId, run) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
return mutateDirectSessionRecord(sessionId, (session) => ({
|
|
133
|
+
...session,
|
|
134
|
+
verification_runs: [...session.verification_runs, run],
|
|
135
|
+
}));
|
|
99
136
|
}
|
|
100
137
|
export function validateDirectSessionFreshness(sessionId) {
|
|
101
138
|
let session;
|
|
@@ -124,20 +161,22 @@ export function validateDirectSessionFreshness(sessionId) {
|
|
|
124
161
|
return { valid: true, failure_reason: null, session };
|
|
125
162
|
}
|
|
126
163
|
// ── Finalization ───────────────────────────────────────────────────
|
|
127
|
-
export function finalizeDirectSessionRecord(sessionId,
|
|
164
|
+
export function finalizeDirectSessionRecord(sessionId, changeArtifacts) {
|
|
128
165
|
const dir = getDirectSessionDir(sessionId);
|
|
129
166
|
// Write change artifacts to session directory
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
atomicWriteJsonFileSync(join(dir, "changed-files.json"), changeArtifacts.changed_files);
|
|
168
|
+
atomicWriteFileSync(join(dir, "diff.patch"), changeArtifacts.diff);
|
|
132
169
|
const summary = {
|
|
133
170
|
changed_files_total: changeArtifacts.changed_files.length,
|
|
134
171
|
additions: changeArtifacts.additions,
|
|
135
172
|
deletions: changeArtifacts.deletions,
|
|
136
173
|
patch_mode: changeArtifacts.patch_mode,
|
|
174
|
+
diff_redacted: changeArtifacts.diff_redacted === true,
|
|
175
|
+
diff_redaction_categories: changeArtifacts.diff_redaction_categories ?? [],
|
|
137
176
|
artifact_hygiene: changeArtifacts.artifact_hygiene,
|
|
138
177
|
};
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
atomicWriteJsonFileSync(join(dir, "summary.json"), summary);
|
|
179
|
+
atomicWriteFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts));
|
|
141
180
|
return updateDirectSession(sessionId, {
|
|
142
181
|
finalized: true,
|
|
143
182
|
finalized_at: new Date().toISOString(),
|
|
@@ -166,6 +205,58 @@ function extractReason(error) {
|
|
|
166
205
|
return error.message.slice(0, 80);
|
|
167
206
|
return String(error).slice(0, 80);
|
|
168
207
|
}
|
|
208
|
+
function invalidDirectSessionRecord(sessionId, detail) {
|
|
209
|
+
return new PatchWardenError("invalid_session_record", `Direct session "${sessionId}" is invalid: ${detail}.`, "Create a new direct session and do not modify its internal session.json file.", true, { session_id: sessionId });
|
|
210
|
+
}
|
|
211
|
+
function getDirectSessionFile(sessionId, config = getConfig()) {
|
|
212
|
+
const dir = getDirectSessionDir(sessionId, config);
|
|
213
|
+
return guardPath(join(dir, "session.json"), config.workspaceRoot, config.directSessionsDir);
|
|
214
|
+
}
|
|
215
|
+
function mutateDirectSessionRecord(sessionId, mutation, config = getConfig()) {
|
|
216
|
+
const file = getDirectSessionFile(sessionId, config);
|
|
217
|
+
if (!existsSync(file)) {
|
|
218
|
+
throw new PatchWardenError("session_not_found", `Direct session "${sessionId}" not found.`, "Call create_direct_session first to get a valid session_id.", true, { session_id: sessionId });
|
|
219
|
+
}
|
|
220
|
+
return mutateLockedJsonFileSync(file, (current) => {
|
|
221
|
+
const session = validateDirectSessionRecord(sessionId, current);
|
|
222
|
+
const next = mutation(session);
|
|
223
|
+
return { next, result: next };
|
|
224
|
+
}, {
|
|
225
|
+
// Session-record appends are short serialized writes. Give Windows
|
|
226
|
+
// antivirus-delayed lock detachment enough time to finish instead of
|
|
227
|
+
// dropping concurrent operation or verification evidence.
|
|
228
|
+
waitMs: 10_000,
|
|
229
|
+
busyError: () => new PatchWardenError("direct_session_busy", `Direct session "${sessionId}" is currently being updated.`, "Retry after the current Direct operation completes.", true, { session_id: sessionId }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function directSessionBusyOptions(sessionId) {
|
|
233
|
+
return {
|
|
234
|
+
waitMs: 0,
|
|
235
|
+
busyError: () => new PatchWardenError("direct_session_busy", `Direct session "${sessionId}" already has a workspace operation in progress.`, "Wait for the active patch, sync, verification, or finalization to finish, then retry.", true, { session_id: sessionId }),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function validateDirectSessionRecord(sessionId, value) {
|
|
239
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
240
|
+
throw invalidDirectSessionRecord(sessionId, "session.json must contain an object");
|
|
241
|
+
}
|
|
242
|
+
const record = value;
|
|
243
|
+
if (record.session_id !== sessionId) {
|
|
244
|
+
throw invalidDirectSessionRecord(sessionId, "session_id does not match its directory");
|
|
245
|
+
}
|
|
246
|
+
if (typeof record.resolved_repo_path !== "string" || record.resolved_repo_path.trim() === "") {
|
|
247
|
+
throw invalidDirectSessionRecord(sessionId, "resolved_repo_path is missing");
|
|
248
|
+
}
|
|
249
|
+
if (typeof record.expires_at !== "string" || !Number.isFinite(Date.parse(record.expires_at))) {
|
|
250
|
+
throw invalidDirectSessionRecord(sessionId, "expires_at is invalid");
|
|
251
|
+
}
|
|
252
|
+
if (typeof record.finalized !== "boolean") {
|
|
253
|
+
throw invalidDirectSessionRecord(sessionId, "finalized must be a boolean");
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(record.operations) || !Array.isArray(record.verification_runs)) {
|
|
256
|
+
throw invalidDirectSessionRecord(sessionId, "operation or verification history is invalid");
|
|
257
|
+
}
|
|
258
|
+
return record;
|
|
259
|
+
}
|
|
169
260
|
function formatSummaryMd(artifacts) {
|
|
170
261
|
const lines = [
|
|
171
262
|
"# Direct Session Change Summary",
|
|
@@ -174,6 +265,7 @@ function formatSummaryMd(artifacts) {
|
|
|
174
265
|
`**Additions:** ${artifacts.additions}`,
|
|
175
266
|
`**Deletions:** ${artifacts.deletions}`,
|
|
176
267
|
`**Patch mode:** ${artifacts.patch_mode}`,
|
|
268
|
+
`**Diff redacted:** ${artifacts.diff_redacted === true ? "yes" : "no"}`,
|
|
177
269
|
"",
|
|
178
270
|
"## Artifact Hygiene",
|
|
179
271
|
"",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import { getConfig } from "../config.js";
|
|
4
3
|
import { guardDirectCommand } from "../security/commandGuard.js";
|
|
5
4
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
6
5
|
import { runSimpleProcess } from "../runner/simpleProcess.js";
|
|
6
|
+
import { resolveTrustedCommandLine } from "../runner/processSecurity.js";
|
|
7
7
|
import { getDirectSessionDir } from "./directSessionStore.js";
|
|
8
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
8
9
|
const MAX_TAIL_CHARS = 10_000;
|
|
9
10
|
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
10
11
|
export async function runDirectVerification(input) {
|
|
@@ -13,7 +14,7 @@ export async function runDirectVerification(input) {
|
|
|
13
14
|
const timeoutSeconds = input.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
14
15
|
const timeoutMs = timeoutSeconds * 1000;
|
|
15
16
|
// Parse command into command + args
|
|
16
|
-
const parts = parseCommand(allowedCommand);
|
|
17
|
+
const parts = parseCommand(allowedCommand, input.resolvedRepoPath);
|
|
17
18
|
if (!parts) {
|
|
18
19
|
throw new Error(`Failed to parse command: "${allowedCommand}"`);
|
|
19
20
|
}
|
|
@@ -73,27 +74,18 @@ export async function runDirectVerification(input) {
|
|
|
73
74
|
],
|
|
74
75
|
spawn_error: result.spawnError,
|
|
75
76
|
};
|
|
76
|
-
|
|
77
|
+
atomicWriteJsonFileSync(join(sessionDir, "verification.json"), verificationJson);
|
|
77
78
|
return { run, log_path: logPath };
|
|
78
79
|
}
|
|
79
80
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
80
|
-
function parseCommand(command) {
|
|
81
|
+
function parseCommand(command, cwd) {
|
|
81
82
|
const trimmed = command.trim();
|
|
82
83
|
if (!trimmed)
|
|
83
84
|
return null;
|
|
84
|
-
|
|
85
|
-
const parts = trimmed.split(/\s+/);
|
|
86
|
-
// On Windows, npm/npx are batch files (.cmd) and spawn() cannot find them
|
|
87
|
-
// without shell resolution. Wrap with cmd.exe /c for those commands.
|
|
88
|
-
if (process.platform === "win32" && (parts[0] === "npm" || parts[0] === "npx")) {
|
|
89
|
-
return {
|
|
90
|
-
command: "cmd",
|
|
91
|
-
args: ["/c", ...parts],
|
|
92
|
-
};
|
|
93
|
-
}
|
|
85
|
+
const invocation = resolveTrustedCommandLine(trimmed, cwd);
|
|
94
86
|
return {
|
|
95
|
-
command:
|
|
96
|
-
args:
|
|
87
|
+
command: invocation.command,
|
|
88
|
+
args: invocation.argsPrefix,
|
|
97
89
|
};
|
|
98
90
|
}
|
|
99
91
|
function truncateTail(content, maxChars) {
|