patchwarden 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +32 -2
- package/README.en.md +194 -26
- package/README.md +160 -22
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +24 -15
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +44 -36
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +98 -11
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +16 -0
- package/dist/control/middleware/auth.js +17 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +78 -0
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +287 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +12 -0
- package/dist/control/routes/process.js +487 -0
- package/dist/control/routes/sessions.d.ts +30 -0
- package/dist/control/routes/sessions.js +287 -0
- package/dist/control/routes/status.d.ts +51 -0
- package/dist/control/routes/status.js +307 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +197 -0
- package/dist/control/routes/tasks.d.ts +24 -0
- package/dist/control/routes/tasks.js +309 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +220 -0
- package/dist/control/runtime.d.ts +92 -0
- package/dist/control/runtime.js +425 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +150 -0
- package/dist/control/shared.d.ts +59 -0
- package/dist/control/shared.js +341 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +72 -37
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +614 -373
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +197 -0
- package/dist/goal/goalStatus.d.ts +8 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +221 -0
- package/dist/goal/subgoalSync.js +13 -10
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +126 -44
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +13 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +14 -6
- package/dist/runner/changeCapture.js +287 -81
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +60 -7
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +339 -287
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +52 -38
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +265 -54
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +24 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +280 -273
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +283 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +116 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +92 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +46 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +10 -14
- package/dist/tools/registry.js +47 -1651
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
- package/dist/tools/tasks/evidencePack.js +387 -0
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +1648 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +23 -0
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/desktop-app.md +175 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +168 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +82 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +97 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/examples/config.example.json +4 -2
- package/package.json +28 -11
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +447 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +39 -33
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +30 -4
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/scripts/generate-demo-gif.py +320 -0
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +29 -15
- package/src/assessments/assessmentStore.ts +55 -43
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +105 -11
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +35 -0
- package/src/control/middleware/static.ts +84 -0
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +329 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +505 -0
- package/src/control/routes/sessions.ts +314 -0
- package/src/control/routes/status.ts +405 -0
- package/src/control/routes/taskActions.ts +214 -0
- package/src/control/routes/tasks.ts +331 -0
- package/src/control/routes/workspace.ts +234 -0
- package/src/control/runtime.ts +508 -0
- package/src/control/server.ts +165 -0
- package/src/control/shared.ts +360 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +100 -51
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +766 -492
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +263 -0
- package/src/goal/goalStatus.ts +25 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +15 -10
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +130 -44
- package/src/index.ts +7 -4
- package/src/logging.ts +20 -4
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +342 -80
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +55 -7
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +441 -292
- package/src/runner/simpleProcess.ts +68 -33
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +290 -57
- package/src/security/contentRedaction.ts +31 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +276 -273
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +379 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
- package/src/tools/dispatch/directDispatch.ts +168 -0
- package/src/tools/dispatch/goalDispatch.ts +128 -0
- package/src/tools/dispatch/releaseDispatch.ts +66 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +54 -1858
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/tasks/evidencePack.ts +504 -0
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +204 -3
- package/ui/pages/dashboard.html +937 -79
- package/ui/pages/direct-sessions.html +609 -60
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +468 -438
- package/ui/pages/tasks.html +648 -65
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -213
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -130
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -190
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -109
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/evidencePack.js +0 -168
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -297
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -142
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -243
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -150
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/evidencePack.ts +0 -205
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
package/src/httpServer.ts
CHANGED
|
@@ -16,52 +16,68 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
22
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
23
|
import { loadConfig, getTasksDir } from "./config.js";
|
|
24
24
|
import { registerTools } from "./tools/registry.js";
|
|
25
|
-
import { healthCheck } from "./tools/healthCheck.js";
|
|
25
|
+
import { healthCheck } from "./tools/diagnostics/healthCheck.js";
|
|
26
26
|
import { getToolCatalogSnapshot } from "./tools/registry.js";
|
|
27
27
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
28
|
+
import { logger } from "./logging.js";
|
|
29
|
+
import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
|
|
30
|
+
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
31
|
+
import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
|
|
32
|
+
import { mutateTaskStatus } from "./runner/taskStatusStore.js";
|
|
33
|
+
import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
|
|
28
34
|
|
|
29
35
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
30
36
|
|
|
31
37
|
const config = loadConfig();
|
|
32
38
|
const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
|
|
33
|
-
|
|
39
|
+
config.httpPort ||
|
|
34
40
|
7331;
|
|
35
41
|
const host = "127.0.0.1";
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
logger.info(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
|
|
44
|
+
logger.info(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
|
|
45
|
+
logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
|
|
40
46
|
|
|
41
47
|
// ── Owner token (optional) ────────────────────────────────────────
|
|
42
48
|
|
|
43
|
-
const httpCfg =
|
|
49
|
+
const httpCfg = config.http || {};
|
|
44
50
|
const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
|
|
45
51
|
const ownerToken = process.env[ownerTokenEnv] || "";
|
|
52
|
+
const MAX_ADMIN_BODY_BYTES = 64 * 1024;
|
|
53
|
+
const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
|
|
54
|
+
const REVIEWABLE_TASK_STATUSES = new Set([
|
|
55
|
+
"done_by_agent",
|
|
56
|
+
"done",
|
|
57
|
+
"accepted",
|
|
58
|
+
"rejected",
|
|
59
|
+
"needs_fix",
|
|
60
|
+
"blocked",
|
|
61
|
+
]);
|
|
46
62
|
|
|
47
63
|
if (ownerToken) {
|
|
48
|
-
|
|
64
|
+
logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
|
|
49
65
|
} else {
|
|
50
|
-
|
|
66
|
+
logger.info(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
function checkOwnerToken(req: IncomingMessage): boolean {
|
|
54
70
|
if (!ownerToken) return true; // no token configured — allow all
|
|
55
71
|
|
|
56
|
-
const authHeader = req.headers["authorization"]
|
|
57
|
-
const customHeader = req.headers["x-patchwarden-token"]
|
|
72
|
+
const authHeader = firstHeaderValue(req.headers["authorization"]);
|
|
73
|
+
const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
|
|
58
74
|
|
|
59
75
|
if (authHeader.startsWith("Bearer ")) {
|
|
60
|
-
return authHeader.slice(7)
|
|
76
|
+
return timingSafeStringEqual(authHeader.slice(7), ownerToken);
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
if (
|
|
64
|
-
return customHeader
|
|
79
|
+
if (customHeader.length > 0) {
|
|
80
|
+
return timingSafeStringEqual(customHeader, ownerToken);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
return false;
|
|
@@ -81,11 +97,22 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
|
|
|
81
97
|
throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
|
|
82
98
|
}
|
|
83
99
|
let notes = "";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
if (body.trim()) {
|
|
101
|
+
let parsed: unknown;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(body);
|
|
104
|
+
} catch {
|
|
105
|
+
throw httpError(400, "Acceptance body must be valid JSON.");
|
|
106
|
+
}
|
|
107
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
|
+
throw httpError(400, "Acceptance body must be a JSON object.");
|
|
109
|
+
}
|
|
110
|
+
const record = parsed as Record<string, unknown>;
|
|
111
|
+
const rawNotes = record.notes ?? record.reason ?? "";
|
|
112
|
+
if (typeof rawNotes !== "string") {
|
|
113
|
+
throw httpError(400, "Acceptance notes or reason must be a string.");
|
|
114
|
+
}
|
|
115
|
+
notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
|
|
89
116
|
}
|
|
90
117
|
const acceptance = {
|
|
91
118
|
status,
|
|
@@ -93,23 +120,69 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
|
|
|
93
120
|
reviewer: "human",
|
|
94
121
|
notes,
|
|
95
122
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Also update the task's status.json with acceptance_status
|
|
123
|
+
// Commit the review artifact and status annotation while holding the same
|
|
124
|
+
// status lock used by the runner, cancel, and audit paths.
|
|
99
125
|
const statusFile = join(taskDir, "status.json");
|
|
100
|
-
if (existsSync(statusFile)) {
|
|
101
|
-
|
|
102
|
-
const current = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
103
|
-
current.acceptance_status = status;
|
|
104
|
-
current.acceptance_reviewed_at = acceptance.reviewed_at;
|
|
105
|
-
writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
|
|
106
|
-
} catch {
|
|
107
|
-
// status.json update is best-effort
|
|
108
|
-
}
|
|
126
|
+
if (!existsSync(statusFile)) {
|
|
127
|
+
throw httpError(409, `Task "${taskId}" has no status.json to review.`);
|
|
109
128
|
}
|
|
129
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
130
|
+
if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
|
|
131
|
+
throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
|
|
132
|
+
}
|
|
133
|
+
atomicWriteJsonFileSync(filePath, acceptance);
|
|
134
|
+
const next = {
|
|
135
|
+
...current,
|
|
136
|
+
acceptance_status: status,
|
|
137
|
+
acceptance_reviewed_at: acceptance.reviewed_at,
|
|
138
|
+
updated_at: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
return { next, result: next };
|
|
141
|
+
});
|
|
110
142
|
return acceptance;
|
|
111
143
|
}
|
|
112
144
|
|
|
145
|
+
function httpError(statusCode: number, message: string): Error & { statusCode: number } {
|
|
146
|
+
return Object.assign(new Error(message), { statusCode });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readBoundedRequestBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
150
|
+
const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
|
|
151
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
152
|
+
req.resume();
|
|
153
|
+
return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
154
|
+
}
|
|
155
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
156
|
+
const chunks: Buffer[] = [];
|
|
157
|
+
let total = 0;
|
|
158
|
+
let settled = false;
|
|
159
|
+
req.on("data", (chunk: Buffer | string) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
162
|
+
total += buffer.length;
|
|
163
|
+
if (total > maxBytes) {
|
|
164
|
+
settled = true;
|
|
165
|
+
req.resume();
|
|
166
|
+
rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
chunks.push(buffer);
|
|
170
|
+
});
|
|
171
|
+
req.on("end", () => {
|
|
172
|
+
if (!settled) {
|
|
173
|
+
settled = true;
|
|
174
|
+
resolveBody(Buffer.concat(chunks).toString("utf-8"));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
req.on("error", (error) => {
|
|
178
|
+
if (!settled) {
|
|
179
|
+
settled = true;
|
|
180
|
+
rejectBody(error);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
113
186
|
function readAcceptance(taskId: string): object {
|
|
114
187
|
const filePath = getAcceptancePath(taskId);
|
|
115
188
|
if (!existsSync(filePath)) {
|
|
@@ -142,7 +215,9 @@ async function handleMcpRequest(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
142
215
|
await mcpServer.connect(transport);
|
|
143
216
|
await transport.handleRequest(req, res);
|
|
144
217
|
} catch (err) {
|
|
145
|
-
|
|
218
|
+
logger.error("[patchwarden-http] Request error", {
|
|
219
|
+
error: err instanceof Error ? err.message : String(err),
|
|
220
|
+
});
|
|
146
221
|
if (!res.headersSent) {
|
|
147
222
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
148
223
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -178,6 +253,16 @@ function parseAdminUrl(url: string) {
|
|
|
178
253
|
// ── HTTP server ───────────────────────────────────────────────────
|
|
179
254
|
|
|
180
255
|
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
256
|
+
if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
|
|
257
|
+
res.writeHead(421, {
|
|
258
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
259
|
+
"Cache-Control": "no-store",
|
|
260
|
+
"X-Content-Type-Options": "nosniff",
|
|
261
|
+
});
|
|
262
|
+
res.end(JSON.stringify({ error: "Untrusted Host header" }));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
181
266
|
// Health check endpoints
|
|
182
267
|
if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
|
|
183
268
|
const health = healthCheck(getToolCatalogSnapshot());
|
|
@@ -203,9 +288,7 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
203
288
|
return;
|
|
204
289
|
}
|
|
205
290
|
if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
|
|
206
|
-
const
|
|
207
|
-
for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }
|
|
208
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
291
|
+
const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
|
|
209
292
|
const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
|
|
210
293
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
211
294
|
res.end(JSON.stringify(acceptance, null, 2));
|
|
@@ -213,10 +296,13 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
213
296
|
}
|
|
214
297
|
res.writeHead(405, { "Content-Type": "application/json" });
|
|
215
298
|
res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
|
|
216
|
-
} catch (err:
|
|
217
|
-
const statusCode = err
|
|
299
|
+
} catch (err: unknown) {
|
|
300
|
+
const statusCode = err && typeof err === "object" && "statusCode" in err
|
|
301
|
+
? Number((err as { statusCode?: unknown }).statusCode) || 500
|
|
302
|
+
: 500;
|
|
303
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
218
304
|
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
219
|
-
res.end(JSON.stringify({ error:
|
|
305
|
+
res.end(JSON.stringify({ error: message }));
|
|
220
306
|
}
|
|
221
307
|
return;
|
|
222
308
|
}
|
|
@@ -253,22 +339,22 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
253
339
|
|
|
254
340
|
httpServer.on("error", (err: NodeJS.ErrnoException) => {
|
|
255
341
|
if (err.code === "EADDRINUSE") {
|
|
256
|
-
|
|
257
|
-
|
|
342
|
+
logger.fatal(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
|
|
343
|
+
logger.fatal("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
|
|
258
344
|
} else {
|
|
259
|
-
|
|
345
|
+
logger.fatal(`[patchwarden-http] Fatal: ${err.message}`);
|
|
260
346
|
}
|
|
261
347
|
process.exit(1);
|
|
262
348
|
});
|
|
263
349
|
|
|
264
350
|
httpServer.listen(port, host, () => {
|
|
265
|
-
|
|
266
|
-
|
|
351
|
+
logger.info(`[patchwarden-http] ✅ Ready`);
|
|
352
|
+
logger.info(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
|
|
267
353
|
});
|
|
268
354
|
|
|
269
355
|
// Graceful shutdown
|
|
270
356
|
process.on("SIGINT", () => {
|
|
271
|
-
|
|
357
|
+
logger.info("[patchwarden-http] Shutting down...");
|
|
272
358
|
httpServer.close(() => process.exit(0));
|
|
273
359
|
});
|
|
274
360
|
|
package/src/index.ts
CHANGED
|
@@ -12,11 +12,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
12
|
import { loadConfig } from "./config.js";
|
|
13
13
|
import { registerTools } from "./tools/registry.js";
|
|
14
14
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
15
|
+
import { logger } from "./logging.js";
|
|
15
16
|
|
|
16
17
|
const config = loadConfig();
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
logger.info(`[patchwarden] Workspace: ${config.workspaceRoot}`);
|
|
20
|
+
logger.info("[patchwarden] Transport: stdio");
|
|
20
21
|
|
|
21
22
|
const server = new Server(
|
|
22
23
|
{ name: "patchwarden", version: PATCHWARDEN_VERSION },
|
|
@@ -27,8 +28,10 @@ registerTools(server);
|
|
|
27
28
|
|
|
28
29
|
const transport = new StdioServerTransport();
|
|
29
30
|
server.connect(transport).catch((err) => {
|
|
30
|
-
|
|
31
|
+
logger.fatal("[patchwarden] Fatal", {
|
|
32
|
+
error: err instanceof Error ? err.message : String(err),
|
|
33
|
+
});
|
|
31
34
|
process.exit(1);
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
logger.info("[patchwarden] MCP server ready on stdio");
|
package/src/logging.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
5
|
-
import { stableJsonStringify } from "./
|
|
5
|
+
import { stableJsonStringify } from "./utils/stableJson.js";
|
|
6
6
|
import { getConfig } from "./config.js";
|
|
7
|
+
import { appendBoundedTextFileSync } from "./utils/boundedFile.js";
|
|
8
|
+
|
|
9
|
+
const MAX_INVOCATION_LOG_BYTES = 5 * 1024 * 1024;
|
|
7
10
|
|
|
8
11
|
// ── Types ─────────────────────────────────────────────────────────
|
|
9
12
|
|
|
10
|
-
export type LogLevel = "info" | "warn" | "error" | "audit";
|
|
13
|
+
export type LogLevel = "info" | "warn" | "error" | "fatal" | "audit";
|
|
11
14
|
|
|
12
15
|
export interface LogEntry {
|
|
13
16
|
timestamp: string;
|
|
@@ -90,6 +93,15 @@ export class Logger {
|
|
|
90
93
|
emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Emit a fatal-level log entry. Use this immediately before a process
|
|
98
|
+
* exits with a non-zero status, to record the unrecoverable condition.
|
|
99
|
+
* Level is higher than `error`.
|
|
100
|
+
*/
|
|
101
|
+
fatal(message: string, context?: Record<string, unknown>): void {
|
|
102
|
+
emit({ timestamp: new Date().toISOString(), level: "fatal", message, ...context });
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
/**
|
|
94
106
|
* Emit a tool-call audit log entry.
|
|
95
107
|
*
|
|
@@ -225,7 +237,11 @@ export function logToolInvocation(
|
|
|
225
237
|
|
|
226
238
|
mkdirSync(logsDir, { recursive: true });
|
|
227
239
|
const logFilePath = join(logsDir, "invocation.log");
|
|
228
|
-
|
|
240
|
+
appendBoundedTextFileSync(
|
|
241
|
+
logFilePath,
|
|
242
|
+
JSON.stringify(entry) + "\n",
|
|
243
|
+
MAX_INVOCATION_LOG_BYTES,
|
|
244
|
+
);
|
|
229
245
|
} catch (err) {
|
|
230
246
|
// 日志失败不应阻断主流程,仅向 stderr 记录错误
|
|
231
247
|
try {
|
|
@@ -334,10 +334,13 @@ function pathMatchesPattern(relPath: string, pattern: string): boolean {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
function parseGithubRepo(repository: unknown): string | null {
|
|
337
|
+
const repositoryObject = repository && typeof repository === "object" && !Array.isArray(repository)
|
|
338
|
+
? repository as Record<string, unknown>
|
|
339
|
+
: null;
|
|
337
340
|
const raw = typeof repository === "string"
|
|
338
341
|
? repository
|
|
339
|
-
:
|
|
340
|
-
?
|
|
342
|
+
: typeof repositoryObject?.url === "string"
|
|
343
|
+
? repositoryObject.url
|
|
341
344
|
: "";
|
|
342
345
|
const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
|
|
343
346
|
return match ? match[1] : null;
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
* GITHUB_TOKEN is used as Bearer auth but never logged or returned.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { execSync } from "node:child_process";
|
|
19
18
|
import { createHash } from "node:crypto";
|
|
20
|
-
import { readFileSync
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
21
20
|
import { join } from "node:path";
|
|
22
21
|
import { get } from "node:https";
|
|
22
|
+
import { resolveTrustedCommandLine } from "../runner/processSecurity.js";
|
|
23
|
+
import { runSimpleProcessSync } from "../runner/simpleProcess.js";
|
|
24
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
23
25
|
|
|
24
26
|
// ── Public types ───────────────────────────────────────────────────
|
|
25
27
|
|
|
@@ -49,7 +51,7 @@ export interface PackedCheckResult extends StageCheckResult {
|
|
|
49
51
|
|
|
50
52
|
export interface HttpResponse {
|
|
51
53
|
statusCode: number;
|
|
52
|
-
data:
|
|
54
|
+
data: unknown;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export type HttpGetFn = (
|
|
@@ -86,6 +88,7 @@ const STAGE_ORDER: ReleaseStage[] = [
|
|
|
86
88
|
|
|
87
89
|
const LOCAL_TIMEOUT_MS = 300000;
|
|
88
90
|
const REMOTE_TIMEOUT_MS = 10000;
|
|
91
|
+
const MAX_REMOTE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
89
92
|
const USER_AGENT = "PatchWarden-ReleaseGate";
|
|
90
93
|
|
|
91
94
|
const PACK_FORBIDDEN_PATTERNS: RegExp[] = [
|
|
@@ -121,6 +124,13 @@ export async function httpsGetJson(
|
|
|
121
124
|
): Promise<HttpResponse> {
|
|
122
125
|
const timeout = timeoutMs ?? REMOTE_TIMEOUT_MS;
|
|
123
126
|
return new Promise<HttpResponse>((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
const fail = (error: Error) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
reject(error);
|
|
133
|
+
};
|
|
124
134
|
const timer = setTimeout(() => {
|
|
125
135
|
req.destroy(new Error(`Request timed out after ${timeout}ms`));
|
|
126
136
|
}, timeout);
|
|
@@ -135,12 +145,32 @@ export async function httpsGetJson(
|
|
|
135
145
|
},
|
|
136
146
|
},
|
|
137
147
|
(res) => {
|
|
148
|
+
const declaredLength = Number(res.headers["content-length"]);
|
|
149
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_RESPONSE_BYTES) {
|
|
150
|
+
res.destroy();
|
|
151
|
+
fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
138
154
|
const chunks: Buffer[] = [];
|
|
139
|
-
|
|
155
|
+
let totalBytes = 0;
|
|
156
|
+
let oversized = false;
|
|
157
|
+
res.on("data", (chunk: Buffer) => {
|
|
158
|
+
if (oversized) return;
|
|
159
|
+
totalBytes += chunk.length;
|
|
160
|
+
if (totalBytes > MAX_REMOTE_RESPONSE_BYTES) {
|
|
161
|
+
oversized = true;
|
|
162
|
+
res.destroy();
|
|
163
|
+
fail(new Error(`Response exceeds ${MAX_REMOTE_RESPONSE_BYTES} bytes`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
});
|
|
140
168
|
res.on("end", () => {
|
|
169
|
+
if (oversized || settled) return;
|
|
170
|
+
settled = true;
|
|
141
171
|
clearTimeout(timer);
|
|
142
172
|
const body = Buffer.concat(chunks).toString("utf-8");
|
|
143
|
-
let data:
|
|
173
|
+
let data: unknown = null;
|
|
144
174
|
if (body.length > 0) {
|
|
145
175
|
try {
|
|
146
176
|
data = JSON.parse(body);
|
|
@@ -150,12 +180,11 @@ export async function httpsGetJson(
|
|
|
150
180
|
}
|
|
151
181
|
resolve({ statusCode: res.statusCode ?? 0, data });
|
|
152
182
|
});
|
|
183
|
+
res.on("aborted", () => fail(new Error("Response was aborted before completion")));
|
|
184
|
+
res.on("error", fail);
|
|
153
185
|
},
|
|
154
186
|
);
|
|
155
|
-
req.on("error",
|
|
156
|
-
clearTimeout(timer);
|
|
157
|
-
reject(err);
|
|
158
|
-
});
|
|
187
|
+
req.on("error", fail);
|
|
159
188
|
});
|
|
160
189
|
}
|
|
161
190
|
|
|
@@ -203,12 +232,12 @@ export function checkLocalReady(repoPath: string): StageCheckResult {
|
|
|
203
232
|
const commands = ["npm.cmd run build", "npm.cmd test", "npm.cmd run doctor:ci"];
|
|
204
233
|
for (const cmdStr of commands) {
|
|
205
234
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
235
|
+
const result = runLocalCommand(repoPath, cmdStr, LOCAL_TIMEOUT_MS);
|
|
236
|
+
if (result.spawnError) throw new Error(result.spawnError);
|
|
237
|
+
if (result.timedOut) throw new Error(`Command timed out after ${LOCAL_TIMEOUT_MS}ms`);
|
|
238
|
+
if (result.exitCode !== 0) {
|
|
239
|
+
throw Object.assign(new Error(`Command exited with code ${result.exitCode ?? -1}`), { status: result.exitCode ?? -1 });
|
|
240
|
+
}
|
|
212
241
|
} catch (err) {
|
|
213
242
|
if (err && typeof err === "object" && "status" in err) {
|
|
214
243
|
const code = (err as { status: unknown }).status;
|
|
@@ -235,12 +264,11 @@ export function checkLocalReady(repoPath: string): StageCheckResult {
|
|
|
235
264
|
export function checkPackedReady(repoPath: string): PackedCheckResult {
|
|
236
265
|
let packOutput: string;
|
|
237
266
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
});
|
|
267
|
+
const result = runLocalCommand(repoPath, "npm.cmd pack --dry-run --json", LOCAL_TIMEOUT_MS, 10 * 1024 * 1024);
|
|
268
|
+
if (result.spawnError) throw new Error(result.spawnError);
|
|
269
|
+
if (result.timedOut) throw new Error(`npm pack timed out after ${LOCAL_TIMEOUT_MS}ms`);
|
|
270
|
+
if (result.exitCode !== 0) throw new Error(`npm pack exited with code ${result.exitCode ?? -1}`);
|
|
271
|
+
packOutput = result.stdout;
|
|
244
272
|
} catch (err) {
|
|
245
273
|
return {
|
|
246
274
|
status: "failed",
|
|
@@ -309,7 +337,7 @@ export function checkPackedReady(repoPath: string): PackedCheckResult {
|
|
|
309
337
|
|
|
310
338
|
const manifestPath = join(repoPath, "release-artifact-manifest.json");
|
|
311
339
|
try {
|
|
312
|
-
|
|
340
|
+
atomicWriteJsonFileSync(manifestPath, manifest);
|
|
313
341
|
} catch (err) {
|
|
314
342
|
return {
|
|
315
343
|
status: "failed",
|
|
@@ -335,6 +363,18 @@ export function checkPackedReady(repoPath: string): PackedCheckResult {
|
|
|
335
363
|
return { status: "passed", manifestPath };
|
|
336
364
|
}
|
|
337
365
|
|
|
366
|
+
function runLocalCommand(repoPath: string, commandLine: string, timeoutMs: number, maxStdoutBytes = 512 * 1024) {
|
|
367
|
+
const invocation = resolveTrustedCommandLine(commandLine, repoPath);
|
|
368
|
+
return runSimpleProcessSync({
|
|
369
|
+
command: invocation.command,
|
|
370
|
+
args: invocation.argsPrefix,
|
|
371
|
+
cwd: repoPath,
|
|
372
|
+
timeoutMs,
|
|
373
|
+
maxStdoutBytes,
|
|
374
|
+
maxStderrBytes: 512 * 1024,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
338
378
|
/**
|
|
339
379
|
* Stage 3: published_verified
|
|
340
380
|
* Queries the npm registry and confirms the given version exists.
|
|
@@ -355,11 +395,12 @@ export async function checkPublishedVerified(
|
|
|
355
395
|
if (res.statusCode === 404) {
|
|
356
396
|
return { status: "failed", reason: `Package "${packageName}" not found on npm registry` };
|
|
357
397
|
}
|
|
358
|
-
const data = res.data;
|
|
359
|
-
|
|
398
|
+
const data = asRecord(res.data);
|
|
399
|
+
const versions = asRecord(data?.versions);
|
|
400
|
+
if (!versions) {
|
|
360
401
|
return { status: "failed", reason: "npm registry response missing versions object" };
|
|
361
402
|
}
|
|
362
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
403
|
+
if (Object.prototype.hasOwnProperty.call(versions, version)) {
|
|
363
404
|
return { status: "passed" };
|
|
364
405
|
}
|
|
365
406
|
return {
|
|
@@ -423,12 +464,12 @@ export async function checkCiVerified(
|
|
|
423
464
|
if (res.statusCode !== 200) {
|
|
424
465
|
return { status: "not_checked", reason: `GitHub API returned ${res.statusCode}` };
|
|
425
466
|
}
|
|
426
|
-
const data = res.data;
|
|
467
|
+
const data = asRecord(res.data);
|
|
427
468
|
const runs = Array.isArray(data?.workflow_runs) ? data.workflow_runs : [];
|
|
428
469
|
if (runs.length === 0) {
|
|
429
470
|
return { status: "not_checked", reason: `No CI runs found for branch "${branch}"` };
|
|
430
471
|
}
|
|
431
|
-
const conclusion = runs[0]?.conclusion;
|
|
472
|
+
const conclusion = asRecord(runs[0])?.conclusion;
|
|
432
473
|
if (conclusion === "success") {
|
|
433
474
|
return { status: "passed" };
|
|
434
475
|
}
|
|
@@ -447,6 +488,12 @@ export async function checkCiVerified(
|
|
|
447
488
|
}
|
|
448
489
|
}
|
|
449
490
|
|
|
491
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
492
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
493
|
+
? value as Record<string, unknown>
|
|
494
|
+
: null;
|
|
495
|
+
}
|
|
496
|
+
|
|
450
497
|
// ── Orchestrator ───────────────────────────────────────────────────
|
|
451
498
|
|
|
452
499
|
/**
|