@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
|
@@ -1,291 +1,58 @@
|
|
|
1
1
|
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import {
|
|
3
|
-
import { readIntegrations } from "../../db/integrations-store.js";
|
|
2
|
+
import { isAutonomousProcessKey, } from "@aitne/shared";
|
|
4
3
|
import { buildExecutionPrompt, buildSummaryPrompt, } from "./prompt-utils.js";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { resolve as resolvePath, isAbsolute } from "node:path";
|
|
7
4
|
import { getContextDir } from "../../config.js";
|
|
8
5
|
import { materializeMcpForSession } from "../../services/mcp/session-materializer.js";
|
|
9
6
|
import { parseMcpToolName } from "../../services/mcp/risk.js";
|
|
10
7
|
import { logMcpToolCall, updateMcpToolCallResult } from "../../services/mcp/tool-audit.js";
|
|
11
|
-
import { BackendQuotaError, BackendDecisiveFailure,
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { buildDelegatedToolPrompt, emptyCost, flattenToolResultContent, tryParseToolResult, withDurationMs, } from "../../services/delegated-tool-runtime.js";
|
|
8
|
+
import { BackendQuotaError, BackendDecisiveFailure, } from "../agent-core.js";
|
|
9
|
+
import { flattenToolResultContent } from "../../services/delegated-tool-runtime.js";
|
|
10
|
+
import { runDelegatedTool as runDelegatedToolFn, runDelegatedTask as runDelegatedTaskFn, } from "./claude-delegated.js";
|
|
15
11
|
import { createSessionWorkdir, cleanupSessionWorkdir } from "../workdir.js";
|
|
16
12
|
import { buildDaemonApiCliEnv } from "../daemon-api-cli.js";
|
|
17
13
|
import { createLogger } from "../../logging.js";
|
|
18
14
|
import { DEFAULT_CLAUDE_HIGH_MODEL, DEFAULT_CLAUDE_MEDIUM_MODEL, findRegisteredModel, getModelsForBackend, } from "./model-registry.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { recordAbsoluteBlockAudit } from "../../safety/absolute-block-audit.js";
|
|
22
|
-
import { isPathInsideOrEqual, shellPathForms, } from "../path-compat.js";
|
|
23
|
-
import { CliPathCache, isPlausibleAnthropicApiKey } from "./cli-utils.js";
|
|
24
|
-
import { probeApiKeyServerSide } from "./api-key-probe.js";
|
|
15
|
+
import { ALWAYS_DISALLOWED_TOOLS } from "../../safety/always-disallowed.js";
|
|
16
|
+
import { CliPathCache } from "./cli-utils.js";
|
|
25
17
|
import { extractSilentApiErrors, logSilentApiErrors, } from "./silent-api-error-detector.js";
|
|
18
|
+
import { CLAUDE_PROBE_TOOLS_PROMPT, computeDelegatedClaudeTools, computeNativeClaudeTools, describeClaudeProbeResultError, extractClaudeProbeTools, } from "./claude-probe.js";
|
|
19
|
+
import { AgentTimeoutError, extractClaudeCodeQuotaResetHint, isClaudeCodeMaxBudgetError, isClaudeCodeQuotaError, } from "./claude-errors.js";
|
|
20
|
+
import { checkAuth as checkAuthFn, checkAuthDetailed as checkAuthDetailedFn, getErrorCode, getErrorMessage, getErrorStatus, getErrorType, isAuthError, } from "./claude-auth.js";
|
|
21
|
+
import { buildSecurityHooks, getAllowedTools as getAllowedToolsFn, getDelegatedClaudeTools as getDelegatedClaudeToolsFn, getNativeClaudeTools as getNativeClaudeToolsFn, getSessionDeniedTools as getSessionDeniedToolsFn, } from "./claude-tool-collection.js";
|
|
22
|
+
// Re-exports kept narrow on purpose: only the symbols `claude-code-core.test.ts`
|
|
23
|
+
// imports from this module. Internal consumers (this file, claude-auth.ts,
|
|
24
|
+
// claude-tool-collection.ts) import directly from `./claude-probe.js` /
|
|
25
|
+
// `./claude-errors.js` so the re-export is not a second public entry point.
|
|
26
|
+
// Each symbol here previously lived in this file before the §8 file-split;
|
|
27
|
+
// keeping them re-exported preserves the test's import path without
|
|
28
|
+
// re-routing the test suite. See `docs/design/appendices/file-split-plan.md` §8.
|
|
29
|
+
export { AgentTimeoutError, CLAUDE_PROBE_TOOLS_PROMPT, computeDelegatedClaudeTools, computeNativeClaudeTools, extractClaudeCodeQuotaResetHint, isClaudeCodeQuotaError, };
|
|
26
30
|
const logger = createLogger("claude-code-core");
|
|
27
31
|
/**
|
|
28
|
-
*
|
|
29
|
-
* configured for, based on the documented `CLAUDE_CODE_USE_*` env flags.
|
|
30
|
-
* Returns null when running in the default direct-API-key / OAuth mode.
|
|
32
|
+
* SDK `settingSources` opt-in for the daemon's `query()` calls.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* GCP credentials flow through ADC, which can be `gcloud auth
|
|
42
|
-
* application-default login` or a `GOOGLE_APPLICATION_CREDENTIALS`
|
|
43
|
-
* file path; we don't require either at this layer.
|
|
44
|
-
* - **Foundry** — `ANTHROPIC_FOUNDRY_RESOURCE` OR
|
|
45
|
-
* `ANTHROPIC_FOUNDRY_BASE_URL`. The API key is optional; without it,
|
|
46
|
-
* Claude Code uses the Azure DefaultAzureCredential chain.
|
|
34
|
+
* The Claude Agent SDK defaults `settingSources` to `[]` ("SDK isolation
|
|
35
|
+
* mode", per `sdk.d.ts` — no filesystem settings loaded). With the default,
|
|
36
|
+
* the spawned Claude Code subprocess does NOT read `~/.claude/settings.json`,
|
|
37
|
+
* which is where the user's claude.ai-bound MCP connectors live
|
|
38
|
+
* (`mcp__claude_ai_Gmail__*`, `mcp__claude_ai_Google_Calendar__*`,
|
|
39
|
+
* `mcp__claude_ai_Notion__*`, etc.). Without that, an integration in
|
|
40
|
+
* `native:claude` mode emits an acquisition plan whose tools do not exist
|
|
41
|
+
* in the session, the pre-pass produces no JSON, and the parent routine
|
|
42
|
+
* collapses to a heartbeat-only shadow run.
|
|
47
43
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*/
|
|
53
|
-
function detectCloudProviderEnv() {
|
|
54
|
-
if (process.env.CLAUDE_CODE_USE_BEDROCK?.trim() === "1") {
|
|
55
|
-
const missing = [];
|
|
56
|
-
if (!process.env.AWS_REGION?.trim())
|
|
57
|
-
missing.push("AWS_REGION");
|
|
58
|
-
return {
|
|
59
|
-
method: "bedrock",
|
|
60
|
-
flagEnvVar: "CLAUDE_CODE_USE_BEDROCK",
|
|
61
|
-
label: "Amazon Bedrock",
|
|
62
|
-
missing,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
if (process.env.CLAUDE_CODE_USE_VERTEX?.trim() === "1") {
|
|
66
|
-
const required = [
|
|
67
|
-
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
68
|
-
"CLOUD_ML_REGION",
|
|
69
|
-
];
|
|
70
|
-
return {
|
|
71
|
-
method: "vertex",
|
|
72
|
-
flagEnvVar: "CLAUDE_CODE_USE_VERTEX",
|
|
73
|
-
label: "Google Vertex AI",
|
|
74
|
-
missing: required.filter((name) => !process.env[name]?.trim()),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
if (process.env.CLAUDE_CODE_USE_FOUNDRY?.trim() === "1") {
|
|
78
|
-
const hasResource = Boolean(process.env.ANTHROPIC_FOUNDRY_RESOURCE?.trim());
|
|
79
|
-
const hasBaseUrl = Boolean(process.env.ANTHROPIC_FOUNDRY_BASE_URL?.trim());
|
|
80
|
-
return {
|
|
81
|
-
method: "foundry",
|
|
82
|
-
flagEnvVar: "CLAUDE_CODE_USE_FOUNDRY",
|
|
83
|
-
label: "Microsoft Foundry",
|
|
84
|
-
missing: hasResource || hasBaseUrl
|
|
85
|
-
? []
|
|
86
|
-
: ["ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL"],
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
// Probe data is derived from `INTEGRATION_DESCRIPTORS.backendConnectors.claude`
|
|
92
|
-
// at module init, so adding a new delegated integration only requires the
|
|
93
|
-
// registry update — the prompt, prefix list, and tool-name regex follow
|
|
94
|
-
// automatically. Before this was registry-driven, every new integration
|
|
95
|
-
// silently broke its own probe (`present` permanently false) until someone
|
|
96
|
-
// remembered to edit four constants in lockstep. See
|
|
97
|
-
// `docs/design/17-delegated-mode-v2.md` §7.1.
|
|
98
|
-
//
|
|
99
|
-
// Tool names span two character classes:
|
|
100
|
-
// - Gmail / Calendar: `search_threads`, `list_events` (snake_case only)
|
|
101
|
-
// - Notion: `notion-search`, `notion-create-pages` (kebab-case)
|
|
102
|
-
// The trailing `[A-Za-z0-9_-]+` class covers both. Namespace strings are
|
|
103
|
-
// regex-escaped because Codex (`._`) and Gemini (`.`) namespaces contain
|
|
104
|
-
// metacharacters.
|
|
105
|
-
function buildClaudeProbeData() {
|
|
106
|
-
const meta = [];
|
|
107
|
-
for (const key of INTEGRATION_KEYS) {
|
|
108
|
-
const descriptor = INTEGRATION_DESCRIPTORS[key];
|
|
109
|
-
const connector = descriptor.backendConnectors.claude;
|
|
110
|
-
if (!connector)
|
|
111
|
-
continue;
|
|
112
|
-
const seen = new Set();
|
|
113
|
-
for (const tools of Object.values(connector.capabilityTools)) {
|
|
114
|
-
for (const t of tools)
|
|
115
|
-
seen.add(t);
|
|
116
|
-
}
|
|
117
|
-
meta.push({
|
|
118
|
-
displayName: descriptor.displayName,
|
|
119
|
-
toolNamespace: connector.toolNamespace,
|
|
120
|
-
requiredCapabilities: connector.requiredCapabilities,
|
|
121
|
-
capabilityToolNames: Array.from(seen),
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
const prefixes = meta.map((m) => m.toolNamespace);
|
|
125
|
-
const lines = ["Use `ToolSearch` only."];
|
|
126
|
-
for (const m of meta) {
|
|
127
|
-
// Keyword query: display name + required capability words split on
|
|
128
|
-
// `_` / `-`. Mirrors the pre-registry-driven prompts (Gmail used
|
|
129
|
-
// `'Gmail search read draft label'`, Calendar used `'Google Calendar
|
|
130
|
-
// list get create update delete event'`) — both are display name +
|
|
131
|
-
// requiredCapabilities expanded into word tokens. Using the
|
|
132
|
-
// capability set (semantic) rather than every capability tool name
|
|
133
|
-
// (mechanical) keeps ToolSearch's token-overlap ranking sharp; a
|
|
134
|
-
// bag-of-tool-name-fragments query dilutes the signal across
|
|
135
|
-
// ~15-30 tokens per integration and demotes the actually-relevant
|
|
136
|
-
// hits.
|
|
137
|
-
const queryWords = [
|
|
138
|
-
m.displayName,
|
|
139
|
-
...m.requiredCapabilities.flatMap((c) => c.split(/[-_]/)),
|
|
140
|
-
]
|
|
141
|
-
.join(" ")
|
|
142
|
-
.replace(/\s+/g, " ")
|
|
143
|
-
.trim();
|
|
144
|
-
lines.push(`Search for ${m.displayName} connector tools with query '${queryWords}' and max_results 20.`);
|
|
145
|
-
}
|
|
146
|
-
lines.push("Do not call any of the searched MCP tools.");
|
|
147
|
-
lines.push(`After the searches, print every full tool name returned that starts with one of: ${prefixes
|
|
148
|
-
.map((p) => `'${p}'`)
|
|
149
|
-
.join(", ")}.`);
|
|
150
|
-
// Per-integration "must include" hints: ToolSearch caps results at
|
|
151
|
-
// max_results, so lower-ranked tools (e.g. Gmail's label_* family) can
|
|
152
|
-
// be missed by keyword search. Listing them explicitly nudges the agent
|
|
153
|
-
// to print them when they do appear in any of the searches above.
|
|
154
|
-
for (const m of meta) {
|
|
155
|
-
if (m.capabilityToolNames.length === 0)
|
|
156
|
-
continue;
|
|
157
|
-
const fullNames = m.capabilityToolNames
|
|
158
|
-
.map((n) => m.toolNamespace + n)
|
|
159
|
-
.join(", ");
|
|
160
|
-
lines.push(`Include these ${m.displayName} tools if present: ${fullNames}.`);
|
|
161
|
-
}
|
|
162
|
-
lines.push("One tool name per line. No markdown fences. No explanation. If no such tools are available, print NONE.");
|
|
163
|
-
// Defense: an empty prefix list would compile to `\b(?:)[A-Za-z0-9_-]+\b`,
|
|
164
|
-
// which matches any word — every connector probe would falsely "succeed".
|
|
165
|
-
// Realistically `INTEGRATION_DESCRIPTORS` is non-empty, but the guard
|
|
166
|
-
// keeps a future registry rollback from corrupting probe semantics.
|
|
167
|
-
const escapedPrefixes = prefixes.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
168
|
-
const regex = escapedPrefixes.length === 0
|
|
169
|
-
? /(?!)/g
|
|
170
|
-
: new RegExp(`\\b(?:${escapedPrefixes.join("|")})[A-Za-z0-9_-]+\\b`, "g");
|
|
171
|
-
return { prompt: lines.join(" "), prefixes, regex };
|
|
172
|
-
}
|
|
173
|
-
const CLAUDE_PROBE_DATA = buildClaudeProbeData();
|
|
174
|
-
export const CLAUDE_PROBE_TOOLS_PROMPT = CLAUDE_PROBE_DATA.prompt;
|
|
175
|
-
const CLAUDE_PROBE_TOOL_PREFIXES = CLAUDE_PROBE_DATA.prefixes;
|
|
176
|
-
const CLAUDE_CONNECTOR_TOOL_RE = CLAUDE_PROBE_DATA.regex;
|
|
177
|
-
/**
|
|
178
|
-
* The built-in Claude Code tool that loads schemas for deferred MCP tools.
|
|
179
|
-
* When a session inherits many MCP servers from the user's global config,
|
|
180
|
-
* the CLI defers a portion of the tool schemas; the model must call
|
|
181
|
-
* `ToolSearch` to bring a specific tool's schema into the working set
|
|
182
|
-
* before invoking it. The proxy explicitly allows it (see runDelegatedTool)
|
|
183
|
-
* and the stream parser excludes it from `wrongToolName` capture so a
|
|
184
|
-
* partial-trace failure (`ToolSearch` + max_turns before the connector
|
|
185
|
-
* call) classifies as `no_tool_call` rather than the misleading
|
|
186
|
-
* `wrong_tool=ToolSearch`.
|
|
187
|
-
*/
|
|
188
|
-
const DEFERRED_TOOL_DISCOVERY_TOOL_NAME = "ToolSearch";
|
|
189
|
-
/**
|
|
190
|
-
* Registry-driven allowlist entries for integrations currently delegated to
|
|
191
|
-
* Claude. Under `permissionMode: "dontAsk"`, any tool not in the SDK's
|
|
192
|
-
* `allowedTools` is silently denied — so a delegated Gmail / Calendar
|
|
193
|
-
* integration whose skill instructs the agent to call
|
|
194
|
-
* `mcp__claude_ai_Gmail__search_threads` will fail with "permission denied"
|
|
195
|
-
* unless that exact tool name is pre-authorized here.
|
|
196
|
-
*
|
|
197
|
-
* Pure over `(integrations)` — callers pass the record read from
|
|
198
|
-
* `db/integrations-store.ts#readIntegrations`. The SDK's MCP tool matcher is
|
|
199
|
-
* literal (`mcp__<server>__<tool>` — see `services/mcp/risk.ts`), so we
|
|
200
|
-
* enumerate every capability tool the registry declares rather than using a
|
|
201
|
-
* wildcard. This guarantees the allowlist only widens by what the registry
|
|
202
|
-
* explicitly advertises; adding a new connector tool demands a registry
|
|
203
|
-
* update first.
|
|
44
|
+
* Opting in to `['user']` brings Claude in line with Codex and Gemini,
|
|
45
|
+
* both of which load `~/.codex/config.toml` / `~/.gemini/settings.json`
|
|
46
|
+
* by default at CLI spawn (they have no `--setting-sources=` equivalent
|
|
47
|
+
* to suppress). This is the parity surface for native mode.
|
|
204
48
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
49
|
+
* Trade-off — user-scoped file hooks (e.g. notify.sh entries under
|
|
50
|
+
* `~/.claude/settings.json` → `hooks.{Notification,Stop,PermissionRequest}`)
|
|
51
|
+
* are also loaded. Programmatic hooks passed via `query({ hooks })`
|
|
52
|
+
* layer on top rather than suppress them. Users with noisy file hooks
|
|
53
|
+
* should scope them with matchers; the daemon does not strip them.
|
|
207
54
|
*/
|
|
208
|
-
|
|
209
|
-
const out = new Set();
|
|
210
|
-
for (const key of INTEGRATION_KEYS) {
|
|
211
|
-
const state = integrations[key];
|
|
212
|
-
if (!state || state.mode !== "delegated")
|
|
213
|
-
continue;
|
|
214
|
-
if (state.delegatedBackend !== "claude")
|
|
215
|
-
continue;
|
|
216
|
-
const connector = INTEGRATION_DESCRIPTORS[key].backendConnectors.claude;
|
|
217
|
-
if (!connector)
|
|
218
|
-
continue;
|
|
219
|
-
for (const toolNames of Object.values(connector.capabilityTools)) {
|
|
220
|
-
for (const toolName of toolNames) {
|
|
221
|
-
out.add(connector.toolNamespace + toolName);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return Array.from(out);
|
|
226
|
-
}
|
|
227
|
-
export class AgentTimeoutError extends Error {
|
|
228
|
-
timeoutMs;
|
|
229
|
-
constructor(timeoutMs) {
|
|
230
|
-
super(`Agent execution exceeded timeout of ${timeoutMs}ms`);
|
|
231
|
-
this.timeoutMs = timeoutMs;
|
|
232
|
-
this.name = "AgentTimeoutError";
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
export function isClaudeCodeQuotaError(error) {
|
|
236
|
-
if (!(error instanceof Error)) {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
const maybeRecord = error;
|
|
240
|
-
const message = error.message.toLowerCase();
|
|
241
|
-
const code = typeof maybeRecord.code === "string" ? maybeRecord.code.toLowerCase() : "";
|
|
242
|
-
const type = typeof maybeRecord.type === "string" ? maybeRecord.type.toLowerCase() : "";
|
|
243
|
-
if (maybeRecord.status === 429) {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
if (code.includes("rate") || code.includes("quota")) {
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
if (type.includes("rate") || type.includes("quota")) {
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
return /rate.?limit|quota|too many requests|you['']?\s*ve hit your limit/.test(message);
|
|
253
|
-
}
|
|
254
|
-
export function isClaudeCodeMaxBudgetError(error) {
|
|
255
|
-
const message = error instanceof Error
|
|
256
|
-
? error.message
|
|
257
|
-
: typeof error === "string"
|
|
258
|
-
? error
|
|
259
|
-
: "";
|
|
260
|
-
const code = typeof error === "object" && error !== null && typeof error.code === "string"
|
|
261
|
-
? error.code
|
|
262
|
-
: "";
|
|
263
|
-
const type = typeof error === "object" && error !== null && typeof error.type === "string"
|
|
264
|
-
? error.type
|
|
265
|
-
: "";
|
|
266
|
-
return /max(?:imum)? budget|max_budget_usd|budget limit|per-turn budget/i.test(`${message} ${code} ${type}`);
|
|
267
|
-
}
|
|
268
|
-
export function extractClaudeCodeQuotaResetHint(error) {
|
|
269
|
-
if (!(error instanceof Error)) {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
const match = /resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(([^)]+)\))?/i.exec(error.message);
|
|
273
|
-
if (!match) {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
const rawHour = Number(match[1]);
|
|
277
|
-
const meridiem = match[3].toLowerCase();
|
|
278
|
-
let hour = rawHour % 12;
|
|
279
|
-
if (meridiem === "pm") {
|
|
280
|
-
hour += 12;
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
hour,
|
|
284
|
-
minute: match[2] ? Number(match[2]) : 0,
|
|
285
|
-
timeZone: match[4]?.trim() || undefined,
|
|
286
|
-
rawLabel: error.message.slice(match.index).replace(/^resets?\s+/i, "").trim(),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
55
|
+
const CLAUDE_SDK_SETTING_SOURCES = ["user"];
|
|
289
56
|
/**
|
|
290
57
|
* ClaudeCodeCore intentionally does NOT run a pre-flight `checkAuth()`
|
|
291
58
|
* gate inside `execute()` / `runTurn()`. Codex and Gemini each call
|
|
@@ -494,7 +261,7 @@ export class ClaudeCodeCore {
|
|
|
494
261
|
return await this.runWithRetry(() => this.executeOnce(params, streamCallbacks), { eventType: params.event.type, modelId: params.modelId });
|
|
495
262
|
}
|
|
496
263
|
async executeOnce(params, streamCallbacks) {
|
|
497
|
-
const { prompt, context, event, modelId, maxTurns, maxBudgetUsd, sessionDir, persistSession = false, conversationHistory, webSearchEnabled = false, } = params;
|
|
264
|
+
const { prompt, context, event, modelId, maxTurns, maxBudgetUsd, sessionDir, persistSession = false, conversationHistory, webSearchEnabled = false, wikiUrlFetchEnabled = false, } = params;
|
|
498
265
|
const fullPrompt = buildExecutionPrompt(prompt, context, event, conversationHistory);
|
|
499
266
|
const startMs = Date.now();
|
|
500
267
|
const actualModelId = this.resolveActualModelId(modelId);
|
|
@@ -502,15 +269,18 @@ export class ClaudeCodeCore {
|
|
|
502
269
|
// that may be resumed later). Otherwise create a disposable temp dir.
|
|
503
270
|
// The disposable path also receives user skills so agent DMs that don't
|
|
504
271
|
// take the persistent-workdir path can still discover user-authored skills.
|
|
272
|
+
const wikiWorkspaceName = typeof event.data?.workspace === "string" ? event.data.workspace : undefined;
|
|
505
273
|
const useSessionDir = sessionDir ?? createSessionWorkdir(this.config.workspaceDir, event.type, `${this.config.dataDir}/skills`, {
|
|
506
274
|
backendId: this.backendId,
|
|
507
275
|
processKey: params.processKey,
|
|
508
276
|
character: this.config.character,
|
|
277
|
+
...(wikiWorkspaceName ? { wikiWorkspaceName } : {}),
|
|
509
278
|
});
|
|
510
279
|
const isOwnedTempDir = !sessionDir;
|
|
511
280
|
const daemonReadToken = this.readTokenManager?.issue(useSessionDir) ?? this.readToken;
|
|
512
281
|
const mcp = await this.materializeMcp(useSessionDir, params.processKey);
|
|
513
282
|
const delegatedTools = this.getDelegatedClaudeTools();
|
|
283
|
+
const nativeTools = this.getNativeClaudeTools();
|
|
514
284
|
const sessionDeniedTools = this.getSessionDeniedTools();
|
|
515
285
|
logger.info({
|
|
516
286
|
eventType: event.type,
|
|
@@ -519,6 +289,7 @@ export class ClaudeCodeCore {
|
|
|
519
289
|
promptLen: fullPrompt.length,
|
|
520
290
|
mcpServers: mcp.servers.map((s) => s.id),
|
|
521
291
|
delegatedToolCount: delegatedTools.length,
|
|
292
|
+
nativeToolCount: nativeTools.length,
|
|
522
293
|
sessionDeniedToolCount: sessionDeniedTools.length,
|
|
523
294
|
}, "Agent execute started");
|
|
524
295
|
try {
|
|
@@ -572,7 +343,20 @@ export class ClaudeCodeCore {
|
|
|
572
343
|
}
|
|
573
344
|
: {
|
|
574
345
|
permissionMode: "dontAsk",
|
|
575
|
-
|
|
346
|
+
// WIKI_BUILDER_DESIGN.md §4.3 — `wikiUrlFetchEnabled` is
|
|
347
|
+
// honoured inside `getAllowedTools` (same gating contract as
|
|
348
|
+
// `webSearchEnabled`: suppressed when the user configured
|
|
349
|
+
// a custom `allowedToolsOverride`). Keeps the widening
|
|
350
|
+
// centralised and unit-testable.
|
|
351
|
+
//
|
|
352
|
+
// `wikiApiOnlyWrites` is the symmetric narrowing for every
|
|
353
|
+
// `wiki.*` process key: strip `Write` / `Edit` from the
|
|
354
|
+
// session allowlist so the agent cannot bypass the Wiki API
|
|
355
|
+
// path-classifier + `agent_actions` audit trail by writing
|
|
356
|
+
// a vault path directly. The wiki-agent profile and every
|
|
357
|
+
// wiki skill body already forbid this in prose; the SDK
|
|
358
|
+
// gate makes the prose enforceable.
|
|
359
|
+
allowedTools: this.getAllowedTools(webSearchEnabled, delegatedTools, nativeTools, wikiUrlFetchEnabled, params.processKey?.startsWith("wiki.") ?? false),
|
|
576
360
|
disallowedTools: [
|
|
577
361
|
...ALWAYS_DISALLOWED_TOOLS,
|
|
578
362
|
...this.config.disallowedTools,
|
|
@@ -585,7 +369,14 @@ export class ClaudeCodeCore {
|
|
|
585
369
|
mcpServers: mcp.claudeMcpServers,
|
|
586
370
|
}
|
|
587
371
|
: {}),
|
|
588
|
-
|
|
372
|
+
settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
|
|
373
|
+
// When the per-execute clamp is active we already swapped Allow
|
|
374
|
+
// mode back to strict `dontAsk` + an explicit allowedTools list.
|
|
375
|
+
// The PreToolUse hooks must follow the same posture: keeping
|
|
376
|
+
// `allowMode=true` here would drop the curl localhost-only check
|
|
377
|
+
// and the jq env/file-flag check, leaving exfil paths open even
|
|
378
|
+
// though the clamp itself permits `Bash(curl *)` and `Bash(jq *)`.
|
|
379
|
+
hooks: this.getSecurityHooks(optimizerClampActive ? false : allowMode),
|
|
589
380
|
persistSession,
|
|
590
381
|
includePartialMessages: !!streamCallbacks,
|
|
591
382
|
...this.buildAdvisorSettings(),
|
|
@@ -628,6 +419,7 @@ export class ClaudeCodeCore {
|
|
|
628
419
|
// keys. `materializeMcp` short-circuits on no context / no servers.
|
|
629
420
|
const mcp = await this.materializeMcp(sessionDir, undefined);
|
|
630
421
|
const delegatedTools = this.getDelegatedClaudeTools();
|
|
422
|
+
const nativeTools = this.getNativeClaudeTools();
|
|
631
423
|
const sessionDeniedTools = this.getSessionDeniedTools();
|
|
632
424
|
logger.info({
|
|
633
425
|
sessionId,
|
|
@@ -635,6 +427,7 @@ export class ClaudeCodeCore {
|
|
|
635
427
|
maxTurns,
|
|
636
428
|
mcpServers: mcp.servers.map((s) => s.id),
|
|
637
429
|
delegatedToolCount: delegatedTools.length,
|
|
430
|
+
nativeToolCount: nativeTools.length,
|
|
638
431
|
sessionDeniedToolCount: sessionDeniedTools.length,
|
|
639
432
|
}, "Agent resume started");
|
|
640
433
|
// Use the same cwd as the original execute() so the SDK can find
|
|
@@ -670,7 +463,7 @@ export class ClaudeCodeCore {
|
|
|
670
463
|
}
|
|
671
464
|
: {
|
|
672
465
|
permissionMode: "dontAsk",
|
|
673
|
-
allowedTools: this.getAllowedTools(webSearchEnabled, delegatedTools),
|
|
466
|
+
allowedTools: this.getAllowedTools(webSearchEnabled, delegatedTools, nativeTools),
|
|
674
467
|
disallowedTools: [
|
|
675
468
|
...ALWAYS_DISALLOWED_TOOLS,
|
|
676
469
|
...this.config.disallowedTools,
|
|
@@ -681,6 +474,7 @@ export class ClaudeCodeCore {
|
|
|
681
474
|
...(mcp.claudeMcpServers
|
|
682
475
|
? { mcpServers: mcp.claudeMcpServers }
|
|
683
476
|
: {}),
|
|
477
|
+
settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
|
|
684
478
|
hooks: this.getSecurityHooks(allowMode),
|
|
685
479
|
includePartialMessages: !!streamCallbacks,
|
|
686
480
|
...this.buildAdvisorSettings(),
|
|
@@ -770,46 +564,26 @@ export class ClaudeCodeCore {
|
|
|
770
564
|
rawLabel: hint.rawLabel,
|
|
771
565
|
};
|
|
772
566
|
}
|
|
567
|
+
// Transitional shims — file-split-plan §15. The implementations live in
|
|
568
|
+
// `./claude-auth.ts`; these methods stay on the class so internal call
|
|
569
|
+
// sites (`this.getErrorMessage(...)`, etc.) and test files that reach in
|
|
570
|
+
// via `(core as any).isAuthError(...)` keep compiling against the same
|
|
571
|
+
// surface. Remove once all callers are migrated to the module-level
|
|
572
|
+
// exports.
|
|
773
573
|
isAuthError(error) {
|
|
774
|
-
|
|
775
|
-
if (status === 401 || status === 403) {
|
|
776
|
-
return true;
|
|
777
|
-
}
|
|
778
|
-
const code = this.getErrorCode(error)?.toLowerCase() ?? "";
|
|
779
|
-
const type = this.getErrorType(error)?.toLowerCase() ?? "";
|
|
780
|
-
if (code.includes("auth") ||
|
|
781
|
-
code.includes("forbidden") ||
|
|
782
|
-
code.includes("unauthorized") ||
|
|
783
|
-
type.includes("auth") ||
|
|
784
|
-
type.includes("forbidden") ||
|
|
785
|
-
type.includes("unauthorized")) {
|
|
786
|
-
return true;
|
|
787
|
-
}
|
|
788
|
-
return /unauthorized|forbidden|authentication|invalid api key|login required/i.test(this.getErrorMessage(error));
|
|
574
|
+
return isAuthError(error);
|
|
789
575
|
}
|
|
790
576
|
getErrorStatus(error) {
|
|
791
|
-
return
|
|
792
|
-
? error.status
|
|
793
|
-
: undefined;
|
|
577
|
+
return getErrorStatus(error);
|
|
794
578
|
}
|
|
795
579
|
getErrorCode(error) {
|
|
796
|
-
return
|
|
797
|
-
? error.code
|
|
798
|
-
: undefined;
|
|
580
|
+
return getErrorCode(error);
|
|
799
581
|
}
|
|
800
582
|
getErrorType(error) {
|
|
801
|
-
return
|
|
802
|
-
? error.type
|
|
803
|
-
: undefined;
|
|
583
|
+
return getErrorType(error);
|
|
804
584
|
}
|
|
805
585
|
getErrorMessage(error) {
|
|
806
|
-
|
|
807
|
-
return error.message;
|
|
808
|
-
}
|
|
809
|
-
if (typeof error === "string") {
|
|
810
|
-
return error;
|
|
811
|
-
}
|
|
812
|
-
return "Claude backend execution failed";
|
|
586
|
+
return getErrorMessage(error);
|
|
813
587
|
}
|
|
814
588
|
async sleep(ms) {
|
|
815
589
|
await new Promise((resolve) => {
|
|
@@ -839,6 +613,7 @@ export class ClaudeCodeCore {
|
|
|
839
613
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
840
614
|
permissionMode: "dontAsk",
|
|
841
615
|
allowedTools: [],
|
|
616
|
+
settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
|
|
842
617
|
},
|
|
843
618
|
});
|
|
844
619
|
const result = await this.withTimeout(stream, () => this.consumeStream(stream, summaryModel, startMs), this.config.executeTimeoutMinutes);
|
|
@@ -849,128 +624,23 @@ export class ClaudeCodeCore {
|
|
|
849
624
|
cleanupSessionWorkdir(sessionDir);
|
|
850
625
|
}
|
|
851
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Cheap presence check used by the reactive execute path. Detailed probe
|
|
629
|
+
* lives in `checkAuthDetailed`. Implementation moved to `./claude-auth.ts`
|
|
630
|
+
* (file-split-plan §8, Tier 2); this stays as a thin forwarder so the
|
|
631
|
+
* `IAgentCore` interface contract is unchanged and existing call sites in
|
|
632
|
+
* `BackendRouter` / `AuthHealthMonitor` continue to work.
|
|
633
|
+
*/
|
|
852
634
|
async checkAuth() {
|
|
853
|
-
|
|
854
|
-
// execute path. Detailed probe lives in checkAuthDetailed().
|
|
855
|
-
const cloud = detectCloudProviderEnv();
|
|
856
|
-
if (cloud) {
|
|
857
|
-
if (cloud.missing.length > 0) {
|
|
858
|
-
return {
|
|
859
|
-
ok: false,
|
|
860
|
-
reason: `${cloud.flagEnvVar}=1 but missing required env vars: ${cloud.missing.join(", ")}`,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
return { ok: true, method: cloud.method };
|
|
864
|
-
}
|
|
865
|
-
const rawApiKey = process.env.ANTHROPIC_API_KEY?.trim();
|
|
866
|
-
if (rawApiKey) {
|
|
867
|
-
if (!isPlausibleAnthropicApiKey(rawApiKey)) {
|
|
868
|
-
return {
|
|
869
|
-
ok: false,
|
|
870
|
-
reason: "ANTHROPIC_API_KEY is set but does not look like an Anthropic key (expected `sk-ant-…`).",
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
return { ok: true, method: "api_key" };
|
|
874
|
-
}
|
|
875
|
-
if (!this.cliPath) {
|
|
876
|
-
return {
|
|
877
|
-
ok: false,
|
|
878
|
-
reason: "Claude Code CLI is not installed or not on PATH. Run `npm install -g @anthropic-ai/claude-code`.",
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
return { ok: true, method: "cli_login" };
|
|
635
|
+
return checkAuthFn({ cliPath: this.cliPath });
|
|
882
636
|
}
|
|
883
637
|
/**
|
|
884
|
-
* Detailed auth probe used by AuthHealthMonitor and the dashboard setup
|
|
885
|
-
* wizard.
|
|
886
|
-
*
|
|
887
|
-
* via `probeApiKeyServerSide("anthropic", ...)` (roadmap §9.1).
|
|
888
|
-
* Throws on network/timeout so `checkAll()` records `probe_network_error`.
|
|
889
|
-
* - **CLI login**: reads `~/.claude/credentials.json` for `refreshToken`.
|
|
890
|
-
* Never writes to the Keychain or credentials file — refresh is left
|
|
891
|
-
* to the CLI (Phase 0 confirmed rotating refresh_tokens; daemon-driven
|
|
892
|
-
* refresh would race and corrupt state).
|
|
638
|
+
* Detailed auth probe used by `AuthHealthMonitor` and the dashboard setup
|
|
639
|
+
* wizard. Implementation moved to `./claude-auth.ts`; see the comment on
|
|
640
|
+
* `checkAuth` above.
|
|
893
641
|
*/
|
|
894
642
|
async checkAuthDetailed() {
|
|
895
|
-
|
|
896
|
-
if (cloud) {
|
|
897
|
-
if (cloud.missing.length > 0) {
|
|
898
|
-
return {
|
|
899
|
-
ok: false,
|
|
900
|
-
status: "missing",
|
|
901
|
-
method: cloud.method,
|
|
902
|
-
detail: `${cloud.flagEnvVar}=1 but missing: ${cloud.missing.join(", ")}`,
|
|
903
|
-
recoveryCommand: `Set the missing env vars or unset ${cloud.flagEnvVar}`,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
// Real auth happens inside the SDK against AWS / GCP / Azure. The
|
|
907
|
-
// daemon does not run a server-side probe for cloud providers — the
|
|
908
|
-
// first execution will surface any credential failure. Mark the
|
|
909
|
-
// status as `ok` here so the dashboard reports "Configured (cloud)";
|
|
910
|
-
// AuthHealthMonitor still re-runs this check hourly so a malformed
|
|
911
|
-
// env (env vars cleared after launch) flips the cache to `missing`.
|
|
912
|
-
return {
|
|
913
|
-
ok: true,
|
|
914
|
-
status: "ok",
|
|
915
|
-
method: cloud.method,
|
|
916
|
-
detail: `Configured via ${cloud.label} — runtime auth verified by Claude Code SDK`,
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
const rawApiKey = process.env.ANTHROPIC_API_KEY?.trim();
|
|
920
|
-
if (rawApiKey) {
|
|
921
|
-
if (!isPlausibleAnthropicApiKey(rawApiKey)) {
|
|
922
|
-
return {
|
|
923
|
-
ok: false,
|
|
924
|
-
status: "expired",
|
|
925
|
-
method: "api_key",
|
|
926
|
-
detail: "ANTHROPIC_API_KEY does not match Anthropic key format (expected `sk-ant-…`).",
|
|
927
|
-
recoveryCommand: "Unset ANTHROPIC_API_KEY or replace it with a valid Anthropic API key",
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
// Format is plausible — attempt a server-side probe to detect
|
|
931
|
-
// revoked keys within 1 hourly cycle (roadmap §9.1). On network
|
|
932
|
-
// failure, the probe throws and the caller (checkAll or check-auth
|
|
933
|
-
// route) records `probe_network_error` without flipping DB cache.
|
|
934
|
-
const probe = await probeApiKeyServerSide("anthropic", rawApiKey);
|
|
935
|
-
return {
|
|
936
|
-
ok: probe.ok,
|
|
937
|
-
status: probe.ok ? "ok" : "expired",
|
|
938
|
-
method: "api_key",
|
|
939
|
-
detail: probe.detail,
|
|
940
|
-
...(!probe.ok && {
|
|
941
|
-
recoveryCommand: "Unset ANTHROPIC_API_KEY or replace it with a valid Anthropic API key",
|
|
942
|
-
}),
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
if (!this.cliPath) {
|
|
946
|
-
return {
|
|
947
|
-
ok: false,
|
|
948
|
-
status: "missing",
|
|
949
|
-
method: "cli_login",
|
|
950
|
-
detail: "Claude Code CLI not found on PATH",
|
|
951
|
-
recoveryCommand: "npm install -g @anthropic-ai/claude-code",
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
const bundle = await readClaudeCredentials();
|
|
955
|
-
if (!bundle) {
|
|
956
|
-
return {
|
|
957
|
-
ok: false,
|
|
958
|
-
status: "expired",
|
|
959
|
-
method: "cli_login",
|
|
960
|
-
detail: "No Claude credentials found",
|
|
961
|
-
recoveryCommand: "claude auth login",
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
if (!bundle.refreshToken) {
|
|
965
|
-
return {
|
|
966
|
-
ok: false,
|
|
967
|
-
status: "expired",
|
|
968
|
-
method: "cli_login",
|
|
969
|
-
detail: "Credentials lack refresh_token — run `claude auth login`",
|
|
970
|
-
recoveryCommand: "claude auth login",
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
return { ok: true, status: "ok", method: "oauth" };
|
|
643
|
+
return checkAuthDetailedFn({ cliPath: this.cliPath });
|
|
974
644
|
}
|
|
975
645
|
/**
|
|
976
646
|
* Phase 5 §4.11 live probe. Claude Code 2.1+ may defer large MCP tool
|
|
@@ -995,6 +665,7 @@ export class ClaudeCodeCore {
|
|
|
995
665
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
996
666
|
permissionMode: "dontAsk",
|
|
997
667
|
allowedTools: ["ToolSearch"],
|
|
668
|
+
settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
|
|
998
669
|
},
|
|
999
670
|
});
|
|
1000
671
|
const tools = await new Promise((resolve, reject) => {
|
|
@@ -1131,6 +802,12 @@ export class ClaudeCodeCore {
|
|
|
1131
802
|
if (blockType === "tool_use" && blockName === "Bash") {
|
|
1132
803
|
const toolUseId = block.id;
|
|
1133
804
|
const cmd = (block.input?.command ?? "");
|
|
805
|
+
logger.info({
|
|
806
|
+
eventType,
|
|
807
|
+
sessionId,
|
|
808
|
+
toolUseId,
|
|
809
|
+
cmd: typeof cmd === "string" ? cmd.slice(0, 400) : null,
|
|
810
|
+
}, "Bash tool_use");
|
|
1134
811
|
if (typeof toolUseId === "string" &&
|
|
1135
812
|
typeof cmd === "string" &&
|
|
1136
813
|
ClaudeCodeCore.isContextUpdateCommand(cmd)) {
|
|
@@ -1217,6 +894,14 @@ export class ClaudeCodeCore {
|
|
|
1217
894
|
}
|
|
1218
895
|
}
|
|
1219
896
|
const resultText = flattenToolResultContent(block.content);
|
|
897
|
+
if (isError) {
|
|
898
|
+
logger.info({
|
|
899
|
+
eventType,
|
|
900
|
+
sessionId,
|
|
901
|
+
toolUseId,
|
|
902
|
+
resultText: resultText.slice(0, 600),
|
|
903
|
+
}, "tool_result error");
|
|
904
|
+
}
|
|
1220
905
|
const apiErrors = extractSilentApiErrors(resultText);
|
|
1221
906
|
if (apiErrors.length > 0) {
|
|
1222
907
|
logSilentApiErrors(logger, apiErrors, {
|
|
@@ -1379,1163 +1064,83 @@ export class ClaudeCodeCore {
|
|
|
1379
1064
|
* curation intent while letting delegated mode widen the surface to
|
|
1380
1065
|
* whatever the registry already advertised.
|
|
1381
1066
|
*/
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
"Skill", // user skills (external-services, obsidian-*, observations, ...)
|
|
1390
|
-
"Bash(curl *)", // curl broadly allowed; hooks restrict to localhost
|
|
1391
|
-
"Bash(git *)", // Git operations
|
|
1392
|
-
"Bash(jq *)", // safe JSON post-processor for curl pipelines
|
|
1393
|
-
];
|
|
1394
|
-
const merged = new Set(base);
|
|
1395
|
-
if (!this.config.allowedToolsOverride && webSearchEnabled) {
|
|
1396
|
-
merged.add("WebSearch");
|
|
1397
|
-
}
|
|
1398
|
-
for (const tool of delegatedTools)
|
|
1399
|
-
merged.add(tool);
|
|
1400
|
-
return Array.from(merged);
|
|
1067
|
+
// Transitional shims — file-split-plan §15. Implementations live in
|
|
1068
|
+
// `./claude-tool-collection.ts`; the class methods stay so existing call
|
|
1069
|
+
// sites and the test file (`(core as any).getAllowedTools(...)`) keep
|
|
1070
|
+
// working without modification. See the comment header of
|
|
1071
|
+
// `claude-tool-collection.ts` for the design rationale.
|
|
1072
|
+
getAllowedTools(webSearchEnabled, delegatedTools = [], nativeTools = [], wikiUrlFetchEnabled = false, wikiApiOnlyWrites = false) {
|
|
1073
|
+
return getAllowedToolsFn(this.config, webSearchEnabled, delegatedTools, nativeTools, wikiUrlFetchEnabled, wikiApiOnlyWrites);
|
|
1401
1074
|
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Thin wrapper around the pure `computeDelegatedClaudeTools` helper that
|
|
1404
|
-
* pulls the integrations record from the wired mcp context. Returns `[]`
|
|
1405
|
-
* when the context is not yet wired (tests, startup ordering) — which
|
|
1406
|
-
* matches the pre-fix behavior for sessions that ran before the daemon
|
|
1407
|
-
* finished composing. A one-shot warning is emitted in `setMcpContext`
|
|
1408
|
-
* confirming wiring.
|
|
1409
|
-
*/
|
|
1410
1075
|
getDelegatedClaudeTools() {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
logger.warn({ err }, "Failed to read integrations for delegated-tool allowlist — proceeding without delegated tools");
|
|
1419
|
-
return [];
|
|
1420
|
-
}
|
|
1076
|
+
return getDelegatedClaudeToolsFn(this.mcpContext);
|
|
1077
|
+
}
|
|
1078
|
+
getNativeClaudeTools() {
|
|
1079
|
+
return getNativeClaudeToolsFn(this.mcpContext);
|
|
1080
|
+
}
|
|
1081
|
+
getSessionDeniedTools() {
|
|
1082
|
+
return getSessionDeniedToolsFn(this.mcpContext);
|
|
1421
1083
|
}
|
|
1422
1084
|
/**
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
* enforcement.
|
|
1429
|
-
*
|
|
1430
|
-
* Returns `[]` when context isn't wired (tests / pre-startup) and on read
|
|
1431
|
-
* failures, matching the conservative pattern used by
|
|
1432
|
-
* `getDelegatedClaudeTools`.
|
|
1085
|
+
* Security hooks — thin shim that forwards to `buildSecurityHooks` in
|
|
1086
|
+
* `./claude-tool-collection.ts`. The implementation lives there as a
|
|
1087
|
+
* pure factory consuming a `SecurityHooksDeps` record. See that module
|
|
1088
|
+
* for hook semantics; see `file-split-plan.md` §8 + §15 for why the
|
|
1089
|
+
* shim stays here.
|
|
1433
1090
|
*/
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
return out;
|
|
1446
|
-
}
|
|
1447
|
-
catch (err) {
|
|
1448
|
-
logger.warn({ err }, "Failed to read integrations for same-backend denied-tools — proceeding without per-integration deny");
|
|
1449
|
-
return [];
|
|
1450
|
-
}
|
|
1091
|
+
getSecurityHooks(allowMode = false) {
|
|
1092
|
+
return buildSecurityHooks({
|
|
1093
|
+
config: this.config,
|
|
1094
|
+
writeTracker: this.writeTracker,
|
|
1095
|
+
// Thunk — see `SecurityHooksDeps.getMcpContext` JSDoc. The hook
|
|
1096
|
+
// reads the live reference at fire time so it picks up any
|
|
1097
|
+
// `setMcpContext` call that happens between hook build and the
|
|
1098
|
+
// SDK invoking the matcher (faithful to original this.mcpContext
|
|
1099
|
+
// semantics).
|
|
1100
|
+
getMcpContext: () => this.mcpContext,
|
|
1101
|
+
}, allowMode);
|
|
1451
1102
|
}
|
|
1452
1103
|
/**
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1104
|
+
* Delegated-execution deps bundle. Built fresh per call so the captured
|
|
1105
|
+
* `readTokenManager` / `readToken` reflect the latest state of the core's
|
|
1106
|
+
* mutable fields (both can be re-wired post-construction via
|
|
1107
|
+
* `setReadToken` / `setReadTokenManager`).
|
|
1457
1108
|
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1109
|
+
* Snapshot semantics — once handed to `runDelegated{Tool,Task}Fn`, the
|
|
1110
|
+
* deps record is treated as immutable for the duration of the call. This
|
|
1111
|
+
* is a deliberate, narrow strengthening of the pre-split behavior: the
|
|
1112
|
+
* original re-read `this.readTokenManager` at revoke time, so a
|
|
1113
|
+
* (hypothetical) mid-call replacement would revoke on the *new* manager
|
|
1114
|
+
* — leaving a scope leak on the manager that issued the token.
|
|
1115
|
+
* Production never replaces the manager after boot (`index.ts` wires it
|
|
1116
|
+
* exactly once via `setReadTokenManager?`), so the two behaviors converge
|
|
1117
|
+
* in practice; the new shape is simply more robust by construction.
|
|
1461
1118
|
*/
|
|
1462
|
-
|
|
1463
|
-
const bashCurlHook = async (input) => {
|
|
1464
|
-
const toolInput = input.tool_input;
|
|
1465
|
-
const cmd = toolInput?.command ?? "";
|
|
1466
|
-
if (/\bcurl\b/.test(cmd)) {
|
|
1467
|
-
const urls = cmd.match(/https?:\/\/[^\s'"]+/g) ?? [];
|
|
1468
|
-
if (urls.length === 0) {
|
|
1469
|
-
return {
|
|
1470
|
-
decision: "block",
|
|
1471
|
-
reason: "curl command must contain an explicit localhost URL",
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
for (const url of urls) {
|
|
1475
|
-
try {
|
|
1476
|
-
const parsed = new URL(url);
|
|
1477
|
-
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
|
|
1478
|
-
return {
|
|
1479
|
-
decision: "block",
|
|
1480
|
-
reason: `curl target not allowed: ${url} (host: ${parsed.hostname})`,
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
const effectivePort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
1484
|
-
if (effectivePort !== String(this.config.apiPort)) {
|
|
1485
|
-
return {
|
|
1486
|
-
decision: "block",
|
|
1487
|
-
reason: `curl target port not allowed: ${effectivePort}`,
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
catch {
|
|
1492
|
-
return {
|
|
1493
|
-
decision: "block",
|
|
1494
|
-
reason: `curl target URL is malformed: ${url}`,
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
if (/--connect-to|--resolve|--config\b|(?:^|\s)-[a-zA-Z]*K|--proxy\b|(?:^|\s)-[a-zA-Z]*x|--socks/.test(cmd)) {
|
|
1499
|
-
return {
|
|
1500
|
-
decision: "block",
|
|
1501
|
-
reason: "curl connection override flags not allowed (--connect-to, --resolve, --config, --proxy)",
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
return { continue: true };
|
|
1506
|
-
};
|
|
1507
|
-
const bashJqHook = async (input) => {
|
|
1508
|
-
const toolInput = input.tool_input;
|
|
1509
|
-
const cmd = toolInput?.command ?? "";
|
|
1510
|
-
if (!/\bjq\b/.test(cmd))
|
|
1511
|
-
return { continue: true };
|
|
1512
|
-
// Narrow to THIS jq invocation's own args (up to the next pipe / chain op)
|
|
1513
|
-
// so that later pipeline stages are not inspected by the jq rules.
|
|
1514
|
-
//
|
|
1515
|
-
// Known approximation: `[^|;&]*` does not respect shell quoting, so a
|
|
1516
|
-
// jq filter with a `|` INSIDE a quoted expression (e.g. `jq 'env | keys'`)
|
|
1517
|
-
// will truncate `jqPart` at the first `|` regardless of whether that `|`
|
|
1518
|
-
// is a jq pipe inside quotes or an actual shell pipeline break. This is
|
|
1519
|
-
// intentionally conservative on the safe side: the env-filter check
|
|
1520
|
-
// below still fires on the truncated left half (`jq 'env `), so attack
|
|
1521
|
-
// payloads are still blocked. The downside is slightly reduced precision
|
|
1522
|
-
// on benign expressions containing the jq `|` operator — those get
|
|
1523
|
-
// scanned only up to the first pipe, not their full extent.
|
|
1524
|
-
const jqMatch = cmd.match(/\bjq\b([^|;&]*)/);
|
|
1525
|
-
if (!jqMatch)
|
|
1526
|
-
return { continue: true };
|
|
1527
|
-
const jqPart = jqMatch[0];
|
|
1528
|
-
// (a) Block file-access flags — --slurpfile / --rawfile read arbitrary
|
|
1529
|
-
// files, which would bypass the Read deny list (~/.ssh/**, .env, etc.).
|
|
1530
|
-
if (/(?:^|\s)--slurpfile\b/.test(jqPart) || /(?:^|\s)--rawfile\b/.test(jqPart)) {
|
|
1531
|
-
return {
|
|
1532
|
-
decision: "block",
|
|
1533
|
-
reason: "jq --slurpfile and --rawfile are not allowed " +
|
|
1534
|
-
"(would bypass Read(.env) / Read(~/.ssh/**) disallow rules).",
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
// (b) Block module loading — -L <dir> + import can load filter code from
|
|
1538
|
-
// the filesystem, effectively RCE inside the jq process.
|
|
1539
|
-
if (/(?:^|\s)-L(?:\s|=|$)/.test(jqPart)) {
|
|
1540
|
-
return {
|
|
1541
|
-
decision: "block",
|
|
1542
|
-
reason: "jq -L (module load path) is not allowed.",
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
// (c) Block the `env` filter. `jq env`, `jq -n env`, `jq 'env.FOO'`,
|
|
1546
|
-
// `jq '. , env'` all dump the daemon's process.env to stdout. Process.env
|
|
1547
|
-
// on this daemon is expected to be clean (secrets live in the keychain),
|
|
1548
|
-
// but defense-in-depth: if OPENAI_API_KEY or similar is ever exported at
|
|
1549
|
-
// launch, the env filter is the shortest exfil path.
|
|
1550
|
-
//
|
|
1551
|
-
// Heuristic: match bare `env` NOT preceded by a field-access dot or word
|
|
1552
|
-
// char, and NOT followed by a word char. This matches jq's env filter
|
|
1553
|
-
// (`env`, `env.HOME`, `(env)`, `env|keys`) while leaving field access
|
|
1554
|
-
// like `.env`, `.env_var`, `.data.environments` untouched.
|
|
1555
|
-
if (/(?:^|[^\w.])env(?!\w)/.test(jqPart)) {
|
|
1556
|
-
return {
|
|
1557
|
-
decision: "block",
|
|
1558
|
-
reason: "jq env filter is not allowed — it dumps the daemon process " +
|
|
1559
|
-
"environment, which is a known exfiltration vector for any " +
|
|
1560
|
-
"secrets loaded via .env at startup.",
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
return { continue: true };
|
|
1564
|
-
};
|
|
1565
|
-
/**
|
|
1566
|
-
* Block any Bash command that references the context-directory path.
|
|
1567
|
-
*
|
|
1568
|
-
* Rationale: the daemon API is the ONLY sanctioned write channel for
|
|
1569
|
-
* context files — it enforces today-write-lock, md_file_snapshots,
|
|
1570
|
-
* CONTEXT_WRITE_PERMISSIONS, and onPromptContextChanged. In strict mode,
|
|
1571
|
-
* the allowlist (Bash narrowed to curl/git/jq) + fileWriteHook keeps
|
|
1572
|
-
* this chokepoint intact. In allow mode Bash is unrestricted, so an
|
|
1573
|
-
* agent could bypass via `echo > today.md`, `tee`, `python -c 'open…'`,
|
|
1574
|
-
* `git log … > context/…`, etc.
|
|
1575
|
-
*
|
|
1576
|
-
* This hook runs in BOTH modes (defense-in-depth). The heuristic is
|
|
1577
|
-
* deliberately conservative: we block the command if its raw text
|
|
1578
|
-
* contains any of the well-known path representations of the context
|
|
1579
|
-
* dir. This has false positives for read-only uses (`cat context/*.md`),
|
|
1580
|
-
* but the agent always has the Read tool and the daemon GET endpoint
|
|
1581
|
-
* available, so a blocked shell-read is an error message, not a
|
|
1582
|
-
* capability loss. Obfuscation beyond the handled variants (dynamic
|
|
1583
|
-
* path construction inside a subshell) is out of scope — the threat
|
|
1584
|
-
* model is "the model picks a shell workaround", not adversarial
|
|
1585
|
-
* prompt injection.
|
|
1586
|
-
*/
|
|
1587
|
-
const bashContextWriteHook = async (input) => {
|
|
1588
|
-
const toolInput = input.tool_input;
|
|
1589
|
-
const cmd = toolInput?.command ?? "";
|
|
1590
|
-
if (typeof cmd !== "string" || cmd.length === 0)
|
|
1591
|
-
return { continue: true };
|
|
1592
|
-
const absContextDir = resolvePath(getContextDir(this.config));
|
|
1593
|
-
const home = homedir();
|
|
1594
|
-
const pathForms = shellPathForms(absContextDir, home);
|
|
1595
|
-
for (const form of pathForms) {
|
|
1596
|
-
if (cmd.includes(form)) {
|
|
1597
|
-
return {
|
|
1598
|
-
decision: "block",
|
|
1599
|
-
reason: `Bash commands that reference the context directory (${absContextDir}) are ` +
|
|
1600
|
-
`not allowed. Use the daemon API: ` +
|
|
1601
|
-
`GET/PUT/PATCH http://localhost:${this.config.apiPort}/api/context/<path>. ` +
|
|
1602
|
-
`The API enforces today-write-lock, md_file_snapshots, CONTEXT_WRITE_PERMISSIONS, ` +
|
|
1603
|
-
`and onPromptContextChanged — bypassing it via shell redirects or script ` +
|
|
1604
|
-
`engines leaves the memory layer inconsistent. Matched path form: ${form}.`,
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
return { continue: true };
|
|
1609
|
-
};
|
|
1610
|
-
const fileWriteHook = async (input) => {
|
|
1611
|
-
const hookInput = input;
|
|
1612
|
-
const toolInput = hookInput.tool_input;
|
|
1613
|
-
const rawFilePath = toolInput?.file_path;
|
|
1614
|
-
if (typeof rawFilePath !== "string" || rawFilePath.length === 0) {
|
|
1615
|
-
return { continue: true };
|
|
1616
|
-
}
|
|
1617
|
-
const filePath = rawFilePath;
|
|
1618
|
-
const cwd = hookInput.cwd;
|
|
1619
|
-
if (!cwd && !isAbsolute(filePath))
|
|
1620
|
-
return { continue: true };
|
|
1621
|
-
const absFile = resolvePath(cwd ?? "/", filePath);
|
|
1622
|
-
// (a) Block writes into the session-local helper dir. The `curl` shim in
|
|
1623
|
-
// `.pa/bin/` carries daemon-auth env at execution time; letting the model
|
|
1624
|
-
// rewrite it would turn the helper into a secret exfiltration vector.
|
|
1625
|
-
const absHelperDir = resolvePath(cwd ?? "/", ".pa");
|
|
1626
|
-
const withinHelperDir = isPathInsideOrEqual(absHelperDir, absFile);
|
|
1627
|
-
if (withinHelperDir) {
|
|
1628
|
-
return {
|
|
1629
|
-
decision: "block",
|
|
1630
|
-
reason: "Direct Write/Edit to .pa is forbidden. " +
|
|
1631
|
-
"Session helper binaries are managed by the daemon.",
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
// (b) Block writes into the context dir.
|
|
1635
|
-
const contextDir = getContextDir(this.config);
|
|
1636
|
-
const absContextDir = resolvePath(contextDir);
|
|
1637
|
-
const withinContext = isPathInsideOrEqual(absContextDir, absFile);
|
|
1638
|
-
if (withinContext) {
|
|
1639
|
-
return {
|
|
1640
|
-
decision: "block",
|
|
1641
|
-
reason: `Direct Write/Edit to context dir is forbidden. ` +
|
|
1642
|
-
`Use the daemon API instead: ` +
|
|
1643
|
-
`PUT http://localhost:${this.config.apiPort}/api/context/<path> (full replace) or ` +
|
|
1644
|
-
`PATCH http://localhost:${this.config.apiPort}/api/context/<path> (section op). ` +
|
|
1645
|
-
`The API enforces CONTEXT_WRITE_PERMISSIONS, morningRoutineLock, md_file_snapshots, ` +
|
|
1646
|
-
`onPromptContextChanged, and expectedMtime concurrency. Path: ${absFile}`,
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
// (c) Mark vault-scoped writes for observer attribution.
|
|
1650
|
-
// Targets the EXTERNAL Obsidian vault; the ObsidianWatcher observer
|
|
1651
|
-
// watches that path and would otherwise misattribute agent writes
|
|
1652
|
-
// as user writes.
|
|
1653
|
-
if (!this.writeTracker)
|
|
1654
|
-
return { continue: true };
|
|
1655
|
-
const vaultPath = this.config.externalObsidianVaultPath;
|
|
1656
|
-
if (!vaultPath)
|
|
1657
|
-
return { continue: true };
|
|
1658
|
-
const absVault = resolvePath(vaultPath);
|
|
1659
|
-
const withinVault = isPathInsideOrEqual(absVault, absFile);
|
|
1660
|
-
if (!withinVault)
|
|
1661
|
-
return { continue: true };
|
|
1662
|
-
this.writeTracker.markWriting(absFile);
|
|
1663
|
-
logger.debug({ filePath: absFile }, "vault write pre-marked for observer attribution");
|
|
1664
|
-
return { continue: true };
|
|
1665
|
-
};
|
|
1666
|
-
// EXECUTION-MODE-DESIGN.md §6 — absolute-block audit hook. Runs ahead
|
|
1667
|
-
// of every other Bash/Read/Write/Edit hook in both modes. The SDK-level
|
|
1668
|
-
// `disallowedTools` rejection is the authoritative block; this hook is
|
|
1669
|
-
// redundant defense-in-depth that also writes the `blocked_absolute`
|
|
1670
|
-
// audit row so the owner can see the layer is active.
|
|
1671
|
-
const makeAbsoluteBlockHook = (toolName, argField) => async (input) => {
|
|
1672
|
-
const toolInput = input.tool_input;
|
|
1673
|
-
const raw = toolInput?.[argField];
|
|
1674
|
-
if (typeof raw !== "string")
|
|
1675
|
-
return { continue: true };
|
|
1676
|
-
const match = classifyAbsoluteBlock(toolName, raw);
|
|
1677
|
-
if (!match)
|
|
1678
|
-
return { continue: true };
|
|
1679
|
-
recordAbsoluteBlockAudit({
|
|
1680
|
-
db: this.mcpContext?.db,
|
|
1681
|
-
backend: "claude",
|
|
1682
|
-
mode: this.config.claudeExecutionPermissionMode,
|
|
1683
|
-
match,
|
|
1684
|
-
toolName,
|
|
1685
|
-
});
|
|
1686
|
-
return {
|
|
1687
|
-
decision: "block",
|
|
1688
|
-
reason: `Absolute-block layer denied this ${toolName} call ` +
|
|
1689
|
-
`(category: ${match.category}). This rule holds in both Safe ` +
|
|
1690
|
-
`and Allow modes — see EXECUTION-MODE-DESIGN.md §6.`,
|
|
1691
|
-
};
|
|
1692
|
-
};
|
|
1693
|
-
const bashAbsoluteBlockHook = makeAbsoluteBlockHook("Bash", "command");
|
|
1694
|
-
const readAbsoluteBlockHook = makeAbsoluteBlockHook("Read", "file_path");
|
|
1695
|
-
const writeAbsoluteBlockHook = makeAbsoluteBlockHook("Write", "file_path");
|
|
1696
|
-
const editAbsoluteBlockHook = makeAbsoluteBlockHook("Edit", "file_path");
|
|
1697
|
-
// The context-write hook is always attached to Bash — it is the only
|
|
1698
|
-
// guarantee that the daemon-API chokepoint for memory files survives
|
|
1699
|
-
// allow mode (where curl/jq restrictions are dropped and Bash can
|
|
1700
|
-
// otherwise redirect into context/*.md freely).
|
|
1701
|
-
//
|
|
1702
|
-
// The absolute-block audit hook is appended LAST on every matcher
|
|
1703
|
-
// (§6.3). Appended rather than prepended so existing per-index hook
|
|
1704
|
-
// tests keep pointing at the same functions; semantically it is a
|
|
1705
|
-
// fallback defense whose practical effect is duplicating the SDK's
|
|
1706
|
-
// `disallowedTools` rejection into an `agent_actions` row.
|
|
1119
|
+
delegatedDeps() {
|
|
1707
1120
|
return {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
hooks: allowMode
|
|
1712
|
-
? [bashContextWriteHook, bashAbsoluteBlockHook]
|
|
1713
|
-
: [
|
|
1714
|
-
bashCurlHook,
|
|
1715
|
-
bashJqHook,
|
|
1716
|
-
bashContextWriteHook,
|
|
1717
|
-
bashAbsoluteBlockHook,
|
|
1718
|
-
],
|
|
1719
|
-
},
|
|
1720
|
-
{ matcher: "Write", hooks: [fileWriteHook, writeAbsoluteBlockHook] },
|
|
1721
|
-
{ matcher: "Edit", hooks: [fileWriteHook, editAbsoluteBlockHook] },
|
|
1722
|
-
{ matcher: "Read", hooks: [readAbsoluteBlockHook] },
|
|
1723
|
-
],
|
|
1121
|
+
apiPort: this.config.apiPort,
|
|
1122
|
+
readToken: this.readToken,
|
|
1123
|
+
readTokenManager: this.readTokenManager,
|
|
1724
1124
|
};
|
|
1725
1125
|
}
|
|
1726
1126
|
/**
|
|
1727
|
-
* Delegated proxy invocation —
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
* handler only sees the connector's structured payload.
|
|
1734
|
-
*
|
|
1735
|
-
* Error classes (DelegatedToolErrorClass):
|
|
1736
|
-
* - `wrong_tool` — model called a tool we did not request (anti-prompt-injection).
|
|
1737
|
-
* - `no_tool_call` — model never invoked the tool within `maxTurns`.
|
|
1738
|
-
* - `tool_error` — connector returned `is_error: true`.
|
|
1739
|
-
* - `parse_error` — stream produced no terminal `result` event.
|
|
1740
|
-
* - `auth_error` — SDK reported `authentication_failed`.
|
|
1741
|
-
* - `timeout` — invoker's wall-clock signal aborted the stream.
|
|
1742
|
-
* - `subprocess_crashed` — exception thrown out of the iterator.
|
|
1743
|
-
*
|
|
1744
|
-
* Cost is captured from the terminal `SDKResultMessage` regardless of
|
|
1745
|
-
* subtype so the invoker can attribute partial spend on the failure
|
|
1746
|
-
* paths (no_tool_call, wrong_tool, tool_error).
|
|
1127
|
+
* Delegated proxy invocation — DELEGATED-PROXY-API-DESIGN.md §4.5.
|
|
1128
|
+
* Implementation moved to `./claude-delegated.ts` (file-split-plan §8,
|
|
1129
|
+
* Tier 2); this stays as a thin forwarder so the `IAgentCore` interface
|
|
1130
|
+
* contract and existing call sites in `BackendRouter` /
|
|
1131
|
+
* `DelegatedBackendInvoker` continue to dispatch through
|
|
1132
|
+
* `core.runDelegatedTool`.
|
|
1747
1133
|
*/
|
|
1748
1134
|
async runDelegatedTool(params) {
|
|
1749
|
-
|
|
1750
|
-
const { toolName, toolArgs, modelId, maxTurns, maxBudgetUsd, sessionDir } = params;
|
|
1751
|
-
const prompt = buildDelegatedToolPrompt(toolName, toolArgs);
|
|
1752
|
-
const daemonReadToken = this.readTokenManager?.issue(sessionDir) ?? this.readToken;
|
|
1753
|
-
let stream = null;
|
|
1754
|
-
const aborted = { value: false };
|
|
1755
|
-
// `abortReason` carries the reason that caused `aborted.value=true`
|
|
1756
|
-
// so the post-loop classifier can map idle-watchdog aborts to
|
|
1757
|
-
// `errorClass="timeout"`. Falls back to the caller's signal reason
|
|
1758
|
-
// when only the caller initiated the abort.
|
|
1759
|
-
let abortReason = null;
|
|
1760
|
-
const closeStream = () => {
|
|
1761
|
-
void (async () => {
|
|
1762
|
-
try {
|
|
1763
|
-
await stream?.return?.(undefined);
|
|
1764
|
-
}
|
|
1765
|
-
catch {
|
|
1766
|
-
/* stream already closed */
|
|
1767
|
-
}
|
|
1768
|
-
})();
|
|
1769
|
-
};
|
|
1770
|
-
const onAbort = () => {
|
|
1771
|
-
aborted.value = true;
|
|
1772
|
-
abortReason = params.abortSignal?.reason ?? null;
|
|
1773
|
-
closeStream();
|
|
1774
|
-
};
|
|
1775
|
-
if (params.abortSignal) {
|
|
1776
|
-
if (params.abortSignal.aborted) {
|
|
1777
|
-
aborted.value = true;
|
|
1778
|
-
abortReason = params.abortSignal.reason ?? null;
|
|
1779
|
-
}
|
|
1780
|
-
else {
|
|
1781
|
-
params.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
// Idle watchdog. Claude SDK runs in-process; cold-start is
|
|
1785
|
-
// negligible (no MCP/CLI load) so the typical first message lands
|
|
1786
|
-
// within 1-3 s. A 30 s idle threshold catches a stuck SDK iterator
|
|
1787
|
-
// (network stall, server-side hang) without false-tripping a slow
|
|
1788
|
-
// tool. On trip we close the stream the same way `onAbort` does and
|
|
1789
|
-
// record the trip in `abortReason` so the classifier returns
|
|
1790
|
-
// `errorClass="timeout"` (uniform with CLI backends).
|
|
1791
|
-
const idleTimeoutMs = DELEGATED_PROXY_DEFAULTS.idleTimeoutMsByBackend.claude
|
|
1792
|
-
?? DELEGATED_PROXY_DEFAULTS.idleTimeoutMs;
|
|
1793
|
-
const idleWatchdog = new IdleWatchdog({
|
|
1794
|
-
idleTimeoutMs,
|
|
1795
|
-
onTimeout: (idleMs) => {
|
|
1796
|
-
if (aborted.value)
|
|
1797
|
-
return;
|
|
1798
|
-
aborted.value = true;
|
|
1799
|
-
abortReason = new DelegatedProxyTimeoutError(`claude SDK stream idle for ${idleMs}ms (limit ${idleTimeoutMs}ms)`);
|
|
1800
|
-
logger.warn({ idleMs, idleTimeoutMs, toolName }, "claude delegated proxy idle watchdog tripped");
|
|
1801
|
-
closeStream();
|
|
1802
|
-
},
|
|
1803
|
-
});
|
|
1804
|
-
try {
|
|
1805
|
-
stream = query({
|
|
1806
|
-
prompt,
|
|
1807
|
-
options: {
|
|
1808
|
-
model: modelId,
|
|
1809
|
-
maxTurns,
|
|
1810
|
-
maxBudgetUsd,
|
|
1811
|
-
cwd: sessionDir,
|
|
1812
|
-
env: buildDaemonApiCliEnv(sessionDir, this.config.apiPort, { readToken: daemonReadToken, sessionBackend: "claude" }),
|
|
1813
|
-
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
1814
|
-
permissionMode: "dontAsk",
|
|
1815
|
-
// The connector tool must be pre-authorized — Claude SDK with
|
|
1816
|
-
// permissionMode="dontAsk" silently denies anything not in
|
|
1817
|
-
// allowedTools.
|
|
1818
|
-
//
|
|
1819
|
-
// ToolSearch is Claude Code's deferred-tool discovery mechanism:
|
|
1820
|
-
// when many MCP servers are registered in the user's global
|
|
1821
|
-
// config (~/.claude.json: Notion, Gmail, GCal, Drive, Figma,
|
|
1822
|
-
// Canva, Hugging Face, …) the CLI ships only a working set of
|
|
1823
|
-
// tool schemas and defers the rest. To call a deferred tool, the
|
|
1824
|
-
// model must first call ToolSearch to load its schema. Without
|
|
1825
|
-
// ToolSearch allowed, the proxy's first turn was wasted on a
|
|
1826
|
-
// denied ToolSearch call (audit log 2026-04-29: 1 Notion failure
|
|
1827
|
-
// logged as `wrong_tool=ToolSearch`, 5 logged as
|
|
1828
|
-
// `subprocess_crashed: Reached maximum number of turns (2)` —
|
|
1829
|
-
// the model retried other approaches and exhausted the budget).
|
|
1830
|
-
//
|
|
1831
|
-
// Allowing ToolSearch is safe: allowedTools enforcement still
|
|
1832
|
-
// gates which tools can be CALLED, and ToolSearch only loads
|
|
1833
|
-
// schemas into context. The proxy parser below also skips
|
|
1834
|
-
// ToolSearch when capturing `wrongToolName` so a ToolSearch+
|
|
1835
|
-
// partial-result trace classifies as `no_tool_call` rather than
|
|
1836
|
-
// misleading `wrong_tool=ToolSearch`.
|
|
1837
|
-
//
|
|
1838
|
-
// TODO(future): a cleaner architectural fix is to materialize a
|
|
1839
|
-
// session-local `.mcp.json` containing only the relevant
|
|
1840
|
-
// connector's MCP server and pass `strictMcpConfig: true` —
|
|
1841
|
-
// that prevents deferral entirely (one MCP server → schemas fit
|
|
1842
|
-
// in the working set). Punted because it requires extracting
|
|
1843
|
-
// server configs from the user's global file per integration.
|
|
1844
|
-
allowedTools: [toolName, DEFERRED_TOOL_DISCOVERY_TOOL_NAME],
|
|
1845
|
-
// Defense-in-depth: even with allowedTools restricted to a tight
|
|
1846
|
-
// set, keep the absolute-block layer (rm -rf, sudo, secret file
|
|
1847
|
-
// reads) merged so a future relaxation of allowedTools can't
|
|
1848
|
-
// accidentally drop these guarantees.
|
|
1849
|
-
disallowedTools: [...ALWAYS_DISALLOWED_TOOLS],
|
|
1850
|
-
// Adaptive thinking is the SDK default for thinking-capable
|
|
1851
|
-
// models (Haiku 4.5+ / Sonnet 4.6+). Per Anthropic's docs
|
|
1852
|
-
// thinking happens within a single API call so it does not
|
|
1853
|
-
// typically burn an extra turn — but for a proxy that issues
|
|
1854
|
-
// one named tool call with explicit args, thinking adds latency
|
|
1855
|
-
// and tokens for no benefit. The proxy.md profile says "no
|
|
1856
|
-
// narration"; disabling thinking aligns runtime behavior with
|
|
1857
|
-
// that intent.
|
|
1858
|
-
thinking: { type: "disabled" },
|
|
1859
|
-
},
|
|
1860
|
-
});
|
|
1861
|
-
let capturedToolUseId = null;
|
|
1862
|
-
let capturedToolResult = undefined;
|
|
1863
|
-
let capturedToolErrorMessage = null;
|
|
1864
|
-
let wrongToolName = null;
|
|
1865
|
-
let cost = emptyCost();
|
|
1866
|
-
let terminalSubtype = null;
|
|
1867
|
-
let terminalIsError = false;
|
|
1868
|
-
let terminalErrors = [];
|
|
1869
|
-
try {
|
|
1870
|
-
idleWatchdog.start();
|
|
1871
|
-
for await (const message of stream) {
|
|
1872
|
-
idleWatchdog.beat();
|
|
1873
|
-
if (aborted.value) {
|
|
1874
|
-
break;
|
|
1875
|
-
}
|
|
1876
|
-
if (message.type === "assistant") {
|
|
1877
|
-
const assistantMsg = message;
|
|
1878
|
-
const blocks = assistantMsg.message?.content;
|
|
1879
|
-
if (!Array.isArray(blocks))
|
|
1880
|
-
continue;
|
|
1881
|
-
for (const block of blocks) {
|
|
1882
|
-
if (!block || typeof block !== "object")
|
|
1883
|
-
continue;
|
|
1884
|
-
const blockType = block.type;
|
|
1885
|
-
if (blockType !== "tool_use")
|
|
1886
|
-
continue;
|
|
1887
|
-
const blockName = block.name;
|
|
1888
|
-
const blockId = block.id;
|
|
1889
|
-
if (typeof blockName !== "string" || typeof blockId !== "string") {
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
if (blockName === toolName) {
|
|
1893
|
-
if (capturedToolUseId === null) {
|
|
1894
|
-
capturedToolUseId = blockId;
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
else if (blockName === DEFERRED_TOOL_DISCOVERY_TOOL_NAME) {
|
|
1898
|
-
// Expected intermediate step for loading the connector's
|
|
1899
|
-
// deferred MCP schema — not a violation. Do not capture as
|
|
1900
|
-
// wrongToolName so a partial trace (ToolSearch + max_turns
|
|
1901
|
-
// before the connector call) classifies as `no_tool_call`
|
|
1902
|
-
// instead of misleading `wrong_tool=ToolSearch`.
|
|
1903
|
-
}
|
|
1904
|
-
else if (wrongToolName === null) {
|
|
1905
|
-
wrongToolName = blockName;
|
|
1906
|
-
// Early abort: bound the wall-clock spend on a wrong_tool
|
|
1907
|
-
// failure to ~5s. Set `aborted` so the next loop iteration
|
|
1908
|
-
// breaks; close the SDK stream so any pending tool_use
|
|
1909
|
-
// doesn't continue. The post-loop classifier checks
|
|
1910
|
-
// `wrongToolName` BEFORE the abort branch, so the failure
|
|
1911
|
-
// is correctly attributed.
|
|
1912
|
-
aborted.value = true;
|
|
1913
|
-
closeStream();
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
else if (message.type === "user") {
|
|
1918
|
-
const userMsg = message;
|
|
1919
|
-
const content = userMsg.message?.content;
|
|
1920
|
-
if (!Array.isArray(content))
|
|
1921
|
-
continue;
|
|
1922
|
-
for (const block of content) {
|
|
1923
|
-
if (!block || typeof block !== "object")
|
|
1924
|
-
continue;
|
|
1925
|
-
if (block.type !== "tool_result")
|
|
1926
|
-
continue;
|
|
1927
|
-
const tuid = block.tool_use_id;
|
|
1928
|
-
if (tuid !== capturedToolUseId)
|
|
1929
|
-
continue;
|
|
1930
|
-
const isToolError = block.is_error === true;
|
|
1931
|
-
const rawContent = block.content;
|
|
1932
|
-
const flat = flattenToolResultContent(rawContent);
|
|
1933
|
-
if (isToolError) {
|
|
1934
|
-
capturedToolErrorMessage =
|
|
1935
|
-
flat.trim().length > 0 ? flat : "tool returned is_error";
|
|
1936
|
-
}
|
|
1937
|
-
else if (capturedToolResult === undefined) {
|
|
1938
|
-
capturedToolResult = tryParseToolResult(flat);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
else if (message.type === "result") {
|
|
1943
|
-
const r = message;
|
|
1944
|
-
terminalSubtype = r.subtype;
|
|
1945
|
-
terminalIsError = r.is_error;
|
|
1946
|
-
cost = {
|
|
1947
|
-
tokensInput: r.usage.input_tokens ?? 0,
|
|
1948
|
-
tokensOutput: r.usage.output_tokens ?? 0,
|
|
1949
|
-
cacheCreationTokens: r.usage.cache_creation_input_tokens ?? 0,
|
|
1950
|
-
cacheReadTokens: r.usage.cache_read_input_tokens ?? 0,
|
|
1951
|
-
costUsd: r.total_cost_usd ?? 0,
|
|
1952
|
-
durationMs: r.duration_ms ?? Date.now() - startMs,
|
|
1953
|
-
numTurns: r.num_turns ?? 0,
|
|
1954
|
-
};
|
|
1955
|
-
if (r.subtype !== "success" && "errors" in r && Array.isArray(r.errors)) {
|
|
1956
|
-
terminalErrors = r.errors;
|
|
1957
|
-
}
|
|
1958
|
-
// The result message is terminal per SDK semantics. Break out
|
|
1959
|
-
// before the next iterator step. When `r.is_error` is true, the
|
|
1960
|
-
// SDK's transport sets `lastErrorResultText` and throws on the
|
|
1961
|
-
// next `readMessages` iteration — wrapping it as
|
|
1962
|
-
// `Error("Claude Code returned an error result: <text>")`. That
|
|
1963
|
-
// throw would land in the outer catch and misclassify as
|
|
1964
|
-
// `subprocess_crashed`, discarding the captured cost. Audit log
|
|
1965
|
-
// (2026-04-29) showed 5 such failures with num_turns=0,
|
|
1966
|
-
// tokens=0, masking that this was actually `error_max_turns`.
|
|
1967
|
-
// Breaking here lets the post-loop classifier run with the
|
|
1968
|
-
// captured terminalSubtype, terminalErrors, wrongToolName, and
|
|
1969
|
-
// cost intact.
|
|
1970
|
-
break;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
finally {
|
|
1975
|
-
idleWatchdog.stop();
|
|
1976
|
-
try {
|
|
1977
|
-
await stream?.return?.(undefined);
|
|
1978
|
-
}
|
|
1979
|
-
catch {
|
|
1980
|
-
/* stream already closed */
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
cost = withDurationMs(cost, startMs);
|
|
1984
|
-
// wrong_tool check hoisted above the abort branch because the
|
|
1985
|
-
// early-abort path sets `aborted.value` AND `wrongToolName`.
|
|
1986
|
-
// Without this ordering the failure would surface as `cancelled`
|
|
1987
|
-
// instead of the actual upstream cause. The `abortReason` field
|
|
1988
|
-
// distinguishes idle-watchdog aborts (errorClass="timeout") from
|
|
1989
|
-
// caller-initiated cancels (errorClass="cancelled" unless the
|
|
1990
|
-
// caller's reason was itself a `DelegatedProxyTimeoutError`).
|
|
1991
|
-
if (wrongToolName !== null) {
|
|
1992
|
-
return {
|
|
1993
|
-
ok: false,
|
|
1994
|
-
errorClass: "wrong_tool",
|
|
1995
|
-
message: `model called '${wrongToolName}' instead of requested '${toolName}'`,
|
|
1996
|
-
cost,
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
if (aborted.value) {
|
|
2000
|
-
const reason = abortReason ?? params.abortSignal?.reason;
|
|
2001
|
-
const errorClass = classifyAbortReason(reason);
|
|
2002
|
-
const idleAbort = reason instanceof DelegatedProxyTimeoutError
|
|
2003
|
-
&& /idle/.test(reason.message);
|
|
2004
|
-
return {
|
|
2005
|
-
ok: false,
|
|
2006
|
-
errorClass,
|
|
2007
|
-
message: errorClass === "timeout"
|
|
2008
|
-
? (idleAbort
|
|
2009
|
-
? `delegated proxy stream went idle (no claude SDK events for ${idleTimeoutMs}ms)`
|
|
2010
|
-
: "delegated proxy timed out (wall-clock)")
|
|
2011
|
-
: "delegated proxy cancelled by caller",
|
|
2012
|
-
cost,
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
if (capturedToolResult !== undefined) {
|
|
2016
|
-
return { ok: true, toolResult: capturedToolResult, cost };
|
|
2017
|
-
}
|
|
2018
|
-
if (capturedToolErrorMessage !== null) {
|
|
2019
|
-
return {
|
|
2020
|
-
ok: false,
|
|
2021
|
-
errorClass: "tool_error",
|
|
2022
|
-
message: capturedToolErrorMessage,
|
|
2023
|
-
cost,
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
// Map specific terminal subtypes before falling through to
|
|
2027
|
-
// no_tool_call. The model can fail for auth or budget reasons
|
|
2028
|
-
// before ever emitting a tool_use block.
|
|
2029
|
-
if (terminalSubtype === "error_during_execution" && terminalErrors.length > 0) {
|
|
2030
|
-
const joined = terminalErrors.join("; ");
|
|
2031
|
-
if (/auth|unauthorized|authentication_failed|invalid api key/i.test(joined)) {
|
|
2032
|
-
return {
|
|
2033
|
-
ok: false,
|
|
2034
|
-
errorClass: "auth_error",
|
|
2035
|
-
message: joined,
|
|
2036
|
-
cost,
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
return {
|
|
2040
|
-
ok: false,
|
|
2041
|
-
errorClass: "tool_error",
|
|
2042
|
-
message: joined,
|
|
2043
|
-
cost,
|
|
2044
|
-
};
|
|
2045
|
-
}
|
|
2046
|
-
if (terminalSubtype === null && !terminalIsError) {
|
|
2047
|
-
// Stream ended before any terminal `result` arrived — abnormal
|
|
2048
|
-
// termination not classified as an abort. Treat as parse_error
|
|
2049
|
-
// so the route handler can surface the bug rather than retrying.
|
|
2050
|
-
return {
|
|
2051
|
-
ok: false,
|
|
2052
|
-
errorClass: "parse_error",
|
|
2053
|
-
message: "Claude SDK stream ended without a terminal result message",
|
|
2054
|
-
cost,
|
|
2055
|
-
};
|
|
2056
|
-
}
|
|
2057
|
-
return {
|
|
2058
|
-
ok: false,
|
|
2059
|
-
errorClass: "no_tool_call",
|
|
2060
|
-
message: `model did not invoke '${toolName}' within ${maxTurns} turns (subtype=${terminalSubtype ?? "unknown"})`,
|
|
2061
|
-
cost,
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
catch (err) {
|
|
2065
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2066
|
-
const cost = withDurationMs(emptyCost(), startMs);
|
|
2067
|
-
// Map auth-shape exceptions before the catch-all subprocess_crashed.
|
|
2068
|
-
if (/authentication_failed|unauthorized|invalid api key|sk-ant-/i.test(message)) {
|
|
2069
|
-
return { ok: false, errorClass: "auth_error", message, cost };
|
|
2070
|
-
}
|
|
2071
|
-
if (aborted.value) {
|
|
2072
|
-
return {
|
|
2073
|
-
ok: false,
|
|
2074
|
-
errorClass: classifyAbortReason(abortReason ?? params.abortSignal?.reason),
|
|
2075
|
-
message,
|
|
2076
|
-
cost,
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
|
-
return { ok: false, errorClass: "subprocess_crashed", message, cost };
|
|
2080
|
-
}
|
|
2081
|
-
finally {
|
|
2082
|
-
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
2083
|
-
this.readTokenManager?.revoke(sessionDir);
|
|
2084
|
-
}
|
|
1135
|
+
return runDelegatedToolFn(this.delegatedDeps(), params);
|
|
2085
1136
|
}
|
|
2086
1137
|
/**
|
|
2087
|
-
* DELEGATED-TASK-MODE-DESIGN.md §9.1
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
* against the caller's `outputSchema`.
|
|
2091
|
-
*
|
|
2092
|
-
* Stream parsing differences from `runDelegatedTool`:
|
|
2093
|
-
* - We accept multiple `tool_use` blocks (counted against `maxToolCalls`).
|
|
2094
|
-
* - We track per-tool durations to feed `onToolStep`.
|
|
2095
|
-
* - We capture the *final* assistant text (after the last tool turn)
|
|
2096
|
-
* as the validation target, not a single tool's `tool_result`.
|
|
2097
|
-
*
|
|
2098
|
-
* Safety:
|
|
2099
|
-
* - `allowedTools` already excludes the destructive set when
|
|
2100
|
-
* `allowDestructive: false`; the SDK will not surface those tools.
|
|
2101
|
-
* - `disallowedTools` is the absolute-block layer + the destructive
|
|
2102
|
-
* set as defense-in-depth (so a future relaxation of allowedTools
|
|
2103
|
-
* can't accidentally widen the surface).
|
|
2104
|
-
*
|
|
2105
|
-
* The §6.2 "no retry after write" rule is enforced at the invoker
|
|
2106
|
-
* layer; this method just signals via `writeClassToolFired` whether
|
|
2107
|
-
* any destructive tool ran during the task.
|
|
1138
|
+
* Delegated task-mode invocation — DELEGATED-TASK-MODE-DESIGN.md §9.1.
|
|
1139
|
+
* Implementation moved to `./claude-delegated.ts`; see the comment on
|
|
1140
|
+
* `runDelegatedTool` above.
|
|
2108
1141
|
*/
|
|
2109
1142
|
async runDelegatedTask(params) {
|
|
2110
|
-
|
|
2111
|
-
const { systemPrompt, allowedTools, destructiveTools, writeClassTools, modelId, maxToolCalls, maxBudgetUsd, sessionDir, onToolStep, } = params;
|
|
2112
|
-
const daemonReadToken = this.readTokenManager?.issue(sessionDir) ?? this.readToken;
|
|
2113
|
-
const trace = [];
|
|
2114
|
-
// §6.2 / §7.4 — match against the *write-class* set (destructive ∪
|
|
2115
|
-
// reversible writes), not just destructive. Otherwise reversible
|
|
2116
|
-
// write tools like `create_draft` slip past the retry guard and the
|
|
2117
|
-
// single retry creates a duplicate side effect.
|
|
2118
|
-
//
|
|
2119
|
-
// Phase 1 (`/exec`) entries are fully-qualified exact names — the
|
|
2120
|
-
// exact-equality fast path inside `matchRunAllowedToolPattern` covers
|
|
2121
|
-
// them at one comparison. Phase 2 (`/api/delegated/run`) may pass
|
|
2122
|
-
// `*`-suffixed glob patterns derived from the caller's allowedTools
|
|
2123
|
-
// (DELEGATED-TASK-MODE-DESIGN.md §4.2); the shared helper handles both.
|
|
2124
|
-
const writeClassMatcher = (name) => writeClassTools.some((pattern) => matchRunAllowedToolPattern(pattern, name));
|
|
2125
|
-
let writeClassToolFired = false;
|
|
2126
|
-
// DELEGATED-TASK-MODE-DESIGN.md §13 Phase 3.1 — Claude SDK
|
|
2127
|
-
// structured-output. When the invoker passed `structuredOutputEnabled:
|
|
2128
|
-
// true` AND a `wrappedSchema`, configure `outputFormat` so the SDK
|
|
2129
|
-
// validates the model's final emission against the schema (with its
|
|
2130
|
-
// own internal retries) and surfaces it on `SDKResultSuccess.structured_output`.
|
|
2131
|
-
// We still capture the assistant text as a fallback — if the SDK
|
|
2132
|
-
// returns success without `structured_output` (older subtype, future
|
|
2133
|
-
// shape change, kill-switch flips off mid-call), the existing text
|
|
2134
|
-
// path takes over.
|
|
2135
|
-
const useStructuredOutput = params.structuredOutputEnabled === true
|
|
2136
|
-
&& !!params.wrappedSchema;
|
|
2137
|
-
let capturedStructured;
|
|
2138
|
-
let sawStructuredOutputRetryError = false;
|
|
2139
|
-
let stream = null;
|
|
2140
|
-
const aborted = { value: false };
|
|
2141
|
-
const onAbort = () => {
|
|
2142
|
-
aborted.value = true;
|
|
2143
|
-
void (async () => {
|
|
2144
|
-
try {
|
|
2145
|
-
await stream?.return?.(undefined);
|
|
2146
|
-
}
|
|
2147
|
-
catch {
|
|
2148
|
-
/* stream already closed */
|
|
2149
|
-
}
|
|
2150
|
-
})();
|
|
2151
|
-
};
|
|
2152
|
-
if (params.abortSignal) {
|
|
2153
|
-
if (params.abortSignal.aborted) {
|
|
2154
|
-
aborted.value = true;
|
|
2155
|
-
}
|
|
2156
|
-
else {
|
|
2157
|
-
params.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
const pendingByUseId = new Map();
|
|
2161
|
-
let toolCallCount = 0;
|
|
2162
|
-
let loopAborted = false;
|
|
2163
|
-
let assistantTextChunks = [];
|
|
2164
|
-
/** The "final" assistant message is the most recent assistant message
|
|
2165
|
-
* that contained NO `tool_use` block. The SDK emits one assistant
|
|
2166
|
-
* message per turn; the planning turns mix text + tool_use, the
|
|
2167
|
-
* closing turn is text-only. */
|
|
2168
|
-
let lastAssistantTextOnlyChunks = [];
|
|
2169
|
-
try {
|
|
2170
|
-
stream = query({
|
|
2171
|
-
prompt: systemPrompt,
|
|
2172
|
-
options: {
|
|
2173
|
-
model: modelId,
|
|
2174
|
-
maxTurns: Math.max(2, maxToolCalls + 1),
|
|
2175
|
-
maxBudgetUsd,
|
|
2176
|
-
cwd: sessionDir,
|
|
2177
|
-
env: buildDaemonApiCliEnv(sessionDir, this.config.apiPort, { readToken: daemonReadToken, sessionBackend: "claude" }),
|
|
2178
|
-
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
2179
|
-
permissionMode: "dontAsk",
|
|
2180
|
-
allowedTools: [...allowedTools],
|
|
2181
|
-
// Defense-in-depth: absolute-block layer + destructive denies.
|
|
2182
|
-
// Destructive entries are redundant with the allowedTools
|
|
2183
|
-
// subtraction (when allowDestructive=false) but kept so a
|
|
2184
|
-
// future allowedTools widening doesn't drop the guarantee.
|
|
2185
|
-
disallowedTools: [
|
|
2186
|
-
...ALWAYS_DISALLOWED_TOOLS,
|
|
2187
|
-
...(params.allowDestructive ? [] : destructiveTools),
|
|
2188
|
-
],
|
|
2189
|
-
// §13 Phase 3.1 — bind the wrapped schema (user schema OR
|
|
2190
|
-
// confirmation envelope OR error envelope) to SDK 0.2.98's
|
|
2191
|
-
// `outputFormat`. Result message carries `structured_output`
|
|
2192
|
-
// which we read below. Off when the kill switch is false or
|
|
2193
|
-
// the invoker omitted the wrapped schema.
|
|
2194
|
-
...(useStructuredOutput && params.wrappedSchema
|
|
2195
|
-
? {
|
|
2196
|
-
outputFormat: {
|
|
2197
|
-
type: "json_schema",
|
|
2198
|
-
schema: params.wrappedSchema,
|
|
2199
|
-
},
|
|
2200
|
-
}
|
|
2201
|
-
: {}),
|
|
2202
|
-
},
|
|
2203
|
-
});
|
|
2204
|
-
let cost = emptyCost();
|
|
2205
|
-
try {
|
|
2206
|
-
for await (const message of stream) {
|
|
2207
|
-
if (aborted.value || loopAborted)
|
|
2208
|
-
break;
|
|
2209
|
-
if (message.type === "assistant") {
|
|
2210
|
-
const assistantMsg = message;
|
|
2211
|
-
const blocks = assistantMsg.message?.content;
|
|
2212
|
-
if (!Array.isArray(blocks))
|
|
2213
|
-
continue;
|
|
2214
|
-
const textChunks = [];
|
|
2215
|
-
let sawToolUse = false;
|
|
2216
|
-
for (const block of blocks) {
|
|
2217
|
-
if (!block || typeof block !== "object")
|
|
2218
|
-
continue;
|
|
2219
|
-
const blockType = block.type;
|
|
2220
|
-
if (blockType === "text") {
|
|
2221
|
-
const text = block.text;
|
|
2222
|
-
if (typeof text === "string")
|
|
2223
|
-
textChunks.push(text);
|
|
2224
|
-
continue;
|
|
2225
|
-
}
|
|
2226
|
-
if (blockType !== "tool_use")
|
|
2227
|
-
continue;
|
|
2228
|
-
sawToolUse = true;
|
|
2229
|
-
const blockName = block.name;
|
|
2230
|
-
const blockId = block.id;
|
|
2231
|
-
const blockArgs = block.input;
|
|
2232
|
-
if (typeof blockName !== "string" || typeof blockId !== "string") {
|
|
2233
|
-
continue;
|
|
2234
|
-
}
|
|
2235
|
-
toolCallCount += 1;
|
|
2236
|
-
if (toolCallCount > maxToolCalls) {
|
|
2237
|
-
// §7.5 — once the cap is exceeded, abort. The next
|
|
2238
|
-
// tool_use is treated as overrun.
|
|
2239
|
-
loopAborted = true;
|
|
2240
|
-
aborted.value = true;
|
|
2241
|
-
try {
|
|
2242
|
-
await stream?.return?.(undefined);
|
|
2243
|
-
}
|
|
2244
|
-
catch {
|
|
2245
|
-
/* already closed */
|
|
2246
|
-
}
|
|
2247
|
-
break;
|
|
2248
|
-
}
|
|
2249
|
-
if (writeClassMatcher(blockName)) {
|
|
2250
|
-
writeClassToolFired = true;
|
|
2251
|
-
}
|
|
2252
|
-
pendingByUseId.set(blockId, {
|
|
2253
|
-
name: blockName,
|
|
2254
|
-
args: blockArgs,
|
|
2255
|
-
startedAt: Date.now(),
|
|
2256
|
-
});
|
|
2257
|
-
}
|
|
2258
|
-
assistantTextChunks = assistantTextChunks.concat(textChunks);
|
|
2259
|
-
if (!sawToolUse && textChunks.length > 0) {
|
|
2260
|
-
lastAssistantTextOnlyChunks = textChunks;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
else if (message.type === "user") {
|
|
2264
|
-
const userMsg = message;
|
|
2265
|
-
const content = userMsg.message?.content;
|
|
2266
|
-
if (!Array.isArray(content))
|
|
2267
|
-
continue;
|
|
2268
|
-
for (const block of content) {
|
|
2269
|
-
if (!block || typeof block !== "object")
|
|
2270
|
-
continue;
|
|
2271
|
-
if (block.type !== "tool_result")
|
|
2272
|
-
continue;
|
|
2273
|
-
const tuid = block.tool_use_id;
|
|
2274
|
-
if (typeof tuid !== "string")
|
|
2275
|
-
continue;
|
|
2276
|
-
const pending = pendingByUseId.get(tuid);
|
|
2277
|
-
if (!pending)
|
|
2278
|
-
continue;
|
|
2279
|
-
pendingByUseId.delete(tuid);
|
|
2280
|
-
const isToolError = block.is_error === true;
|
|
2281
|
-
// `tool_result` content is either a string or an array of
|
|
2282
|
-
// content blocks (typically a single `{type:"text", text}`).
|
|
2283
|
-
// The MCP SDK wraps connector JSON responses by serializing
|
|
2284
|
-
// to that text body, so the response-shape walker
|
|
2285
|
-
// downstream wants the parsed object. Pull the first text
|
|
2286
|
-
// block, JSON-parse when possible, fallback to the raw
|
|
2287
|
-
// string so the field is always populated for ok steps.
|
|
2288
|
-
let parsedToolResult;
|
|
2289
|
-
const blockContent = block.content;
|
|
2290
|
-
if (typeof blockContent === "string") {
|
|
2291
|
-
try {
|
|
2292
|
-
parsedToolResult = JSON.parse(blockContent);
|
|
2293
|
-
}
|
|
2294
|
-
catch {
|
|
2295
|
-
parsedToolResult = blockContent;
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
else if (Array.isArray(blockContent)) {
|
|
2299
|
-
const firstText = blockContent.find((b) => !!b
|
|
2300
|
-
&& typeof b === "object"
|
|
2301
|
-
&& b.type === "text"
|
|
2302
|
-
&& typeof b.text === "string");
|
|
2303
|
-
if (firstText) {
|
|
2304
|
-
try {
|
|
2305
|
-
parsedToolResult = JSON.parse(firstText.text);
|
|
2306
|
-
}
|
|
2307
|
-
catch {
|
|
2308
|
-
parsedToolResult = firstText.text;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
else {
|
|
2312
|
-
parsedToolResult = blockContent;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
const step = {
|
|
2316
|
-
toolName: pending.name,
|
|
2317
|
-
toolArgs: pending.args,
|
|
2318
|
-
durationMs: Date.now() - pending.startedAt,
|
|
2319
|
-
status: isToolError ? "error" : "ok",
|
|
2320
|
-
costUsd: null,
|
|
2321
|
-
tokensInput: null,
|
|
2322
|
-
tokensOutput: null,
|
|
2323
|
-
toolResult: parsedToolResult,
|
|
2324
|
-
};
|
|
2325
|
-
trace.push(step);
|
|
2326
|
-
onToolStep?.(step);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
else if (message.type === "result") {
|
|
2330
|
-
const r = message;
|
|
2331
|
-
cost = {
|
|
2332
|
-
tokensInput: r.usage.input_tokens ?? 0,
|
|
2333
|
-
tokensOutput: r.usage.output_tokens ?? 0,
|
|
2334
|
-
cacheCreationTokens: r.usage.cache_creation_input_tokens ?? 0,
|
|
2335
|
-
cacheReadTokens: r.usage.cache_read_input_tokens ?? 0,
|
|
2336
|
-
costUsd: r.total_cost_usd ?? 0,
|
|
2337
|
-
durationMs: r.duration_ms ?? Date.now() - startMs,
|
|
2338
|
-
numTurns: r.num_turns ?? 0,
|
|
2339
|
-
};
|
|
2340
|
-
// §13 Phase 3.1 — capture structured output when present, and
|
|
2341
|
-
// map the SDK's structured-output-retry-exhausted subtype to
|
|
2342
|
-
// a parse_error so the invoker classifies it consistently
|
|
2343
|
-
// with the text-extract path.
|
|
2344
|
-
if (r.subtype === "success") {
|
|
2345
|
-
const success = r;
|
|
2346
|
-
if (success.structured_output !== undefined) {
|
|
2347
|
-
capturedStructured = success.structured_output;
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
else if (r.subtype === "error_max_structured_output_retries") {
|
|
2351
|
-
sawStructuredOutputRetryError = true;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
finally {
|
|
2357
|
-
try {
|
|
2358
|
-
await stream?.return?.(undefined);
|
|
2359
|
-
}
|
|
2360
|
-
catch {
|
|
2361
|
-
/* already closed */
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
cost = withDurationMs(cost, startMs);
|
|
2365
|
-
if (loopAborted) {
|
|
2366
|
-
return {
|
|
2367
|
-
ok: false,
|
|
2368
|
-
errorClass: "loop_aborted",
|
|
2369
|
-
message: `subprocess exceeded maxToolCalls=${maxToolCalls}`,
|
|
2370
|
-
cost,
|
|
2371
|
-
trace,
|
|
2372
|
-
writeClassToolFired,
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
|
-
if (aborted.value) {
|
|
2376
|
-
const errorClass = classifyAbortReason(params.abortSignal?.reason);
|
|
2377
|
-
return {
|
|
2378
|
-
ok: false,
|
|
2379
|
-
errorClass,
|
|
2380
|
-
message: errorClass === "timeout"
|
|
2381
|
-
? "delegated task timed out (wall-clock)"
|
|
2382
|
-
: "delegated task cancelled by caller",
|
|
2383
|
-
cost,
|
|
2384
|
-
trace,
|
|
2385
|
-
writeClassToolFired,
|
|
2386
|
-
};
|
|
2387
|
-
}
|
|
2388
|
-
const finalText = lastAssistantTextOnlyChunks.length > 0
|
|
2389
|
-
? lastAssistantTextOnlyChunks.join("\n").trim()
|
|
2390
|
-
: assistantTextChunks.join("\n").trim();
|
|
2391
|
-
// §13 Phase 3.1 — `error_max_structured_output_retries` typically
|
|
2392
|
-
// fires when the model wanted to emit a §7.2 confirmation envelope
|
|
2393
|
-
// or §5.1 error envelope, neither of which satisfies the user's
|
|
2394
|
-
// narrow schema. The assistant text emissions captured during those
|
|
2395
|
-
// retries land in `assistantTextChunks` / `lastAssistantTextOnlyChunks`,
|
|
2396
|
-
// so the invoker's text-extract chain can route them via
|
|
2397
|
-
// `detectConfirmationEnvelope` / `detectErrorEnvelope`. Only return
|
|
2398
|
-
// `parse_error` if there is also no usable text — otherwise fall
|
|
2399
|
-
// through to the text-emission path (no `structuredOutput` field
|
|
2400
|
-
// set, so the invoker uses `rawAssistantText`).
|
|
2401
|
-
if (sawStructuredOutputRetryError
|
|
2402
|
-
&& capturedStructured === undefined
|
|
2403
|
-
&& finalText.length === 0) {
|
|
2404
|
-
return {
|
|
2405
|
-
ok: false,
|
|
2406
|
-
errorClass: "parse_error",
|
|
2407
|
-
message: "Claude SDK exhausted structured-output retries and emitted no text fallback.",
|
|
2408
|
-
cost,
|
|
2409
|
-
trace,
|
|
2410
|
-
writeClassToolFired,
|
|
2411
|
-
};
|
|
2412
|
-
}
|
|
2413
|
-
// §13 Phase 3.1 — when the SDK supplied `structured_output`, that
|
|
2414
|
-
// is the validated final emission; the assistant text may be empty
|
|
2415
|
-
// (the SDK consumes the JSON internally on success). Skip the
|
|
2416
|
-
// empty-text parse_error guard in that case.
|
|
2417
|
-
if (capturedStructured === undefined && finalText.length === 0) {
|
|
2418
|
-
return {
|
|
2419
|
-
ok: false,
|
|
2420
|
-
errorClass: "parse_error",
|
|
2421
|
-
message: "Claude SDK stream ended without a text-only assistant turn",
|
|
2422
|
-
cost,
|
|
2423
|
-
trace,
|
|
2424
|
-
writeClassToolFired,
|
|
2425
|
-
};
|
|
2426
|
-
}
|
|
2427
|
-
return {
|
|
2428
|
-
ok: true,
|
|
2429
|
-
// When structured output is present, `rawAssistantText` is purely
|
|
2430
|
-
// a fallback; the invoker prefers `structuredOutput`. Carry both
|
|
2431
|
-
// so a future kill-switch flip mid-restart still sees an
|
|
2432
|
-
// extractable text emission.
|
|
2433
|
-
rawAssistantText: finalText,
|
|
2434
|
-
cost,
|
|
2435
|
-
trace,
|
|
2436
|
-
writeClassToolFired,
|
|
2437
|
-
...(capturedStructured !== undefined
|
|
2438
|
-
? { structuredOutput: capturedStructured }
|
|
2439
|
-
: {}),
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
catch (err) {
|
|
2443
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2444
|
-
const cost = withDurationMs(emptyCost(), startMs);
|
|
2445
|
-
if (/authentication_failed|unauthorized|invalid api key|sk-ant-/i.test(message)) {
|
|
2446
|
-
return {
|
|
2447
|
-
ok: false,
|
|
2448
|
-
errorClass: "auth_error",
|
|
2449
|
-
message,
|
|
2450
|
-
cost,
|
|
2451
|
-
trace,
|
|
2452
|
-
writeClassToolFired,
|
|
2453
|
-
};
|
|
2454
|
-
}
|
|
2455
|
-
if (aborted.value) {
|
|
2456
|
-
return {
|
|
2457
|
-
ok: false,
|
|
2458
|
-
errorClass: classifyAbortReason(params.abortSignal?.reason),
|
|
2459
|
-
message,
|
|
2460
|
-
cost,
|
|
2461
|
-
trace,
|
|
2462
|
-
writeClassToolFired,
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
return {
|
|
2466
|
-
ok: false,
|
|
2467
|
-
errorClass: "subprocess_crashed",
|
|
2468
|
-
message,
|
|
2469
|
-
cost,
|
|
2470
|
-
trace,
|
|
2471
|
-
writeClassToolFired,
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
finally {
|
|
2475
|
-
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
2476
|
-
this.readTokenManager?.revoke(sessionDir);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
function extractClaudeProbeTools(message) {
|
|
2481
|
-
if (!message || typeof message !== "object")
|
|
2482
|
-
return [];
|
|
2483
|
-
const out = [];
|
|
2484
|
-
const record = message;
|
|
2485
|
-
if (record.type === "system" && record.subtype === "init" && Array.isArray(record.tools)) {
|
|
2486
|
-
addClaudeProbeTools(record.tools, out);
|
|
2487
|
-
}
|
|
2488
|
-
if (record.type === "assistant" || record.type === "user") {
|
|
2489
|
-
addClaudeProbeTools(record.message, out);
|
|
2490
|
-
}
|
|
2491
|
-
if (record.type === "result") {
|
|
2492
|
-
addClaudeProbeTools(record.result, out);
|
|
2493
|
-
}
|
|
2494
|
-
return out;
|
|
2495
|
-
}
|
|
2496
|
-
function addClaudeProbeTools(value, out, depth = 0) {
|
|
2497
|
-
if (depth > 8 || value === null || value === undefined)
|
|
2498
|
-
return;
|
|
2499
|
-
if (typeof value === "string") {
|
|
2500
|
-
for (const match of value.matchAll(CLAUDE_CONNECTOR_TOOL_RE)) {
|
|
2501
|
-
out.push(match[0]);
|
|
2502
|
-
}
|
|
2503
|
-
if (isClaudeProbeToolName(value))
|
|
2504
|
-
out.push(value);
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
if (Array.isArray(value)) {
|
|
2508
|
-
for (const item of value)
|
|
2509
|
-
addClaudeProbeTools(item, out, depth + 1);
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
|
-
if (typeof value !== "object")
|
|
2513
|
-
return;
|
|
2514
|
-
const record = value;
|
|
2515
|
-
addClaudeProbeTools(record.tool_name, out, depth + 1);
|
|
2516
|
-
addClaudeProbeTools(record.text, out, depth + 1);
|
|
2517
|
-
addClaudeProbeTools(record.content, out, depth + 1);
|
|
2518
|
-
addClaudeProbeTools(record.message, out, depth + 1);
|
|
2519
|
-
addClaudeProbeTools(record.result, out, depth + 1);
|
|
2520
|
-
addClaudeProbeTools(record.tools, out, depth + 1);
|
|
2521
|
-
}
|
|
2522
|
-
function isClaudeProbeToolName(value) {
|
|
2523
|
-
return CLAUDE_PROBE_TOOL_PREFIXES.some((prefix) => {
|
|
2524
|
-
if (!value.startsWith(prefix))
|
|
2525
|
-
return false;
|
|
2526
|
-
// Hyphen is part of the alphabet for kebab-case connectors (Notion's
|
|
2527
|
-
// `notion-search` etc.). Snake-case connectors keep working because
|
|
2528
|
-
// `_` is still in the class.
|
|
2529
|
-
return /^[A-Za-z0-9_-]+$/.test(value.slice(prefix.length));
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
function describeClaudeProbeResultError(result) {
|
|
2533
|
-
if ("result" in result && typeof result.result === "string" && result.result.trim()) {
|
|
2534
|
-
return result.result.trim();
|
|
2535
|
-
}
|
|
2536
|
-
if ("errors" in result && Array.isArray(result.errors) && result.errors.length > 0) {
|
|
2537
|
-
return result.errors.join("; ");
|
|
1143
|
+
return runDelegatedTaskFn(this.delegatedDeps(), params);
|
|
2538
1144
|
}
|
|
2539
|
-
return result.subtype;
|
|
2540
1145
|
}
|
|
2541
1146
|
//# sourceMappingURL=claude-code-core.js.map
|