@aitne/daemon 0.1.3 → 0.1.6
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/dist/adapters/notification-manager.d.ts +12 -0
- package/dist/adapters/notification-manager.d.ts.map +1 -1
- package/dist/adapters/notification-manager.js +39 -1
- package/dist/adapters/notification-manager.js.map +1 -1
- package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
- package/dist/adapters/whatsapp-adapter.js +0 -1
- package/dist/adapters/whatsapp-adapter.js.map +1 -1
- package/dist/api/integration-route-gate.d.ts +15 -11
- package/dist/api/integration-route-gate.d.ts.map +1 -1
- package/dist/api/integration-route-gate.js +60 -23
- package/dist/api/integration-route-gate.js.map +1 -1
- package/dist/api/json-body.d.ts +22 -7
- package/dist/api/json-body.d.ts.map +1 -1
- package/dist/api/json-body.js +27 -8
- package/dist/api/json-body.js.map +1 -1
- package/dist/api/routes/agent.d.ts.map +1 -1
- package/dist/api/routes/agent.js +25 -0
- package/dist/api/routes/agent.js.map +1 -1
- package/dist/api/routes/backends.d.ts.map +1 -1
- package/dist/api/routes/backends.js +96 -1
- package/dist/api/routes/backends.js.map +1 -1
- package/dist/api/routes/books.js +1 -1
- package/dist/api/routes/books.js.map +1 -1
- package/dist/api/routes/commands.d.ts.map +1 -1
- package/dist/api/routes/commands.js +16 -13
- package/dist/api/routes/commands.js.map +1 -1
- package/dist/api/routes/context.d.ts.map +1 -1
- package/dist/api/routes/context.js +26 -3
- package/dist/api/routes/context.js.map +1 -1
- package/dist/api/routes/dashboard.d.ts.map +1 -1
- package/dist/api/routes/dashboard.js +103 -5
- package/dist/api/routes/dashboard.js.map +1 -1
- package/dist/api/routes/fs.d.ts +23 -0
- package/dist/api/routes/fs.d.ts.map +1 -0
- package/dist/api/routes/fs.js +156 -0
- package/dist/api/routes/fs.js.map +1 -0
- package/dist/api/routes/fs.logic.d.ts +62 -0
- package/dist/api/routes/fs.logic.d.ts.map +1 -0
- package/dist/api/routes/fs.logic.js +137 -0
- package/dist/api/routes/fs.logic.js.map +1 -0
- package/dist/api/routes/github.d.ts.map +1 -1
- package/dist/api/routes/github.js +38 -5
- package/dist/api/routes/github.js.map +1 -1
- package/dist/api/routes/health.d.ts.map +1 -1
- package/dist/api/routes/health.js +4 -2
- package/dist/api/routes/health.js.map +1 -1
- package/dist/api/routes/integrations.d.ts +35 -6
- package/dist/api/routes/integrations.d.ts.map +1 -1
- package/dist/api/routes/integrations.js +192 -15
- package/dist/api/routes/integrations.js.map +1 -1
- package/dist/api/routes/mail.d.ts.map +1 -1
- package/dist/api/routes/mail.js +112 -46
- package/dist/api/routes/mail.js.map +1 -1
- package/dist/api/routes/metrics.d.ts +1 -0
- package/dist/api/routes/metrics.d.ts.map +1 -1
- package/dist/api/routes/metrics.js +24 -0
- package/dist/api/routes/metrics.js.map +1 -1
- package/dist/api/routes/observations.d.ts.map +1 -1
- package/dist/api/routes/observations.js +696 -30
- package/dist/api/routes/observations.js.map +1 -1
- package/dist/api/routes/setup-migrate.d.ts +9 -1
- package/dist/api/routes/setup-migrate.d.ts.map +1 -1
- package/dist/api/routes/setup-migrate.js +4 -2
- package/dist/api/routes/setup-migrate.js.map +1 -1
- package/dist/api/routes/skills.d.ts +9 -1
- package/dist/api/routes/skills.d.ts.map +1 -1
- package/dist/api/routes/skills.js +77 -17
- package/dist/api/routes/skills.js.map +1 -1
- package/dist/api/routes/voice.d.ts.map +1 -1
- package/dist/api/routes/voice.js +62 -4
- package/dist/api/routes/voice.js.map +1 -1
- package/dist/api/routes/wiki.d.ts +4 -0
- package/dist/api/routes/wiki.d.ts.map +1 -0
- package/dist/api/routes/wiki.js +1075 -0
- package/dist/api/routes/wiki.js.map +1 -0
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +27 -1
- package/dist/api/server.js.map +1 -1
- package/dist/bootstrap/adapters.d.ts +109 -0
- package/dist/bootstrap/adapters.d.ts.map +1 -0
- package/dist/bootstrap/adapters.js +237 -0
- package/dist/bootstrap/adapters.js.map +1 -0
- package/dist/bootstrap/catchup.d.ts +23 -0
- package/dist/bootstrap/catchup.d.ts.map +1 -0
- package/dist/bootstrap/catchup.js +124 -0
- package/dist/bootstrap/catchup.js.map +1 -0
- package/dist/bootstrap/schedule-helpers.d.ts +18 -0
- package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
- package/dist/bootstrap/schedule-helpers.js +96 -0
- package/dist/bootstrap/schedule-helpers.js.map +1 -0
- package/dist/bootstrap/services.d.ts +60 -0
- package/dist/bootstrap/services.d.ts.map +1 -0
- package/dist/bootstrap/services.js +209 -0
- package/dist/bootstrap/services.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-core.d.ts +25 -0
- package/dist/core/agent-core.d.ts.map +1 -1
- package/dist/core/agent-core.js.map +1 -1
- package/dist/core/backends/backend-router.d.ts +28 -1
- package/dist/core/backends/backend-router.d.ts.map +1 -1
- package/dist/core/backends/backend-router.js +58 -4
- package/dist/core/backends/backend-router.js.map +1 -1
- package/dist/core/backends/claude-auth.d.ts +70 -0
- package/dist/core/backends/claude-auth.d.ts.map +1 -0
- package/dist/core/backends/claude-auth.js +198 -0
- package/dist/core/backends/claude-auth.js.map +1 -0
- package/dist/core/backends/claude-code-core.d.ts +47 -119
- package/dist/core/backends/claude-code-core.d.ts.map +1 -1
- package/dist/core/backends/claude-code-core.js +166 -1561
- package/dist/core/backends/claude-code-core.js.map +1 -1
- package/dist/core/backends/claude-delegated.d.ts +86 -0
- package/dist/core/backends/claude-delegated.d.ts.map +1 -0
- package/dist/core/backends/claude-delegated.js +801 -0
- package/dist/core/backends/claude-delegated.js.map +1 -0
- package/dist/core/backends/claude-errors.d.ts +39 -0
- package/dist/core/backends/claude-errors.d.ts.map +1 -0
- package/dist/core/backends/claude-errors.js +71 -0
- package/dist/core/backends/claude-errors.js.map +1 -0
- package/dist/core/backends/claude-probe.d.ts +103 -0
- package/dist/core/backends/claude-probe.d.ts.map +1 -0
- package/dist/core/backends/claude-probe.js +336 -0
- package/dist/core/backends/claude-probe.js.map +1 -0
- package/dist/core/backends/claude-tool-collection.d.ts +135 -0
- package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
- package/dist/core/backends/claude-tool-collection.js +1093 -0
- package/dist/core/backends/claude-tool-collection.js.map +1 -0
- package/dist/core/backends/codex-core.d.ts.map +1 -1
- package/dist/core/backends/codex-core.js +36 -0
- package/dist/core/backends/codex-core.js.map +1 -1
- package/dist/core/backends/gemini-cli-core.d.ts +45 -5
- package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
- package/dist/core/backends/gemini-cli-core.js +146 -36
- package/dist/core/backends/gemini-cli-core.js.map +1 -1
- package/dist/core/backends/plan-presets.d.ts +3 -1
- package/dist/core/backends/plan-presets.d.ts.map +1 -1
- package/dist/core/backends/plan-presets.js +42 -2
- package/dist/core/backends/plan-presets.js.map +1 -1
- package/dist/core/backends/prompt-utils.d.ts +1 -0
- package/dist/core/backends/prompt-utils.d.ts.map +1 -1
- package/dist/core/backends/prompt-utils.js +60 -3
- package/dist/core/backends/prompt-utils.js.map +1 -1
- package/dist/core/bang-commands/commands-help.d.ts +5 -0
- package/dist/core/bang-commands/commands-help.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-help.js +69 -0
- package/dist/core/bang-commands/commands-help.js.map +1 -0
- package/dist/core/bang-commands/commands-wiki.d.ts +75 -0
- package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-wiki.js +574 -0
- package/dist/core/bang-commands/commands-wiki.js.map +1 -0
- package/dist/core/bang-commands/index.d.ts +4 -2
- package/dist/core/bang-commands/index.d.ts.map +1 -1
- package/dist/core/bang-commands/index.js +15 -1
- package/dist/core/bang-commands/index.js.map +1 -1
- package/dist/core/bang-commands/registry.d.ts +47 -4
- package/dist/core/bang-commands/registry.d.ts.map +1 -1
- package/dist/core/bang-commands/registry.js +85 -15
- package/dist/core/bang-commands/registry.js.map +1 -1
- package/dist/core/context-builder.d.ts +53 -12
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +240 -92
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/daemon-api-cli.d.ts.map +1 -1
- package/dist/core/daemon-api-cli.js +50 -2
- package/dist/core/daemon-api-cli.js.map +1 -1
- package/dist/core/dispatcher-date-utils.d.ts +49 -0
- package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
- package/dist/core/dispatcher-date-utils.js +132 -0
- package/dist/core/dispatcher-date-utils.js.map +1 -0
- package/dist/core/dispatcher-error-handling.d.ts +159 -0
- package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
- package/dist/core/dispatcher-error-handling.js +393 -0
- package/dist/core/dispatcher-error-handling.js.map +1 -0
- package/dist/core/dispatcher-hourly-check.d.ts +150 -0
- package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
- package/dist/core/dispatcher-hourly-check.js +665 -0
- package/dist/core/dispatcher-hourly-check.js.map +1 -0
- package/dist/core/dispatcher-message-handler.d.ts +170 -0
- package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
- package/dist/core/dispatcher-message-handler.js +1064 -0
- package/dist/core/dispatcher-message-handler.js.map +1 -0
- package/dist/core/dispatcher-morning-routine.d.ts +169 -0
- package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
- package/dist/core/dispatcher-morning-routine.js +449 -0
- package/dist/core/dispatcher-morning-routine.js.map +1 -0
- package/dist/core/dispatcher-prompt.d.ts +107 -0
- package/dist/core/dispatcher-prompt.d.ts.map +1 -0
- package/dist/core/dispatcher-prompt.js +227 -0
- package/dist/core/dispatcher-prompt.js.map +1 -0
- package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
- package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
- package/dist/core/dispatcher-repository-helpers.js +86 -0
- package/dist/core/dispatcher-repository-helpers.js.map +1 -0
- package/dist/core/dispatcher-result-processor.d.ts +168 -0
- package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
- package/dist/core/dispatcher-result-processor.js +533 -0
- package/dist/core/dispatcher-result-processor.js.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.js +1032 -0
- package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
- package/dist/core/dispatcher-types.d.ts +411 -0
- package/dist/core/dispatcher-types.d.ts.map +1 -0
- package/dist/core/dispatcher-types.js +106 -0
- package/dist/core/dispatcher-types.js.map +1 -0
- package/dist/core/dispatcher.d.ts +122 -610
- package/dist/core/dispatcher.d.ts.map +1 -1
- package/dist/core/dispatcher.js +365 -3521
- package/dist/core/dispatcher.js.map +1 -1
- package/dist/core/integration-health.d.ts +18 -10
- package/dist/core/integration-health.d.ts.map +1 -1
- package/dist/core/integration-health.js +31 -1
- package/dist/core/integration-health.js.map +1 -1
- package/dist/core/integration-lifecycle.d.ts +65 -0
- package/dist/core/integration-lifecycle.d.ts.map +1 -1
- package/dist/core/integration-lifecycle.js +163 -14
- package/dist/core/integration-lifecycle.js.map +1 -1
- package/dist/core/integration-main-backend.d.ts +40 -0
- package/dist/core/integration-main-backend.d.ts.map +1 -1
- package/dist/core/integration-main-backend.js +89 -2
- package/dist/core/integration-main-backend.js.map +1 -1
- package/dist/core/management-md.d.ts +51 -17
- package/dist/core/management-md.d.ts.map +1 -1
- package/dist/core/management-md.js +233 -56
- package/dist/core/management-md.js.map +1 -1
- package/dist/core/metrics.d.ts +127 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +256 -1
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/output-language-policy.d.ts +74 -0
- package/dist/core/output-language-policy.d.ts.map +1 -0
- package/dist/core/output-language-policy.js +194 -0
- package/dist/core/output-language-policy.js.map +1 -0
- package/dist/core/prompts.d.ts +3 -1
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/prompts.js +161 -3
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/repository-management-docs.d.ts +24 -0
- package/dist/core/repository-management-docs.d.ts.map +1 -1
- package/dist/core/repository-management-docs.js +210 -26
- package/dist/core/repository-management-docs.js.map +1 -1
- package/dist/core/roadmap-validate.js +13 -1
- package/dist/core/roadmap-validate.js.map +1 -1
- package/dist/core/routine-acquisition-plan.d.ts +182 -0
- package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
- package/dist/core/routine-acquisition-plan.js +367 -0
- package/dist/core/routine-acquisition-plan.js.map +1 -0
- package/dist/core/routine-fetch-window-retry.d.ts +109 -0
- package/dist/core/routine-fetch-window-retry.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-retry.js +210 -0
- package/dist/core/routine-fetch-window-retry.js.map +1 -0
- package/dist/core/routine-fetch-window-runner.d.ts +427 -0
- package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-runner.js +1591 -0
- package/dist/core/routine-fetch-window-runner.js.map +1 -0
- package/dist/core/routine-windows.d.ts +171 -0
- package/dist/core/routine-windows.d.ts.map +1 -0
- package/dist/core/routine-windows.js +377 -0
- package/dist/core/routine-windows.js.map +1 -0
- package/dist/core/scheduler.d.ts +50 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +88 -7
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/skill-curation/declarations.d.ts.map +1 -1
- package/dist/core/skill-curation/declarations.js +11 -12
- package/dist/core/skill-curation/declarations.js.map +1 -1
- package/dist/core/skill-source-paths.d.ts +14 -0
- package/dist/core/skill-source-paths.d.ts.map +1 -0
- package/dist/core/skill-source-paths.js +82 -0
- package/dist/core/skill-source-paths.js.map +1 -0
- package/dist/core/skills-compiler.d.ts +29 -0
- package/dist/core/skills-compiler.d.ts.map +1 -1
- package/dist/core/skills-compiler.js +166 -30
- package/dist/core/skills-compiler.js.map +1 -1
- package/dist/core/skills-manifest.d.ts.map +1 -1
- package/dist/core/skills-manifest.js +72 -0
- package/dist/core/skills-manifest.js.map +1 -1
- package/dist/core/system-reset.d.ts +25 -0
- package/dist/core/system-reset.d.ts.map +1 -1
- package/dist/core/system-reset.js +72 -2
- package/dist/core/system-reset.js.map +1 -1
- package/dist/core/wiki/approval-queue.d.ts +31 -0
- package/dist/core/wiki/approval-queue.d.ts.map +1 -0
- package/dist/core/wiki/approval-queue.js +44 -0
- package/dist/core/wiki/approval-queue.js.map +1 -0
- package/dist/core/wiki/bridge.d.ts +74 -0
- package/dist/core/wiki/bridge.d.ts.map +1 -0
- package/dist/core/wiki/bridge.js +405 -0
- package/dist/core/wiki/bridge.js.map +1 -0
- package/dist/core/wiki/compile-lock.d.ts +42 -0
- package/dist/core/wiki/compile-lock.d.ts.map +1 -0
- package/dist/core/wiki/compile-lock.js +55 -0
- package/dist/core/wiki/compile-lock.js.map +1 -0
- package/dist/core/wiki/compile-preview.d.ts +8 -0
- package/dist/core/wiki/compile-preview.d.ts.map +1 -0
- package/dist/core/wiki/compile-preview.js +200 -0
- package/dist/core/wiki/compile-preview.js.map +1 -0
- package/dist/core/wiki/cost-estimate.d.ts +30 -0
- package/dist/core/wiki/cost-estimate.d.ts.map +1 -0
- package/dist/core/wiki/cost-estimate.js +243 -0
- package/dist/core/wiki/cost-estimate.js.map +1 -0
- package/dist/core/wiki/dispatcher.d.ts +48 -0
- package/dist/core/wiki/dispatcher.d.ts.map +1 -0
- package/dist/core/wiki/dispatcher.js +92 -0
- package/dist/core/wiki/dispatcher.js.map +1 -0
- package/dist/core/wiki/git-precompile.d.ts +86 -0
- package/dist/core/wiki/git-precompile.d.ts.map +1 -0
- package/dist/core/wiki/git-precompile.js +96 -0
- package/dist/core/wiki/git-precompile.js.map +1 -0
- package/dist/core/wiki/import-migrate.d.ts +38 -0
- package/dist/core/wiki/import-migrate.d.ts.map +1 -0
- package/dist/core/wiki/import-migrate.js +310 -0
- package/dist/core/wiki/import-migrate.js.map +1 -0
- package/dist/core/wiki/import-probe.d.ts +76 -0
- package/dist/core/wiki/import-probe.d.ts.map +1 -0
- package/dist/core/wiki/import-probe.js +245 -0
- package/dist/core/wiki/import-probe.js.map +1 -0
- package/dist/core/wiki/index-cache.d.ts +39 -0
- package/dist/core/wiki/index-cache.d.ts.map +1 -0
- package/dist/core/wiki/index-cache.js +152 -0
- package/dist/core/wiki/index-cache.js.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts +52 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.js +72 -0
- package/dist/core/wiki/multi-url-dispatch.js.map +1 -0
- package/dist/core/wiki/wiki-fts.d.ts +75 -0
- package/dist/core/wiki/wiki-fts.d.ts.map +1 -0
- package/dist/core/wiki/wiki-fts.js +265 -0
- package/dist/core/wiki/wiki-fts.js.map +1 -0
- package/dist/core/wiki/workspaces.d.ts +101 -0
- package/dist/core/wiki/workspaces.d.ts.map +1 -0
- package/dist/core/wiki/workspaces.js +352 -0
- package/dist/core/wiki/workspaces.js.map +1 -0
- package/dist/core/wiki/write-strategy.d.ts +70 -0
- package/dist/core/wiki/write-strategy.d.ts.map +1 -0
- package/dist/core/wiki/write-strategy.js +112 -0
- package/dist/core/wiki/write-strategy.js.map +1 -0
- package/dist/core/workdir.d.ts +8 -1
- package/dist/core/workdir.d.ts.map +1 -1
- package/dist/core/workdir.js +4 -1
- package/dist/core/workdir.js.map +1 -1
- package/dist/db/observations.d.ts +45 -2
- package/dist/db/observations.d.ts.map +1 -1
- package/dist/db/observations.js +112 -14
- package/dist/db/observations.js.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +135 -25
- package/dist/db/schema.js.map +1 -1
- package/dist/db/wiki-store.d.ts +3 -0
- package/dist/db/wiki-store.d.ts.map +1 -0
- package/dist/db/wiki-store.js +7 -0
- package/dist/db/wiki-store.js.map +1 -0
- package/dist/index.js +159 -610
- package/dist/index.js.map +1 -1
- package/dist/messaging/url-extract.d.ts +8 -0
- package/dist/messaging/url-extract.d.ts.map +1 -0
- package/dist/messaging/url-extract.js +41 -0
- package/dist/messaging/url-extract.js.map +1 -0
- package/dist/observers/delegated-sync-worker.d.ts +52 -1
- package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
- package/dist/observers/delegated-sync-worker.js +75 -18
- package/dist/observers/delegated-sync-worker.js.map +1 -1
- package/dist/observers/imminent-event-scheduler.d.ts +20 -7
- package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
- package/dist/observers/imminent-event-scheduler.js +134 -29
- package/dist/observers/imminent-event-scheduler.js.map +1 -1
- package/dist/observers/mail-poller.d.ts +12 -5
- package/dist/observers/mail-poller.d.ts.map +1 -1
- package/dist/observers/mail-poller.js +36 -14
- package/dist/observers/mail-poller.js.map +1 -1
- package/dist/observers/manager.d.ts +37 -5
- package/dist/observers/manager.d.ts.map +1 -1
- package/dist/observers/manager.js +28 -10
- package/dist/observers/manager.js.map +1 -1
- package/dist/safety/always-disallowed.d.ts +65 -0
- package/dist/safety/always-disallowed.d.ts.map +1 -1
- package/dist/safety/always-disallowed.js +106 -10
- package/dist/safety/always-disallowed.js.map +1 -1
- package/dist/safety/audit.d.ts +46 -1
- package/dist/safety/audit.d.ts.map +1 -1
- package/dist/safety/audit.js +79 -16
- package/dist/safety/audit.js.map +1 -1
- package/dist/safety/risk-classifier.d.ts.map +1 -1
- package/dist/safety/risk-classifier.js +29 -0
- package/dist/safety/risk-classifier.js.map +1 -1
- package/dist/services/delegated-backend-invoker.d.ts +1 -51
- package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
- package/dist/services/delegated-backend-invoker.js +41 -480
- package/dist/services/delegated-backend-invoker.js.map +1 -1
- package/dist/services/delegated-invoker-audit.d.ts +94 -0
- package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
- package/dist/services/delegated-invoker-audit.js +238 -0
- package/dist/services/delegated-invoker-audit.js.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.js +104 -0
- package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
- package/dist/services/delegated-invoker-janitors.d.ts +28 -0
- package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
- package/dist/services/delegated-invoker-janitors.js +104 -0
- package/dist/services/delegated-invoker-janitors.js.map +1 -0
- package/dist/services/delegated-invoker-utils.d.ts +42 -0
- package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
- package/dist/services/delegated-invoker-utils.js +100 -0
- package/dist/services/delegated-invoker-utils.js.map +1 -0
- package/dist/services/delegated-task-runtime.d.ts +1 -1
- package/dist/services/delegated-task-runtime.js +1 -1
- package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
- package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
- package/dist/services/integrations/snapshot-partitions.js +12 -0
- package/dist/services/integrations/snapshot-partitions.js.map +1 -1
- package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
- package/dist/services/voice/transcriber-impl.js +7 -8
- package/dist/services/voice/transcriber-impl.js.map +1 -1
- package/dist/settings/runtime-settings.d.ts +12 -1
- package/dist/settings/runtime-settings.d.ts.map +1 -1
- package/dist/settings/runtime-settings.js +59 -1
- package/dist/settings/runtime-settings.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude tool surface — pure helpers split out of `claude-code-core.ts` as
|
|
3
|
+
* part of the file-split plan (Tier 2, §8). Owns five responsibilities:
|
|
4
|
+
*
|
|
5
|
+
* - `getAllowedTools` — assemble the SDK `allowedTools` list from the
|
|
6
|
+
* configured default + the runtime override + any delegated- and native-
|
|
7
|
+
* integration tools the registry exposes.
|
|
8
|
+
* - `getDelegatedClaudeTools` — read the current `integrations` registry
|
|
9
|
+
* state and project it through `computeDelegatedClaudeTools`. Returns
|
|
10
|
+
* `[]` when the MCP context is not yet wired or on DB read failure.
|
|
11
|
+
* - `getNativeClaudeTools` — same shape as `getDelegatedClaudeTools` but
|
|
12
|
+
* projects through `computeNativeClaudeTools` (native-mode parallel).
|
|
13
|
+
* - `getSessionDeniedTools` — DELEGATED-MODE-V2-DESIGN.md §4.3.3 — expand
|
|
14
|
+
* per-integration `deniedTools` into namespaced tool names that the SDK
|
|
15
|
+
* rejects via `disallowedTools` regardless of the allow list.
|
|
16
|
+
* - `buildSecurityHooks` — build the PreToolUse hook record that enforces
|
|
17
|
+
* curl localhost-only, jq env/file-flag denials, context-dir chokepoint,
|
|
18
|
+
* vault write attribution, and the absolute-block audit layer.
|
|
19
|
+
*
|
|
20
|
+
* Pattern A (file-split-plan §5): each function reads its dependencies via
|
|
21
|
+
* an explicit argument record rather than `this.<field>`. The pure shape
|
|
22
|
+
* means these can be unit tested without instantiating `ClaudeCodeCore`,
|
|
23
|
+
* and lets tests inspect the hook closures directly. Thin shims on
|
|
24
|
+
* `ClaudeCodeCore` (`private getAllowedTools(...) { return ... }`) remain
|
|
25
|
+
* for the transitional period (file-split-plan §15).
|
|
26
|
+
*/
|
|
27
|
+
import { collectSessionDeniedTools } from "@aitne/shared";
|
|
28
|
+
import { realpathSync } from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { dirname, resolve as resolvePath, isAbsolute } from "node:path";
|
|
31
|
+
import { getContextDir } from "../../config.js";
|
|
32
|
+
import { readIntegrations } from "../../db/integrations-store.js";
|
|
33
|
+
import { recordAbsoluteBlockAudit } from "../../safety/absolute-block-audit.js";
|
|
34
|
+
import { classifyAbsoluteBlock, stripBashHeredocs, stripBashStringContent, } from "../../safety/always-disallowed.js";
|
|
35
|
+
import { createLogger } from "../../logging.js";
|
|
36
|
+
import { computeDelegatedClaudeTools, computeNativeClaudeTools } from "./claude-probe.js";
|
|
37
|
+
import { isPathInsideOrEqual, shellPathForms } from "../path-compat.js";
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a path through symlinks, even when the leaf does not yet exist.
|
|
40
|
+
*
|
|
41
|
+
* `fs.realpathSync` throws ENOENT on a non-existent leaf, which is the
|
|
42
|
+
* common case for a Write hook (the target file is the *next* write).
|
|
43
|
+
* Walk upwards until an existing ancestor is found, realpath that, then
|
|
44
|
+
* rejoin the missing suffix. Used by both `fileWriteHook` and
|
|
45
|
+
* `bashContextWriteHook` to defeat symlink-based bypasses that point
|
|
46
|
+
* back into the context dir.
|
|
47
|
+
*/
|
|
48
|
+
function realpathLenient(absPath) {
|
|
49
|
+
const segments = [];
|
|
50
|
+
let current = absPath;
|
|
51
|
+
// Hard ceiling on iterations so a pathological path never spins forever.
|
|
52
|
+
for (let i = 0; i < 64; i++) {
|
|
53
|
+
try {
|
|
54
|
+
const real = realpathSync(current);
|
|
55
|
+
return segments.length === 0
|
|
56
|
+
? real
|
|
57
|
+
: resolvePath(real, ...segments.reverse());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current)
|
|
62
|
+
return absPath;
|
|
63
|
+
segments.push(current.slice(parent.length).replace(/^[/\\]+/, ""));
|
|
64
|
+
current = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return absPath;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Best-effort shell tokenizer for path-token scanning. Splits on
|
|
71
|
+
* whitespace while honouring single, double, and back-tick quotes; ignores
|
|
72
|
+
* shell operators (`|`, `;`, `&`, `<`, `>`, parentheses). Returns tokens
|
|
73
|
+
* with their quote wrappers stripped.
|
|
74
|
+
*
|
|
75
|
+
* Not a full shell parser — it cannot resolve variable expansions,
|
|
76
|
+
* subshells, or function definitions. Exists to surface *literal* path
|
|
77
|
+
* arguments so that an obvious form like
|
|
78
|
+
* `echo > /Users/shuto/.personal-agent/context/today.md` is caught. The
|
|
79
|
+
* absolute-block layer is the authoritative defence for the things this
|
|
80
|
+
* heuristic misses.
|
|
81
|
+
*/
|
|
82
|
+
function tokenizeShellCommand(cmd) {
|
|
83
|
+
const tokens = [];
|
|
84
|
+
const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|\$\(([^)]*)\)|([^\s|;&<>()]+)/g;
|
|
85
|
+
let match;
|
|
86
|
+
while ((match = re.exec(cmd)) !== null) {
|
|
87
|
+
const tok = match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? "";
|
|
88
|
+
if (tok.length > 0)
|
|
89
|
+
tokens.push(tok);
|
|
90
|
+
}
|
|
91
|
+
return tokens;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Expand the leading `~`, `$HOME`, and `${HOME}` segments of a token
|
|
95
|
+
* to the supplied home directory. No other shell expansion is performed.
|
|
96
|
+
*/
|
|
97
|
+
function expandHomeForms(token, home) {
|
|
98
|
+
if (token === "~")
|
|
99
|
+
return home;
|
|
100
|
+
if (token.startsWith("~/"))
|
|
101
|
+
return home + token.slice(1);
|
|
102
|
+
if (token.startsWith("$HOME/"))
|
|
103
|
+
return home + token.slice(5);
|
|
104
|
+
if (token.startsWith("${HOME}/"))
|
|
105
|
+
return home + token.slice(7);
|
|
106
|
+
if (token === "$HOME" || token === "${HOME}")
|
|
107
|
+
return home;
|
|
108
|
+
return token;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether a shell token (after `expandHomeForms` normalisation)
|
|
112
|
+
* resembles a filesystem path argument. Replaces the older "contains
|
|
113
|
+
* `/` or `\`" filter, which false-positived on quoted JSON bodies and
|
|
114
|
+
* HTTP header values whenever the session cwd was inside the data dir
|
|
115
|
+
* (production cwd is `<dataDir>/agent-sessions/<id>`):
|
|
116
|
+
*
|
|
117
|
+
* - `Content-Type: application/json` → `/` inside `application/json`
|
|
118
|
+
* was treated as a path separator, the token was resolved relative
|
|
119
|
+
* to the (data-dir-internal) cwd, the resulting candidate landed
|
|
120
|
+
* inside `absDataDir`, and the hook blocked an otherwise benign
|
|
121
|
+
* `curl -X PATCH -H '...'` invocation.
|
|
122
|
+
* - `'{"content":"line1\nline2"}'` → the literal `\` in `\n` triggered
|
|
123
|
+
* the same data-dir resolution path even though the token is a JSON
|
|
124
|
+
* payload, not a filename.
|
|
125
|
+
*
|
|
126
|
+
* The shape rules below are deliberately positive (a token must look
|
|
127
|
+
* like a path) rather than negative (skip if it contains JSON chars):
|
|
128
|
+
*
|
|
129
|
+
* 1. Absolute on POSIX (`/foo`) — also catches tokens that
|
|
130
|
+
* `expandHomeForms` rewrote from `~` / `$HOME` / `${HOME}` forms.
|
|
131
|
+
* 2. Explicit relative anchor (`./foo`, `../foo`, exactly `.` / `..`).
|
|
132
|
+
* 3. Unresolved home / env-var prefix that survived `expandHomeForms`
|
|
133
|
+
* (e.g. `~user/foo`, `$OTHER/foo` when the variable is unknown).
|
|
134
|
+
* Treating these as path candidates is a defensive belt — the
|
|
135
|
+
* static analysis can't know what they expand to at runtime, so
|
|
136
|
+
* err on the side of forwarding them through the data-dir check.
|
|
137
|
+
* 4. Bare multi-segment path made of filename-safe characters
|
|
138
|
+
* (`context/today.md`, `agent-sessions/foo/bar`). The character
|
|
139
|
+
* class deliberately excludes whitespace, `:`, `=`, `{`, `}`,
|
|
140
|
+
* `"`, `'`, `` ` ``, `?`, `*`, `<`, `>` — all of which appear in
|
|
141
|
+
* header values, JSON bodies, and query strings but never in
|
|
142
|
+
* well-formed filename segments.
|
|
143
|
+
*
|
|
144
|
+
* URL-shaped tokens (`http://...`) are filtered by the caller before
|
|
145
|
+
* this helper runs, so rule 1 / rule 4 cannot misfire on them.
|
|
146
|
+
*/
|
|
147
|
+
function looksLikePathArg(token) {
|
|
148
|
+
if (token.length === 0)
|
|
149
|
+
return false;
|
|
150
|
+
// Rules 1 + 2 — POSIX-absolute or anchored-relative.
|
|
151
|
+
if (token.startsWith("/"))
|
|
152
|
+
return true;
|
|
153
|
+
if (token === "." || token === ".." || token.startsWith("./") || token.startsWith("../")) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Rule 3 — unresolved home / env-var prefix.
|
|
157
|
+
if (token.startsWith("~") || token.startsWith("$"))
|
|
158
|
+
return true;
|
|
159
|
+
// Rule 4 — bare relative path with filename-safe segments only.
|
|
160
|
+
// `[A-Za-z0-9_.\-+@]` is the segment alphabet — broad enough to
|
|
161
|
+
// cover typical project filenames (dashes, underscores, dots,
|
|
162
|
+
// version suffixes, `@scope/pkg` style) without admitting tokens
|
|
163
|
+
// that came from a JSON body or header value. The trailing `/?`
|
|
164
|
+
// tolerates a directory-shape suffix (`context/`).
|
|
165
|
+
if (/^[A-Za-z0-9_.\-+@]+(?:\/[A-Za-z0-9_.\-+@]+)+\/?$/.test(token)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const logger = createLogger("claude-tool-collection");
|
|
171
|
+
/** Default allowed-tools list when the dashboard override is unset. */
|
|
172
|
+
export const CLAUDE_DEFAULT_ALLOWED_TOOLS = [
|
|
173
|
+
"Read",
|
|
174
|
+
"Glob",
|
|
175
|
+
"Grep",
|
|
176
|
+
"Write",
|
|
177
|
+
"Edit",
|
|
178
|
+
"Skill", // user skills (external-services, obsidian-*, observations, ...)
|
|
179
|
+
"Bash(curl *)", // curl broadly allowed; hooks restrict to localhost
|
|
180
|
+
"Bash(git *)", // Git operations
|
|
181
|
+
"Bash(jq *)", // safe JSON post-processor for curl pipelines
|
|
182
|
+
];
|
|
183
|
+
/**
|
|
184
|
+
* Allowed tools whitelist for dontAsk permission mode.
|
|
185
|
+
*
|
|
186
|
+
* `delegatedTools` and `nativeTools` are UNION'd onto the returned list —
|
|
187
|
+
* even when `allowedToolsOverride` is set. This is a deliberate deviation
|
|
188
|
+
* from the override's otherwise-absolute "replace everything" contract (see
|
|
189
|
+
* `CRITICAL_OVERRIDE_TOOLS` in `claude-code-core.ts`, which warns but does
|
|
190
|
+
* not union). Rationale: delegated / native modes are runtime-configurable
|
|
191
|
+
* axes orthogonal to the dashboard's tool-customization override. If a user
|
|
192
|
+
* set the override before flipping an integration, silently dropping the
|
|
193
|
+
* registry-declared connector tools would break mail/calendar with a
|
|
194
|
+
* misleading "permission denied" DM. Union semantics keep the override's
|
|
195
|
+
* curation intent while letting either mode widen the surface to whatever
|
|
196
|
+
* the registry already advertised.
|
|
197
|
+
*
|
|
198
|
+
* Native and delegated lists are accepted separately (rather than a single
|
|
199
|
+
* `extraMcpTools` parameter) so callers — and tests — surface the
|
|
200
|
+
* provenance of every widening: an audit log entry with
|
|
201
|
+
* `delegatedToolCount` and `nativeToolCount` makes a misconfigured flip
|
|
202
|
+
* diagnosable without re-running the resolver.
|
|
203
|
+
*/
|
|
204
|
+
export function getAllowedTools(config, webSearchEnabled, delegatedTools = [], nativeTools = [],
|
|
205
|
+
// WIKI_BUILDER_DESIGN.md §4.3 — wiki.ingest_url turns need WebFetch on
|
|
206
|
+
// top of the default surface to read external pages (the `Bash(curl *)`
|
|
207
|
+
// PreToolUse hook keeps curl restricted to localhost). Gated on the
|
|
208
|
+
// same `!allowedToolsOverride` clause as `webSearchEnabled` so a user
|
|
209
|
+
// who configured a custom override gets the override verbatim — they
|
|
210
|
+
// are expected to add `WebFetch` themselves if they need it (matches
|
|
211
|
+
// the WebSearch contract; documented in /settings/wiki).
|
|
212
|
+
wikiUrlFetchEnabled = false,
|
|
213
|
+
// Wiki sessions must write only through the daemon Wiki API
|
|
214
|
+
// (`POST /api/wiki/<ws>/files/...`) — every wiki.* process key has a
|
|
215
|
+
// skill body and the wiki-agent profile both stating "no `Write` /
|
|
216
|
+
// `Edit` against the vault." Skill frontmatter `allowed-tools` is
|
|
217
|
+
// human-facing metadata and does NOT propagate into the SDK's
|
|
218
|
+
// session-level allowlist, so without this hard strip a wiki turn
|
|
219
|
+
// can bypass the API path-classifier, the agent_actions audit row,
|
|
220
|
+
// and the result-processor's write-verifier by Writing a vault
|
|
221
|
+
// path directly. Pass true for any `processKey.startsWith("wiki.")`.
|
|
222
|
+
wikiApiOnlyWrites = false) {
|
|
223
|
+
const base = config.allowedToolsOverride ?? [...CLAUDE_DEFAULT_ALLOWED_TOOLS];
|
|
224
|
+
const merged = new Set(base);
|
|
225
|
+
if (!config.allowedToolsOverride && webSearchEnabled) {
|
|
226
|
+
merged.add("WebSearch");
|
|
227
|
+
}
|
|
228
|
+
if (!config.allowedToolsOverride && wikiUrlFetchEnabled) {
|
|
229
|
+
merged.add("WebFetch");
|
|
230
|
+
}
|
|
231
|
+
for (const tool of delegatedTools)
|
|
232
|
+
merged.add(tool);
|
|
233
|
+
for (const tool of nativeTools)
|
|
234
|
+
merged.add(tool);
|
|
235
|
+
// Claude Code 2.1+ defers large MCP manifests (`mcp__claude_ai_*`) behind
|
|
236
|
+
// `ToolSearch` — the tools appear by name but their schemas are not
|
|
237
|
+
// loaded until the agent calls `ToolSearch select:<name>`. Without
|
|
238
|
+
// ToolSearch allowed, the model cannot invoke any unioned MCP tool and
|
|
239
|
+
// silently falls back to denied surfaces (raw Bash, WebFetch), surfacing
|
|
240
|
+
// as "Bash and WebFetch denied" failure DMs from native/delegated-same
|
|
241
|
+
// routines. Mirrors the same widening already applied by
|
|
242
|
+
// `composePrePassAllowedTools` (pre-pass), `CLAUDE_PROBE_TOOLS_PROMPT`
|
|
243
|
+
// (probe), and `claude-delegated.ts` (cross-backend proxy). Unioned even
|
|
244
|
+
// under `allowedToolsOverride` for the same orthogonality reason the
|
|
245
|
+
// MCP tools themselves bypass the override above — silently dropping
|
|
246
|
+
// ToolSearch while keeping the MCP names defeats the widening.
|
|
247
|
+
if (delegatedTools.length > 0 || nativeTools.length > 0) {
|
|
248
|
+
merged.add("ToolSearch");
|
|
249
|
+
}
|
|
250
|
+
if (wikiApiOnlyWrites) {
|
|
251
|
+
merged.delete("Write");
|
|
252
|
+
merged.delete("Edit");
|
|
253
|
+
}
|
|
254
|
+
return Array.from(merged);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Read the integrations record from the wired MCP context and project it
|
|
258
|
+
* through the `computeDelegatedClaudeTools` allowlist computation. Returns
|
|
259
|
+
* `[]` when the context is not yet wired (tests / startup ordering) or on
|
|
260
|
+
* DB read failure — the latter is logged as a warning so a corrupt
|
|
261
|
+
* integrations table is visible without halting the session.
|
|
262
|
+
*/
|
|
263
|
+
export function getDelegatedClaudeTools(mcpContext) {
|
|
264
|
+
if (!mcpContext)
|
|
265
|
+
return [];
|
|
266
|
+
try {
|
|
267
|
+
const integrations = readIntegrations(mcpContext.db);
|
|
268
|
+
return computeDelegatedClaudeTools(integrations);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
logger.warn({ err }, "Failed to read integrations for delegated-tool allowlist — proceeding without delegated tools");
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Sibling of `getDelegatedClaudeTools` — projects integrations record
|
|
277
|
+
* through `computeNativeClaudeTools`. Returns `[]` when the context is
|
|
278
|
+
* not yet wired or on DB read failure, matching the conservative pattern
|
|
279
|
+
* used by the delegated counterpart.
|
|
280
|
+
*
|
|
281
|
+
* Required because the SDK's `dontAsk` permission mode silently denies
|
|
282
|
+
* tools not in `allowedTools`. Native-mode skill bodies instruct the
|
|
283
|
+
* agent to call connector MCP tools directly (e.g.
|
|
284
|
+
* `mcp__claude_ai_Gmail__search_threads`), so the registry-declared tool
|
|
285
|
+
* names for every `mode === "native" && nativeBackend === "claude"` row
|
|
286
|
+
* must be pre-authorized.
|
|
287
|
+
*/
|
|
288
|
+
export function getNativeClaudeTools(mcpContext) {
|
|
289
|
+
if (!mcpContext)
|
|
290
|
+
return [];
|
|
291
|
+
try {
|
|
292
|
+
const integrations = readIntegrations(mcpContext.db);
|
|
293
|
+
return computeNativeClaudeTools(integrations);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
logger.warn({ err }, "Failed to read integrations for native-tool allowlist — proceeding without native tools");
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* DELEGATED-MODE-V2-DESIGN.md §4.3.3 — same-backend deny enforcement at
|
|
302
|
+
* the SDK boundary. For every integration whose `delegatedBackend === "claude"`,
|
|
303
|
+
* expand `state.deniedTools` against the connector's known tools and emit
|
|
304
|
+
* the namespaced names (`mcp__claude_ai_<X>__<tool>`). The SDK refuses any
|
|
305
|
+
* tool listed in `disallowedTools` regardless of `allowedTools` — hard
|
|
306
|
+
* enforcement.
|
|
307
|
+
*
|
|
308
|
+
* Returns `[]` when context isn't wired (tests / pre-startup) and on read
|
|
309
|
+
* failures, matching the conservative pattern used by
|
|
310
|
+
* `getDelegatedClaudeTools`.
|
|
311
|
+
*/
|
|
312
|
+
export function getSessionDeniedTools(mcpContext) {
|
|
313
|
+
if (!mcpContext)
|
|
314
|
+
return [];
|
|
315
|
+
try {
|
|
316
|
+
const integrations = readIntegrations(mcpContext.db);
|
|
317
|
+
const map = collectSessionDeniedTools(integrations, "claude");
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const names of map.values()) {
|
|
320
|
+
for (const n of names)
|
|
321
|
+
out.push(n);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
logger.warn({ err }, "Failed to read integrations for same-backend denied-tools — proceeding without per-integration deny");
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Security hooks:
|
|
332
|
+
* 1. Bash(curl *) — restrict to localhost Daemon API, block connection-override flags. (strict only)
|
|
333
|
+
* 2. Bash(jq *) — block file-access flags and the `env` filter (process env exfiltration). (strict only)
|
|
334
|
+
* 3. Write/Edit — block writes into the session helper dir and context dir, mark vault writes.
|
|
335
|
+
*
|
|
336
|
+
* In allow mode the curl and jq hooks are dropped, but the Write/Edit hook
|
|
337
|
+
* stays: the context-dir chokepoint exists for memory integrity (today-write
|
|
338
|
+
* lock, md_file_snapshots, CONTEXT_WRITE_PERMISSIONS), not permissions.
|
|
339
|
+
*/
|
|
340
|
+
export function buildSecurityHooks(deps, allowMode = false) {
|
|
341
|
+
const { config, writeTracker, getMcpContext } = deps;
|
|
342
|
+
// Per-Bash-hook block logging. The SDK's `dontAsk` mode silently
|
|
343
|
+
// denies any Bash command that doesn't match an allowed prefix —
|
|
344
|
+
// no tool_result, no error feedback — and PreToolUse hooks that
|
|
345
|
+
// return `block` emit a generic reason that the agent often
|
|
346
|
+
// misinterprets as "Bash is blocked entirely." Without this log,
|
|
347
|
+
// diagnosing a failed wiki / context update means guessing at the
|
|
348
|
+
// command the model produced. The line is logged at warn level
|
|
349
|
+
// (one per actual block, not per call) so steady-state cost is
|
|
350
|
+
// negligible; the cmd is truncated to 400 chars to keep secrets
|
|
351
|
+
// out of logs and the entry parseable.
|
|
352
|
+
const wrapBashHook = (hookName, inner) => async (input) => {
|
|
353
|
+
const result = await inner(input);
|
|
354
|
+
if (result && result.decision === "block") {
|
|
355
|
+
const toolInput = input.tool_input;
|
|
356
|
+
const cmd = toolInput?.command ?? "";
|
|
357
|
+
logger.warn({
|
|
358
|
+
hook: hookName,
|
|
359
|
+
reason: result.reason,
|
|
360
|
+
cmd: cmd.slice(0, 400),
|
|
361
|
+
}, "Bash hook block");
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
365
|
+
const bashCurlHook = async (input) => {
|
|
366
|
+
const toolInput = input.tool_input;
|
|
367
|
+
const cmd = toolInput?.command ?? "";
|
|
368
|
+
// Three views of the command, each used by a different class of check:
|
|
369
|
+
//
|
|
370
|
+
// - `cmd` (raw) — the initial `\bcurl\b` keyword presence test.
|
|
371
|
+
// Must see literal token text so a `-d
|
|
372
|
+
// '{"text":"see curl docs"}'` body doesn't
|
|
373
|
+
// suppress the hook entirely.
|
|
374
|
+
// - `scan` — substring scans for flag PRESENCE (chained
|
|
375
|
+
// curl, --next, --proxy, -L, -o, -c, -b, etc.).
|
|
376
|
+
// Strips single-quoted strings AND heredoc
|
|
377
|
+
// bodies so prose inside a JSON payload like
|
|
378
|
+
// "set -o pipefail in scripts" cannot trip
|
|
379
|
+
// the flag detectors.
|
|
380
|
+
// - `tokenizable` — tokenizer walks and value extractors
|
|
381
|
+
// (top-level URL collection, `-d @file` arg
|
|
382
|
+
// walker, `-o <file>` path capture). Strips
|
|
383
|
+
// ONLY heredoc bodies (which are stdin
|
|
384
|
+
// payload, never shell argv) and PRESERVES
|
|
385
|
+
// quoted strings so the value extractors can
|
|
386
|
+
// still recognise quoted URL targets and
|
|
387
|
+
// quoted file paths.
|
|
388
|
+
//
|
|
389
|
+
// The wiki.ingest_url skill is the canonical case where this matters:
|
|
390
|
+
// it POSTs an article body via `-d @- <<'JSON' … JSON`, and the body
|
|
391
|
+
// routinely contains the source URL ("Source: https://news.example.com/…").
|
|
392
|
+
// Before this layered design the URL extractor scanned `cmd`, found
|
|
393
|
+
// the body URL, and falsely blocked with "Multiple URL targets".
|
|
394
|
+
const scan = stripBashStringContent(cmd);
|
|
395
|
+
const tokenizable = stripBashHeredocs(cmd);
|
|
396
|
+
if (/\bcurl\b/.test(cmd)) {
|
|
397
|
+
// ── Multi-request defenses (run BEFORE host/port loop) ─────────
|
|
398
|
+
// The SDK `allowedTools` glob is a prefix match against the full
|
|
399
|
+
// command, so a permitted `Bash(curl http://localhost:<port>/api/x/*)`
|
|
400
|
+
// entry still matches a chained `curl http://localhost/api/x/y ;
|
|
401
|
+
// curl http://localhost/api/notify -d @evil`. The URL host/port
|
|
402
|
+
// loop below validates every URL but does NOT count invocations
|
|
403
|
+
// or request transactions, so a second HTTP request slips through.
|
|
404
|
+
// The three rules below cap a curl-bearing command to a single
|
|
405
|
+
// HTTP request.
|
|
406
|
+
//
|
|
407
|
+
// 1. Chained curl invocations — mirrors the `cmdStart` anchor
|
|
408
|
+
// pattern in `safety/always-disallowed.ts`. Count `curl`
|
|
409
|
+
// tokens at start-of-string / after `;` / `&&` / `||` / `|` /
|
|
410
|
+
// newline / backtick / `$(`. A single `jq -n '…' | curl URL`
|
|
411
|
+
// pipeline counts as ONE curl (only the `curl` token itself
|
|
412
|
+
// is matched; the leading `jq` is not). Two or more anchored
|
|
413
|
+
// `curl` tokens → chained invocation → block.
|
|
414
|
+
const chainedCurlMatches = scan.match(/(?:^|[;&|`\n]|\$\()\s*curl\b/g) ?? [];
|
|
415
|
+
if (chainedCurlMatches.length > 1) {
|
|
416
|
+
return {
|
|
417
|
+
decision: "block",
|
|
418
|
+
reason: `Chained curl invocations are not allowed `
|
|
419
|
+
+ `(detected ${chainedCurlMatches.length} curl commands; `
|
|
420
|
+
+ `one curl per Bash invocation).`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// 2. `--next` / `-:` URL multiplexing — curl's `--next` (short
|
|
424
|
+
// form `-:`) starts a new transaction with reset option state
|
|
425
|
+
// inside the same invocation. The URL loop below still passes
|
|
426
|
+
// because both URLs hit the same host:port, but curl issues
|
|
427
|
+
// one HTTP request per `--next` separator. Same exfil shape
|
|
428
|
+
// as chained curl, different syntax.
|
|
429
|
+
if (/(?:^|\s)--next(?:[\s=]|$)/.test(scan)
|
|
430
|
+
|| /(?:^|\s)-:(?:\s|$)/.test(scan)) {
|
|
431
|
+
return {
|
|
432
|
+
decision: "block",
|
|
433
|
+
reason: "curl --next / -: (URL multiplexing) is not allowed "
|
|
434
|
+
+ "— one HTTP request per Bash invocation.",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// 3. Multi-positional URL targets — `curl URL1 URL2 -X PUT -d
|
|
438
|
+
// @body` sends the same options to BOTH URLs sequentially,
|
|
439
|
+
// which `--next` blocking above does not catch. Tokenize the
|
|
440
|
+
// heredoc-stripped command and collect tokens that are URLs:
|
|
441
|
+
//
|
|
442
|
+
// - Bare URL token: `curl http://localhost:8321/api/x`
|
|
443
|
+
// - Fully single-quoted URL: `curl 'http://localhost:8321/api/x'`
|
|
444
|
+
// - Fully double-quoted URL: `curl "http://localhost:8321/api/x"`
|
|
445
|
+
//
|
|
446
|
+
// URLs that appear INSIDE a quoted body / header value
|
|
447
|
+
// (e.g. `-d '{"link":"https://example.com"}'` or
|
|
448
|
+
// `-H "X-Source: https://example.com"`) are NOT counted: the
|
|
449
|
+
// surrounding quoted token carries other characters, so the
|
|
450
|
+
// "entire content is the URL" patterns below do not match.
|
|
451
|
+
//
|
|
452
|
+
// Heredoc bodies (`<<'JSON' … JSON`) are stripped from
|
|
453
|
+
// `tokenizable` above because they are stdin payload, never
|
|
454
|
+
// shell argv — without that strip, the routine wiki.ingest_url
|
|
455
|
+
// shape of "store an article body that mentions other URLs"
|
|
456
|
+
// would trip this multi-URL rule on the body URL.
|
|
457
|
+
const topLevelTokenRe = /'[^']*'|"[^"]*"|[^'"\s]+/g;
|
|
458
|
+
const topLevelUrls = [];
|
|
459
|
+
let tokenMatch;
|
|
460
|
+
while ((tokenMatch = topLevelTokenRe.exec(tokenizable)) !== null) {
|
|
461
|
+
const token = tokenMatch[0];
|
|
462
|
+
if (/^https?:\/\//.test(token)) {
|
|
463
|
+
topLevelUrls.push(token);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// Fully-quoted URL token: the WHOLE content between matching
|
|
467
|
+
// single or double quotes must be the URL — anything else
|
|
468
|
+
// (JSON body, header value) starts with non-URL characters
|
|
469
|
+
// after the quote.
|
|
470
|
+
const quoted = /^(['"])(https?:\/\/[^'"\s]+)\1$/.exec(token);
|
|
471
|
+
if (quoted) {
|
|
472
|
+
topLevelUrls.push(quoted[2]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (topLevelUrls.length > 1) {
|
|
476
|
+
return {
|
|
477
|
+
decision: "block",
|
|
478
|
+
reason: `Multiple URL targets in a single curl invocation are not allowed `
|
|
479
|
+
+ `(detected ${topLevelUrls.length} top-level URL tokens; quote `
|
|
480
|
+
+ `body URLs inside -d/-H string args).`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (topLevelUrls.length === 0) {
|
|
484
|
+
return {
|
|
485
|
+
decision: "block",
|
|
486
|
+
reason: "curl command must contain an explicit localhost URL",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
for (const url of topLevelUrls) {
|
|
490
|
+
try {
|
|
491
|
+
const parsed = new URL(url);
|
|
492
|
+
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
|
|
493
|
+
return {
|
|
494
|
+
decision: "block",
|
|
495
|
+
reason: `curl target not allowed: ${url} (host: ${parsed.hostname})`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const effectivePort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
499
|
+
if (effectivePort !== String(config.apiPort)) {
|
|
500
|
+
return {
|
|
501
|
+
decision: "block",
|
|
502
|
+
reason: `curl target port not allowed: ${effectivePort}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return {
|
|
508
|
+
decision: "block",
|
|
509
|
+
reason: `curl target URL is malformed: ${url}`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Connection-override flags — host/proxy/socket redirection that
|
|
514
|
+
// would let curl reach something other than the configured loopback
|
|
515
|
+
// HTTP endpoint.
|
|
516
|
+
if (/--connect-to|--resolve|--config\b|(?:^|\s)-[a-zA-Z]*K|--proxy\b|(?:^|\s)-[a-zA-Z]*x|--socks|--unix-socket|--abstract-unix-socket|--interface\b|--local-port\b/.test(scan)) {
|
|
517
|
+
return {
|
|
518
|
+
decision: "block",
|
|
519
|
+
reason: "curl connection override flags not allowed " +
|
|
520
|
+
"(--connect-to, --resolve, --config, --proxy, " +
|
|
521
|
+
"--unix-socket, --abstract-unix-socket, " +
|
|
522
|
+
"--interface, --local-port)",
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// File-read exfil flags. curl can read arbitrary files into the
|
|
526
|
+
// request body via `@<path>` in -d / --data / --form, or via the
|
|
527
|
+
// upload-file flag. The daemon API is loopback so the request
|
|
528
|
+
// body would land in `agent_actions` / notification surfaces that
|
|
529
|
+
// the agent reads back — a confused-deputy exfil.
|
|
530
|
+
//
|
|
531
|
+
// --upload-file / -T — PUT a local file as the body
|
|
532
|
+
// -d @path / --data @path — body literal from file
|
|
533
|
+
// --data-binary @path — same, raw bytes
|
|
534
|
+
// --data-raw @path — same, no escape
|
|
535
|
+
// --data-urlencode @path — same, urlencoded
|
|
536
|
+
// --data-ascii @path — same, ascii
|
|
537
|
+
// -F name=@path / --form …=@ — multipart file part
|
|
538
|
+
// -F name=<path / --form …=< — multipart text from file
|
|
539
|
+
// Short-flag combined forms (`curl -fsT /etc/passwd`) must be
|
|
540
|
+
// caught alongside the single-flag form (`curl -T /etc/passwd`).
|
|
541
|
+
// The leading `-[a-zA-Z]*` permits zero-or-more other short flags
|
|
542
|
+
// before the dangerous letter, mirroring the pattern proven for
|
|
543
|
+
// `-L`. Same shape applied to every short-flag below — without it
|
|
544
|
+
// an attacker can stuff the dangerous letter into a benign-looking
|
|
545
|
+
// flag bundle like `-fs<X>` and bypass the deny rule entirely.
|
|
546
|
+
if (/(?:^|\s)(?:--upload-file\b|-[a-zA-Z]*T(?:\s|=|$))/.test(scan)) {
|
|
547
|
+
return {
|
|
548
|
+
decision: "block",
|
|
549
|
+
reason: "curl --upload-file / -T not allowed — would read arbitrary files",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
// `@-` is curl's stdin marker (canonical: `-d @-` reads the body
|
|
553
|
+
// from stdin, used by pipelines like `echo $body | curl ... -d @-`).
|
|
554
|
+
// Block `@<anything-other-than-stdin-marker>`. The lookahead
|
|
555
|
+
// `(?!-["']?(?:\s|$))` lets `@-`, `@-"`, `@-'`, `@- ` through.
|
|
556
|
+
// `-d / --data* / -F / --form` value-content checks. The previous
|
|
557
|
+
// regex `(?:^|\s)…\s+["']?@(?!-…)` matched the @-file syntax with
|
|
558
|
+
// the value attached, which meant a JSON BODY containing literal
|
|
559
|
+
// text like ` -d @<chars>` (an agent journal entry that quotes a
|
|
560
|
+
// shell example) also tripped it. Conversely, switching to the
|
|
561
|
+
// `scan` form alone loses single-quoted attack content (the
|
|
562
|
+
// legitimate `-d '@/etc/passwd'` form): scan strips the body and
|
|
563
|
+
// the regex no longer sees the `@`.
|
|
564
|
+
//
|
|
565
|
+
// The walker below is value-aware: tokenize the command (already
|
|
566
|
+
// quote-aware via the same regex used for URL extraction), find
|
|
567
|
+
// every `-d` / `--data*` / `-F` / `--form` flag token, recover the
|
|
568
|
+
// unquoted value (either after `=` in the same token or in the
|
|
569
|
+
// adjacent token), and reject if the value's FIRST CHARACTER is
|
|
570
|
+
// `@` (with the canonical stdin marker `@-` excluded). That
|
|
571
|
+
// discriminates:
|
|
572
|
+
// - `-d '@/etc/passwd'` → value starts with `@` and is not `@-`
|
|
573
|
+
// → block (matches the original protection).
|
|
574
|
+
// - `-d '{"content":"a -d @x b"}'` → value starts with `{` →
|
|
575
|
+
// allow (the body contains @ but is not an @-file argument).
|
|
576
|
+
//
|
|
577
|
+
// For `-F`/`--form`, the file-read syntax is `name=@file` /
|
|
578
|
+
// `name=<file` (first `=` in the value followed by `@` or `<`),
|
|
579
|
+
// which the same walker can test in the value once recovered.
|
|
580
|
+
// Adjacent-token merge: bash treats `-d='value'` as the SINGLE
|
|
581
|
+
// argument `-d=value` (the quote is stripped, the bare prefix and
|
|
582
|
+
// the quoted body are joined when there is no whitespace between
|
|
583
|
+
// them). The regex pass below splits the two pieces — track each
|
|
584
|
+
// match's start vs. the previous match's end and concatenate any
|
|
585
|
+
// pair with no whitespace gap. A composite token is treated as
|
|
586
|
+
// "effectively bare" if either constituent was bare, so the flag
|
|
587
|
+
// walker still recognises `-d='@/path'` as a `-d=` flag carrying
|
|
588
|
+
// the value `@/path` (which the regex form `["']?@` used to catch).
|
|
589
|
+
const argRe = /'([^']*)'|"([^"]*)"|`([^`]*)`|([^\s'"`]+)/g;
|
|
590
|
+
const argList = [];
|
|
591
|
+
let am;
|
|
592
|
+
let lastEnd = -1;
|
|
593
|
+
// Walks `tokenizable` (heredoc-stripped) so a body line like
|
|
594
|
+
// `prose mentioning -d @/etc/passwd` cannot be parsed as a real
|
|
595
|
+
// `-d` flag carrying an `@file` value. Single / double quotes are
|
|
596
|
+
// preserved so the `quoted` discriminator still tracks user intent
|
|
597
|
+
// correctly for the dataFlag / formFlag checks below.
|
|
598
|
+
while ((am = argRe.exec(tokenizable)) !== null) {
|
|
599
|
+
const value = am[1] ?? am[2] ?? am[3] ?? am[4] ?? "";
|
|
600
|
+
const quoted = am[4] === undefined;
|
|
601
|
+
if (am.index === lastEnd && argList.length > 0) {
|
|
602
|
+
const prev = argList[argList.length - 1];
|
|
603
|
+
prev.value = prev.value + value;
|
|
604
|
+
prev.quoted = prev.quoted && quoted;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
argList.push({ value, quoted });
|
|
608
|
+
}
|
|
609
|
+
lastEnd = argRe.lastIndex;
|
|
610
|
+
}
|
|
611
|
+
const dataFlag = /^(?:--data(?:-binary|-raw|-urlencode|-ascii)?|--data|-d)(?:=(.*))?$/;
|
|
612
|
+
const formFlag = /^(?:--form|-F)(?:=(.*))?$/;
|
|
613
|
+
for (let i = 0; i < argList.length; i++) {
|
|
614
|
+
const tok = argList[i];
|
|
615
|
+
if (!tok || tok.quoted)
|
|
616
|
+
continue;
|
|
617
|
+
const dm = tok.value.match(dataFlag);
|
|
618
|
+
if (dm) {
|
|
619
|
+
const value = dm[1] ?? argList[i + 1]?.value ?? "";
|
|
620
|
+
if (value.length > 0 && value[0] === "@" && value !== "@-") {
|
|
621
|
+
return {
|
|
622
|
+
decision: "block",
|
|
623
|
+
reason: "curl -d/--data with `@file` syntax not allowed — reads local files",
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const fm = tok.value.match(formFlag);
|
|
629
|
+
if (fm) {
|
|
630
|
+
const value = fm[1] ?? argList[i + 1]?.value ?? "";
|
|
631
|
+
// `name=@path` / `name=<path`: first `=` then `@` or `<`.
|
|
632
|
+
if (/^[^=\s]*=[@<]/.test(value)) {
|
|
633
|
+
return {
|
|
634
|
+
decision: "block",
|
|
635
|
+
reason: "curl -F/--form with `=@file` or `=<file` syntax not allowed — reads local files",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// File-write flags. The agent can land bytes anywhere on disk —
|
|
641
|
+
// overwriting shims, ssh keys, shell rc files, etc. Daemon API is
|
|
642
|
+
// the sole sanctioned write path; Bash curl writes are denied.
|
|
643
|
+
//
|
|
644
|
+
// -o / --output FILE — write response to FILE
|
|
645
|
+
// -O / --remote-name — write to basename-of-URL
|
|
646
|
+
// --remote-name-all — same, for every URL
|
|
647
|
+
// -D / --dump-header FILE — write response headers
|
|
648
|
+
// -c / --cookie-jar FILE — write Set-Cookie state
|
|
649
|
+
// --trace / --trace-ascii F — write protocol trace
|
|
650
|
+
// -w / --write-out FORMAT — format-string output
|
|
651
|
+
// (`%{stderr}` writes to stderr;
|
|
652
|
+
// combined with shell redirect
|
|
653
|
+
// it's another write channel)
|
|
654
|
+
// `-o <file>` / `--output <file>` — used to download binary
|
|
655
|
+
// payloads from the daemon API (e.g. `curl -o receipt.pdf
|
|
656
|
+
// /api/receipts/1/download`). Permit only simple relative
|
|
657
|
+
// filenames so absolute (`-o /etc/passwd`) and parent-escape
|
|
658
|
+
// (`-o ../../foo`) forms are still blocked. Tilde / env-var
|
|
659
|
+
// prefixes are likewise refused because they bypass cwd
|
|
660
|
+
// containment. Quoted paths with spaces (`-o "my file"`) are
|
|
661
|
+
// ALSO rejected so a denylist regex that stops at the space
|
|
662
|
+
// inside the quotes cannot be smuggled past.
|
|
663
|
+
// Flag PRESENCE detection runs on the scan (quote-stripped) command
|
|
664
|
+
// so a body containing prose like "set -o pipefail" does not falsely
|
|
665
|
+
// claim there is an output flag. The subsequent VALUE extraction
|
|
666
|
+
// reads `tokenizable` (heredoc-stripped, quotes preserved) so an
|
|
667
|
+
// earlier heredoc-body occurrence of `-o /etc/passwd` cannot be
|
|
668
|
+
// captured ahead of the real flag — while quoted paths like
|
|
669
|
+
// `-o "my file.pdf"` are still readable.
|
|
670
|
+
const hasOutputFlag = /(?:^|\s)(?:--output(?:\b|=)|-[a-zA-Z]*o(?:\s|=|$))/.test(scan);
|
|
671
|
+
if (hasOutputFlag) {
|
|
672
|
+
// Three capture-group alternatives so quoted paths with spaces
|
|
673
|
+
// are caught — `[^\s'"]+` alone fails on `"my file"`.
|
|
674
|
+
const valueMatch = tokenizable.match(/(?:^|\s)(?:--output(?:\s+|=)|-o(?:\s+|=))(?:"([^"]*)"|'([^']*)'|([^\s'"]+))/);
|
|
675
|
+
const target = valueMatch?.[1] ?? valueMatch?.[2] ?? valueMatch?.[3] ?? "";
|
|
676
|
+
const isSafeRelative = target.length > 0 &&
|
|
677
|
+
!target.startsWith("/") &&
|
|
678
|
+
!target.startsWith("~") &&
|
|
679
|
+
!target.startsWith("$") &&
|
|
680
|
+
!target.split("/").includes("..") &&
|
|
681
|
+
!target.split("\\").includes("..");
|
|
682
|
+
if (!isSafeRelative) {
|
|
683
|
+
return {
|
|
684
|
+
decision: "block",
|
|
685
|
+
reason: `curl --output/-o target must be a simple relative path; ` +
|
|
686
|
+
`got: ${target || "<unparseable>"} ` +
|
|
687
|
+
`(no absolute paths, parent-dir escapes, or shell expansions).`,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (/(?:^|\s)(?:--remote-name(?:-all)?\b|-[a-zA-Z]*O(?:\s|=|$))/.test(scan)) {
|
|
692
|
+
return {
|
|
693
|
+
decision: "block",
|
|
694
|
+
reason: "curl --remote-name/-O not allowed — would write to URL-derived path",
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (/(?:^|\s)(?:--dump-header\b|-[a-zA-Z]*D(?:\s|=|$))/.test(scan)) {
|
|
698
|
+
return {
|
|
699
|
+
decision: "block",
|
|
700
|
+
reason: "curl --dump-header/-D not allowed — writes response headers to disk",
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (/(?:^|\s)(?:--cookie-jar\b|-[a-zA-Z]*c(?:\s|=|$))/.test(scan)) {
|
|
704
|
+
return {
|
|
705
|
+
decision: "block",
|
|
706
|
+
reason: "curl --cookie-jar/-c not allowed — writes cookie state to disk",
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// `--cookie` / `-b` reads cookies from a file when the value
|
|
710
|
+
// is a filename (curl's documented semantics: `-b "FILE"` if
|
|
711
|
+
// the value has no `=`). Same exfil shape as `-d @file` — the
|
|
712
|
+
// file content is sent in the request header. Allowing
|
|
713
|
+
// `-b name=value` would require parsing the value; the simpler
|
|
714
|
+
// safe stance is to refuse the flag outright since the daemon
|
|
715
|
+
// API uses bearer tokens, not cookies.
|
|
716
|
+
if (/(?:^|\s)(?:--cookie\b|-[a-zA-Z]*b(?:\s|=|$))/.test(scan)) {
|
|
717
|
+
return {
|
|
718
|
+
decision: "block",
|
|
719
|
+
reason: "curl --cookie/-b not allowed — when the value is a path, " +
|
|
720
|
+
"the file contents are sent as the Cookie header (file read).",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (/(?:^|\s)--trace(?:-ascii)?\b/.test(scan)) {
|
|
724
|
+
return {
|
|
725
|
+
decision: "block",
|
|
726
|
+
reason: "curl --trace / --trace-ascii not allowed — writes protocol trace to disk",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (/(?:^|\s)(?:--write-out\b|-[a-zA-Z]*w(?:\s|=|$))/.test(scan)) {
|
|
730
|
+
return {
|
|
731
|
+
decision: "block",
|
|
732
|
+
reason: "curl --write-out/-w not allowed — format strings include file/stderr sinks",
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// Cert / key file references. The daemon API is plain HTTP on
|
|
736
|
+
// loopback; none of these flags are needed for legitimate
|
|
737
|
+
// operation and they all read arbitrary files from disk.
|
|
738
|
+
if (/(?:^|\s)(?:--cert\b|--key\b|--cacert\b|--capath\b|-[a-zA-Z]*E(?:\s|=|$))/.test(scan)) {
|
|
739
|
+
return {
|
|
740
|
+
decision: "block",
|
|
741
|
+
reason: "curl --cert/--key/--cacert/--capath/-E not allowed — read arbitrary files",
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
// Follow-redirect flags. The localhost URL check above is
|
|
745
|
+
// bypass-able if curl follows a 3xx off-localhost. The daemon
|
|
746
|
+
// never emits redirects so this flag has no legitimate use.
|
|
747
|
+
//
|
|
748
|
+
// Combined-short-flag forms (`-fsSL`, `-vL`) are caught by the
|
|
749
|
+
// `[a-zA-Z]*L` alternation; the literal `--location` and
|
|
750
|
+
// `--location-trusted` long forms are matched explicitly.
|
|
751
|
+
if (/(?:^|\s)(?:-[a-zA-Z]*L(?:\s|=|$)|--location(?:-trusted)?\b)/.test(scan)) {
|
|
752
|
+
return {
|
|
753
|
+
decision: "block",
|
|
754
|
+
reason: "curl -L / --location not allowed — would follow redirects off localhost",
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { continue: true };
|
|
759
|
+
};
|
|
760
|
+
const bashJqHook = async (input) => {
|
|
761
|
+
const toolInput = input.tool_input;
|
|
762
|
+
const cmd = toolInput?.command ?? "";
|
|
763
|
+
if (!/\bjq\b/.test(cmd))
|
|
764
|
+
return { continue: true };
|
|
765
|
+
// Narrow to THIS jq invocation's own args (up to the next pipe / chain op)
|
|
766
|
+
// so that later pipeline stages are not inspected by the jq rules.
|
|
767
|
+
//
|
|
768
|
+
// The match runs against `stripBashHeredocs(cmd)` so that prose inside
|
|
769
|
+
// a heredoc body (e.g. a wiki article that mentions "the jq env
|
|
770
|
+
// filter") cannot trip the env / -L / --slurpfile checks below.
|
|
771
|
+
// Quoted strings remain intact because the env-filter detector
|
|
772
|
+
// intentionally peers inside the single-quoted jq filter argument
|
|
773
|
+
// (jq syntax lives inside shell quotes, so blanket quote-stripping
|
|
774
|
+
// would lose the very thing we need to inspect).
|
|
775
|
+
//
|
|
776
|
+
// Known approximation: `[^|;&]*` does not respect shell quoting, so a
|
|
777
|
+
// jq filter with a `|` INSIDE a quoted expression (e.g. `jq 'env | keys'`)
|
|
778
|
+
// will truncate `jqPart` at the first `|` regardless of whether that `|`
|
|
779
|
+
// is a jq pipe inside quotes or an actual shell pipeline break. This is
|
|
780
|
+
// intentionally conservative on the safe side: the env-filter check
|
|
781
|
+
// below still fires on the truncated left half (`jq 'env `), so attack
|
|
782
|
+
// payloads are still blocked. The downside is slightly reduced precision
|
|
783
|
+
// on benign expressions containing the jq `|` operator — those get
|
|
784
|
+
// scanned only up to the first pipe, not their full extent.
|
|
785
|
+
const jqMatch = stripBashHeredocs(cmd).match(/\bjq\b([^|;&]*)/);
|
|
786
|
+
if (!jqMatch)
|
|
787
|
+
return { continue: true };
|
|
788
|
+
const jqPart = jqMatch[0];
|
|
789
|
+
// (a) Block file-access flags — --slurpfile / --rawfile read arbitrary
|
|
790
|
+
// files, which would bypass the Read deny list (~/.ssh/**, .env, etc.).
|
|
791
|
+
if (/(?:^|\s)--slurpfile\b/.test(jqPart) || /(?:^|\s)--rawfile\b/.test(jqPart)) {
|
|
792
|
+
return {
|
|
793
|
+
decision: "block",
|
|
794
|
+
reason: "jq --slurpfile and --rawfile are not allowed " +
|
|
795
|
+
"(would bypass Read(.env) / Read(~/.ssh/**) disallow rules).",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// (b) Block module loading — -L <dir> + import can load filter code from
|
|
799
|
+
// the filesystem, effectively RCE inside the jq process.
|
|
800
|
+
if (/(?:^|\s)-L(?:\s|=|$)/.test(jqPart)) {
|
|
801
|
+
return {
|
|
802
|
+
decision: "block",
|
|
803
|
+
reason: "jq -L (module load path) is not allowed.",
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
// (c) Block the `env` filter. `jq env`, `jq -n env`, `jq 'env.FOO'`,
|
|
807
|
+
// `jq '. , env'` all dump the daemon's process.env to stdout. Process.env
|
|
808
|
+
// on this daemon is expected to be clean (secrets live in the keychain),
|
|
809
|
+
// but defense-in-depth: if OPENAI_API_KEY or similar is ever exported at
|
|
810
|
+
// launch, the env filter is the shortest exfil path.
|
|
811
|
+
//
|
|
812
|
+
// Heuristic: match bare `env` NOT preceded by a field-access dot or word
|
|
813
|
+
// char, and NOT followed by a word char. This matches jq's env filter
|
|
814
|
+
// (`env`, `env.HOME`, `(env)`, `env|keys`) while leaving field access
|
|
815
|
+
// like `.env`, `.env_var`, `.data.environments` untouched.
|
|
816
|
+
if (/(?:^|[^\w.])env(?!\w)/.test(jqPart)) {
|
|
817
|
+
return {
|
|
818
|
+
decision: "block",
|
|
819
|
+
reason: "jq env filter is not allowed — it dumps the daemon process " +
|
|
820
|
+
"environment, which is a known exfiltration vector for any " +
|
|
821
|
+
"secrets loaded via .env at startup.",
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return { continue: true };
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Block any Bash command that references the context-directory path.
|
|
828
|
+
*
|
|
829
|
+
* Rationale: the daemon API is the ONLY sanctioned write channel for
|
|
830
|
+
* context files — it enforces today-write-lock, md_file_snapshots,
|
|
831
|
+
* CONTEXT_WRITE_PERMISSIONS, and onPromptContextChanged. In strict mode,
|
|
832
|
+
* the allowlist (Bash narrowed to curl/git/jq) + fileWriteHook keeps
|
|
833
|
+
* this chokepoint intact. In allow mode Bash is unrestricted, so an
|
|
834
|
+
* agent could bypass via `echo > today.md`, `tee`, `python -c 'open…'`,
|
|
835
|
+
* `git log … > context/…`, etc. The defence here is layered:
|
|
836
|
+
*
|
|
837
|
+
* 1. Original substring match against `shellPathForms`. Cheap and
|
|
838
|
+
* catches the obvious literal form an honest model would emit.
|
|
839
|
+
* 2. Best-effort shell tokenizer + `~`/`$HOME` expansion + symlink
|
|
840
|
+
* realpath. Catches `cd ~/.personal-agent && echo > ./context/X`
|
|
841
|
+
* (the `./context/X` token, once joined to the cwd or after a
|
|
842
|
+
* separate `cd` token is detected, lands in the context dir),
|
|
843
|
+
* `ln -s ~/.personal-agent/context /tmp/x` followed by writes
|
|
844
|
+
* to `/tmp/x/today.md`, and `~/.personal-agent/./context/X`.
|
|
845
|
+
* 3. Hard block on interpreter escape hatches (`python -c`, `node
|
|
846
|
+
* -e`, `bash -c`, etc.). Static analysis cannot see what these
|
|
847
|
+
* will do; in allow-mode Bash they are the most direct route
|
|
848
|
+
* around the chokepoint.
|
|
849
|
+
*
|
|
850
|
+
* Defence-in-depth, not authoritative: a prompt-injection-driven
|
|
851
|
+
* variable-construction attack (`P=context; D=today; cd ~/.personal-agent;
|
|
852
|
+
* echo > "$P/$D.md"`) can still slip past static analysis. The static
|
|
853
|
+
* absolute-block layer covers the highest-risk patterns; if a new
|
|
854
|
+
* shape of bypass is observed in audit, codify it here.
|
|
855
|
+
*/
|
|
856
|
+
const bashContextWriteHook = async (input) => {
|
|
857
|
+
const hookInput = input;
|
|
858
|
+
const toolInput = hookInput.tool_input;
|
|
859
|
+
const cmd = toolInput?.command ?? "";
|
|
860
|
+
if (typeof cmd !== "string" || cmd.length === 0)
|
|
861
|
+
return { continue: true };
|
|
862
|
+
const absContextDir = resolvePath(getContextDir(config));
|
|
863
|
+
const home = homedir();
|
|
864
|
+
const realContextDir = realpathLenient(absContextDir);
|
|
865
|
+
// The data dir is the context dir's parent. `cd ~/.personal-agent`
|
|
866
|
+
// followed by `echo > context/today.md` lands in context via a
|
|
867
|
+
// post-cd relative path that Layer 2 cannot resolve (the hook only
|
|
868
|
+
// sees the *initial* cwd). Treating any reference to the data dir
|
|
869
|
+
// as out-of-bounds preempts that bypass — the agent has no
|
|
870
|
+
// legitimate reason to touch the data dir directly when the daemon
|
|
871
|
+
// API is the sanctioned write channel.
|
|
872
|
+
const absDataDir = resolvePath(config.dataDir);
|
|
873
|
+
const realDataDir = realpathLenient(absDataDir);
|
|
874
|
+
// Use the quote/heredoc-stripped form for Layer 1 (substring) and
|
|
875
|
+
// Layer 3 (interpreter regex) so a JSON body or heredoc payload that
|
|
876
|
+
// legitimately contains the absolute context-dir path string, or the
|
|
877
|
+
// literal text `bash -c …`, does not trip these layers. Layer 2
|
|
878
|
+
// still uses `cmd` because its tokenizer is already quote-aware via
|
|
879
|
+
// `looksLikePathArg`.
|
|
880
|
+
const scan = stripBashStringContent(cmd);
|
|
881
|
+
// ── Layer 1: substring match against well-known path forms ──
|
|
882
|
+
const pathForms = shellPathForms(absContextDir, home);
|
|
883
|
+
for (const form of pathForms) {
|
|
884
|
+
if (scan.includes(form)) {
|
|
885
|
+
return blockContextWrite(absContextDir, `substring match: ${form}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// ── Layer 2: tokenized realpath check ──
|
|
889
|
+
//
|
|
890
|
+
// Resolve every path-looking token to its absolute form (relative
|
|
891
|
+
// to the hook-provided cwd) and to its realpath. If either lands
|
|
892
|
+
// inside the context dir OR the data dir, block.
|
|
893
|
+
const cwd = hookInput.cwd ?? "/";
|
|
894
|
+
const tokens = tokenizeShellCommand(cmd);
|
|
895
|
+
for (const rawTok of tokens) {
|
|
896
|
+
const tok = expandHomeForms(rawTok, home);
|
|
897
|
+
// Skip URL-shaped tokens; they are not filesystem paths. Must
|
|
898
|
+
// come before `looksLikePathArg` because `http://localhost/...`
|
|
899
|
+
// satisfies the "starts with `/`" rule once the scheme prefix
|
|
900
|
+
// is removed — and the bare-path rule too — but is never a
|
|
901
|
+
// filesystem reference.
|
|
902
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(tok))
|
|
903
|
+
continue;
|
|
904
|
+
// The old filter (`!tok.includes("/") && !tok.includes("\\")`)
|
|
905
|
+
// forwarded any quoted token with a `/` or `\` into the data-dir
|
|
906
|
+
// resolution branch, which produced false positives on JSON
|
|
907
|
+
// bodies (`{"content":"a\nb"}`) and header values (`Content-Type:
|
|
908
|
+
// application/json`) whenever cwd lived under the data dir. See
|
|
909
|
+
// `looksLikePathArg` for the replacement rules.
|
|
910
|
+
if (!looksLikePathArg(tok))
|
|
911
|
+
continue;
|
|
912
|
+
const candidate = isAbsolute(tok) ? tok : resolvePath(cwd, tok);
|
|
913
|
+
const real = realpathLenient(candidate);
|
|
914
|
+
const landsInsideContext = isPathInsideOrEqual(absContextDir, candidate) ||
|
|
915
|
+
isPathInsideOrEqual(realContextDir, real);
|
|
916
|
+
const landsInsideData = isPathInsideOrEqual(absDataDir, candidate) ||
|
|
917
|
+
isPathInsideOrEqual(realDataDir, real);
|
|
918
|
+
if (landsInsideContext || landsInsideData) {
|
|
919
|
+
return blockContextWrite(absContextDir, landsInsideContext
|
|
920
|
+
? `path token resolves into context dir: ${rawTok} → ${real}`
|
|
921
|
+
: `path token resolves into the data dir (${absDataDir}); ` +
|
|
922
|
+
`the agent should never reference the data dir directly: ${rawTok} → ${real}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// ── Layer 3: interpreter escape hatches ──
|
|
926
|
+
//
|
|
927
|
+
// `bash -c "..."`, `python -c "..."`, etc. tunnel arbitrary code
|
|
928
|
+
// through an opaque argument that static analysis cannot see into.
|
|
929
|
+
// Even in allow-mode Bash the agent should never need these — the
|
|
930
|
+
// SDK Write/Edit tools and the daemon API cover legitimate
|
|
931
|
+
// file-touching use cases. Blocking the patterns themselves is the
|
|
932
|
+
// only way to keep this hook's guarantees meaningful.
|
|
933
|
+
if (/(?:^|[\s|;&])(?:bash|sh|zsh|ksh|dash|busybox)\s+-c\b/.test(scan) ||
|
|
934
|
+
/(?:^|[\s|;&])(?:python3?|node|ruby|perl|php|deno|bun)\s+-[ce]\b/.test(scan)) {
|
|
935
|
+
return {
|
|
936
|
+
decision: "block",
|
|
937
|
+
reason: `Bash commands that invoke an interpreter with -c / -e are not ` +
|
|
938
|
+
`allowed. Their argument is opaque to static analysis, which ` +
|
|
939
|
+
`defeats the context-write chokepoint. Use the Write/Edit tools ` +
|
|
940
|
+
`or the daemon API at http://localhost:${config.apiPort}/api/context/.`,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
return { continue: true };
|
|
944
|
+
};
|
|
945
|
+
function blockContextWrite(absContextDir, reasonDetail) {
|
|
946
|
+
return {
|
|
947
|
+
decision: "block",
|
|
948
|
+
reason: `Bash commands that reference the context directory (${absContextDir}) are ` +
|
|
949
|
+
`not allowed. Use the daemon API: ` +
|
|
950
|
+
`GET/PUT/PATCH http://localhost:${config.apiPort}/api/context/<path>. ` +
|
|
951
|
+
`The API enforces today-write-lock, md_file_snapshots, CONTEXT_WRITE_PERMISSIONS, ` +
|
|
952
|
+
`and onPromptContextChanged — bypassing it via shell redirects or script ` +
|
|
953
|
+
`engines leaves the memory layer inconsistent. ${reasonDetail}.`,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const fileWriteHook = async (input) => {
|
|
957
|
+
const hookInput = input;
|
|
958
|
+
const toolInput = hookInput.tool_input;
|
|
959
|
+
const rawFilePath = toolInput?.file_path;
|
|
960
|
+
if (typeof rawFilePath !== "string" || rawFilePath.length === 0) {
|
|
961
|
+
return { continue: true };
|
|
962
|
+
}
|
|
963
|
+
const filePath = rawFilePath;
|
|
964
|
+
const cwd = hookInput.cwd;
|
|
965
|
+
if (!cwd && !isAbsolute(filePath))
|
|
966
|
+
return { continue: true };
|
|
967
|
+
const absFile = resolvePath(cwd ?? "/", filePath);
|
|
968
|
+
// Resolve symlinks. A lexical containment check accepts a symlink
|
|
969
|
+
// whose target lives inside a forbidden dir, because the link
|
|
970
|
+
// itself sits outside. The kernel write follows the link, so the
|
|
971
|
+
// forbidden bytes land anyway. Realpath both sides of every
|
|
972
|
+
// comparison closes that bypass.
|
|
973
|
+
const realFile = realpathLenient(absFile);
|
|
974
|
+
// (a) Block writes into the session-local helper dir. The `curl` shim in
|
|
975
|
+
// `.pa/bin/` carries daemon-auth env at execution time; letting the model
|
|
976
|
+
// rewrite it would turn the helper into a secret exfiltration vector.
|
|
977
|
+
const absHelperDir = resolvePath(cwd ?? "/", ".pa");
|
|
978
|
+
const realHelperDir = realpathLenient(absHelperDir);
|
|
979
|
+
const withinHelperDir = isPathInsideOrEqual(absHelperDir, absFile) ||
|
|
980
|
+
isPathInsideOrEqual(realHelperDir, realFile);
|
|
981
|
+
if (withinHelperDir) {
|
|
982
|
+
return {
|
|
983
|
+
decision: "block",
|
|
984
|
+
reason: "Direct Write/Edit to .pa is forbidden. " +
|
|
985
|
+
"Session helper binaries are managed by the daemon.",
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
// (b) Block writes into the context dir.
|
|
989
|
+
const contextDir = getContextDir(config);
|
|
990
|
+
const absContextDir = resolvePath(contextDir);
|
|
991
|
+
const realContextDir = realpathLenient(absContextDir);
|
|
992
|
+
const withinContext = isPathInsideOrEqual(absContextDir, absFile) ||
|
|
993
|
+
isPathInsideOrEqual(realContextDir, realFile);
|
|
994
|
+
if (withinContext) {
|
|
995
|
+
return {
|
|
996
|
+
decision: "block",
|
|
997
|
+
reason: `Direct Write/Edit to context dir is forbidden. ` +
|
|
998
|
+
`Use the daemon API instead: ` +
|
|
999
|
+
`PUT http://localhost:${config.apiPort}/api/context/<path> (full replace) or ` +
|
|
1000
|
+
`PATCH http://localhost:${config.apiPort}/api/context/<path> (section op). ` +
|
|
1001
|
+
`The API enforces CONTEXT_WRITE_PERMISSIONS, morningRoutineLock, md_file_snapshots, ` +
|
|
1002
|
+
`onPromptContextChanged, and expectedMtime concurrency. Path: ${absFile}` +
|
|
1003
|
+
(realFile !== absFile ? ` (realpath: ${realFile})` : ""),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
// (c) Mark vault-scoped writes for observer attribution.
|
|
1007
|
+
// Targets the EXTERNAL Obsidian vault; the ObsidianWatcher observer
|
|
1008
|
+
// watches that path and would otherwise misattribute agent writes
|
|
1009
|
+
// as user writes.
|
|
1010
|
+
if (!writeTracker)
|
|
1011
|
+
return { continue: true };
|
|
1012
|
+
const vaultPath = config.externalObsidianVaultPath;
|
|
1013
|
+
if (!vaultPath)
|
|
1014
|
+
return { continue: true };
|
|
1015
|
+
const absVault = resolvePath(vaultPath);
|
|
1016
|
+
const realVault = realpathLenient(absVault);
|
|
1017
|
+
const withinVault = isPathInsideOrEqual(absVault, absFile) ||
|
|
1018
|
+
isPathInsideOrEqual(realVault, realFile);
|
|
1019
|
+
if (!withinVault)
|
|
1020
|
+
return { continue: true };
|
|
1021
|
+
// Mark BOTH paths so the observer can match whichever form the
|
|
1022
|
+
// ObsidianWatcher emits. Most filesystems report the lexical path;
|
|
1023
|
+
// the realpath form is belt-and-braces.
|
|
1024
|
+
writeTracker.markWriting(absFile);
|
|
1025
|
+
if (realFile !== absFile)
|
|
1026
|
+
writeTracker.markWriting(realFile);
|
|
1027
|
+
logger.debug({ filePath: absFile, realPath: realFile }, "vault write pre-marked for observer attribution");
|
|
1028
|
+
return { continue: true };
|
|
1029
|
+
};
|
|
1030
|
+
// EXECUTION-MODE-DESIGN.md §6 — absolute-block audit hook. Runs ahead
|
|
1031
|
+
// of every other Bash/Read/Write/Edit hook in both modes. The SDK-level
|
|
1032
|
+
// `disallowedTools` rejection is the authoritative block; this hook is
|
|
1033
|
+
// redundant defense-in-depth that also writes the `blocked_absolute`
|
|
1034
|
+
// audit row so the owner can see the layer is active.
|
|
1035
|
+
const makeAbsoluteBlockHook = (toolName, argField) => async (input) => {
|
|
1036
|
+
const toolInput = input.tool_input;
|
|
1037
|
+
const raw = toolInput?.[argField];
|
|
1038
|
+
if (typeof raw !== "string")
|
|
1039
|
+
return { continue: true };
|
|
1040
|
+
const match = classifyAbsoluteBlock(toolName, raw);
|
|
1041
|
+
if (!match)
|
|
1042
|
+
return { continue: true };
|
|
1043
|
+
recordAbsoluteBlockAudit({
|
|
1044
|
+
db: getMcpContext?.()?.db,
|
|
1045
|
+
backend: "claude",
|
|
1046
|
+
mode: config.claudeExecutionPermissionMode,
|
|
1047
|
+
match,
|
|
1048
|
+
toolName,
|
|
1049
|
+
});
|
|
1050
|
+
return {
|
|
1051
|
+
decision: "block",
|
|
1052
|
+
reason: `Absolute-block layer denied this ${toolName} call ` +
|
|
1053
|
+
`(category: ${match.category}). This rule holds in both Safe ` +
|
|
1054
|
+
`and Allow modes — see EXECUTION-MODE-DESIGN.md §6.`,
|
|
1055
|
+
};
|
|
1056
|
+
};
|
|
1057
|
+
const bashAbsoluteBlockHook = makeAbsoluteBlockHook("Bash", "command");
|
|
1058
|
+
const readAbsoluteBlockHook = makeAbsoluteBlockHook("Read", "file_path");
|
|
1059
|
+
const writeAbsoluteBlockHook = makeAbsoluteBlockHook("Write", "file_path");
|
|
1060
|
+
const editAbsoluteBlockHook = makeAbsoluteBlockHook("Edit", "file_path");
|
|
1061
|
+
// The context-write hook is always attached to Bash — it is the only
|
|
1062
|
+
// guarantee that the daemon-API chokepoint for memory files survives
|
|
1063
|
+
// allow mode (where curl/jq restrictions are dropped and Bash can
|
|
1064
|
+
// otherwise redirect into context/*.md freely).
|
|
1065
|
+
//
|
|
1066
|
+
// The absolute-block audit hook is appended LAST on every matcher
|
|
1067
|
+
// (§6.3). Appended rather than prepended so existing per-index hook
|
|
1068
|
+
// tests keep pointing at the same functions; semantically it is a
|
|
1069
|
+
// fallback defense whose practical effect is duplicating the SDK's
|
|
1070
|
+
// `disallowedTools` rejection into an `agent_actions` row.
|
|
1071
|
+
return {
|
|
1072
|
+
PreToolUse: [
|
|
1073
|
+
{
|
|
1074
|
+
matcher: "Bash",
|
|
1075
|
+
hooks: allowMode
|
|
1076
|
+
? [
|
|
1077
|
+
wrapBashHook("bashContextWriteHook", bashContextWriteHook),
|
|
1078
|
+
wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
|
|
1079
|
+
]
|
|
1080
|
+
: [
|
|
1081
|
+
wrapBashHook("bashCurlHook", bashCurlHook),
|
|
1082
|
+
wrapBashHook("bashJqHook", bashJqHook),
|
|
1083
|
+
wrapBashHook("bashContextWriteHook", bashContextWriteHook),
|
|
1084
|
+
wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
|
|
1085
|
+
],
|
|
1086
|
+
},
|
|
1087
|
+
{ matcher: "Write", hooks: [fileWriteHook, writeAbsoluteBlockHook] },
|
|
1088
|
+
{ matcher: "Edit", hooks: [fileWriteHook, editAbsoluteBlockHook] },
|
|
1089
|
+
{ matcher: "Read", hooks: [readAbsoluteBlockHook] },
|
|
1090
|
+
],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
//# sourceMappingURL=claude-tool-collection.js.map
|