@namzu/sdk 51.1.0 → 52.1.0
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/CHANGELOG.md +119 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +80 -4
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +36 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -0
- package/dist/authorization/protected-paths.js +107 -0
- package/dist/authorization/protected-paths.js.map +1 -0
- package/dist/authorization/readonly-commands.d.ts +39 -0
- package/dist/authorization/readonly-commands.d.ts.map +1 -0
- package/dist/authorization/readonly-commands.js +797 -0
- package/dist/authorization/readonly-commands.js.map +1 -0
- package/dist/authorization/readonly-flags.d.ts +19 -0
- package/dist/authorization/readonly-flags.d.ts.map +1 -0
- package/dist/authorization/readonly-flags.js +178 -0
- package/dist/authorization/readonly-flags.js.map +1 -0
- package/dist/authorization/removal.d.ts +52 -0
- package/dist/authorization/removal.d.ts.map +1 -0
- package/dist/authorization/removal.js +283 -0
- package/dist/authorization/removal.js.map +1 -0
- package/dist/authorization/shell-paths.d.ts +66 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -0
- package/dist/authorization/shell-paths.js +697 -0
- package/dist/authorization/shell-paths.js.map +1 -0
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/compaction/context-window.d.ts +24 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +34 -0
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +10 -2
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/plan.d.ts +4 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +6 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/verifier.d.ts +11 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +75 -23
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +55 -7
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +23 -2
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
- package/dist/connector/web/guarded-fetch.js +24 -0
- package/dist/connector/web/guarded-fetch.js.map +1 -1
- package/dist/connector/web/index.d.ts +1 -1
- package/dist/connector/web/index.d.ts.map +1 -1
- package/dist/connector/web/page.d.ts +59 -0
- package/dist/connector/web/page.d.ts.map +1 -0
- package/dist/connector/web/page.js +112 -0
- package/dist/connector/web/page.js.map +1 -0
- package/dist/connector/web/types.d.ts +47 -0
- package/dist/connector/web/types.d.ts.map +1 -1
- package/dist/connector/web/types.js.map +1 -1
- package/dist/constants/continuation.d.ts +8 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +8 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +1 -1
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +1 -1
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/contracts/a2a.d.ts +2 -2
- package/dist/manager/agent/lifecycle.d.ts +7 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +11 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +4 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +6 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +98 -114
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +90 -22
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +434 -41
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/provider/capabilities.d.ts +9 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +15 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +13 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +10 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/image-input-refusal.d.ts +9 -0
- package/dist/provider/image-input-refusal.d.ts.map +1 -0
- package/dist/provider/image-input-refusal.js +54 -0
- package/dist/provider/image-input-refusal.js.map +1 -0
- package/dist/provider/index.d.ts +2 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/retry.d.ts +7 -2
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +38 -4
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +15 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
- package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
- package/dist/runtime/query/deferred-sandbox.js +75 -0
- package/dist/runtime/query/deferred-sandbox.js.map +1 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +74 -13
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +51 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +214 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +6 -0
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +1 -1
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +108 -28
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +57 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +380 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +61 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +94 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +203 -23
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
- package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
- package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
- package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
- package/dist/runtime/query/iteration/request-repair.js +593 -0
- package/dist/runtime/query/iteration/request-repair.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +2 -2
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +36 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -0
- package/dist/runtime/query/path-escalation.js +184 -0
- package/dist/runtime/query/path-escalation.js.map +1 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +8 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/reminders.d.ts +35 -0
- package/dist/runtime/query/reminders.d.ts.map +1 -0
- package/dist/runtime/query/reminders.js +7 -0
- package/dist/runtime/query/reminders.js.map +1 -0
- package/dist/runtime/query/request-rich-content.d.ts +5 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +67 -25
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +26 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +26 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +79 -5
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +46 -0
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +85 -1
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/session/paths.d.ts +5 -1
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +8 -0
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +2 -2
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +2 -2
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/naming.d.ts +9 -0
- package/dist/store/memory/naming.d.ts.map +1 -1
- package/dist/store/memory/naming.js +12 -0
- package/dist/store/memory/naming.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +30 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +170 -26
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +12 -5
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +4 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +60 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +152 -33
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +44 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +52 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +143 -0
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/web.d.ts +13 -2
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +225 -32
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +256 -92
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/question-options.d.ts +1 -0
- package/dist/tools/coordinator/question-options.d.ts.map +1 -1
- package/dist/tools/coordinator/question-options.js +6 -1
- package/dist/tools/coordinator/question-options.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -1
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +6 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/paths.d.ts +6 -0
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +1 -1
- package/dist/tools/paths.js.map +1 -1
- package/dist/types/agent/query.d.ts +3 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +104 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +93 -19
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +3 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/errors.d.ts +19 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +54 -5
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/interface.d.ts +12 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +1 -1
- package/dist/types/session/config.d.ts +14 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +27 -4
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +9 -9
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +5 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +52 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +4 -2
- package/src/agents/QueryAgent.ts +5 -1
- package/src/agents/SupervisorAgent.ts +1 -1
- package/src/agents/examples/SupervisorAgent.ts +217 -106
- package/src/agents/index.ts +1 -1
- package/src/authorization/protected-paths.ts +110 -0
- package/src/authorization/readonly-commands.ts +771 -0
- package/src/authorization/readonly-flags.ts +196 -0
- package/src/authorization/removal.ts +326 -0
- package/src/authorization/shell-paths.ts +927 -0
- package/src/bridge/acp/update.ts +1 -0
- package/src/compaction/context-window.ts +41 -0
- package/src/compaction/manual.ts +10 -2
- package/src/compaction/plan.ts +11 -2
- package/src/compaction/verifier.ts +84 -22
- package/src/config/runtime.ts +23 -2
- package/src/connector/web/guarded-fetch.ts +26 -0
- package/src/connector/web/index.ts +3 -0
- package/src/connector/web/page.ts +130 -0
- package/src/connector/web/types.ts +47 -0
- package/src/constants/continuation.ts +10 -0
- package/src/constants/plugin/index.ts +1 -1
- package/src/manager/agent/lifecycle.ts +12 -4
- package/src/manager/session/turn-recorder.ts +10 -2
- package/src/plugin/index.ts +1 -0
- package/src/plugin/lifecycle.ts +107 -119
- package/src/plugin/shell-hook.ts +477 -43
- package/src/provider/capabilities.ts +17 -0
- package/src/provider/errors.ts +13 -0
- package/src/provider/fallback.ts +10 -0
- package/src/provider/idle-timeout.ts +5 -0
- package/src/provider/image-input-refusal.ts +54 -0
- package/src/provider/index.ts +2 -0
- package/src/provider/retry.ts +42 -4
- package/src/public-runtime.ts +11 -0
- package/src/public-types.ts +4 -0
- package/src/runtime/jobs/registry.ts +39 -2
- package/src/runtime/query/deferred-sandbox.ts +110 -0
- package/src/runtime/query/executor/tool-call-admission.ts +79 -13
- package/src/runtime/query/executor.ts +267 -11
- package/src/runtime/query/finalize-turn.ts +7 -1
- package/src/runtime/query/index.ts +169 -52
- package/src/runtime/query/iteration/index.ts +439 -35
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +108 -0
- package/src/runtime/query/iteration/phases/compaction.ts +241 -38
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
- package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
- package/src/runtime/query/iteration/request-repair.ts +743 -0
- package/src/runtime/query/iteration/stream-turn.ts +11 -1
- package/src/runtime/query/path-escalation.ts +204 -0
- package/src/runtime/query/plugin-hooks.ts +8 -0
- package/src/runtime/query/reminders.ts +45 -0
- package/src/runtime/query/request-rich-content.ts +77 -27
- package/src/runtime/query/resume-pending.ts +27 -0
- package/src/runtime/query/review-policy.ts +106 -5
- package/src/runtime/query/tool-output-budget.ts +115 -1
- package/src/runtime/query/tooling.ts +6 -0
- package/src/session/paths.ts +11 -1
- package/src/store/memory/disk.ts +2 -1
- package/src/store/memory/markdown.ts +2 -1
- package/src/store/memory/naming.ts +13 -0
- package/src/tools/builtins/bash.ts +203 -34
- package/src/tools/builtins/computer-use.ts +13 -4
- package/src/tools/builtins/content-fingerprint.ts +5 -0
- package/src/tools/builtins/edit.ts +69 -19
- package/src/tools/builtins/grep.ts +159 -38
- package/src/tools/builtins/read-file.ts +48 -5
- package/src/tools/builtins/read-render.ts +193 -0
- package/src/tools/builtins/web.ts +258 -33
- package/src/tools/coordinator/ask-user-question.ts +309 -103
- package/src/tools/coordinator/question-options.ts +8 -1
- package/src/tools/defineTool.ts +11 -2
- package/src/tools/paths.ts +1 -1
- package/src/types/agent/query.ts +3 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +104 -0
- package/src/types/message/index.ts +2 -0
- package/src/types/plugin/index.ts +101 -18
- package/src/types/provider/errors.ts +66 -5
- package/src/types/provider/interface.ts +13 -0
- package/src/types/provider/stream.ts +1 -1
- package/src/types/session/config.ts +14 -0
- package/src/types/session/events.ts +32 -4
- package/src/types/session/records.ts +1 -0
- package/src/types/session/stop-reason.ts +5 -0
- package/src/types/tool/index.ts +53 -2
package/src/plugin/shell-hook.ts
CHANGED
|
@@ -23,13 +23,19 @@
|
|
|
23
23
|
* one inherited from the host's own environment. A hook that needs "which
|
|
24
24
|
* turn" has it exactly when there is one.
|
|
25
25
|
*
|
|
26
|
-
* Its exit code
|
|
26
|
+
* Its answer is the exit code and, on exit `0`, an optional JSON object on
|
|
27
|
+
* stdout. The exit codes:
|
|
27
28
|
*
|
|
28
|
-
* - `0` — carry on.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* - `0` — carry on. On `user_prompt_submit`, plain (non-JSON) stdout becomes
|
|
30
|
+
* context for the model.
|
|
31
|
+
* - `2` — **block**, read per event: before a tool (`pre_tool_use`) the call is
|
|
32
|
+
* skipped and the model is told why, with the hook's stderr as the reason;
|
|
33
|
+
* on `user_prompt_submit` the turn ends; on `turn_end` and `subagent_stop`
|
|
34
|
+
* the stderr is fed back to the model and the turn goes on; on
|
|
35
|
+
* `pre_compact` a compaction the context did not force is cancelled; on
|
|
36
|
+
* `post_tool_use` and `post_tool_use_failure` the stderr is shown to the
|
|
37
|
+
* model with that call's result. On every other event a 2 is reported and
|
|
38
|
+
* the turn carries on.
|
|
33
39
|
* - anything else — the hook's own failure. Reported through the logger,
|
|
34
40
|
* never blocking. A formatter that crashed must not stop the agent editing,
|
|
35
41
|
* and a script missing its interpreter (127) must not read as "forbidden".
|
|
@@ -37,12 +43,41 @@
|
|
|
37
43
|
* `2` rather than any non-zero, because a verdict and a failure are different
|
|
38
44
|
* facts and one exit code cannot carry both.
|
|
39
45
|
*
|
|
40
|
-
* ##
|
|
46
|
+
* ## The JSON answer
|
|
41
47
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
48
|
+
* Exit `0` with a JSON object on stdout carries the fields below. Output that
|
|
49
|
+
* does not start with `{` is plain text and is not read as JSON; output that
|
|
50
|
+
* starts with `{` but does not parse, or has a field of the wrong type, is
|
|
51
|
+
* reported and ignored, and the call carries on.
|
|
52
|
+
*
|
|
53
|
+
* - `continue: false` with `stopReason` — stop the turn after the current
|
|
54
|
+
* step; `stopReason` is shown to the operator.
|
|
55
|
+
* - `decision: "block"` with `reason` — refuse; `decision: "approve"` allows a
|
|
56
|
+
* tool call or a permission request.
|
|
57
|
+
* - `systemMessage` — a warning for the operator, reported through the logger.
|
|
58
|
+
* - `suppressOutput` — keep this hook's stdout out of the debug log.
|
|
59
|
+
* - `hookSpecificOutput` with `hookEventName` naming this event (Namzu's
|
|
60
|
+
* name or the reference spelling `PreToolUse`, `Stop`, ...):
|
|
61
|
+
* `permissionDecision` (`allow`, `deny` or `ask`) with
|
|
62
|
+
* `permissionDecisionReason`, `updatedInput` and `additionalContext` for
|
|
63
|
+
* `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
|
|
64
|
+
* `post_tool_use`, `post_tool_use_failure`, `turn_end`, `subagent_stop`,
|
|
65
|
+
* `session_start`, `notification`; and for `permission_request` a `decision`
|
|
66
|
+
* `{ behavior: "allow" }` or `{ behavior: "deny", message, interrupt }`.
|
|
67
|
+
*
|
|
68
|
+
* ## What a hook may decide, and what it may not
|
|
69
|
+
*
|
|
70
|
+
* A hook may rewrite a tool call's input (`updatedInput`) and answer a
|
|
71
|
+
* permission question. Both stay inside the kernel's own rules:
|
|
72
|
+
*
|
|
73
|
+
* - `updatedInput` goes through the same validation as a model-supplied
|
|
74
|
+
* input, and the rewritten call is then checked by the authorization gate
|
|
75
|
+
* and the review policy as it now reads.
|
|
76
|
+
* - `allow` stands in for a person's yes only on a call that would have been
|
|
77
|
+
* asked about because it changes something. It never covers a sandbox
|
|
78
|
+
* escape, a path outside the roots, a call the tool always asks about, a
|
|
79
|
+
* call a rule sent to review explicitly, or a destructive one, and it
|
|
80
|
+
* never reopens what a rule denied. `deny` always wins over `allow`.
|
|
46
81
|
*
|
|
47
82
|
* ## Bounds
|
|
48
83
|
*
|
|
@@ -53,13 +88,18 @@
|
|
|
53
88
|
* {@link attachShellHooks}, and the refusal names the new one: a hook that
|
|
54
89
|
* silently stopped firing is worse than a config that fails to load.
|
|
55
90
|
*
|
|
56
|
-
* Each hook has a deadline (`timeoutMs`, default
|
|
91
|
+
* Each hook has a deadline (`timeoutMs`, default ten minutes, also the cap)
|
|
57
92
|
* and its output is captured to a bound. A hook that times out is reported
|
|
58
93
|
* and does not block: one that could not answer in time has not answered no.
|
|
59
94
|
*/
|
|
60
95
|
|
|
61
96
|
import type { spawn } from 'node:child_process'
|
|
62
97
|
|
|
98
|
+
import { decodedCommands, decomposeCommandLine } from '../authorization/command-line.js'
|
|
99
|
+
import {
|
|
100
|
+
SKILL_TOOL_NAME_ALIASES,
|
|
101
|
+
permissionPatternToRegExpSource,
|
|
102
|
+
} from '../authorization/skill-grant.js'
|
|
63
103
|
import { spawnHostShell } from '../tools/command-shell.js'
|
|
64
104
|
import type { PluginId } from '../types/ids/index.js'
|
|
65
105
|
import type {
|
|
@@ -67,7 +107,9 @@ import type {
|
|
|
67
107
|
PluginHookContext,
|
|
68
108
|
PluginHookDefinition,
|
|
69
109
|
PluginHookEvent,
|
|
110
|
+
PluginHookPermission,
|
|
70
111
|
PluginHookResult,
|
|
112
|
+
PluginNotificationInfo,
|
|
71
113
|
} from '../types/plugin/index.js'
|
|
72
114
|
import { RENAMED_PLUGIN_HOOK_EVENTS } from '../types/plugin/index.js'
|
|
73
115
|
import { asPluginId } from '../utils/id.js'
|
|
@@ -87,6 +129,9 @@ export type ShellHookEvent = Extract<
|
|
|
87
129
|
| 'session_end'
|
|
88
130
|
| 'pre_tool_use'
|
|
89
131
|
| 'post_tool_use'
|
|
132
|
+
| 'post_tool_use_failure'
|
|
133
|
+
| 'permission_request'
|
|
134
|
+
| 'notification'
|
|
90
135
|
| 'pre_compact'
|
|
91
136
|
| 'post_compact'
|
|
92
137
|
| 'subagent_stop'
|
|
@@ -100,6 +145,9 @@ export const SHELL_HOOK_EVENTS: readonly ShellHookEvent[] = [
|
|
|
100
145
|
'session_end',
|
|
101
146
|
'pre_tool_use',
|
|
102
147
|
'post_tool_use',
|
|
148
|
+
'post_tool_use_failure',
|
|
149
|
+
'permission_request',
|
|
150
|
+
'notification',
|
|
103
151
|
'pre_compact',
|
|
104
152
|
'post_compact',
|
|
105
153
|
'subagent_stop',
|
|
@@ -111,10 +159,27 @@ export const SHELL_HOOK_EVENTS: readonly ShellHookEvent[] = [
|
|
|
111
159
|
export interface ShellHookEntry {
|
|
112
160
|
/** Run with `sh -c`, in the working directory, with the event JSON on stdin. */
|
|
113
161
|
readonly command: string
|
|
114
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* What this applies to. On a tool event (and `permission_request`) the tool
|
|
164
|
+
* name; on `notification` the notification type. `*` or absent matches
|
|
165
|
+
* everything; a list of names (`a|b`, `a,b`, each optionally ending in `*`
|
|
166
|
+
* as a prefix) matches those; anything else is a regular expression tested
|
|
167
|
+
* against the name.
|
|
168
|
+
*/
|
|
115
169
|
readonly matcher?: string
|
|
116
170
|
/** Deadline in milliseconds; default `DEFAULT_SHELL_HOOK_TIMEOUT_MS`, capped at `MAX_SHELL_HOOK_TIMEOUT_MS`. */
|
|
117
171
|
readonly timeoutMs?: number
|
|
172
|
+
/**
|
|
173
|
+
* A permission-rule filter on a tool event: `bash(git *)` runs the hook
|
|
174
|
+
* only for a `bash` call whose command matches, `edit` for any `edit`.
|
|
175
|
+
* Checked before the process is spawned. A filter that cannot be read
|
|
176
|
+
* runs the hook, so a guard is never switched off by a typo.
|
|
177
|
+
*/
|
|
178
|
+
readonly if?: string
|
|
179
|
+
/** Run once in a session, then never again in it. */
|
|
180
|
+
readonly once?: boolean
|
|
181
|
+
/** Text for the operator's status line while the hook runs. */
|
|
182
|
+
readonly statusMessage?: string
|
|
118
183
|
}
|
|
119
184
|
|
|
120
185
|
/** Event → entries, the shape a host's config file carries. */
|
|
@@ -125,15 +190,20 @@ export type ShellHooksConfig = {
|
|
|
125
190
|
/** The plugin id host shell hooks register under when the host names none. */
|
|
126
191
|
export const SHELL_HOOKS_PLUGIN_ID: PluginId = asPluginId('6d6f6bec-9d54-485b-90ca-52edbac1009d')
|
|
127
192
|
|
|
128
|
-
export const DEFAULT_SHELL_HOOK_TIMEOUT_MS =
|
|
193
|
+
export const DEFAULT_SHELL_HOOK_TIMEOUT_MS = 600_000
|
|
129
194
|
export const MAX_SHELL_HOOK_TIMEOUT_MS = 10 * 60_000
|
|
130
195
|
/** Captured output bound per stream. */
|
|
131
196
|
const MAX_CAPTURE_BYTES = 64 * 1024
|
|
132
197
|
|
|
133
198
|
/**
|
|
134
|
-
* `matcher` against a
|
|
135
|
-
*
|
|
136
|
-
*
|
|
199
|
+
* `matcher` against a name (a tool's, or a notification's type).
|
|
200
|
+
*
|
|
201
|
+
* Empty or `*` matches everything. A list of names separated by `|` or `,`
|
|
202
|
+
* matches exactly, each part optionally ending in `*` as a prefix (the
|
|
203
|
+
* spelling this adapter has always taken). Anything else is a regular
|
|
204
|
+
* expression, tested unanchored against the name; one that does not compile
|
|
205
|
+
* matches nothing. Case-sensitive, because tool names are. A matcher on an
|
|
206
|
+
* event that has no name (`turn_start`) matches nothing unless it is `*`.
|
|
137
207
|
*/
|
|
138
208
|
export function shellHookMatches(
|
|
139
209
|
matcher: string | undefined,
|
|
@@ -141,13 +211,64 @@ export function shellHookMatches(
|
|
|
141
211
|
): boolean {
|
|
142
212
|
if (matcher === undefined || matcher.trim() === '' || matcher.trim() === '*') return true
|
|
143
213
|
if (toolName === undefined) return false
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
214
|
+
const parts = matcher.split(/[|,]/).map((part) => part.trim())
|
|
215
|
+
if (parts.every((part) => /^[A-Za-z0-9_.:-]+\*?$/.test(part))) {
|
|
216
|
+
return parts.some((candidate) =>
|
|
217
|
+
candidate.endsWith('*')
|
|
218
|
+
? toolName.startsWith(candidate.slice(0, -1))
|
|
219
|
+
: toolName === candidate,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return new RegExp(matcher).test(toolName)
|
|
224
|
+
} catch {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A hook's `if` against one tool call: `Tool` or `Tool(pattern)`. The tool
|
|
231
|
+
* part is case-insensitive and takes the names skills write (`Bash`,
|
|
232
|
+
* `Read`). A pattern is matched against a shell tool's command line and only
|
|
233
|
+
* there; for a tool with no command line the filter cannot be read, which
|
|
234
|
+
* runs the hook.
|
|
235
|
+
*/
|
|
236
|
+
export function shellHookIfMatches(
|
|
237
|
+
filter: string | undefined,
|
|
238
|
+
toolName: string | undefined,
|
|
239
|
+
toolInput: unknown,
|
|
240
|
+
): boolean {
|
|
241
|
+
if (filter === undefined || filter.trim() === '') return true
|
|
242
|
+
if (toolName === undefined) return true
|
|
243
|
+
const shape = /^([A-Za-z0-9_.-]+)(?:\(([\s\S]*)\))?$/.exec(filter.trim())
|
|
244
|
+
if (!shape) return true
|
|
245
|
+
const written = (shape[1] ?? '').toLowerCase()
|
|
246
|
+
const tool = SKILL_TOOL_NAME_ALIASES[written] ?? written
|
|
247
|
+
if (tool !== toolName.toLowerCase()) return false
|
|
248
|
+
const specifier = shape[2]?.trim()
|
|
249
|
+
if (specifier === undefined || specifier === '' || specifier.replaceAll('*', '') === '')
|
|
250
|
+
return true
|
|
251
|
+
if (tool !== 'bash') return true
|
|
252
|
+
const line =
|
|
253
|
+
toolInput !== null && typeof toolInput === 'object'
|
|
254
|
+
? (toolInput as Record<string, unknown>).command
|
|
255
|
+
: undefined
|
|
256
|
+
if (typeof line !== 'string') return false
|
|
257
|
+
const pattern = specifier.endsWith(':*') ? `${specifier.slice(0, -2)} *` : specifier
|
|
258
|
+
try {
|
|
259
|
+
const matcher = new RegExp(permissionPatternToRegExpSource(pattern))
|
|
260
|
+
const decomposed = decomposeCommandLine(line, 'sh')
|
|
261
|
+
// A line the reader cannot see through may run the very command the
|
|
262
|
+
// filter names, so the hook runs. A filter only ever narrows when the
|
|
263
|
+
// whole line is visible and none of its commands matches.
|
|
264
|
+
if (decomposed.opaque) return true
|
|
265
|
+
const candidates = [...decomposed.segments, ...decodedCommands(line, 'sh')].map((each) =>
|
|
266
|
+
each.trim(),
|
|
267
|
+
)
|
|
268
|
+
return candidates.some((each) => matcher.test(each))
|
|
269
|
+
} catch {
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
151
272
|
}
|
|
152
273
|
|
|
153
274
|
/** What one hook run came back with, before it is mapped to a verdict. */
|
|
@@ -180,6 +301,10 @@ export interface ShellHookInput {
|
|
|
180
301
|
readonly output: string
|
|
181
302
|
readonly error?: string
|
|
182
303
|
}
|
|
304
|
+
/** On `notification`: what the host is telling the operator. */
|
|
305
|
+
readonly notification?: PluginNotificationInfo
|
|
306
|
+
/** On `turn_end` and `subagent_stop`: an earlier answer already kept this turn going. */
|
|
307
|
+
readonly stopHookActive?: boolean
|
|
183
308
|
}
|
|
184
309
|
|
|
185
310
|
/** Run one hook command to completion, bounded by its deadline and capture. */
|
|
@@ -204,6 +329,16 @@ export function runShellHook(
|
|
|
204
329
|
...(input.toolName !== undefined ? { tool_name: input.toolName } : {}),
|
|
205
330
|
...(input.toolInput !== undefined ? { tool_input: input.toolInput } : {}),
|
|
206
331
|
...(input.toolResult !== undefined ? { tool_result: input.toolResult } : {}),
|
|
332
|
+
...(input.event === 'post_tool_use_failure' && input.toolResult?.error !== undefined
|
|
333
|
+
? { error: input.toolResult.error }
|
|
334
|
+
: {}),
|
|
335
|
+
...(input.notification !== undefined
|
|
336
|
+
? {
|
|
337
|
+
notification_type: input.notification.type,
|
|
338
|
+
message: input.notification.message,
|
|
339
|
+
}
|
|
340
|
+
: {}),
|
|
341
|
+
...(input.stopHookActive === true ? { stop_hook_active: true } : {}),
|
|
207
342
|
})
|
|
208
343
|
const toolPath = pathOf(input.toolInput)
|
|
209
344
|
// A hook outside a turn must not see a turn id, including one the host
|
|
@@ -323,6 +458,167 @@ function pathOf(toolInput: unknown): string | undefined {
|
|
|
323
458
|
return undefined
|
|
324
459
|
}
|
|
325
460
|
|
|
461
|
+
/** The reference spelling of each event name, which a hook's JSON may use. */
|
|
462
|
+
const REFERENCE_EVENT_NAMES: Readonly<Partial<Record<ShellHookEvent, string>>> = {
|
|
463
|
+
user_prompt_submit: 'UserPromptSubmit',
|
|
464
|
+
session_start: 'SessionStart',
|
|
465
|
+
session_end: 'SessionEnd',
|
|
466
|
+
pre_tool_use: 'PreToolUse',
|
|
467
|
+
post_tool_use: 'PostToolUse',
|
|
468
|
+
post_tool_use_failure: 'PostToolUseFailure',
|
|
469
|
+
permission_request: 'PermissionRequest',
|
|
470
|
+
notification: 'Notification',
|
|
471
|
+
pre_compact: 'PreCompact',
|
|
472
|
+
post_compact: 'PostCompact',
|
|
473
|
+
subagent_stop: 'SubagentStop',
|
|
474
|
+
turn_end: 'Stop',
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** What a hook's stdout JSON said, before it is mapped to a verdict for an event. */
|
|
478
|
+
interface ShellHookReading {
|
|
479
|
+
/** Refuse (or, on a stop event, keep going; on `pre_compact`, cancel). */
|
|
480
|
+
block?: string
|
|
481
|
+
permission?: PluginHookPermission
|
|
482
|
+
updatedInput?: Record<string, unknown>
|
|
483
|
+
context?: string
|
|
484
|
+
halt?: { reason: string }
|
|
485
|
+
notice?: string
|
|
486
|
+
/** The hook asked for its stdout to stay out of the debug log. */
|
|
487
|
+
suppressOutput?: boolean
|
|
488
|
+
/** Why the JSON was set aside, when it was. */
|
|
489
|
+
invalid?: string
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
493
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Read a hook's stdout as its JSON answer, or `undefined` when it is plain
|
|
498
|
+
* text. Strict about types the way the contract is: a wrong type anywhere
|
|
499
|
+
* sets the whole object aside rather than half-obeying it, so a typo in a
|
|
500
|
+
* guard's `decision` is reported instead of silently becoming "continue".
|
|
501
|
+
*/
|
|
502
|
+
function readShellHookJson(event: ShellHookEvent, stdout: string): ShellHookReading | undefined {
|
|
503
|
+
const text = stdout.trim()
|
|
504
|
+
if (!text.startsWith('{')) return undefined
|
|
505
|
+
let parsed: unknown
|
|
506
|
+
try {
|
|
507
|
+
parsed = JSON.parse(text)
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return {
|
|
510
|
+
invalid: `stdout starts with "{" but is not valid JSON (${(error as Error).message})`,
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!isRecord(parsed)) return { invalid: 'stdout JSON is not an object' }
|
|
514
|
+
const optionalString = (
|
|
515
|
+
key: string,
|
|
516
|
+
from: Record<string, unknown> = parsed,
|
|
517
|
+
): string | undefined => (typeof from[key] === 'string' ? (from[key] as string) : undefined)
|
|
518
|
+
const wrongType = (
|
|
519
|
+
key: string,
|
|
520
|
+
type: 'string' | 'boolean',
|
|
521
|
+
from: Record<string, unknown> = parsed,
|
|
522
|
+
) => {
|
|
523
|
+
const value = from[key]
|
|
524
|
+
if (value === undefined) return undefined
|
|
525
|
+
const fits = type === 'string' ? typeof value === 'string' : typeof value === 'boolean'
|
|
526
|
+
return fits ? undefined : `\`${key}\` must be a ${type}`
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (const [key, type] of [
|
|
530
|
+
['continue', 'boolean'],
|
|
531
|
+
['suppressOutput', 'boolean'],
|
|
532
|
+
['stopReason', 'string'],
|
|
533
|
+
['reason', 'string'],
|
|
534
|
+
['systemMessage', 'string'],
|
|
535
|
+
] as const) {
|
|
536
|
+
const problem = wrongType(key, type)
|
|
537
|
+
if (problem) return { invalid: problem }
|
|
538
|
+
}
|
|
539
|
+
if (
|
|
540
|
+
parsed.decision !== undefined &&
|
|
541
|
+
parsed.decision !== 'approve' &&
|
|
542
|
+
parsed.decision !== 'block'
|
|
543
|
+
) {
|
|
544
|
+
return { invalid: '`decision` must be "approve" or "block"' }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const reading: ShellHookReading = {}
|
|
548
|
+
if (parsed.suppressOutput === true) reading.suppressOutput = true
|
|
549
|
+
const systemMessage = optionalString('systemMessage')
|
|
550
|
+
if (systemMessage) reading.notice = systemMessage
|
|
551
|
+
if (parsed.continue === false) {
|
|
552
|
+
reading.halt = {
|
|
553
|
+
reason: optionalString('stopReason') ?? 'Stopped by a hook',
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const reason = optionalString('reason')
|
|
557
|
+
if (parsed.decision === 'block') reading.block = reason ?? 'Blocked by hook'
|
|
558
|
+
if (parsed.decision === 'approve')
|
|
559
|
+
reading.permission = { decision: 'allow', ...(reason ? { reason } : {}) }
|
|
560
|
+
|
|
561
|
+
const specific = parsed.hookSpecificOutput
|
|
562
|
+
if (specific === undefined) return reading
|
|
563
|
+
if (!isRecord(specific)) return { invalid: '`hookSpecificOutput` must be an object' }
|
|
564
|
+
const named = specific.hookEventName
|
|
565
|
+
if (typeof named !== 'string') {
|
|
566
|
+
return { invalid: '`hookSpecificOutput` is missing its `hookEventName`' }
|
|
567
|
+
}
|
|
568
|
+
if (named !== event && named !== REFERENCE_EVENT_NAMES[event]) {
|
|
569
|
+
return {
|
|
570
|
+
invalid: `\`hookSpecificOutput.hookEventName\` is "${named}" but this hook ran for ${event}`,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const [key, type] of [
|
|
574
|
+
['permissionDecisionReason', 'string'],
|
|
575
|
+
['additionalContext', 'string'],
|
|
576
|
+
] as const) {
|
|
577
|
+
const problem = wrongType(key, type, specific)
|
|
578
|
+
if (problem) return { invalid: `hookSpecificOutput: ${problem}` }
|
|
579
|
+
}
|
|
580
|
+
const additional = optionalString('additionalContext', specific)
|
|
581
|
+
if (additional && additional.trim().length > 0) reading.context = additional.trim()
|
|
582
|
+
|
|
583
|
+
if (event === 'pre_tool_use') {
|
|
584
|
+
const decision = specific.permissionDecision
|
|
585
|
+
const why = optionalString('permissionDecisionReason', specific)
|
|
586
|
+
if (decision !== undefined) {
|
|
587
|
+
if (decision === 'deny') reading.block = why ?? reason ?? 'Blocked by hook'
|
|
588
|
+
else if (decision === 'allow' || decision === 'ask') {
|
|
589
|
+
reading.permission = { decision, ...(why ? { reason: why } : {}) }
|
|
590
|
+
} else if (decision !== 'defer') {
|
|
591
|
+
return {
|
|
592
|
+
invalid: '`permissionDecision` must be "allow", "deny" or "ask"',
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// "defer" hands the decision to a separate permission service the kernel
|
|
596
|
+
// does not have: nothing is decided, and the ordinary rules apply.
|
|
597
|
+
}
|
|
598
|
+
if (specific.updatedInput !== undefined) {
|
|
599
|
+
if (!isRecord(specific.updatedInput)) return { invalid: '`updatedInput` must be an object' }
|
|
600
|
+
reading.updatedInput = specific.updatedInput
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (event === 'permission_request') {
|
|
604
|
+
const decision = specific.decision
|
|
605
|
+
if (!isRecord(decision)) {
|
|
606
|
+
return {
|
|
607
|
+
invalid: '`decision` must be {"behavior":"allow"} or {"behavior":"deny","message":"..."}',
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (decision.behavior === 'allow') {
|
|
611
|
+
reading.permission = { decision: 'allow' }
|
|
612
|
+
} else if (decision.behavior === 'deny') {
|
|
613
|
+
reading.block = optionalString('message', decision) ?? 'Denied by hook'
|
|
614
|
+
if (decision.interrupt === true) reading.halt = { reason: reading.block }
|
|
615
|
+
} else {
|
|
616
|
+
return { invalid: '`decision.behavior` must be "allow" or "deny"' }
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return reading
|
|
620
|
+
}
|
|
621
|
+
|
|
326
622
|
/**
|
|
327
623
|
* The verdict, from the outcome and the event. Pure apart from the logger,
|
|
328
624
|
* so the mapping — the part that decides what the model is told — is
|
|
@@ -349,31 +645,157 @@ export function shellHookVerdict(
|
|
|
349
645
|
log.warn('hook timed out', attrs)
|
|
350
646
|
return { action: 'continue' }
|
|
351
647
|
}
|
|
352
|
-
if (outcome.exitCode ===
|
|
648
|
+
if (outcome.exitCode === 2) {
|
|
649
|
+
const reason =
|
|
650
|
+
outcome.stderr.trim() || outcome.stdout.trim() || `blocked by hook \`${entry.command}\``
|
|
651
|
+
switch (event) {
|
|
652
|
+
case 'pre_tool_use':
|
|
653
|
+
case 'user_prompt_submit':
|
|
654
|
+
// Fed back to the model, and the turn goes on: the model is not
|
|
655
|
+
// finished until the hook is satisfied.
|
|
656
|
+
case 'turn_end':
|
|
657
|
+
case 'subagent_stop':
|
|
658
|
+
case 'pre_compact':
|
|
659
|
+
return { action: 'skip', reason }
|
|
660
|
+
// The call has run. What the hook has to say rides with its result.
|
|
661
|
+
case 'post_tool_use':
|
|
662
|
+
case 'post_tool_use_failure':
|
|
663
|
+
return { action: 'continue', context: reason }
|
|
664
|
+
default:
|
|
665
|
+
log.warn('hook returned 2 on an event it cannot block', {
|
|
666
|
+
...attrs,
|
|
667
|
+
'namzu.hook.reason': reason,
|
|
668
|
+
})
|
|
669
|
+
return { action: 'continue' }
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (outcome.exitCode !== 0) {
|
|
673
|
+
log.warn('hook failed', {
|
|
674
|
+
...attrs,
|
|
675
|
+
'namzu.hook.exit': outcome.exitCode ?? -1,
|
|
676
|
+
'namzu.hook.stderr': outcome.stderr.trim().slice(0, 500),
|
|
677
|
+
})
|
|
678
|
+
return { action: 'continue' }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Exit 0.
|
|
682
|
+
const text = outcome.stdout.trim()
|
|
683
|
+
const reading = readShellHookJson(event, outcome.stdout)
|
|
684
|
+
if (reading === undefined) {
|
|
353
685
|
// On the prompt event, what the hook printed is context for the
|
|
354
686
|
// model — the branch, the time, a ticket. Everywhere else stdout is
|
|
355
687
|
// the operator's to read and the model's business not at all.
|
|
356
|
-
const text = outcome.stdout.trim()
|
|
357
688
|
if (event === 'user_prompt_submit' && text.length > 0) return { action: 'annotate', text }
|
|
358
689
|
return { action: 'continue' }
|
|
359
690
|
}
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
outcome.stderr.trim() || outcome.stdout.trim() || `blocked by hook \`${entry.command}\``
|
|
363
|
-
if (event === 'pre_tool_use' || event === 'user_prompt_submit')
|
|
364
|
-
return { action: 'skip', reason }
|
|
365
|
-
log.warn('hook returned 2 on an event it cannot block', {
|
|
691
|
+
if (reading.invalid !== undefined) {
|
|
692
|
+
log.warn('hook output ignored', {
|
|
366
693
|
...attrs,
|
|
367
|
-
'namzu.hook.
|
|
694
|
+
'namzu.hook.problem': reading.invalid,
|
|
368
695
|
})
|
|
369
696
|
return { action: 'continue' }
|
|
370
697
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
698
|
+
if (!reading.suppressOutput) {
|
|
699
|
+
log.debug('hook output', {
|
|
700
|
+
...attrs,
|
|
701
|
+
'namzu.hook.stdout': text.slice(0, 500),
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
if (reading.notice) {
|
|
705
|
+
log.warn('hook warning', { ...attrs, 'namzu.hook.notice': reading.notice })
|
|
706
|
+
}
|
|
707
|
+
const notes = {
|
|
708
|
+
...(reading.context !== undefined ? { context: reading.context } : {}),
|
|
709
|
+
...(reading.halt !== undefined ? { halt: reading.halt } : {}),
|
|
710
|
+
...(reading.notice !== undefined ? { notice: reading.notice } : {}),
|
|
711
|
+
}
|
|
712
|
+
switch (event) {
|
|
713
|
+
case 'pre_tool_use': {
|
|
714
|
+
// A stop request also withholds the call: the hook asked for nothing
|
|
715
|
+
// further to happen.
|
|
716
|
+
const block = reading.block ?? reading.halt?.reason
|
|
717
|
+
if (block !== undefined) return { action: 'skip', reason: block, ...notes }
|
|
718
|
+
if (reading.updatedInput !== undefined) {
|
|
719
|
+
return {
|
|
720
|
+
action: 'modify',
|
|
721
|
+
input: reading.updatedInput,
|
|
722
|
+
...(reading.permission ? { permission: reading.permission } : {}),
|
|
723
|
+
...notes,
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
action: 'continue',
|
|
728
|
+
...(reading.permission ? { permission: reading.permission } : {}),
|
|
729
|
+
...notes,
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
case 'user_prompt_submit': {
|
|
733
|
+
const block = reading.block ?? reading.halt?.reason
|
|
734
|
+
if (block !== undefined) return { action: 'skip', reason: block }
|
|
735
|
+
return reading.context !== undefined
|
|
736
|
+
? { action: 'annotate', text: reading.context }
|
|
737
|
+
: { action: 'continue' }
|
|
738
|
+
}
|
|
739
|
+
case 'post_tool_use':
|
|
740
|
+
case 'post_tool_use_failure': {
|
|
741
|
+
// The call has run, so a "block" cannot undo it: its reason is what the
|
|
742
|
+
// model is told about the result.
|
|
743
|
+
const context = [reading.block, reading.context].filter(Boolean).join('\n')
|
|
744
|
+
return {
|
|
745
|
+
action: 'continue',
|
|
746
|
+
...(context.length > 0 ? { context } : {}),
|
|
747
|
+
...(reading.halt ? { halt: reading.halt } : {}),
|
|
748
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
case 'turn_end':
|
|
752
|
+
case 'subagent_stop': {
|
|
753
|
+
// A stop request outranks keeping the turn going.
|
|
754
|
+
if (reading.halt) {
|
|
755
|
+
return {
|
|
756
|
+
action: 'continue',
|
|
757
|
+
halt: reading.halt,
|
|
758
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const feedback = reading.block ?? reading.context
|
|
762
|
+
return feedback !== undefined
|
|
763
|
+
? {
|
|
764
|
+
action: 'skip',
|
|
765
|
+
reason: feedback,
|
|
766
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
767
|
+
}
|
|
768
|
+
: {
|
|
769
|
+
action: 'continue',
|
|
770
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
case 'pre_compact':
|
|
774
|
+
return reading.block !== undefined
|
|
775
|
+
? { action: 'skip', reason: reading.block, ...notes }
|
|
776
|
+
: { action: 'continue', ...notes }
|
|
777
|
+
case 'permission_request': {
|
|
778
|
+
if (reading.block !== undefined) {
|
|
779
|
+
return {
|
|
780
|
+
action: 'skip',
|
|
781
|
+
reason: reading.block,
|
|
782
|
+
...(reading.halt ? { halt: reading.halt } : {}),
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
action: 'continue',
|
|
787
|
+
...(reading.permission ? { permission: reading.permission } : {}),
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
default:
|
|
791
|
+
// Nothing here can be changed by a verdict, but a stop request still
|
|
792
|
+
// can end the turn, and a warning still reaches the operator.
|
|
793
|
+
return {
|
|
794
|
+
action: 'continue',
|
|
795
|
+
...(reading.halt ? { halt: reading.halt } : {}),
|
|
796
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
797
|
+
}
|
|
798
|
+
}
|
|
377
799
|
}
|
|
378
800
|
|
|
379
801
|
export interface ShellHookOptions {
|
|
@@ -389,12 +811,23 @@ export function createShellHook(
|
|
|
389
811
|
options: ShellHookOptions,
|
|
390
812
|
): PluginHookDefinition {
|
|
391
813
|
const log = options.log ?? NOOP_LOGGER
|
|
814
|
+
// `once` is per definition, and a definition is per session: the manager
|
|
815
|
+
// is built for one session and its hooks are attached to it.
|
|
816
|
+
let spent = false
|
|
392
817
|
// The shell verdict branches on `event`, but TypeScript cannot retain that
|
|
393
818
|
// correlation across the async handler closure and the union of events.
|
|
394
819
|
return {
|
|
395
820
|
event,
|
|
821
|
+
...(entry.statusMessage !== undefined ? { statusMessage: entry.statusMessage } : {}),
|
|
396
822
|
handler: async (context: PluginHookContext) => {
|
|
397
|
-
if (
|
|
823
|
+
if (spent) return { action: 'continue' }
|
|
824
|
+
// A notification is matched on its type; every other event on the tool.
|
|
825
|
+
const subject = event === 'notification' ? context.notification?.type : context.toolName
|
|
826
|
+
if (!shellHookMatches(entry.matcher, subject)) return { action: 'continue' }
|
|
827
|
+
if (!shellHookIfMatches(entry.if, context.toolName, context.toolInput)) {
|
|
828
|
+
return { action: 'continue' }
|
|
829
|
+
}
|
|
830
|
+
if (entry.once === true) spent = true
|
|
398
831
|
const outcome = await runShellHook(
|
|
399
832
|
entry,
|
|
400
833
|
{
|
|
@@ -423,6 +856,8 @@ export function createShellHook(
|
|
|
423
856
|
},
|
|
424
857
|
}
|
|
425
858
|
: {}),
|
|
859
|
+
...(context.notification !== undefined ? { notification: context.notification } : {}),
|
|
860
|
+
...(context.stopHookActive === true ? { stopHookActive: true } : {}),
|
|
426
861
|
},
|
|
427
862
|
context.signal,
|
|
428
863
|
)
|
|
@@ -433,9 +868,8 @@ export function createShellHook(
|
|
|
433
868
|
|
|
434
869
|
/**
|
|
435
870
|
* Register every entry on the manager, in file order within an event. The
|
|
436
|
-
* manager
|
|
437
|
-
*
|
|
438
|
-
* attached.
|
|
871
|
+
* manager starts all of an event's hooks together and reads their answers in
|
|
872
|
+
* that order. Returns how many were attached.
|
|
439
873
|
*
|
|
440
874
|
* Throws, before registering anything, when the config names a renamed event
|
|
441
875
|
* (`run_start` and the rest); the message names the event that replaced it.
|
|
@@ -65,6 +65,23 @@ export function resolveProviderCapabilities(
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether a request for `model` through `provider` may carry images.
|
|
70
|
+
*
|
|
71
|
+
* False when the driver cannot map them at all, or when the driver's own
|
|
72
|
+
* catalogue says this model is text-only. Anything the driver does not know is
|
|
73
|
+
* assumed to read images: a wrong "no" silently blinds a vision model, while a
|
|
74
|
+
* wrong "yes" is recovered from when the provider refuses the request.
|
|
75
|
+
*/
|
|
76
|
+
export function modelReadsImages(
|
|
77
|
+
provider: Pick<LLMProvider, 'capabilities' | 'supportsImageInputFor'>,
|
|
78
|
+
model: string | undefined,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (provider.capabilities?.supportsVision === false) return false
|
|
81
|
+
if (model === undefined || model === '') return true
|
|
82
|
+
return provider.supportsImageInputFor?.(model) ?? true
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
/** Require explicit wire support before dispatching a native JSON Schema request. */
|
|
69
86
|
export function assertNativeStructuredOutputSupported(
|
|
70
87
|
provider: Pick<LLMProvider, 'id' | 'capabilities'>,
|