@sema-agent/server 7.131.0 → 7.133.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/MIGRATION.md +575 -0
- package/README.md +12 -9
- package/README.zh-CN.md +9 -6
- package/USAGE.md +326 -98
- package/deploy/sema-up/README.md +19 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -2
- package/deploy/sema-up/chart/templates/server.yaml +5 -2
- package/deploy/sema-up/stack/compose.yaml +3 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
- package/dist/adoption/plan.d.ts +3 -2
- package/dist/adoption/plan.js +7 -5
- package/dist/adoption/runner.js +3 -10
- package/dist/adoption/sql.d.ts +3 -2
- package/dist/adoption/sql.js +3 -5
- package/dist/adoption/wire.d.ts +4 -1
- package/dist/adoption/wire.js +1 -0
- package/dist/approval-ask-audit-store.d.ts +31 -13
- package/dist/approval-ask-audit-store.js +272 -74
- package/dist/approval-ask-machine.d.ts +15 -0
- package/dist/approval-ask-machine.js +6 -0
- package/dist/approval-ask-retention.d.ts +136 -0
- package/dist/approval-ask-retention.js +102 -0
- package/dist/approval-card.d.ts +82 -20
- package/dist/approval-card.js +46 -16
- package/dist/approval-deny-reasons.d.ts +1 -1
- package/dist/approval.d.ts +9 -0
- package/dist/approval.js +24 -3
- package/dist/audit.d.ts +33 -19
- package/dist/audit.js +12 -16
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/bake-runner/main.js +3 -2
- package/dist/boot/deferred-sandbox-path-env.js +1 -1
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +27 -12
- package/dist/boot/execution-lane.js +11 -0
- package/dist/boot/leader.js +2 -0
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-boundary.js +2 -2
- package/dist/boot/memory-consolidation.d.ts +5 -4
- package/dist/boot/memory-consolidation.js +7 -6
- package/dist/boot/org-delivery.d.ts +28 -0
- package/dist/boot/org-delivery.js +20 -0
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +21 -1
- package/dist/boot/resolve-spec.d.ts +4 -5
- package/dist/boot/resolve-spec.js +74 -123
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +15 -2
- package/dist/boot/session-faces.d.ts +2 -7
- package/dist/boot/session-faces.js +4 -2
- package/dist/boot/stage-02-web-admin-pull.js +4 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +4 -2
- package/dist/boot/stage-06-runners.js +11 -7
- package/dist/boot/stage-07-capability-layer.d.ts +4 -2
- package/dist/boot/stage-07-capability-layer.js +27 -6
- package/dist/boot/stage-08-reapers.d.ts +4 -2
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +6 -9
- package/dist/boot/stage-10-http-server.d.ts +6 -9
- package/dist/boot/stage-10-http-server.js +7 -9
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +7 -8
- package/dist/boot/web-admin.d.ts +4 -3
- package/dist/boot/web-admin.js +186 -98
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +2 -1
- package/dist/brain.js +25 -25
- package/dist/breaker-key.d.ts +6 -0
- package/dist/breaker-key.js +29 -0
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +23 -0
- package/dist/caller-text-echo.js +21 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/center-plugins.d.ts +7 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/execution-lane.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +30 -6
- package/dist/capabilities/hands-lane.js +11 -1
- package/dist/capabilities/mcp-probe.d.ts +48 -3
- package/dist/capabilities/mcp-probe.js +47 -135
- package/dist/capabilities/scenarios.d.ts +95 -45
- package/dist/capabilities/scenarios.js +53 -38
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/capability-facts.d.ts +34 -4
- package/dist/capability-facts.js +28 -2
- package/dist/center-config-tier.d.ts +69 -0
- package/dist/center-config-tier.js +42 -0
- package/dist/checkpoint-identity.d.ts +16 -0
- package/dist/checkpoint-identity.js +19 -0
- package/dist/config-catalog.d.ts +17 -10
- package/dist/config-catalog.js +47 -32
- package/dist/config-d-migration.d.ts +3 -1
- package/dist/config-d-migration.js +2 -2
- package/dist/config-dir-trust.d.ts +114 -0
- package/dist/config-dir-trust.js +388 -0
- package/dist/config-invariants.d.ts +18 -5
- package/dist/config-invariants.js +35 -3
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +9 -2
- package/dist/config-provider.js +18 -6
- package/dist/config-types.d.ts +75 -20
- package/dist/config-types.js +8 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +137 -62
- package/dist/delegated-session-owner.d.ts +185 -0
- package/dist/delegated-session-owner.js +119 -0
- package/dist/device-protocol.d.ts +5 -3
- package/dist/device-protocol.js +1 -1
- package/dist/device-ws-hub.js +21 -2
- package/dist/device-ws-protocol.d.ts +70 -6
- package/dist/device-ws-protocol.js +50 -22
- package/dist/elicitation.js +3 -2
- package/dist/entry-problems.d.ts +34 -0
- package/dist/entry-problems.js +14 -0
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +56 -10
- package/dist/execution-lane-caps.js +50 -0
- package/dist/field-conflict.d.ts +43 -0
- package/dist/field-conflict.js +20 -0
- package/dist/fleet/fleet-bus.d.ts +80 -6
- package/dist/fleet/fleet-bus.js +41 -9
- package/dist/hooks/classic-stations.d.ts +91 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/command-spawn.d.ts +57 -0
- package/dist/hooks/command-spawn.js +139 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +79 -36
- package/dist/hooks/hook-runner.d.ts +349 -95
- package/dist/hooks/hook-runner.js +855 -568
- package/dist/hooks/hook-source.d.ts +44 -0
- package/dist/hooks/hook-source.js +40 -0
- package/dist/http/active-run-conflict.d.ts +5 -13
- package/dist/http/active-run-conflict.js +17 -27
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +188 -101
- package/dist/http/approval-sweeps.js +99 -21
- package/dist/http/caller-trust.d.ts +77 -0
- package/dist/http/caller-trust.js +143 -0
- package/dist/http/checkpoint-ctx.d.ts +41 -24
- package/dist/http/checkpoint-ctx.js +23 -16
- package/dist/http/claimed-run.d.ts +14 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.d.ts +2 -4
- package/dist/http/device-placement.js +7 -11
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +15 -13
- package/dist/http/durable-run-leg.d.ts +6 -33
- package/dist/http/durable-run-leg.js +36 -10
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/owner-access.d.ts +56 -0
- package/dist/http/owner-access.js +42 -0
- package/dist/http/principal-gate.d.ts +70 -28
- package/dist/http/principal-gate.js +61 -15
- package/dist/http/readiness.d.ts +11 -5
- package/dist/http/readiness.js +10 -3
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +43 -23
- package/dist/http/request-guards.js +53 -17
- package/dist/http/resume-ctx-read.d.ts +2 -5
- package/dist/http/resume-ctx-read.js +7 -4
- package/dist/http/resume-legs.d.ts +28 -4
- package/dist/http/resume-legs.js +169 -91
- package/dist/http/retired-service-deps.d.ts +43 -0
- package/dist/http/retired-service-deps.js +18 -0
- package/dist/http/route-ctx.d.ts +44 -28
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/a2a-serve.js +3 -4
- package/dist/http/routes/admin-config-refresh.js +1 -1
- package/dist/http/routes/admin-drain.js +1 -1
- package/dist/http/routes/admin-memory-optout.js +5 -1
- package/dist/http/routes/adoption.js +2 -9
- package/dist/http/routes/approvals-assistant.js +49 -48
- package/dist/http/routes/background.js +3 -2
- package/dist/http/routes/capabilities.js +21 -5
- package/dist/http/routes/config-catalog.d.ts +3 -3
- package/dist/http/routes/config-catalog.js +1 -1
- package/dist/http/routes/devices.js +3 -17
- package/dist/http/routes/diagnostics.js +6 -2
- package/dist/http/routes/images.js +6 -7
- package/dist/http/routes/leader.js +1 -8
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +3 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/memory-policy.d.ts +1 -1
- package/dist/http/routes/memory-policy.js +3 -2
- package/dist/http/routes/notify-wake.js +8 -21
- package/dist/http/routes/observability.js +2 -2
- package/dist/http/routes/principal-inbox.d.ts +39 -0
- package/dist/http/routes/principal-inbox.js +76 -0
- package/dist/http/routes/retention-ops.js +2 -9
- package/dist/http/routes/rules.js +5 -18
- package/dist/http/routes/runs.d.ts +5 -0
- package/dist/http/routes/runs.js +34 -107
- package/dist/http/routes/session-sync.js +15 -26
- package/dist/http/routes/sessions.js +30 -40
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +8 -7
- package/dist/http/routes/tasks.js +67 -21
- package/dist/http/routes/workflows.d.ts +351 -0
- package/dist/http/routes/workflows.js +153 -119
- package/dist/http/send.d.ts +33 -10
- package/dist/http/send.js +21 -4
- package/dist/http/server.d.ts +27 -18
- package/dist/http/server.js +34 -6
- package/dist/http/stored-body-replay.js +17 -4
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +40 -0
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +5 -4
- package/dist/http/wire-types.d.ts +11 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/dist/lane-home-probe.d.ts +112 -0
- package/dist/lane-home-probe.js +101 -0
- package/dist/leader/endpoint.d.ts +4 -1
- package/dist/leader/endpoint.js +11 -10
- package/dist/limit-refusal.d.ts +59 -0
- package/dist/limit-refusal.js +15 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +32 -19
- package/dist/mcp-assertion-header.js +10 -9
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +6 -4
- package/dist/memory-project-layer.d.ts +37 -0
- package/dist/memory-project-layer.js +46 -0
- package/dist/memory-scope.d.ts +24 -17
- package/dist/memory-scope.js +24 -8
- package/dist/memory-sync-client.d.ts +27 -13
- package/dist/memory-sync-client.js +107 -18
- package/dist/model-entry-refusal.d.ts +9 -1
- package/dist/model-entry-refusal.js +6 -1
- package/dist/model-plane-face.d.ts +51 -0
- package/dist/model-plane-face.js +24 -0
- package/dist/model-provider.d.ts +44 -12
- package/dist/model-provider.js +21 -2
- package/dist/model-route-endpoint.d.ts +9 -12
- package/dist/model-route-endpoint.js +8 -11
- package/dist/model-route-env.d.ts +22 -0
- package/dist/model-route-env.js +2 -0
- package/dist/model-route-gap.d.ts +60 -30
- package/dist/model-route-gap.js +21 -15
- package/dist/observability/corrupt-read-seat.d.ts +7 -0
- package/dist/observability/corrupt-read-seat.js +9 -0
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +18 -4
- package/dist/observability/fail-open.d.ts +76 -14
- package/dist/observability/fail-open.js +76 -14
- package/dist/observability/metrics.js +4 -1
- package/dist/observability/prompt-manifest.d.ts +33 -82
- package/dist/observability/prompt-manifest.js +122 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +58 -49
- package/dist/operator-ask.d.ts +10 -0
- package/dist/operator-ask.js +3 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
- package/dist/orchestration/workflow-agent-session-index.js +9 -13
- package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
- package/dist/orchestration/workflow-completion-inbox.js +13 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
- package/dist/orchestration/workflow-notify-journal.js +27 -34
- package/dist/orchestration/workflow-park-decision.d.ts +89 -0
- package/dist/orchestration/workflow-park-decision.js +118 -0
- package/dist/org-delivery-gate.d.ts +81 -0
- package/dist/org-delivery-gate.js +59 -0
- package/dist/panel-tool-names.d.ts +23 -0
- package/dist/panel-tool-names.js +8 -0
- package/dist/parked-decide.d.ts +39 -6
- package/dist/parked-decide.js +98 -51
- package/dist/parked-revive-surface.d.ts +9 -2
- package/dist/parked-revive-surface.js +11 -8
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/permission-rule-list.d.ts +1 -1
- package/dist/permission-rule-list.js +2 -2
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/approval-ask-store-file.d.ts +16 -1
- package/dist/plugins/approval-ask-store-file.js +21 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
- package/dist/plugins/approval-ask-store-memory.js +34 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
- package/dist/plugins/approval-ask-store-sql.js +37 -1
- package/dist/plugins/blob-backend.d.ts +83 -15
- package/dist/plugins/blob-backend.js +68 -15
- package/dist/plugins/breaker-state-sql.d.ts +2 -2
- package/dist/plugins/breaker-state-sql.js +4 -4
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +28 -4
- package/dist/plugins/checkpoint-store-sql.js +59 -28
- package/dist/plugins/file-history-store-sql.d.ts +5 -2
- package/dist/plugins/file-history-store-sql.js +3 -1
- package/dist/plugins/file-run-store.d.ts +5 -13
- package/dist/plugins/file-run-store.js +9 -0
- package/dist/plugins/fork-routing-session-store.d.ts +9 -1
- package/dist/plugins/fork-routing-session-store.js +12 -1
- package/dist/plugins/host-platform.d.ts +2 -0
- package/dist/plugins/host-platform.js +2 -1
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.d.ts +2 -0
- package/dist/plugins/local-checkpoint-store.js +11 -3
- package/dist/plugins/local-session-store.d.ts +1 -1
- package/dist/plugins/local-session-store.js +1 -1
- package/dist/plugins/local-session-window.d.ts +39 -0
- package/dist/plugins/local-session-window.js +54 -0
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
- package/dist/plugins/mailbox-store-sql.d.ts +30 -48
- package/dist/plugins/mailbox-store-sql.js +77 -52
- package/dist/plugins/memory-embedder-fingerprint.js +2 -1
- package/dist/plugins/memory-run-store.d.ts +5 -13
- package/dist/plugins/memory-run-store.js +8 -0
- package/dist/plugins/one-time-migrations.d.ts +245 -93
- package/dist/plugins/one-time-migrations.js +668 -207
- package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
- package/dist/plugins/org-delivery-quota-store.js +228 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-pool.js +39 -7
- package/dist/plugins/pg-session-storage.d.ts +20 -8
- package/dist/plugins/pg-session-storage.js +34 -21
- package/dist/plugins/remote-env-adb.d.ts +12 -12
- package/dist/plugins/remote-env-adb.js +21 -6
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +103 -38
- package/dist/plugins/remote-env-k8s.js +288 -95
- package/dist/plugins/remote-env-local-docker.d.ts +27 -13
- package/dist/plugins/remote-env-local-docker.js +82 -11
- package/dist/plugins/remote-env-ssh.d.ts +21 -12
- package/dist/plugins/remote-env-ssh.js +117 -5
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/run-store-sql.d.ts +16 -14
- package/dist/plugins/run-store-sql.js +22 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/send-user-file.js +2 -2
- package/dist/plugins/session-epoch-anchor.d.ts +39 -0
- package/dist/plugins/session-epoch-anchor.js +22 -0
- package/dist/plugins/session-store.d.ts +18 -8
- package/dist/plugins/session-store.js +15 -4
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +205 -1
- package/dist/plugins/sql-driver.js +325 -13
- package/dist/plugins/sql-errors.d.ts +64 -0
- package/dist/plugins/sql-errors.js +57 -0
- package/dist/plugins/store-backend.d.ts +36 -1
- package/dist/plugins/store-backend.js +42 -15
- package/dist/plugins/store-contracts.d.ts +42 -0
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/tidb-pool.d.ts +6 -1
- package/dist/plugins/tidb-pool.js +44 -10
- package/dist/plugins/tidb-session-storage.d.ts +8 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.d.ts +24 -4
- package/dist/plugins/tidb-session-store.js +29 -5
- package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
- package/dist/plugins/workflow-journal-store-sql.js +19 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
- package/dist/plugins/workflow-run-store-sql.js +50 -9
- package/dist/posture-source.d.ts +18 -10
- package/dist/posture-source.js +4 -1
- package/dist/project-identity.d.ts +45 -6
- package/dist/project-identity.js +162 -8
- package/dist/project-layer-key.d.ts +28 -0
- package/dist/project-layer-key.js +29 -0
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/question.js +3 -2
- package/dist/read-deny-entry.d.ts +25 -0
- package/dist/read-deny-entry.js +12 -0
- package/dist/read-face-posture.d.ts +37 -11
- package/dist/read-face-posture.js +32 -3
- package/dist/request-key-closure.d.ts +9 -2
- package/dist/request-key-closure.js +11 -5
- package/dist/request-value-closure.d.ts +5 -0
- package/dist/request-value-closure.js +11 -3
- package/dist/rule-entry-list.d.ts +38 -11
- package/dist/rule-entry-list.js +106 -30
- package/dist/rules-consent.js +3 -4
- package/dist/run-acceptance.d.ts +19 -3
- package/dist/run-acceptance.js +13 -1
- package/dist/run-local.js +54 -24
- package/dist/runs.d.ts +15 -8
- package/dist/runs.js +35 -1
- package/dist/runtime-caps-resolver.d.ts +16 -6
- package/dist/runtime-caps-resolver.js +18 -5
- package/dist/sandbox-pkg-source.d.ts +19 -1
- package/dist/sandbox-pkg-source.js +29 -7
- package/dist/security.d.ts +87 -9
- package/dist/security.js +52 -9
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-error-wire.d.ts +19 -2
- package/dist/session-error-wire.js +5 -1
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titled-frame.d.ts +33 -0
- package/dist/session-titled-frame.js +4 -0
- package/dist/session-titler.d.ts +9 -1
- package/dist/session-titler.js +27 -6
- package/dist/spec-fields.d.ts +28 -16
- package/dist/spec-fields.js +66 -54
- package/dist/sql-engine-posture.d.ts +31 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-tool-axes-m19.d.ts +19 -4
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +46 -58
- package/dist/task-mcp.js +82 -107
- package/dist/task-settings.d.ts +95 -108
- package/dist/task-settings.js +77 -98
- package/dist/text-bidi.d.ts +1 -48
- package/dist/text-bidi.js +3 -5
- package/dist/tighten-only-seats.d.ts +150 -0
- package/dist/tighten-only-seats.js +196 -0
- package/dist/tool-approval.d.ts +304 -88
- package/dist/tool-approval.js +623 -239
- package/dist/trace/core-keyset-guard.d.ts +122 -20
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/exit-seat.d.ts +28 -0
- package/dist/trace/exit-seat.js +76 -0
- package/dist/trace/ledger-events.d.ts +6 -3
- package/dist/trace/ledger-sink.js +4 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +98 -17
- package/dist/trace/project.js +98 -50
- package/dist/trace/projection-table.d.ts +100 -0
- package/dist/trace/projection-table.js +130 -0
- package/dist/trace/redact.d.ts +54 -0
- package/dist/trace/redact.js +778 -152
- package/dist/trace/self-minted-wire.d.ts +345 -1
- package/dist/trace/self-minted-wire.js +117 -1
- package/dist/trace/wire-projection-faces.d.ts +377 -3
- package/dist/trace/wire-projection-faces.js +164 -1
- package/dist/url-refusal.js +2 -0
- package/dist/web-admin-client/apply-effective.d.ts +12 -0
- package/dist/web-admin-client/apply-effective.js +56 -5
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-face.js +5 -9
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +11 -9
- package/dist/web-admin-client/types.d.ts +2 -3
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +65 -10
- package/package.json +8 -4
- package/scripts/drain-check-local.mjs +34 -1
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
|
|
2
3
|
import vm from "node:vm";
|
|
3
|
-
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
|
+
import { hostShell, resolveHostShell, adoptHostForegroundChild, killTreeSoft, killTreeHard } from "../plugins/host-platform.js";
|
|
4
5
|
import { classifyHooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_ENTRY_TYPES, HOOK_EVENTS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
|
|
5
6
|
import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
|
|
8
|
+
import { describeToolCatalog } from "@sema-agent/core";
|
|
9
|
+
import { hangClassic } from "./classic-stations.js";
|
|
10
|
+
import { MAX_HOOK_NOTICE_TEXT_CHARS, boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
8
11
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
12
|
+
import { LIMIT_REASONS } from "../limit-refusal.js";
|
|
13
|
+
import { clipDataText, echoCallerText } from "../caller-text-echo.js";
|
|
9
14
|
import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
|
|
15
|
+
import { canonicalJson } from "../canonical-json.js";
|
|
10
16
|
import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
|
|
11
17
|
import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
|
|
12
18
|
import { renderBranchTranscript } from "./branch-transcript.js";
|
|
19
|
+
import { HOOK_PHASES, HOOK_SOURCE_VOCABULARY_SENTENCE, hookPhaseOf, readHookGroupSource } from "./hook-source.js";
|
|
20
|
+
import { commandLineChars, composeHookEnvLayer, hookPluginChars, planCommandSpawn, readHookArgs, readHookPlugin } from "./command-spawn.js";
|
|
13
21
|
export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
|
|
14
22
|
export const MAX_HOOK_COMMAND_CHARS = 8_192;
|
|
15
23
|
export const MAX_HOOK_TIMEOUT_SECONDS = 600;
|
|
@@ -26,26 +34,41 @@ const HOOK_ENV_ALLOWLIST = [
|
|
|
26
34
|
"PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
|
|
27
35
|
"LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
|
|
28
36
|
];
|
|
37
|
+
function hookBaseEnv(spawning) {
|
|
38
|
+
return spawning
|
|
39
|
+
? hostSubprocessEnv({ site: "server.hooks.spawn-env", policy: [...HOOK_ENV_ALLOWLIST], allow: [] })
|
|
40
|
+
: composeHostSubprocessEnv({ policy: [...HOOK_ENV_ALLOWLIST], allow: [] });
|
|
41
|
+
}
|
|
42
|
+
function hookEnvOverBase(layer, spawning) {
|
|
43
|
+
return { ...hookBaseEnv(spawning), ...layer };
|
|
44
|
+
}
|
|
29
45
|
function hookEnv(ctx) {
|
|
30
|
-
|
|
31
|
-
for (const k of HOOK_ENV_ALLOWLIST) {
|
|
32
|
-
const v = process.env[k];
|
|
33
|
-
if (typeof v === "string")
|
|
34
|
-
env[k] = v;
|
|
35
|
-
}
|
|
36
|
-
return { ...env, ...ctx.shellEnv };
|
|
46
|
+
return hookEnvOverBase(composeHookEnvLayer(ctx.shellEnv, ctx.cwd, undefined), false);
|
|
37
47
|
}
|
|
38
|
-
const
|
|
39
|
-
"PreToolUse",
|
|
40
|
-
"PostToolUse",
|
|
41
|
-
"PostToolUseFailure",
|
|
42
|
-
"PostToolBatch",
|
|
43
|
-
"UserPromptSubmit",
|
|
44
|
-
"Stop",
|
|
45
|
-
"StopFailure",
|
|
46
|
-
"PreCompact",
|
|
47
|
-
"PostCompact",
|
|
48
|
-
|
|
48
|
+
export const SETTINGS_HOOK_STATIONS = {
|
|
49
|
+
PreToolUse: ["classic.PreToolUse"],
|
|
50
|
+
PostToolUse: ["classic.PostToolUse"],
|
|
51
|
+
PostToolUseFailure: ["classic.PostToolUseFailure"],
|
|
52
|
+
PostToolBatch: ["classic.PostToolBatch"],
|
|
53
|
+
UserPromptSubmit: ["classic.UserPromptSubmit"],
|
|
54
|
+
Stop: ["classic.Stop", "classic.SubagentStop"],
|
|
55
|
+
StopFailure: ["classic.StopFailure"],
|
|
56
|
+
PreCompact: ["classic.PreCompact"],
|
|
57
|
+
PostCompact: ["classic.PostCompact"],
|
|
58
|
+
};
|
|
59
|
+
function hangTranslator(on, event, translators) {
|
|
60
|
+
const translate = translators[event];
|
|
61
|
+
if (translate === undefined)
|
|
62
|
+
return;
|
|
63
|
+
for (const station of SETTINGS_HOOK_STATIONS[event])
|
|
64
|
+
hangClassic(on, station, translate);
|
|
65
|
+
}
|
|
66
|
+
export const SETTINGS_HOOKS_REGISTRATION = "settings.hooks";
|
|
67
|
+
export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
|
|
68
|
+
export function settingsHooksBudgetMs(config) {
|
|
69
|
+
return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
|
|
70
|
+
}
|
|
71
|
+
export const HOOK_KILL_GRACE_MS = 1_000;
|
|
49
72
|
const BRAIN_TO_TURN_ERROR = {
|
|
50
73
|
auth: "authentication_failed",
|
|
51
74
|
rate_limit: "rate_limit",
|
|
@@ -76,8 +99,12 @@ function isRunnableHookEntry(entry) {
|
|
|
76
99
|
return isSupportedHookEntryType(entry.type);
|
|
77
100
|
}
|
|
78
101
|
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
79
|
-
export const
|
|
102
|
+
export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
|
|
80
103
|
export const MATCHER_ECHO_CHARS = 200;
|
|
104
|
+
export const IF_ECHO_CHARS = 200;
|
|
105
|
+
const LOG_PREVIEW_ECHO_CHARS = 200;
|
|
106
|
+
const LOG_ERROR_ECHO_CHARS = 300;
|
|
107
|
+
const LOG_MESSAGE_ECHO_CHARS = 500;
|
|
81
108
|
const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
|
|
82
109
|
function matcherReadsWireToolName(event) {
|
|
83
110
|
return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
|
|
@@ -130,7 +157,11 @@ const MATCHER_EVALUATION = {
|
|
|
130
157
|
regex: { regex: "budgeted", literal: "not_applicable" },
|
|
131
158
|
invalid: { regex: "unreadable", literal: "not_applicable" },
|
|
132
159
|
};
|
|
133
|
-
|
|
160
|
+
const RUNTIME_EVAL_DEPTH = "regex";
|
|
161
|
+
function matcherSelectsBy(evaluation, matcher, name, budgetMs) {
|
|
162
|
+
return evaluation === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs);
|
|
163
|
+
}
|
|
164
|
+
function judgeMatcher(matcher, depth, ledger, judgesAlias) {
|
|
134
165
|
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
135
166
|
return { kind: "unjudged", reason: "oversize" };
|
|
136
167
|
const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
|
|
@@ -141,14 +172,34 @@ function judgeMatcher(matcher, depth, budgetMs) {
|
|
|
141
172
|
return { kind: "invalid_regex" };
|
|
142
173
|
case "inline":
|
|
143
174
|
case "budgeted":
|
|
175
|
+
if (!judgesAlias)
|
|
176
|
+
return undefined;
|
|
144
177
|
break;
|
|
145
178
|
default: {
|
|
146
179
|
const unreachable = evaluate;
|
|
147
180
|
return unreachable;
|
|
148
181
|
}
|
|
149
182
|
}
|
|
150
|
-
const selects = (name) =>
|
|
151
|
-
|
|
183
|
+
const selects = (name) => {
|
|
184
|
+
if (evaluate === "inline")
|
|
185
|
+
return matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
|
|
186
|
+
const startedAt = ledger.now();
|
|
187
|
+
const answer = matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
|
|
188
|
+
ledger.spentMs += ledger.now() - startedAt;
|
|
189
|
+
if (answer === "over_budget" || answer === "invalid")
|
|
190
|
+
return answer;
|
|
191
|
+
return ledger.spentMs > ledger.totalMs ? "budget_exceeded" : answer;
|
|
192
|
+
};
|
|
193
|
+
const unanswered = (verdict) => {
|
|
194
|
+
switch (verdict) {
|
|
195
|
+
case "over_budget":
|
|
196
|
+
return { kind: "over_budget" };
|
|
197
|
+
case "budget_exceeded":
|
|
198
|
+
return { kind: "budget_exceeded" };
|
|
199
|
+
case "invalid":
|
|
200
|
+
return { kind: "invalid_regex" };
|
|
201
|
+
}
|
|
202
|
+
};
|
|
152
203
|
for (const [alias, canonical] of engineToolAliases()) {
|
|
153
204
|
const selectsAlias = selects(alias);
|
|
154
205
|
if (typeof selectsAlias === "string")
|
|
@@ -163,50 +214,111 @@ function judgeMatcher(matcher, depth, budgetMs) {
|
|
|
163
214
|
}
|
|
164
215
|
return undefined;
|
|
165
216
|
}
|
|
166
|
-
|
|
217
|
+
const HOOKS_CAPS = {
|
|
218
|
+
matcher_groups: { measure: "entries", cap: MAX_HOOK_MATCHER_GROUPS, sentence: (event, count) => `settings.hooks.${event}: ${count} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` },
|
|
219
|
+
event_entries: { measure: "entries", cap: MAX_HOOK_ENTRIES_PER_EVENT, sentence: (event, count) => `settings.hooks.${event}: ${count} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` },
|
|
220
|
+
command: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
|
|
221
|
+
url: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
|
|
222
|
+
headers: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event, count) => `settings.hooks.${event}: ${count} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
|
|
223
|
+
header_chars: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` },
|
|
224
|
+
allowed_env_vars: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event) => `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
|
|
225
|
+
plugin: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: plugin context exceeds ${MAX_HOOK_COMMAND_CHARS} chars total` },
|
|
226
|
+
};
|
|
227
|
+
const HOOKS_WIRE_KEY_SHAPE = {
|
|
228
|
+
source: `source must be one of ${HOOK_SOURCE_VOCABULARY_SENTENCE}`,
|
|
229
|
+
plugin: "plugin must be { name, root, dataDir?, options? } — name a non-empty string, root and dataDir absolute paths with no \"..\" segment and no NUL byte, " +
|
|
230
|
+
"options an object of string values whose keys are identifiers ([A-Za-z_][A-Za-z0-9_]*); nothing else",
|
|
231
|
+
args: "args must be an array of strings without NUL bytes (exec form: command is the executable, args its arguments)",
|
|
232
|
+
};
|
|
233
|
+
function hooksCapIssue(limit, event, path, count) {
|
|
234
|
+
const c = HOOKS_CAPS[limit];
|
|
235
|
+
return { reason: LIMIT_REASONS[c.measure], limit, event, path, count, cap: c.cap };
|
|
236
|
+
}
|
|
237
|
+
const matcherPath = (event, groupIndex) => ["hooks", event, groupIndex, "matcher"];
|
|
238
|
+
export function buildHooksRefusal(issue) {
|
|
239
|
+
const field = "settings.hooks";
|
|
167
240
|
switch (issue.reason) {
|
|
168
241
|
case "dead_alias":
|
|
169
242
|
return {
|
|
170
243
|
message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
|
|
171
|
-
field
|
|
172
|
-
detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
|
|
244
|
+
field,
|
|
245
|
+
detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical, path: matcherPath(issue.event, issue.groupIndex) },
|
|
173
246
|
};
|
|
174
247
|
case "over_budget":
|
|
175
248
|
return {
|
|
176
249
|
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
|
|
177
|
-
field
|
|
178
|
-
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
|
|
250
|
+
field,
|
|
251
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs, path: matcherPath(issue.event, issue.groupIndex) },
|
|
252
|
+
};
|
|
253
|
+
case "budget_exceeded":
|
|
254
|
+
return {
|
|
255
|
+
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating this configuration's regular-expression matchers took longer than ${issue.totalMs} ms in total (very slow patterns) — write tool names as a list such as "Read|Edit", or simplify the patterns`,
|
|
256
|
+
field,
|
|
257
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, totalMs: issue.totalMs, path: matcherPath(issue.event, issue.groupIndex) },
|
|
179
258
|
};
|
|
180
259
|
case "invalid_regex":
|
|
181
260
|
return {
|
|
182
261
|
message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
|
|
183
|
-
field
|
|
184
|
-
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
|
|
262
|
+
field,
|
|
263
|
+
detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, path: matcherPath(issue.event, issue.groupIndex) },
|
|
264
|
+
};
|
|
265
|
+
case "unsupported_entry_type":
|
|
266
|
+
return {
|
|
267
|
+
message: `${hooksCoordinate(issue.path)}: this server does not run hook entries of type ${issue.type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`,
|
|
268
|
+
field,
|
|
269
|
+
detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), type: issue.type },
|
|
185
270
|
};
|
|
271
|
+
case "over_cap":
|
|
272
|
+
case "too_long": {
|
|
273
|
+
const cap = issue;
|
|
274
|
+
return { message: HOOKS_CAPS[cap.limit].sentence(cap.event, cap.count), field, detail: { reason: cap.reason, path: hooksOutgoingPath(cap.path), count: cap.count, cap: cap.cap } };
|
|
275
|
+
}
|
|
276
|
+
case "unnamed_refusal":
|
|
277
|
+
return { message: `${hooksCoordinate(issue.path)}: the hooks reader refused this configuration without naming a piece`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path) } };
|
|
278
|
+
case "wire_key_invalid":
|
|
279
|
+
return { message: `${hooksCoordinate(issue.path)}: ${HOOKS_WIRE_KEY_SHAPE[issue.key]}`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), key: issue.key } };
|
|
186
280
|
default: {
|
|
187
|
-
const
|
|
188
|
-
|
|
281
|
+
const piece = issue;
|
|
282
|
+
const key = piece.key !== undefined ? hooksOutgoingKey(piece.key) : undefined;
|
|
283
|
+
return {
|
|
284
|
+
message: hooksPieceRefusal(piece),
|
|
285
|
+
field,
|
|
286
|
+
detail: { reason: piece.reason, path: hooksOutgoingPath(piece.path), ...(key !== undefined ? { key } : {}) },
|
|
287
|
+
};
|
|
189
288
|
}
|
|
190
289
|
}
|
|
191
290
|
}
|
|
192
|
-
export function
|
|
193
|
-
const
|
|
291
|
+
export function hooksMatcherIssue(config, depth, budget = {}) {
|
|
292
|
+
const ledger = {
|
|
293
|
+
budgetMs: budget.budgetMs ?? ADMISSION_MATCHER_BUDGET.budgetMs,
|
|
294
|
+
totalMs: budget.totalMs ?? ADMISSION_MATCHER_BUDGET.totalMs,
|
|
295
|
+
now: budget.now ?? (() => performance.now()),
|
|
296
|
+
spentMs: 0,
|
|
297
|
+
};
|
|
298
|
+
const budgetMs = ledger.budgetMs;
|
|
194
299
|
for (const [event, groups] of Object.entries(config)) {
|
|
195
|
-
|
|
196
|
-
continue;
|
|
300
|
+
const judgesAlias = matcherReadsWireToolName(event);
|
|
197
301
|
for (const [groupIndex, group] of (groups ?? []).entries()) {
|
|
198
302
|
if (group.matcher === undefined)
|
|
199
303
|
continue;
|
|
200
|
-
const verdict = judgeMatcher(group.matcher, depth,
|
|
304
|
+
const verdict = judgeMatcher(group.matcher, depth, ledger, judgesAlias);
|
|
201
305
|
if (verdict === undefined)
|
|
202
306
|
continue;
|
|
203
307
|
switch (verdict.kind) {
|
|
204
308
|
case "dead":
|
|
205
|
-
return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
|
|
309
|
+
return { reason: "dead_alias", event, groupIndex, alias: verdict.alias, canonical: verdict.canonical };
|
|
206
310
|
case "over_budget":
|
|
207
311
|
return { reason: "over_budget", event, groupIndex, budgetMs };
|
|
312
|
+
case "budget_exceeded":
|
|
313
|
+
return { reason: "budget_exceeded", event, groupIndex, totalMs: ledger.totalMs };
|
|
208
314
|
case "invalid_regex":
|
|
209
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
reason: "invalid_regex",
|
|
317
|
+
event,
|
|
318
|
+
groupIndex,
|
|
319
|
+
matcher: echoCallerText(group.matcher, { maxChars: MATCHER_ECHO_CHARS }),
|
|
320
|
+
compileError: echoCallerText(matcherCompileSentence(group.matcher), { maxChars: MATCHER_ECHO_CHARS }),
|
|
321
|
+
};
|
|
210
322
|
case "unjudged":
|
|
211
323
|
recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
|
|
212
324
|
break;
|
|
@@ -232,80 +344,121 @@ const HOOKS_PIECE_MEANING = {
|
|
|
232
344
|
"guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
|
|
233
345
|
"guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
|
|
234
346
|
};
|
|
347
|
+
export function hooksOutgoingPath(path) {
|
|
348
|
+
return outgoingPath(path).map((seg) => (typeof seg === "string" ? echoCallerText(seg) : seg));
|
|
349
|
+
}
|
|
235
350
|
export function hooksCoordinate(path) {
|
|
236
|
-
return `settings.${formatConfigPath(
|
|
351
|
+
return `settings.${formatConfigPath(hooksOutgoingPath(path))}`;
|
|
352
|
+
}
|
|
353
|
+
function hooksOutgoingKey(key) {
|
|
354
|
+
return echoCallerText(String(outgoingPath([key])[0] ?? ""));
|
|
355
|
+
}
|
|
356
|
+
function hooksPieceIssue(piece) {
|
|
357
|
+
return piece.field !== undefined ? { reason: piece.code, path: piece.path, key: piece.field } : { reason: piece.code, path: piece.path };
|
|
237
358
|
}
|
|
238
359
|
function hooksPieceRefusal(piece) {
|
|
239
|
-
const field = piece.
|
|
240
|
-
return `${hooksCoordinate(piece.path)}: ${piece.
|
|
360
|
+
const field = piece.key !== undefined ? ` (field: ${hooksOutgoingKey(piece.key)})` : "";
|
|
361
|
+
return `${hooksCoordinate(piece.path)}: ${piece.reason} — ${HOOKS_PIECE_MEANING[piece.reason]}${field}`;
|
|
241
362
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
363
|
+
function hooksRefused(issue, pieces) {
|
|
364
|
+
const error = buildHooksRefusal(issue).message;
|
|
365
|
+
return pieces === undefined ? { issue, error } : { issue, error, pieces };
|
|
244
366
|
}
|
|
245
367
|
export function parseHooksConfig(raw) {
|
|
246
368
|
const classified = classifyHooksConfig({ hooks: raw });
|
|
247
369
|
if (classified.outcome !== "loaded") {
|
|
248
370
|
const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
|
|
249
371
|
const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
|
|
250
|
-
return
|
|
372
|
+
return hooksRefused(first !== undefined ? hooksPieceIssue(first) : { reason: "unnamed_refusal", path: ["hooks"] }, pieces);
|
|
251
373
|
}
|
|
252
374
|
const loaded = classified.hooks;
|
|
253
375
|
if (loaded === undefined) {
|
|
254
|
-
return
|
|
376
|
+
return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
|
|
255
377
|
}
|
|
256
378
|
const config = {};
|
|
379
|
+
const rawMember = (node, key) => node !== null && typeof node === "object" && Object.hasOwn(node, key) ? node[key] : undefined;
|
|
257
380
|
for (const key of Object.keys(loaded)) {
|
|
258
381
|
if (!isHookEventName(key)) {
|
|
259
|
-
return
|
|
382
|
+
return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
|
|
260
383
|
}
|
|
261
384
|
const event = key;
|
|
262
385
|
const groups = loaded[event];
|
|
263
386
|
if (!groups)
|
|
264
387
|
continue;
|
|
265
|
-
if (groups.length > MAX_HOOK_MATCHER_GROUPS)
|
|
266
|
-
return
|
|
267
|
-
}
|
|
388
|
+
if (groups.length > MAX_HOOK_MATCHER_GROUPS)
|
|
389
|
+
return hooksRefused(hooksCapIssue("matcher_groups", event, ["hooks", event], groups.length));
|
|
268
390
|
let entries = 0;
|
|
391
|
+
let firstPastCap;
|
|
269
392
|
const kept = [];
|
|
393
|
+
const rawGroups = rawMember(raw, event);
|
|
270
394
|
for (const [groupIndex, group] of groups.entries()) {
|
|
395
|
+
const rawGroup = rawMember(rawGroups, groupIndex);
|
|
396
|
+
const rawSource = rawMember(rawGroup, "source");
|
|
397
|
+
const source = rawSource === undefined ? undefined : readHookGroupSource(rawSource);
|
|
398
|
+
if (rawSource !== undefined && source === undefined)
|
|
399
|
+
return hooksRefused({ reason: "wire_key_invalid", path: ["hooks", event, groupIndex, "source"], key: "source" });
|
|
271
400
|
const hooks = [];
|
|
272
|
-
for (const [entryIndex,
|
|
401
|
+
for (const [entryIndex, contractEntry] of group.hooks.entries()) {
|
|
273
402
|
entries++;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
return {
|
|
403
|
+
const at = ["hooks", event, groupIndex, "hooks", entryIndex];
|
|
404
|
+
if (entries === MAX_HOOK_ENTRIES_PER_EVENT + 1)
|
|
405
|
+
firstPastCap = at;
|
|
406
|
+
if (!isRunnableHookEntry(contractEntry))
|
|
407
|
+
return hooksRefused({ reason: "unsupported_entry_type", path: [...at, "type"], type: contractEntry.type });
|
|
408
|
+
let entry = contractEntry;
|
|
409
|
+
if (entry.type === "command") {
|
|
410
|
+
const rawEntry = rawMember(rawMember(rawGroup, "hooks"), entryIndex);
|
|
411
|
+
const rawArgs = rawMember(rawEntry, "args");
|
|
412
|
+
const rawPlugin = rawMember(rawEntry, "plugin");
|
|
413
|
+
const args = rawArgs === undefined ? undefined : readHookArgs(rawArgs);
|
|
414
|
+
if (rawArgs !== undefined && args === undefined)
|
|
415
|
+
return hooksRefused({ reason: "wire_key_invalid", path: [...at, "args"], key: "args" });
|
|
416
|
+
const plugin = rawPlugin === undefined ? undefined : readHookPlugin(rawPlugin);
|
|
417
|
+
if (rawPlugin !== undefined && plugin === undefined)
|
|
418
|
+
return hooksRefused({ reason: "wire_key_invalid", path: [...at, "plugin"], key: "plugin" });
|
|
419
|
+
entry = { ...entry, ...(args !== undefined ? { args } : {}), ...(plugin !== undefined ? { plugin } : {}) };
|
|
420
|
+
const line = commandLineChars(entry);
|
|
421
|
+
if (line > MAX_HOOK_COMMAND_CHARS)
|
|
422
|
+
return hooksRefused(hooksCapIssue("command", event, [...at, "command"], line));
|
|
423
|
+
if (plugin !== undefined && hookPluginChars(plugin) > MAX_HOOK_COMMAND_CHARS)
|
|
424
|
+
return hooksRefused(hooksCapIssue("plugin", event, [...at, "plugin"], hookPluginChars(plugin)));
|
|
279
425
|
}
|
|
280
426
|
if (entry.type === "http") {
|
|
281
|
-
if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
427
|
+
if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
|
|
428
|
+
return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
|
|
284
429
|
const headerEntries = Object.entries(entry.headers ?? {});
|
|
285
|
-
if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
430
|
+
if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
|
|
431
|
+
return hooksRefused(hooksCapIssue("headers", event, [...at, "headers"], headerEntries.length));
|
|
288
432
|
const headerChars = headerEntries.reduce((n, [k, v]) => n + k.length + v.length, 0);
|
|
289
|
-
if (headerChars > MAX_HOOK_COMMAND_CHARS)
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
return
|
|
294
|
-
}
|
|
433
|
+
if (headerChars > MAX_HOOK_COMMAND_CHARS)
|
|
434
|
+
return hooksRefused(hooksCapIssue("header_chars", event, [...at, "headers"], headerChars));
|
|
435
|
+
const envVars = entry.allowedEnvVars?.length ?? 0;
|
|
436
|
+
if (envVars > MAX_HOOK_HTTP_HEADERS)
|
|
437
|
+
return hooksRefused(hooksCapIssue("allowed_env_vars", event, [...at, "allowedEnvVars"], envVars));
|
|
295
438
|
}
|
|
296
439
|
hooks.push(entry);
|
|
297
440
|
}
|
|
298
|
-
kept.push(group.matcher !== undefined ? { matcher: group.matcher,
|
|
299
|
-
}
|
|
300
|
-
if (entries > MAX_HOOK_ENTRIES_PER_EVENT) {
|
|
301
|
-
return { error: `settings.hooks.${event}: ${entries} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` };
|
|
441
|
+
kept.push({ ...(group.matcher !== undefined ? { matcher: group.matcher } : {}), ...(source !== undefined ? { source } : {}), hooks });
|
|
302
442
|
}
|
|
443
|
+
if (firstPastCap !== undefined)
|
|
444
|
+
return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
|
|
303
445
|
config[event] = kept;
|
|
304
446
|
}
|
|
305
447
|
return { config };
|
|
306
448
|
}
|
|
307
449
|
export function hookMatcherMatches(matcher, value) {
|
|
308
|
-
|
|
450
|
+
const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][RUNTIME_EVAL_DEPTH];
|
|
451
|
+
switch (evaluate) {
|
|
452
|
+
case "inline":
|
|
453
|
+
case "budgeted":
|
|
454
|
+
return matcherSelectsBy(evaluate, matcher, value, ADMISSION_MATCHER_BUDGET.budgetMs);
|
|
455
|
+
case "unreadable":
|
|
456
|
+
return "invalid";
|
|
457
|
+
default: {
|
|
458
|
+
const unreachable = evaluate;
|
|
459
|
+
return unreachable;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
309
462
|
}
|
|
310
463
|
export function hookIfMatches(cond, payload) {
|
|
311
464
|
if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
|
|
@@ -318,10 +471,20 @@ export function hookIfMatches(cond, payload) {
|
|
|
318
471
|
if (!m)
|
|
319
472
|
return "unevaluable";
|
|
320
473
|
const nameOk = hookMatcherMatches(m[1].trim(), toolName);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
474
|
+
switch (nameOk) {
|
|
475
|
+
case "invalid":
|
|
476
|
+
return "unevaluable";
|
|
477
|
+
case "over_budget":
|
|
478
|
+
return "over_budget";
|
|
479
|
+
case false:
|
|
480
|
+
return "not_matched";
|
|
481
|
+
case true:
|
|
482
|
+
break;
|
|
483
|
+
default: {
|
|
484
|
+
const unreachable = nameOk;
|
|
485
|
+
return unreachable;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
325
488
|
const spec = m[2];
|
|
326
489
|
if (spec === undefined || spec === "*" || spec === "")
|
|
327
490
|
return true;
|
|
@@ -340,8 +503,11 @@ export function hookIfMatches(cond, payload) {
|
|
|
340
503
|
const MAX_LLM_ENTRIES_PER_EVENT = 4;
|
|
341
504
|
const DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS = 30;
|
|
342
505
|
const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
|
|
343
|
-
function
|
|
344
|
-
|
|
506
|
+
function logHookCommandFailed(ctx, event, run) {
|
|
507
|
+
ctx.logger.warn("hook_command_failed", { event, code: run.code, stderr: echoCallerText(run.stderr, { maxChars: LOG_ERROR_ECHO_CHARS }) });
|
|
508
|
+
}
|
|
509
|
+
function logHookSystemMessage(ctx, event, message) {
|
|
510
|
+
ctx.logger.info("hook_system_message", { event, message: echoCallerText(message, { maxChars: LOG_MESSAGE_ECHO_CHARS }) });
|
|
345
511
|
}
|
|
346
512
|
function hookDisplayName(entry) {
|
|
347
513
|
if (typeof entry.statusMessage === "string" && entry.statusMessage.length > 0)
|
|
@@ -370,7 +536,7 @@ function emitHookNotice(ctx, n) {
|
|
|
370
536
|
}
|
|
371
537
|
catch (e) {
|
|
372
538
|
recordFailOpen("server.hooks.notice-sink-threw", `kind=${n.kind} event=${n.event}`);
|
|
373
|
-
ctx.logger.warn("hook_notice_sink_threw", { kind: n.kind, event: n.event, error:
|
|
539
|
+
ctx.logger.warn("hook_notice_sink_threw", { kind: n.kind, event: n.event, error: echoCallerText(String(e), { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
|
|
374
540
|
}
|
|
375
541
|
}
|
|
376
542
|
function noteHookFailure(ctx, args) {
|
|
@@ -389,84 +555,212 @@ function noteHookFailure(ctx, args) {
|
|
|
389
555
|
...(args.detail !== undefined ? { detail: boundedNoticeText(args.detail) } : {}),
|
|
390
556
|
});
|
|
391
557
|
}
|
|
558
|
+
function noteMatcherOverBudget(ctx, args) {
|
|
559
|
+
const budgetMs = ADMISSION_MATCHER_BUDGET.budgetMs;
|
|
560
|
+
const shown = (text) => echoCallerText(boundedNoticeText(text), { maxChars: MATCHER_ECHO_CHARS });
|
|
561
|
+
const expression = shown(args.expression);
|
|
562
|
+
const value = shown(args.value);
|
|
563
|
+
ctx.logger.warn("hook_matcher_over_budget", { event: args.event, scope: args.scope, expression, value, budgetMs, selected: true });
|
|
564
|
+
const first = args.entries[0];
|
|
565
|
+
if (first === undefined)
|
|
566
|
+
return;
|
|
567
|
+
const what = args.scope === "if" ? "if condition" : "matcher";
|
|
568
|
+
const count = args.entries.length;
|
|
569
|
+
noteHookFailure(ctx, {
|
|
570
|
+
event: args.event,
|
|
571
|
+
entry: first,
|
|
572
|
+
reason: "matcher_over_budget",
|
|
573
|
+
...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
|
|
574
|
+
detail: `${what} \`${expression}\` did not finish within ${budgetMs} ms on \`${value}\`; treated as selected (${count} ${count === 1 ? "entry" : "entries"})`,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function matcherGroupSelected(ctx, event, group, value, toolName) {
|
|
578
|
+
const answer = hookMatcherMatches(group.matcher, value);
|
|
579
|
+
switch (answer) {
|
|
580
|
+
case true:
|
|
581
|
+
return true;
|
|
582
|
+
case false:
|
|
583
|
+
return false;
|
|
584
|
+
case "invalid":
|
|
585
|
+
ctx.logger.warn("hook_matcher_invalid", { event, matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
|
|
586
|
+
return "invalid";
|
|
587
|
+
case "over_budget":
|
|
588
|
+
noteMatcherOverBudget(ctx, { event, scope: "matcher", expression: group.matcher ?? "", value, entries: group.hooks, ...(toolName !== undefined ? { toolName } : {}) });
|
|
589
|
+
return "over_budget";
|
|
590
|
+
default: {
|
|
591
|
+
const unreachable = answer;
|
|
592
|
+
return unreachable;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
export const PERMISSIVE_VERDICT_EVENTS = {
|
|
597
|
+
PreToolUse: true,
|
|
598
|
+
PostToolUse: false,
|
|
599
|
+
PostToolUseFailure: false,
|
|
600
|
+
PostToolBatch: false,
|
|
601
|
+
Notification: false,
|
|
602
|
+
UserPromptSubmit: false,
|
|
603
|
+
UserPromptExpansion: false,
|
|
604
|
+
SessionStart: false,
|
|
605
|
+
SessionEnd: false,
|
|
606
|
+
Stop: false,
|
|
607
|
+
StopFailure: false,
|
|
608
|
+
SubagentStart: false,
|
|
609
|
+
SubagentStop: false,
|
|
610
|
+
PreCompact: false,
|
|
611
|
+
PostCompact: false,
|
|
612
|
+
PermissionRequest: false,
|
|
613
|
+
PermissionDenied: false,
|
|
614
|
+
Setup: false,
|
|
615
|
+
TeammateIdle: false,
|
|
616
|
+
TaskCreated: false,
|
|
617
|
+
TaskCompleted: false,
|
|
618
|
+
Elicitation: false,
|
|
619
|
+
ElicitationResult: false,
|
|
620
|
+
ConfigChange: false,
|
|
621
|
+
WorktreeCreate: false,
|
|
622
|
+
WorktreeRemove: false,
|
|
623
|
+
InstructionsLoaded: false,
|
|
624
|
+
CwdChanged: false,
|
|
625
|
+
FileChanged: false,
|
|
626
|
+
MessageDisplay: false,
|
|
627
|
+
DirectoryAdded: false,
|
|
628
|
+
PreModelSwitch: false,
|
|
629
|
+
PostModelSwitch: false,
|
|
630
|
+
};
|
|
631
|
+
const DISTRUST_DETAIL = {
|
|
632
|
+
input_truncated: `the hook did not receive the whole payload (tool_input over ${MAX_TOOL_INPUT_CHARS} characters was replaced by a truncation marker, or a prompt / agent entry's payload text was clipped at that size); its allow / updatedInput are not honored`,
|
|
633
|
+
matcher_over_budget: "the matcher (or the if condition's tool-name part) did not finish within its budget; the entry ran as selected, so its allow / updatedInput are not honored",
|
|
634
|
+
};
|
|
635
|
+
function noteVerdictUntrusted(ctx, args) {
|
|
636
|
+
const notice = {
|
|
637
|
+
kind: "hook_verdict_untrusted",
|
|
638
|
+
event: args.event,
|
|
639
|
+
reason: args.reason,
|
|
640
|
+
lane: hookPhaseOf(args.source) === "unmanaged" ? "unmanaged" : "managed",
|
|
641
|
+
hookName: boundedNoticeText(hookDisplayName(args.entry)),
|
|
642
|
+
entryType: args.entry.type,
|
|
643
|
+
...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
|
|
644
|
+
...(args.source !== undefined ? { source: boundedNoticeText(args.source) } : {}),
|
|
645
|
+
detail: boundedNoticeText(DISTRUST_DETAIL[args.reason]),
|
|
646
|
+
};
|
|
647
|
+
const logEcho = (text) => echoCallerText(text, { maxChars: MAX_HOOK_NOTICE_TEXT_CHARS });
|
|
648
|
+
ctx.logger.warn("hook_verdict_not_honored", {
|
|
649
|
+
event: notice.event,
|
|
650
|
+
reason: notice.reason,
|
|
651
|
+
lane: notice.lane,
|
|
652
|
+
hookName: logEcho(notice.hookName),
|
|
653
|
+
entryType: notice.entryType,
|
|
654
|
+
...(notice.toolName !== undefined ? { toolName: notice.toolName } : {}),
|
|
655
|
+
...(notice.source !== undefined ? { source: logEcho(notice.source) } : {}),
|
|
656
|
+
detail: notice.detail,
|
|
657
|
+
});
|
|
658
|
+
if (!ctx.onHookNotice)
|
|
659
|
+
return;
|
|
660
|
+
emitHookNotice(ctx, notice);
|
|
661
|
+
}
|
|
662
|
+
function createUnrunLedger(ctx, event, toolName) {
|
|
663
|
+
let managedWithoutVerdict = false;
|
|
664
|
+
return {
|
|
665
|
+
untrusted(source, entry, reason) {
|
|
666
|
+
if (hookPhaseOf(source) === "unmanaged")
|
|
667
|
+
return;
|
|
668
|
+
managedWithoutVerdict = true;
|
|
669
|
+
noteVerdictUntrusted(ctx, { event, reason, entry, source, toolName });
|
|
670
|
+
},
|
|
671
|
+
unrun(source, entry, reason, detail) {
|
|
672
|
+
if (source === undefined || hookPhaseOf(source) === "unmanaged")
|
|
673
|
+
return;
|
|
674
|
+
managedWithoutVerdict = true;
|
|
675
|
+
ctx.logger.warn("hook_managed_unrun", { event, reason, source: echoCallerText(source), type: entry.type, ...(toolName !== undefined ? { tool: toolName } : {}) });
|
|
676
|
+
if (!ctx.onHookNotice)
|
|
677
|
+
return;
|
|
678
|
+
emitHookNotice(ctx, {
|
|
679
|
+
kind: "hook_managed_unrun",
|
|
680
|
+
event,
|
|
681
|
+
reason,
|
|
682
|
+
source: boundedNoticeText(source),
|
|
683
|
+
hookName: boundedNoticeText(hookDisplayName(entry)),
|
|
684
|
+
entryType: entry.type,
|
|
685
|
+
...(toolName !== undefined ? { toolName } : {}),
|
|
686
|
+
...(detail !== undefined ? { detail: boundedNoticeText(detail) } : {}),
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
get managedWithoutVerdict() {
|
|
690
|
+
return managedWithoutVerdict;
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function noteExitNonzero(ctx, ledger, event, ran, toolName, detail) {
|
|
695
|
+
logHookCommandFailed(ctx, event, ran.run);
|
|
696
|
+
noteHookFailure(ctx, { event, entry: ran.entry, reason: "exit_nonzero", exitCode: ran.run.code, stderr: ran.run.stderr, ...(toolName !== undefined ? { toolName } : {}), ...(detail !== undefined ? { detail } : {}) });
|
|
697
|
+
ledger.unrun(ran.source, ran.entry, "exit_nonzero");
|
|
698
|
+
}
|
|
392
699
|
export function buildHookContext(parts) {
|
|
393
|
-
const present = parts.filter((p) => p.length > 0)
|
|
700
|
+
const present = parts.filter((p) => p.length > 0);
|
|
394
701
|
if (present.length === 0)
|
|
395
702
|
return undefined;
|
|
396
703
|
const kept = [];
|
|
397
704
|
let used = 0;
|
|
398
705
|
let truncating = false;
|
|
399
|
-
let messages = 0;
|
|
400
706
|
let omitted = 0;
|
|
401
707
|
let droppedChars = 0;
|
|
402
708
|
for (const part of present) {
|
|
403
|
-
messages += part.messages;
|
|
404
|
-
omitted += part.omitted;
|
|
405
|
-
droppedChars += part.dropped;
|
|
406
709
|
if (!truncating) {
|
|
407
710
|
const sep = kept.length > 0 ? 1 : 0;
|
|
408
711
|
const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
|
|
409
|
-
if (room >= part.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
used += sep + part.body.length;
|
|
413
|
-
}
|
|
712
|
+
if (room >= part.length) {
|
|
713
|
+
kept.push(part);
|
|
714
|
+
used += sep + part.length;
|
|
414
715
|
continue;
|
|
415
716
|
}
|
|
416
717
|
truncating = true;
|
|
417
|
-
if (
|
|
418
|
-
kept.push(part.
|
|
718
|
+
if (room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
|
|
719
|
+
kept.push(part.slice(0, room));
|
|
419
720
|
used += sep + room;
|
|
420
|
-
droppedChars += part.
|
|
721
|
+
droppedChars += part.length - room;
|
|
421
722
|
continue;
|
|
422
723
|
}
|
|
423
724
|
}
|
|
424
|
-
omitted +=
|
|
425
|
-
droppedChars += part.
|
|
725
|
+
omitted += 1;
|
|
726
|
+
droppedChars += part.length;
|
|
426
727
|
}
|
|
427
728
|
const body = kept.join("\n");
|
|
428
729
|
if (droppedChars === 0)
|
|
429
730
|
return body;
|
|
430
|
-
const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${
|
|
731
|
+
const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${present.length} hook messages omitted, ${droppedChars} characters dropped]`;
|
|
431
732
|
return kept.length > 0 ? `${body}\n${marker}` : marker;
|
|
432
733
|
}
|
|
433
|
-
const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
|
|
434
|
-
const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
|
|
435
|
-
function readHookContextPart(part) {
|
|
436
|
-
const m = HOOK_CONTEXT_MARKER_RE.exec(part);
|
|
437
|
-
if (!m)
|
|
438
|
-
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
439
|
-
const omitted = Number(m[1]);
|
|
440
|
-
const messages = Number(m[2]);
|
|
441
|
-
const dropped = Number(m[3]);
|
|
442
|
-
const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
|
|
443
|
-
if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
|
|
444
|
-
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
445
|
-
}
|
|
446
|
-
return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
|
|
447
|
-
}
|
|
448
734
|
function buildHookContextField(parts) {
|
|
449
735
|
const text = buildHookContext(parts);
|
|
450
736
|
return text === undefined ? {} : { additionalContext: text };
|
|
451
737
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
738
|
+
function spawnPlannedCommand(plan, ctx, stdio) {
|
|
739
|
+
const env = hookEnvOverBase(plan.env, true);
|
|
740
|
+
if (plan.kind === "exec")
|
|
741
|
+
return spawn(plan.file, [...plan.args], { cwd: ctx.cwd, env, stdio, detached: true });
|
|
742
|
+
const sh = hostShell();
|
|
743
|
+
return spawn(sh.shell, [...sh.args, plan.command], { cwd: ctx.cwd, env, stdio, detached: true });
|
|
744
|
+
}
|
|
745
|
+
async function runCommandHook(entry, payload, ctx, halt) {
|
|
746
|
+
const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
|
|
747
|
+
if (plan.kind === "refused")
|
|
748
|
+
return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: plan.error };
|
|
749
|
+
if (plan.kind === "shell") {
|
|
750
|
+
try {
|
|
751
|
+
await resolveHostShell();
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) };
|
|
755
|
+
}
|
|
458
756
|
}
|
|
757
|
+
if (halt.aborted)
|
|
758
|
+
return { code: null, stdout: "", stderr: "", timedOut: false, aborted: {} };
|
|
459
759
|
return new Promise((resolve) => {
|
|
460
760
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
461
761
|
let child;
|
|
462
762
|
try {
|
|
463
|
-
|
|
464
|
-
child = spawn(sh.shell, [...sh.args, entry.command], {
|
|
465
|
-
cwd: ctx.cwd,
|
|
466
|
-
env: hookEnv(ctx),
|
|
467
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
468
|
-
detached: true,
|
|
469
|
-
});
|
|
763
|
+
child = spawnPlannedCommand(plan, ctx, ["pipe", "pipe", "pipe"]);
|
|
470
764
|
}
|
|
471
765
|
catch (e) {
|
|
472
766
|
resolve({ code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) });
|
|
@@ -477,16 +771,20 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
477
771
|
let stderr = "";
|
|
478
772
|
let timedOut = false;
|
|
479
773
|
let settled = false;
|
|
774
|
+
let sent;
|
|
480
775
|
const settle = (code) => {
|
|
481
776
|
if (settled)
|
|
482
777
|
return;
|
|
483
778
|
settled = true;
|
|
484
779
|
clearTimeout(timer);
|
|
780
|
+
halt.removeEventListener("abort", onHalt);
|
|
485
781
|
ticket?.release();
|
|
486
|
-
resolve({ code, stdout, stderr, timedOut });
|
|
782
|
+
resolve({ code, stdout, stderr, timedOut, ...(sent !== undefined ? { aborted: { signal: sent } } : {}) });
|
|
487
783
|
};
|
|
488
784
|
const timer = setTimeout(() => {
|
|
489
785
|
timedOut = true;
|
|
786
|
+
if (sent !== undefined)
|
|
787
|
+
sent = "SIGKILL";
|
|
490
788
|
try {
|
|
491
789
|
if (child.pid)
|
|
492
790
|
process.kill(-child.pid, "SIGKILL");
|
|
@@ -498,6 +796,31 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
498
796
|
}
|
|
499
797
|
settle(null);
|
|
500
798
|
}, timeoutMs);
|
|
799
|
+
const signalGroup = (sig) => {
|
|
800
|
+
try {
|
|
801
|
+
if (child.pid)
|
|
802
|
+
(sig === "SIGTERM" ? killTreeSoft : killTreeHard)(child.pid);
|
|
803
|
+
else
|
|
804
|
+
child.kill(sig);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
child.kill(sig);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
const onHalt = () => {
|
|
811
|
+
if (settled)
|
|
812
|
+
return;
|
|
813
|
+
sent = "SIGTERM";
|
|
814
|
+
signalGroup("SIGTERM");
|
|
815
|
+
const escalate = setTimeout(() => {
|
|
816
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
817
|
+
return;
|
|
818
|
+
sent = "SIGKILL";
|
|
819
|
+
signalGroup("SIGKILL");
|
|
820
|
+
}, HOOK_KILL_GRACE_MS);
|
|
821
|
+
escalate.unref();
|
|
822
|
+
};
|
|
823
|
+
halt.addEventListener("abort", onHalt, { once: true });
|
|
501
824
|
child.stdout?.on("data", (d) => {
|
|
502
825
|
if (stdout.length < MAX_HOOK_OUTPUT_BYTES)
|
|
503
826
|
stdout += d.toString("utf8").slice(0, MAX_HOOK_OUTPUT_BYTES - stdout.length);
|
|
@@ -508,6 +831,7 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
508
831
|
});
|
|
509
832
|
child.on("error", (e) => {
|
|
510
833
|
clearTimeout(timer);
|
|
834
|
+
halt.removeEventListener("abort", onHalt);
|
|
511
835
|
if (!settled) {
|
|
512
836
|
settled = true;
|
|
513
837
|
resolve({ code: null, stdout, stderr, timedOut, spawnError: String(e) });
|
|
@@ -518,18 +842,18 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
518
842
|
child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
|
|
519
843
|
});
|
|
520
844
|
}
|
|
521
|
-
async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
845
|
+
async function fireAsyncCommandHook(entry, payload, ctx, event, halt) {
|
|
522
846
|
try {
|
|
523
847
|
const rewake = entry.asyncRewake === true;
|
|
524
848
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
525
|
-
await
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
849
|
+
const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
|
|
850
|
+
if (plan.kind === "refused")
|
|
851
|
+
throw new Error(plan.error);
|
|
852
|
+
if (plan.kind === "shell")
|
|
853
|
+
await resolveHostShell();
|
|
854
|
+
if (halt.aborted)
|
|
855
|
+
return;
|
|
856
|
+
const child = spawnPlannedCommand(plan, ctx, ["pipe", "ignore", rewake ? "pipe" : "ignore"]);
|
|
533
857
|
const ticket = adoptHostForegroundChild(child);
|
|
534
858
|
let killedByTimeout = false;
|
|
535
859
|
const timer = setTimeout(() => {
|
|
@@ -555,7 +879,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
555
879
|
}
|
|
556
880
|
child.on("error", (e) => {
|
|
557
881
|
clearTimeout(timer);
|
|
558
|
-
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error:
|
|
882
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: echoCallerText(String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
|
|
559
883
|
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
560
884
|
});
|
|
561
885
|
child.on("close", (code) => {
|
|
@@ -578,14 +902,14 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
578
902
|
ctx.logger.info("hook_async_rewake_delivered", { event, chars: text.length });
|
|
579
903
|
else
|
|
580
904
|
ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "no_live_stream" });
|
|
581
|
-
}, (e) => ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "wake_failed", error: String(e)
|
|
905
|
+
}, (e) => ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "wake_failed", error: echoCallerText(String(e), { maxChars: LOG_PREVIEW_ECHO_CHARS }) }));
|
|
582
906
|
});
|
|
583
907
|
child.stdin?.on("error", () => { });
|
|
584
908
|
child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
|
|
585
909
|
child.unref();
|
|
586
910
|
}
|
|
587
911
|
catch (e) {
|
|
588
|
-
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error:
|
|
912
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: echoCallerText(String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
|
|
589
913
|
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
590
914
|
}
|
|
591
915
|
}
|
|
@@ -610,14 +934,19 @@ function noteAsyncOutcome(entry, ctx, event, payload, outcome) {
|
|
|
610
934
|
...(code === 2 ? { detail: "exit 2 on a fire-and-forget async hook: there is no blocking or wake face to honor it (asyncRewake would wake the model)" } : {}),
|
|
611
935
|
});
|
|
612
936
|
}
|
|
937
|
+
function llmHookArguments(payload) {
|
|
938
|
+
const bounded = boundedToolInputPayload(payload);
|
|
939
|
+
const json = JSON.stringify(bounded);
|
|
940
|
+
const text = clipDataText(json, MAX_TOOL_INPUT_CHARS);
|
|
941
|
+
return { text, truncated: bounded !== payload || text !== json };
|
|
942
|
+
}
|
|
613
943
|
async function runLlmHook(entry, payload, ctx, extra) {
|
|
614
944
|
const call = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
|
|
615
945
|
if (!call)
|
|
616
946
|
return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: `no ${entry.type} hook carrier wired on this deployment` };
|
|
617
947
|
const defSec = entry.type === "prompt" ? DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS : DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS;
|
|
618
948
|
const timeoutMs = Math.min(entry.timeout ?? defSec, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
619
|
-
const
|
|
620
|
-
const substituted = entry.prompt.split("$ARGUMENTS").join(args);
|
|
949
|
+
const substituted = entry.prompt.split("$ARGUMENTS").join(llmHookArguments(payload).text);
|
|
621
950
|
const prompt = extra?.transcript !== undefined ? `${extra.transcript}\n\n---\n\n${wrapCondition("Stop", substituted)}` : substituted;
|
|
622
951
|
let hardTopTimer;
|
|
623
952
|
const hardTop = new Promise((r) => {
|
|
@@ -647,9 +976,9 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
647
976
|
if (!res.ok) {
|
|
648
977
|
return res.code === "hard_timeout"
|
|
649
978
|
? { code: null, stdout: "", stderr: "", timedOut: true, ...(noticed ? { noticed } : {}) }
|
|
650
|
-
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError:
|
|
979
|
+
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: res.error, ...(noticed ? { noticed } : {}) };
|
|
651
980
|
}
|
|
652
|
-
return { code: 0, stdout:
|
|
981
|
+
return { code: 0, stdout: clipDataText(res.text, 1024 * 1024), stderr: "", timedOut: false };
|
|
653
982
|
}
|
|
654
983
|
finally {
|
|
655
984
|
if (hardTopTimer !== undefined)
|
|
@@ -665,7 +994,9 @@ async function runHttpHook(entry, payload, ctx, event) {
|
|
|
665
994
|
event,
|
|
666
995
|
url: buildDisplaySafeUrl(entry.url),
|
|
667
996
|
blankedCount: interpolated.blanked.length,
|
|
668
|
-
blanked: interpolated.blanked
|
|
997
|
+
blanked: interpolated.blanked
|
|
998
|
+
.slice(0, MAX_BLANKED_REFERENCES_LOGGED)
|
|
999
|
+
.map(({ header, name, reason }) => ({ header: echoCallerText(header, { maxChars: LOG_PREVIEW_ECHO_CHARS }), name: echoCallerText(name, { maxChars: LOG_PREVIEW_ECHO_CHARS }), reason })),
|
|
669
1000
|
});
|
|
670
1001
|
}
|
|
671
1002
|
const headers = { "content-type": "application/json", ...interpolated.headers };
|
|
@@ -703,7 +1034,7 @@ async function runHttpHook(entry, payload, ctx, event) {
|
|
|
703
1034
|
if (trimmed === "")
|
|
704
1035
|
return { code: 0, stdout: "{}", stderr: "", timedOut: false };
|
|
705
1036
|
if (!parseHookStdout(body)) {
|
|
706
|
-
ctx.logger.warn("hook_http_non_json_response", { url: buildDisplaySafeUrl(entry.url), preview:
|
|
1037
|
+
ctx.logger.warn("hook_http_non_json_response", { url: buildDisplaySafeUrl(entry.url), preview: echoCallerText(trimmed, { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
|
|
707
1038
|
noteHookFailure(ctx, {
|
|
708
1039
|
event: eventOfPayload(payload),
|
|
709
1040
|
entry,
|
|
@@ -712,7 +1043,7 @@ async function runHttpHook(entry, payload, ctx, event) {
|
|
|
712
1043
|
stderr: trimmed,
|
|
713
1044
|
toolName: toolNameOf(payload),
|
|
714
1045
|
});
|
|
715
|
-
return { code: 0, stdout: "", stderr: "", timedOut: false };
|
|
1046
|
+
return { code: 0, stdout: "", stderr: "", timedOut: false, badJson: true };
|
|
716
1047
|
}
|
|
717
1048
|
return { code: 0, stdout: body, stderr: "", timedOut: false };
|
|
718
1049
|
}
|
|
@@ -748,14 +1079,16 @@ function boundedToolInputPayload(payload) {
|
|
|
748
1079
|
const bounded = boundedInputValue(p.tool_input);
|
|
749
1080
|
return bounded === p.tool_input ? payload : { ...p, tool_input: bounded };
|
|
750
1081
|
}
|
|
751
|
-
function readHookStdout(
|
|
1082
|
+
function readHookStdout(ran, event, ctx, payload, ledger) {
|
|
1083
|
+
const { run, entry } = ran;
|
|
752
1084
|
const out = parseHookStdout(run.stdout);
|
|
753
1085
|
if (out !== undefined)
|
|
754
1086
|
return out;
|
|
755
1087
|
const t = run.stdout.trim();
|
|
756
1088
|
if (t.startsWith("{") || t.startsWith("[")) {
|
|
757
|
-
ctx.logger.warn("hook_bad_json_output", { event, type: entry.type, preview:
|
|
1089
|
+
ctx.logger.warn("hook_bad_json_output", { event, type: entry.type, preview: echoCallerText(t, { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
|
|
758
1090
|
noteHookFailure(ctx, { event, entry, reason: "bad_json", exitCode: run.code, stderr: run.stderr, toolName: toolNameOf(payload) });
|
|
1091
|
+
ledger.unrun(ran.source, entry, "bad_json");
|
|
759
1092
|
}
|
|
760
1093
|
return undefined;
|
|
761
1094
|
}
|
|
@@ -771,7 +1104,7 @@ function parseHookStdout(stdout) {
|
|
|
771
1104
|
return undefined;
|
|
772
1105
|
}
|
|
773
1106
|
}
|
|
774
|
-
function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
1107
|
+
function runRunnableEntry(entry, payload, ctx, event, llmExtra, halt) {
|
|
775
1108
|
switch (entry.type) {
|
|
776
1109
|
case "http":
|
|
777
1110
|
return runHttpHook(entry, payload, ctx, event);
|
|
@@ -780,7 +1113,7 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
|
780
1113
|
case "agent":
|
|
781
1114
|
return runLlmHook(entry, payload, ctx, { system: ccAgentHookSystemFor(event), event });
|
|
782
1115
|
case "command":
|
|
783
|
-
return runCommandHook(entry, payload, ctx);
|
|
1116
|
+
return runCommandHook(entry, payload, ctx, halt);
|
|
784
1117
|
default: {
|
|
785
1118
|
const unreachable = entry;
|
|
786
1119
|
void unreachable;
|
|
@@ -788,79 +1121,162 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
|
788
1121
|
}
|
|
789
1122
|
}
|
|
790
1123
|
}
|
|
791
|
-
|
|
1124
|
+
function isSyncEntry(entry) {
|
|
1125
|
+
return !(entry.type === "command" && (entry.async === true || entry.asyncRewake === true));
|
|
1126
|
+
}
|
|
1127
|
+
async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, halt, deadlineAt, ledger, llmExtra) {
|
|
792
1128
|
const results = [];
|
|
793
1129
|
let llmEntries = 0;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (entry.type === "command" && entry.shell === "powershell") {
|
|
822
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "powershell_not_supported" });
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
if (entry.if !== undefined) {
|
|
826
|
-
const iv = hookIfMatches(entry.if, payload);
|
|
827
|
-
if (iv !== true) {
|
|
828
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: iv === "unevaluable" ? "if_unevaluable" : "if_not_matched", if: clip(entry.if, 200) });
|
|
829
|
-
continue;
|
|
1130
|
+
const toolName = toolNameOf(payload);
|
|
1131
|
+
const inputTruncated = boundedToolInputPayload(payload) !== payload;
|
|
1132
|
+
const truncatedFor = (entry) => (entry.type === "prompt" || entry.type === "agent" ? llmHookArguments(payload).truncated : inputTruncated);
|
|
1133
|
+
const settle = async (entry, onceKey) => {
|
|
1134
|
+
if (halt.aborted)
|
|
1135
|
+
return { kind: "halted" };
|
|
1136
|
+
if (entry.once && onceFired.has(onceKey))
|
|
1137
|
+
return { kind: "passed" };
|
|
1138
|
+
let ifOverBudget = false;
|
|
1139
|
+
if (entry.if !== undefined) {
|
|
1140
|
+
const iv = hookIfMatches(entry.if, payload);
|
|
1141
|
+
switch (iv) {
|
|
1142
|
+
case true:
|
|
1143
|
+
break;
|
|
1144
|
+
case "over_budget":
|
|
1145
|
+
noteMatcherOverBudget(ctx, { event, scope: "if", expression: entry.if, value: toolName ?? "", entries: [entry], ...(toolName !== undefined ? { toolName } : {}) });
|
|
1146
|
+
ifOverBudget = true;
|
|
1147
|
+
break;
|
|
1148
|
+
case "not_matched":
|
|
1149
|
+
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "if_not_matched", if: echoCallerText(entry.if, { maxChars: IF_ECHO_CHARS }) });
|
|
1150
|
+
return { kind: "passed" };
|
|
1151
|
+
case "unevaluable":
|
|
1152
|
+
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "if_unevaluable", if: echoCallerText(entry.if, { maxChars: IF_ECHO_CHARS }) });
|
|
1153
|
+
return { kind: "unrun", reason: "if_unevaluable" };
|
|
1154
|
+
default: {
|
|
1155
|
+
const unreachable = iv;
|
|
1156
|
+
return unreachable;
|
|
830
1157
|
}
|
|
831
1158
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1159
|
+
}
|
|
1160
|
+
if (entry.type === "command" && entry.shell === "powershell") {
|
|
1161
|
+
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "powershell_not_supported" });
|
|
1162
|
+
return isSyncEntry(entry) ? { kind: "unrun", reason: "powershell_unsupported" } : { kind: "passed" };
|
|
1163
|
+
}
|
|
1164
|
+
if (entry.type === "prompt" || entry.type === "agent") {
|
|
1165
|
+
const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
|
|
1166
|
+
if (!carrier) {
|
|
1167
|
+
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_carrier_unavailable" });
|
|
1168
|
+
return { kind: "unrun", reason: "llm_carrier_unavailable" };
|
|
837
1169
|
}
|
|
838
|
-
if (
|
|
839
|
-
ctx.logger.warn("
|
|
840
|
-
return
|
|
1170
|
+
if (llmEntries >= MAX_LLM_ENTRIES_PER_EVENT) {
|
|
1171
|
+
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_entries_per_event_cap", cap: MAX_LLM_ENTRIES_PER_EVENT });
|
|
1172
|
+
return { kind: "unrun", reason: "llm_cap" };
|
|
841
1173
|
}
|
|
1174
|
+
}
|
|
1175
|
+
if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
|
|
842
1176
|
if (entry.once)
|
|
843
1177
|
onceFired.add(onceKey);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1178
|
+
void fireAsyncCommandHook(entry, payload, ctx, event, halt);
|
|
1179
|
+
return { kind: "passed" };
|
|
1180
|
+
}
|
|
1181
|
+
if (Date.now() > deadlineAt) {
|
|
1182
|
+
ctx.logger.warn("hook_event_deadline_exceeded", { event, budgetSec: MAX_HOOK_EVENT_TOTAL_SECONDS });
|
|
1183
|
+
return { kind: "unrun", reason: "budget_exhausted" };
|
|
1184
|
+
}
|
|
1185
|
+
if (entry.once)
|
|
1186
|
+
onceFired.add(onceKey);
|
|
1187
|
+
if (entry.type === "prompt" || entry.type === "agent")
|
|
1188
|
+
llmEntries++;
|
|
1189
|
+
const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra, halt);
|
|
1190
|
+
if (run.aborted !== undefined) {
|
|
1191
|
+
if (run.aborted.signal !== undefined)
|
|
1192
|
+
ctx.logger.warn("hook_command_killed", { event, type: entry.type, killed: true, signal: run.aborted.signal });
|
|
1193
|
+
return { kind: "halted" };
|
|
1194
|
+
}
|
|
1195
|
+
if (run.spawnError) {
|
|
1196
|
+
ctx.logger.warn(entry.type === "http" ? "hook_http_request_failed" : entry.type === "prompt" || entry.type === "agent" ? "hook_llm_failed" : "hook_command_spawn_failed", { event, type: entry.type, error: echoCallerText(run.spawnError, { maxChars: LOG_ERROR_ECHO_CHARS }) });
|
|
1197
|
+
if (!run.noticed)
|
|
1198
|
+
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", exitCode: run.code, stderr: run.spawnError, ...(toolName !== undefined ? { toolName } : {}) });
|
|
1199
|
+
return { kind: "unrun", reason: "spawn_failed" };
|
|
1200
|
+
}
|
|
1201
|
+
if (run.timedOut) {
|
|
1202
|
+
ctx.logger.warn("hook_command_timeout", { event, timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
|
|
1203
|
+
if (!run.noticed)
|
|
1204
|
+
noteHookFailure(ctx, { event, entry, reason: "timeout", exitCode: run.code, stderr: run.stderr, ...(toolName !== undefined ? { toolName } : {}) });
|
|
1205
|
+
return { kind: "unrun", reason: "timeout" };
|
|
1206
|
+
}
|
|
1207
|
+
if (run.badJson)
|
|
1208
|
+
return { kind: "unrun", reason: "bad_json" };
|
|
1209
|
+
return ifOverBudget ? { kind: "ran", run, overBudget: true } : { kind: "ran", run };
|
|
1210
|
+
};
|
|
1211
|
+
for (const { group, index } of groups) {
|
|
1212
|
+
const selected = matcherGroupSelected(ctx, event, group, matchValue, toolName);
|
|
1213
|
+
if (selected === "invalid") {
|
|
1214
|
+
for (const entry of group.hooks)
|
|
1215
|
+
if (isSyncEntry(entry))
|
|
1216
|
+
ledger.unrun(group.source, entry, "matcher_invalid");
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (selected === false)
|
|
1220
|
+
continue;
|
|
1221
|
+
for (const [ei, entry] of group.hooks.entries()) {
|
|
1222
|
+
const fate = await settle(entry, `${event}:${index}:${ei}`);
|
|
1223
|
+
switch (fate.kind) {
|
|
1224
|
+
case "ran": {
|
|
1225
|
+
const distrust = !PERMISSIVE_VERDICT_EVENTS[event]
|
|
1226
|
+
? undefined
|
|
1227
|
+
: truncatedFor(entry)
|
|
1228
|
+
? "input_truncated"
|
|
1229
|
+
: selected === "over_budget" || fate.overBudget === true
|
|
1230
|
+
? "matcher_over_budget"
|
|
1231
|
+
: undefined;
|
|
1232
|
+
results.push({ entry, run: fate.run, source: group.source, ...(distrust !== undefined ? { distrust } : {}) });
|
|
1233
|
+
if (distrust !== undefined)
|
|
1234
|
+
ledger.untrusted(group.source, entry, distrust);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
case "unrun":
|
|
1238
|
+
ledger.unrun(group.source, entry, fate.reason);
|
|
1239
|
+
if (fate.reason === "budget_exhausted")
|
|
1240
|
+
return results;
|
|
1241
|
+
break;
|
|
1242
|
+
case "passed":
|
|
1243
|
+
break;
|
|
1244
|
+
case "halted":
|
|
1245
|
+
return results;
|
|
1246
|
+
default: {
|
|
1247
|
+
const unreachable = fate;
|
|
1248
|
+
return unreachable;
|
|
1249
|
+
}
|
|
858
1250
|
}
|
|
859
|
-
results.push({ entry, run });
|
|
860
1251
|
}
|
|
861
1252
|
}
|
|
862
1253
|
return results;
|
|
863
1254
|
}
|
|
1255
|
+
function createLaneClocks() {
|
|
1256
|
+
const startedAt = {};
|
|
1257
|
+
return (phase) => {
|
|
1258
|
+
const lane = phase === "unmanaged" ? "unmanaged" : "managed";
|
|
1259
|
+
return (startedAt[lane] ??= Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function hookLanesOf(groups) {
|
|
1263
|
+
const indexed = groups.map((group, index) => ({ group, index, phase: hookPhaseOf(group.source) }));
|
|
1264
|
+
return HOOK_PHASES.map((phase) => ({ phase, groups: indexed.filter((g) => g.phase === phase).map(({ group, index }) => ({ group, index })) })).filter((l) => l.groups.length > 0);
|
|
1265
|
+
}
|
|
1266
|
+
function trustRank(phase) {
|
|
1267
|
+
return HOOK_PHASES.length - 1 - HOOK_PHASES.indexOf(phase);
|
|
1268
|
+
}
|
|
1269
|
+
async function runEventLanes(event, groups, matchValue, payload, ctx, onceFired, halt, ledger, llmExtra) {
|
|
1270
|
+
const deadlineFor = createLaneClocks();
|
|
1271
|
+
const ran = [];
|
|
1272
|
+
for (const lane of hookLanesOf(groups)) {
|
|
1273
|
+
if (halt.aborted)
|
|
1274
|
+
break;
|
|
1275
|
+
for (const r of await runMatchingEntries(event, lane.groups, matchValue, payload, ctx, onceFired, halt, deadlineFor(lane.phase), ledger, llmExtra))
|
|
1276
|
+
ran.push({ ...r, phase: lane.phase });
|
|
1277
|
+
}
|
|
1278
|
+
return ran.sort((a, b) => trustRank(a.phase) - trustRank(b.phase)).map(({ entry, run, source }) => ({ entry, run, source }));
|
|
1279
|
+
}
|
|
864
1280
|
function basePayload(ctx, permissionMode) {
|
|
865
1281
|
return {
|
|
866
1282
|
session_id: ctx.sessionId,
|
|
@@ -872,38 +1288,27 @@ function basePayload(ctx, permissionMode) {
|
|
|
872
1288
|
function ignoreMatchers(groups) {
|
|
873
1289
|
return groups.map((g) => (g.matcher === undefined ? g : { ...g, matcher: undefined }));
|
|
874
1290
|
}
|
|
875
|
-
async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired) {
|
|
876
|
-
const
|
|
877
|
-
|
|
1291
|
+
async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired, halt) {
|
|
1292
|
+
const ledger = createUnrunLedger(ctx, event, toolNameOf(payload));
|
|
1293
|
+
const singles = await runEventLanes(event, groups, matchValue, payload, ctx, onceFired, halt, ledger);
|
|
1294
|
+
for (const ran of singles) {
|
|
1295
|
+
const { run } = ran;
|
|
878
1296
|
if (run.code !== 0) {
|
|
879
|
-
ctx
|
|
880
|
-
noteHookFailure(ctx, {
|
|
881
|
-
event,
|
|
882
|
-
entry,
|
|
883
|
-
reason: "exit_nonzero",
|
|
884
|
-
exitCode: run.code,
|
|
885
|
-
stderr: run.stderr,
|
|
886
|
-
toolName: toolNameOf(payload),
|
|
887
|
-
...(run.code === 2 ? { detail: `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` } : {}),
|
|
888
|
-
});
|
|
1297
|
+
noteExitNonzero(ctx, ledger, event, ran, toolNameOf(payload), run.code === 2 ? `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` : undefined);
|
|
889
1298
|
continue;
|
|
890
1299
|
}
|
|
891
|
-
const out = readHookStdout(
|
|
1300
|
+
const out = readHookStdout(ran, event, ctx, payload, ledger);
|
|
892
1301
|
if (!out)
|
|
893
1302
|
continue;
|
|
894
1303
|
if (typeof out.systemMessage === "string")
|
|
895
|
-
ctx
|
|
1304
|
+
logHookSystemMessage(ctx, event, out.systemMessage);
|
|
896
1305
|
if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
|
|
897
1306
|
ctx.logger.warn("hook_result_unsupported", { event });
|
|
898
1307
|
}
|
|
899
1308
|
}
|
|
900
1309
|
}
|
|
901
|
-
export function
|
|
1310
|
+
export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
902
1311
|
const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
|
|
903
|
-
const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
|
|
904
|
-
if (dark.length > 0) {
|
|
905
|
-
ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
|
|
906
|
-
}
|
|
907
1312
|
const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
|
|
908
1313
|
if (shellOwned.length > 0) {
|
|
909
1314
|
ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
|
|
@@ -925,98 +1330,131 @@ export function createTaskHooks(config, ctx) {
|
|
|
925
1330
|
return undefined;
|
|
926
1331
|
}
|
|
927
1332
|
const onceFired = new Set();
|
|
928
|
-
const
|
|
1333
|
+
const translators = {};
|
|
929
1334
|
if (pre) {
|
|
930
|
-
|
|
931
|
-
|
|
1335
|
+
translators.PreToolUse = async (e, signal) => {
|
|
1336
|
+
const toolName = e.tool;
|
|
1337
|
+
const ledger = createUnrunLedger(ctx, "PreToolUse", toolName);
|
|
1338
|
+
const deadlineFor = createLaneClocks();
|
|
1339
|
+
let currentInput = e.input;
|
|
932
1340
|
let rewritten = false;
|
|
933
1341
|
let asked;
|
|
934
1342
|
let allowedFor;
|
|
935
1343
|
let unknownDecision = false;
|
|
936
|
-
const contexts = [];
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
};
|
|
956
|
-
const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
|
|
957
|
-
for (const { entry, run } of singles) {
|
|
958
|
-
if (run.code === 2) {
|
|
959
|
-
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
|
|
960
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
961
|
-
}
|
|
962
|
-
if (run.code !== 0) {
|
|
963
|
-
ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
|
|
964
|
-
noteHookFailure(ctx, { event: "PreToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
const out = readHookStdout(run, entry, "PreToolUse", ctx, payload);
|
|
968
|
-
if (!out)
|
|
969
|
-
continue;
|
|
970
|
-
if (typeof out.systemMessage === "string")
|
|
971
|
-
ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
|
|
972
|
-
if (out.continue === false) {
|
|
973
|
-
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
974
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
975
|
-
}
|
|
976
|
-
if (out.decision === "block") {
|
|
977
|
-
const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
978
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
979
|
-
}
|
|
980
|
-
const hso = out.hookSpecificOutput;
|
|
981
|
-
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
|
|
982
|
-
const h = hso;
|
|
983
|
-
if (typeof h.additionalContext === "string")
|
|
984
|
-
contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
985
|
-
const decision = h.permissionDecision;
|
|
986
|
-
if (decision === "deny") {
|
|
987
|
-
const reason = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
988
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
1344
|
+
const contexts = { unmanaged: [], managedPlugin: [], managedPolicy: [] };
|
|
1345
|
+
const contextField = () => buildHookContextField([...HOOK_PHASES].sort((a, b) => trustRank(a) - trustRank(b)).flatMap((p) => contexts[p]));
|
|
1346
|
+
const runPass = async (phase, groups, once) => {
|
|
1347
|
+
let rewroteBy;
|
|
1348
|
+
for (const laneGroup of groups) {
|
|
1349
|
+
const groupInput = currentInput;
|
|
1350
|
+
const payload = {
|
|
1351
|
+
...basePayload(ctx, e.permissionMode),
|
|
1352
|
+
hook_event_name: "PreToolUse",
|
|
1353
|
+
tool_name: toolName,
|
|
1354
|
+
tool_input: groupInput,
|
|
1355
|
+
tool_use_id: e.tool_use_id,
|
|
1356
|
+
};
|
|
1357
|
+
const singles = await runMatchingEntries("PreToolUse", [laneGroup], toolName, payload, ctx, once, signal, deadlineFor(phase), ledger);
|
|
1358
|
+
for (const ran of singles) {
|
|
1359
|
+
const { entry, run } = ran;
|
|
1360
|
+
if (run.code === 2) {
|
|
1361
|
+
const reason = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
|
|
1362
|
+
return { deny: { action: "deny", message: reason, ...contextField() } };
|
|
989
1363
|
}
|
|
990
|
-
if (
|
|
991
|
-
|
|
1364
|
+
if (run.code !== 0) {
|
|
1365
|
+
noteExitNonzero(ctx, ledger, "PreToolUse", ran, toolName);
|
|
1366
|
+
continue;
|
|
992
1367
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1368
|
+
const out = readHookStdout(ran, "PreToolUse", ctx, payload, ledger);
|
|
1369
|
+
if (!out)
|
|
1370
|
+
continue;
|
|
1371
|
+
if (typeof out.systemMessage === "string")
|
|
1372
|
+
logHookSystemMessage(ctx, "PreToolUse", out.systemMessage);
|
|
1373
|
+
if (out.continue === false) {
|
|
1374
|
+
const reason = clipDataText(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
1375
|
+
return { deny: { action: "deny", message: reason, ...contextField() } };
|
|
1001
1376
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1377
|
+
if (out.decision === "block") {
|
|
1378
|
+
const reason = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1379
|
+
return { deny: { action: "deny", message: reason, ...contextField() } };
|
|
1380
|
+
}
|
|
1381
|
+
const hso = out.hookSpecificOutput;
|
|
1382
|
+
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
|
|
1383
|
+
const h = hso;
|
|
1384
|
+
if (typeof h.additionalContext === "string")
|
|
1385
|
+
contexts[phase].push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1386
|
+
const decision = h.permissionDecision;
|
|
1387
|
+
if (decision === "deny") {
|
|
1388
|
+
const reason = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1389
|
+
return { deny: { action: "deny", message: reason, ...contextField() } };
|
|
1390
|
+
}
|
|
1391
|
+
if (decision === "ask") {
|
|
1392
|
+
asked = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
|
|
1393
|
+
}
|
|
1394
|
+
if (decision === "allow" || decision === undefined) {
|
|
1395
|
+
const proposed = h.updatedInput !== null && typeof h.updatedInput === "object" ? h.updatedInput : undefined;
|
|
1396
|
+
const proposesRewrite = proposed !== undefined && canonicalJson(proposed) !== canonicalJson(groupInput);
|
|
1397
|
+
if (ran.distrust !== undefined && hookPhaseOf(ran.source) === "unmanaged" && (decision === "allow" || proposesRewrite)) {
|
|
1398
|
+
noteVerdictUntrusted(ctx, { event: "PreToolUse", reason: ran.distrust, entry, source: ran.source, toolName });
|
|
1399
|
+
}
|
|
1400
|
+
const offered = ran.distrust === undefined ? proposed : undefined;
|
|
1401
|
+
const rewrites = offered !== undefined && proposesRewrite;
|
|
1402
|
+
if (rewrites) {
|
|
1403
|
+
currentInput = offered;
|
|
1404
|
+
rewritten = true;
|
|
1405
|
+
rewroteBy = ran;
|
|
1406
|
+
}
|
|
1407
|
+
if (decision === "allow" && ran.distrust === undefined)
|
|
1408
|
+
allowedFor = { input: rewrites ? offered : groupInput };
|
|
1409
|
+
}
|
|
1410
|
+
else if (decision !== "deny" && decision !== "ask") {
|
|
1411
|
+
unknownDecision = true;
|
|
1412
|
+
ctx.logger.warn("hook_permission_decision_unknown", {
|
|
1413
|
+
event: "PreToolUse",
|
|
1414
|
+
decision: echoCallerText(typeof decision === "string" ? decision : typeof decision, { maxChars: LOG_PREVIEW_ECHO_CHARS }),
|
|
1415
|
+
tool: toolName,
|
|
1416
|
+
effect: "hook ruling not honored (closed word set is allow|deny|ask); this call falls back to the engine gate",
|
|
1417
|
+
});
|
|
1418
|
+
ledger.unrun(ran.source, entry, "decision_out_of_set");
|
|
1419
|
+
}
|
|
1010
1420
|
}
|
|
1011
1421
|
}
|
|
1012
1422
|
}
|
|
1423
|
+
return { rewroteBy };
|
|
1424
|
+
};
|
|
1425
|
+
for (const lane of hookLanesOf(pre)) {
|
|
1426
|
+
if (lane.phase === "unmanaged") {
|
|
1427
|
+
const pass = await runPass(lane.phase, lane.groups, onceFired);
|
|
1428
|
+
if (pass.deny !== undefined)
|
|
1429
|
+
return pass.deny;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const onceBefore = new Set(onceFired);
|
|
1433
|
+
const first = await runPass(lane.phase, lane.groups, onceFired);
|
|
1434
|
+
if (first.deny !== undefined)
|
|
1435
|
+
return first.deny;
|
|
1436
|
+
if (first.rewroteBy === undefined)
|
|
1437
|
+
continue;
|
|
1438
|
+
contexts[lane.phase].length = 0;
|
|
1439
|
+
const rejudgeOnce = new Set(onceBefore);
|
|
1440
|
+
const second = await runPass(lane.phase, lane.groups, rejudgeOnce);
|
|
1441
|
+
for (const key of rejudgeOnce)
|
|
1442
|
+
onceFired.add(key);
|
|
1443
|
+
if (second.deny !== undefined)
|
|
1444
|
+
return second.deny;
|
|
1445
|
+
if (second.rewroteBy !== undefined) {
|
|
1446
|
+
ledger.unrun(second.rewroteBy.source, second.rewroteBy.entry, "rewrite_unsettled", "the managed hooks of this phase rewrote the tool input again after being re-run on their own rewrite; approval required");
|
|
1447
|
+
}
|
|
1013
1448
|
}
|
|
1014
1449
|
if (asked !== undefined) {
|
|
1450
|
+
return { action: "ask", message: asked, ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
|
|
1451
|
+
}
|
|
1452
|
+
if (ledger.managedWithoutVerdict) {
|
|
1015
1453
|
return {
|
|
1016
1454
|
action: "ask",
|
|
1017
|
-
message:
|
|
1455
|
+
message: "a managed PreToolUse hook did not run to a verdict — approval required",
|
|
1018
1456
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
1019
|
-
...
|
|
1457
|
+
...contextField(),
|
|
1020
1458
|
};
|
|
1021
1459
|
}
|
|
1022
1460
|
if (unknownDecision) {
|
|
@@ -1024,60 +1462,63 @@ export function createTaskHooks(config, ctx) {
|
|
|
1024
1462
|
action: "ask",
|
|
1025
1463
|
message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
|
|
1026
1464
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
1027
|
-
...
|
|
1465
|
+
...contextField(),
|
|
1028
1466
|
};
|
|
1029
1467
|
}
|
|
1030
|
-
const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...
|
|
1468
|
+
const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
|
|
1031
1469
|
if (allowedFor !== undefined && allowedFor.input === currentInput)
|
|
1032
1470
|
return { action: "allow", ...carried };
|
|
1033
|
-
if (rewritten || contexts.length > 0)
|
|
1471
|
+
if (rewritten || HOOK_PHASES.some((p) => contexts[p].length > 0))
|
|
1034
1472
|
return carried;
|
|
1035
1473
|
return undefined;
|
|
1036
1474
|
};
|
|
1037
|
-
hooks.preToolUse = preToolUseHook;
|
|
1038
1475
|
}
|
|
1039
1476
|
if (post) {
|
|
1040
|
-
|
|
1477
|
+
translators.PostToolUse = async (e, signal) => {
|
|
1478
|
+
const toolName = e.tool;
|
|
1479
|
+
const input = e.input;
|
|
1480
|
+
const output = e.output;
|
|
1041
1481
|
const contexts = [];
|
|
1042
1482
|
const responseText = output.content
|
|
1043
1483
|
.map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
|
|
1044
1484
|
.join("");
|
|
1045
1485
|
const payload = {
|
|
1046
|
-
...basePayload(ctx,
|
|
1486
|
+
...basePayload(ctx, e.permissionMode),
|
|
1047
1487
|
hook_event_name: "PostToolUse",
|
|
1048
1488
|
tool_name: toolName,
|
|
1049
1489
|
tool_input: input,
|
|
1050
|
-
tool_response: { isError: output.isError, text:
|
|
1051
|
-
tool_use_id:
|
|
1490
|
+
tool_response: { isError: output.isError, text: clipDataText(responseText, MAX_TOOL_RESPONSE_CHARS) },
|
|
1491
|
+
tool_use_id: e.tool_use_id,
|
|
1052
1492
|
};
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1493
|
+
const ledger = createUnrunLedger(ctx, "PostToolUse", toolName);
|
|
1494
|
+
const singles = await runEventLanes("PostToolUse", post, toolName, payload, ctx, onceFired, signal, ledger);
|
|
1495
|
+
for (const ran of singles) {
|
|
1496
|
+
const { entry, run } = ran;
|
|
1055
1497
|
if (run.code === 2) {
|
|
1056
|
-
const fb =
|
|
1498
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1057
1499
|
if (fb)
|
|
1058
1500
|
contexts.push(fb);
|
|
1059
1501
|
continue;
|
|
1060
1502
|
}
|
|
1061
1503
|
if (run.code !== 0) {
|
|
1062
|
-
ctx
|
|
1063
|
-
noteHookFailure(ctx, { event: "PostToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
1504
|
+
noteExitNonzero(ctx, ledger, "PostToolUse", ran, toolName);
|
|
1064
1505
|
continue;
|
|
1065
1506
|
}
|
|
1066
|
-
const out = readHookStdout(
|
|
1507
|
+
const out = readHookStdout(ran, "PostToolUse", ctx, payload, ledger);
|
|
1067
1508
|
if (!out)
|
|
1068
1509
|
continue;
|
|
1069
1510
|
if (typeof out.systemMessage === "string")
|
|
1070
|
-
ctx
|
|
1511
|
+
logHookSystemMessage(ctx, "PostToolUse", out.systemMessage);
|
|
1071
1512
|
if (out.continue === false) {
|
|
1072
1513
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
|
|
1073
1514
|
}
|
|
1074
1515
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1075
|
-
contexts.push(
|
|
1516
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1076
1517
|
const hso = out.hookSpecificOutput;
|
|
1077
1518
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUse") {
|
|
1078
1519
|
const h = hso;
|
|
1079
1520
|
if (typeof h.additionalContext === "string")
|
|
1080
|
-
contexts.push(
|
|
1521
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1081
1522
|
if (h.updatedMCPToolOutput !== undefined) {
|
|
1082
1523
|
ctx.logger.warn("hook_updated_mcp_output_unsupported", { event: "PostToolUse" });
|
|
1083
1524
|
}
|
|
@@ -1086,139 +1527,140 @@ export function createTaskHooks(config, ctx) {
|
|
|
1086
1527
|
const additionalContext = buildHookContext(contexts);
|
|
1087
1528
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1088
1529
|
};
|
|
1089
|
-
hooks.postToolUse = postToolUseHook;
|
|
1090
1530
|
}
|
|
1091
1531
|
if (postFailure) {
|
|
1092
|
-
|
|
1532
|
+
translators.PostToolUseFailure = async (e, signal) => {
|
|
1533
|
+
const toolName = e.tool;
|
|
1534
|
+
const f = e.failure;
|
|
1093
1535
|
const contexts = [];
|
|
1094
1536
|
const payload = {
|
|
1095
|
-
...basePayload(ctx,
|
|
1537
|
+
...basePayload(ctx, e.permissionMode),
|
|
1096
1538
|
hook_event_name: "PostToolUseFailure",
|
|
1097
1539
|
tool_name: toolName,
|
|
1098
|
-
tool_input: input,
|
|
1099
|
-
tool_use_id:
|
|
1100
|
-
error:
|
|
1540
|
+
tool_input: e.input,
|
|
1541
|
+
tool_use_id: e.tool_use_id,
|
|
1542
|
+
error: clipDataText(f.error, MAX_TOOL_RESPONSE_CHARS),
|
|
1101
1543
|
is_interrupt: f.isInterrupt,
|
|
1102
1544
|
};
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1545
|
+
const ledger = createUnrunLedger(ctx, "PostToolUseFailure", toolName);
|
|
1546
|
+
const singles = await runEventLanes("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, signal, ledger);
|
|
1547
|
+
for (const ran of singles) {
|
|
1548
|
+
const { entry, run } = ran;
|
|
1105
1549
|
if (run.code === 2) {
|
|
1106
|
-
const fb =
|
|
1550
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1107
1551
|
if (fb)
|
|
1108
1552
|
contexts.push(fb);
|
|
1109
1553
|
continue;
|
|
1110
1554
|
}
|
|
1111
1555
|
if (run.code !== 0) {
|
|
1112
|
-
ctx
|
|
1113
|
-
noteHookFailure(ctx, { event: "PostToolUseFailure", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
1556
|
+
noteExitNonzero(ctx, ledger, "PostToolUseFailure", ran, toolName);
|
|
1114
1557
|
continue;
|
|
1115
1558
|
}
|
|
1116
|
-
const out = readHookStdout(
|
|
1559
|
+
const out = readHookStdout(ran, "PostToolUseFailure", ctx, payload, ledger);
|
|
1117
1560
|
if (!out)
|
|
1118
1561
|
continue;
|
|
1119
1562
|
if (typeof out.systemMessage === "string")
|
|
1120
|
-
ctx
|
|
1563
|
+
logHookSystemMessage(ctx, "PostToolUseFailure", out.systemMessage);
|
|
1121
1564
|
if (out.continue === false)
|
|
1122
1565
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
|
|
1123
1566
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1124
|
-
contexts.push(
|
|
1567
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1125
1568
|
const hso = out.hookSpecificOutput;
|
|
1126
1569
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUseFailure") {
|
|
1127
1570
|
const h = hso;
|
|
1128
1571
|
if (typeof h.additionalContext === "string")
|
|
1129
|
-
contexts.push(
|
|
1572
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1130
1573
|
}
|
|
1131
1574
|
}
|
|
1132
1575
|
const additionalContext = buildHookContext(contexts);
|
|
1133
1576
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1134
1577
|
};
|
|
1135
|
-
hooks.postToolUseFailure = postToolUseFailureHook;
|
|
1136
1578
|
}
|
|
1137
1579
|
if (postBatch) {
|
|
1138
|
-
|
|
1580
|
+
translators.PostToolBatch = async (e, signal) => {
|
|
1139
1581
|
const contexts = [];
|
|
1140
1582
|
const payload = {
|
|
1141
|
-
...basePayload(ctx,
|
|
1583
|
+
...basePayload(ctx, e.permissionMode),
|
|
1142
1584
|
hook_event_name: "PostToolBatch",
|
|
1143
|
-
tool_calls:
|
|
1585
|
+
tool_calls: e.batch.map((c) => ({
|
|
1144
1586
|
tool_name: c.toolName,
|
|
1145
1587
|
tool_input: boundedInputValue(c.input),
|
|
1146
1588
|
tool_use_id: c.toolCallId,
|
|
1147
|
-
tool_response: { isError: c.isError, ...(c.response !== undefined ? { text:
|
|
1589
|
+
tool_response: { isError: c.isError, ...(c.response !== undefined ? { text: clipDataText(c.response, MAX_TOOL_RESPONSE_CHARS) } : {}) },
|
|
1148
1590
|
})),
|
|
1149
1591
|
};
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1592
|
+
const ledger = createUnrunLedger(ctx, "PostToolBatch", undefined);
|
|
1593
|
+
const singles = await runEventLanes("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, signal, ledger);
|
|
1594
|
+
for (const ran of singles) {
|
|
1595
|
+
const { entry, run } = ran;
|
|
1152
1596
|
if (run.code === 2) {
|
|
1153
|
-
const fb =
|
|
1597
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1154
1598
|
if (fb)
|
|
1155
1599
|
contexts.push(fb);
|
|
1156
1600
|
continue;
|
|
1157
1601
|
}
|
|
1158
1602
|
if (run.code !== 0) {
|
|
1159
|
-
ctx
|
|
1160
|
-
noteHookFailure(ctx, { event: "PostToolBatch", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1603
|
+
noteExitNonzero(ctx, ledger, "PostToolBatch", ran);
|
|
1161
1604
|
continue;
|
|
1162
1605
|
}
|
|
1163
|
-
const out = readHookStdout(
|
|
1606
|
+
const out = readHookStdout(ran, "PostToolBatch", ctx, payload, ledger);
|
|
1164
1607
|
if (!out)
|
|
1165
1608
|
continue;
|
|
1166
1609
|
if (typeof out.systemMessage === "string")
|
|
1167
|
-
ctx
|
|
1610
|
+
logHookSystemMessage(ctx, "PostToolBatch", out.systemMessage);
|
|
1168
1611
|
if (out.continue === false)
|
|
1169
1612
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
|
|
1170
1613
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1171
|
-
contexts.push(
|
|
1614
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1172
1615
|
if (out.hookSpecificOutput !== undefined)
|
|
1173
1616
|
ctx.logger.warn("hook_result_unsupported", { event: "PostToolBatch" });
|
|
1174
1617
|
}
|
|
1175
1618
|
const additionalContext = buildHookContext(contexts);
|
|
1176
1619
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1177
1620
|
};
|
|
1178
|
-
hooks.postToolBatch = postToolBatchHook;
|
|
1179
1621
|
}
|
|
1180
1622
|
if (promptSubmit) {
|
|
1181
|
-
|
|
1623
|
+
translators.UserPromptSubmit = async (e, signal) => {
|
|
1182
1624
|
const contexts = [];
|
|
1183
|
-
const payload = { ...basePayload(ctx,
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1625
|
+
const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clipDataText(e.prompt, MAX_TOOL_INPUT_CHARS) };
|
|
1626
|
+
const ledger = createUnrunLedger(ctx, "UserPromptSubmit", undefined);
|
|
1627
|
+
const singles = await runEventLanes("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, signal, ledger);
|
|
1628
|
+
for (const ran of singles) {
|
|
1629
|
+
const { entry, run } = ran;
|
|
1186
1630
|
if (run.code === 2) {
|
|
1187
|
-
const reason =
|
|
1631
|
+
const reason = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
|
|
1188
1632
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1189
1633
|
}
|
|
1190
1634
|
if (run.code !== 0) {
|
|
1191
|
-
ctx
|
|
1192
|
-
noteHookFailure(ctx, { event: "UserPromptSubmit", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1635
|
+
noteExitNonzero(ctx, ledger, "UserPromptSubmit", ran);
|
|
1193
1636
|
continue;
|
|
1194
1637
|
}
|
|
1195
|
-
const out = readHookStdout(
|
|
1638
|
+
const out = readHookStdout(ran, "UserPromptSubmit", ctx, payload, ledger);
|
|
1196
1639
|
if (!out)
|
|
1197
1640
|
continue;
|
|
1198
1641
|
if (typeof out.systemMessage === "string")
|
|
1199
|
-
ctx
|
|
1642
|
+
logHookSystemMessage(ctx, "UserPromptSubmit", out.systemMessage);
|
|
1200
1643
|
if (out.continue === false) {
|
|
1201
|
-
const reason =
|
|
1644
|
+
const reason = clipDataText(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
1202
1645
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1203
1646
|
}
|
|
1204
1647
|
if (out.decision === "block") {
|
|
1205
|
-
const reason =
|
|
1648
|
+
const reason = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a UserPromptSubmit hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1206
1649
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1207
1650
|
}
|
|
1208
1651
|
const hso = out.hookSpecificOutput;
|
|
1209
1652
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "UserPromptSubmit") {
|
|
1210
1653
|
const h = hso;
|
|
1211
1654
|
if (typeof h.additionalContext === "string")
|
|
1212
|
-
contexts.push(
|
|
1655
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1213
1656
|
}
|
|
1214
1657
|
}
|
|
1215
1658
|
const additionalContext = buildHookContext(contexts);
|
|
1216
1659
|
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1217
1660
|
};
|
|
1218
|
-
hooks.userPromptSubmit = userPromptSubmitHook;
|
|
1219
1661
|
}
|
|
1220
1662
|
if (stopEntries) {
|
|
1221
|
-
|
|
1663
|
+
translators.Stop = async (sctx, signal) => {
|
|
1222
1664
|
const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
|
|
1223
1665
|
let llmExtra;
|
|
1224
1666
|
const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
|
|
@@ -1233,7 +1675,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1233
1675
|
branch = await sctx.getBranch?.();
|
|
1234
1676
|
}
|
|
1235
1677
|
catch (e) {
|
|
1236
|
-
ctx.logger.warn("hook_stop_branch_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
1678
|
+
ctx.logger.warn("hook_stop_branch_failed", { error: echoCallerText(e instanceof Error ? e.message : String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
|
|
1237
1679
|
}
|
|
1238
1680
|
const rendered = renderBranchTranscript(branch);
|
|
1239
1681
|
if (rendered.sufficient) {
|
|
@@ -1246,22 +1688,29 @@ export function createTaskHooks(config, ctx) {
|
|
|
1246
1688
|
}
|
|
1247
1689
|
}
|
|
1248
1690
|
const stopGroups = ignoreMatchers(stopEntries);
|
|
1691
|
+
const ledger = createUnrunLedger(ctx, "Stop", undefined);
|
|
1692
|
+
if (!llmExtra && wantsPrompt) {
|
|
1693
|
+
for (const g of stopGroups)
|
|
1694
|
+
for (const h of g.hooks)
|
|
1695
|
+
if (h.type === "prompt")
|
|
1696
|
+
ledger.unrun(g.source, h, "evidence_unavailable", ctx.stopPromptTranscript === false ? "transcript disabled" : "no transcript evidence");
|
|
1697
|
+
}
|
|
1249
1698
|
const effectiveGroups = llmExtra || !wantsPrompt
|
|
1250
1699
|
? stopGroups
|
|
1251
1700
|
: stopGroups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => h.type !== "prompt") })).filter((g) => g.hooks.length > 0);
|
|
1252
|
-
const singles = await
|
|
1701
|
+
const singles = await runEventLanes("Stop", effectiveGroups, "", payload, ctx, onceFired, signal, ledger, llmExtra);
|
|
1253
1702
|
let block;
|
|
1254
1703
|
let sawContinueFalse = false;
|
|
1255
1704
|
const contexts = [];
|
|
1256
|
-
for (const
|
|
1705
|
+
for (const ran of singles) {
|
|
1706
|
+
const { entry, run } = ran;
|
|
1257
1707
|
if (run.code === 2) {
|
|
1258
1708
|
if (block === undefined)
|
|
1259
|
-
block =
|
|
1709
|
+
block = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a Stop hook";
|
|
1260
1710
|
continue;
|
|
1261
1711
|
}
|
|
1262
1712
|
if (run.code !== 0) {
|
|
1263
|
-
ctx
|
|
1264
|
-
noteHookFailure(ctx, { event: "Stop", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1713
|
+
noteExitNonzero(ctx, ledger, "Stop", ran);
|
|
1265
1714
|
continue;
|
|
1266
1715
|
}
|
|
1267
1716
|
if (llmExtra && entry.type === "prompt" && parseCcVerdict(run.stdout)) {
|
|
@@ -1269,33 +1718,34 @@ export function createTaskHooks(config, ctx) {
|
|
|
1269
1718
|
if (v.ok)
|
|
1270
1719
|
continue;
|
|
1271
1720
|
if (v.impossible) {
|
|
1272
|
-
ctx.logger.warn("hook_stop_condition_impossible", { event: "Stop", reason:
|
|
1721
|
+
ctx.logger.warn("hook_stop_condition_impossible", { event: "Stop", reason: echoCallerText(v.reason?.trim() ?? "", { maxChars: LOG_MESSAGE_ECHO_CHARS }) });
|
|
1273
1722
|
continue;
|
|
1274
1723
|
}
|
|
1275
1724
|
if (block === undefined)
|
|
1276
|
-
block =
|
|
1725
|
+
block = clipDataText(v.reason?.trim() || "stop condition not satisfied", MAX_HOOK_FEEDBACK_CHARS);
|
|
1277
1726
|
continue;
|
|
1278
1727
|
}
|
|
1279
|
-
const out = llmExtra && entry.type === "prompt" ? parseHookStdout(run.stdout) : readHookStdout(
|
|
1728
|
+
const out = llmExtra && entry.type === "prompt" ? parseHookStdout(run.stdout) : readHookStdout(ran, "Stop", ctx, payload, ledger);
|
|
1280
1729
|
if (!out) {
|
|
1281
1730
|
if (llmExtra && entry.type === "prompt") {
|
|
1282
|
-
ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head:
|
|
1731
|
+
ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head: echoCallerText(run.stdout.trim(), { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
|
|
1283
1732
|
emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: "Stop", reason: "unparsed" });
|
|
1733
|
+
ledger.unrun(ran.source, entry, "bad_json", "the prompt hook's answer could not be read as a verdict");
|
|
1284
1734
|
}
|
|
1285
1735
|
continue;
|
|
1286
1736
|
}
|
|
1287
1737
|
if (typeof out.systemMessage === "string")
|
|
1288
|
-
ctx
|
|
1738
|
+
logHookSystemMessage(ctx, "Stop", out.systemMessage);
|
|
1289
1739
|
if (out.continue === false)
|
|
1290
1740
|
sawContinueFalse = true;
|
|
1291
1741
|
if (out.decision === "block" && block === undefined) {
|
|
1292
|
-
block =
|
|
1742
|
+
block = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a Stop hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1293
1743
|
}
|
|
1294
1744
|
const hso = out.hookSpecificOutput;
|
|
1295
1745
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "Stop") {
|
|
1296
1746
|
const h = hso;
|
|
1297
1747
|
if (typeof h.additionalContext === "string")
|
|
1298
|
-
contexts.push(
|
|
1748
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1299
1749
|
}
|
|
1300
1750
|
}
|
|
1301
1751
|
if (sawContinueFalse)
|
|
@@ -1307,236 +1757,73 @@ export function createTaskHooks(config, ctx) {
|
|
|
1307
1757
|
...buildHookContextField(contexts),
|
|
1308
1758
|
};
|
|
1309
1759
|
};
|
|
1310
|
-
hooks.stop = stopHook;
|
|
1311
1760
|
}
|
|
1312
1761
|
if (stopFailureEntries) {
|
|
1313
|
-
|
|
1762
|
+
translators.StopFailure = async (fctx, signal) => {
|
|
1314
1763
|
const turnError = toAssistantTurnError(fctx.errorKind);
|
|
1315
1764
|
const payload = {
|
|
1316
1765
|
...basePayload(ctx, undefined),
|
|
1317
1766
|
hook_event_name: "StopFailure",
|
|
1318
1767
|
error: turnError,
|
|
1319
|
-
...(fctx.error ? { error_details:
|
|
1768
|
+
...(fctx.error ? { error_details: clipDataText(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
|
|
1320
1769
|
};
|
|
1321
|
-
await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
|
|
1770
|
+
await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired, signal);
|
|
1322
1771
|
};
|
|
1323
|
-
hooks.stopFailure = stopFailureHook;
|
|
1324
1772
|
}
|
|
1325
1773
|
if (preCompactEntries) {
|
|
1326
|
-
|
|
1774
|
+
translators.PreCompact = async (pctx, signal) => {
|
|
1327
1775
|
const payload = {
|
|
1328
1776
|
...basePayload(ctx, undefined),
|
|
1329
1777
|
hook_event_name: "PreCompact",
|
|
1330
1778
|
trigger: pctx.trigger,
|
|
1331
1779
|
custom_instructions: pctx.customInstructions ?? null,
|
|
1332
1780
|
};
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1781
|
+
const ledger = createUnrunLedger(ctx, "PreCompact", undefined);
|
|
1782
|
+
const singles = await runEventLanes("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, signal, ledger);
|
|
1783
|
+
for (const ran of singles) {
|
|
1784
|
+
const { entry, run } = ran;
|
|
1335
1785
|
if (run.code === 2) {
|
|
1336
|
-
return { block:
|
|
1786
|
+
return { block: clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
|
|
1337
1787
|
}
|
|
1338
1788
|
if (run.code !== 0) {
|
|
1339
|
-
ctx
|
|
1340
|
-
noteHookFailure(ctx, { event: "PreCompact", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1789
|
+
noteExitNonzero(ctx, ledger, "PreCompact", ran);
|
|
1341
1790
|
continue;
|
|
1342
1791
|
}
|
|
1343
|
-
const out = readHookStdout(
|
|
1792
|
+
const out = readHookStdout(ran, "PreCompact", ctx, payload, ledger);
|
|
1344
1793
|
if (!out)
|
|
1345
1794
|
continue;
|
|
1346
1795
|
if (typeof out.systemMessage === "string")
|
|
1347
|
-
ctx
|
|
1796
|
+
logHookSystemMessage(ctx, "PreCompact", out.systemMessage);
|
|
1348
1797
|
if (out.continue === false) {
|
|
1349
1798
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
|
|
1350
1799
|
}
|
|
1351
1800
|
if (out.decision === "block") {
|
|
1352
|
-
return { block:
|
|
1801
|
+
return { block: clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
|
|
1353
1802
|
}
|
|
1354
1803
|
}
|
|
1355
1804
|
return undefined;
|
|
1356
1805
|
};
|
|
1357
|
-
hooks.preCompact = preCompactHook;
|
|
1358
1806
|
}
|
|
1359
1807
|
if (postCompactEntries) {
|
|
1360
|
-
|
|
1808
|
+
translators.PostCompact = async (cctx, signal) => {
|
|
1361
1809
|
const payload = {
|
|
1362
1810
|
...basePayload(ctx, undefined),
|
|
1363
1811
|
hook_event_name: "PostCompact",
|
|
1364
1812
|
trigger: cctx.trigger,
|
|
1365
|
-
compact_summary:
|
|
1366
|
-
};
|
|
1367
|
-
await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
|
|
1368
|
-
};
|
|
1369
|
-
hooks.postCompact = postCompactHook;
|
|
1370
|
-
}
|
|
1371
|
-
return hooks;
|
|
1372
|
-
}
|
|
1373
|
-
const HOOK_SLOT_FOLD = {
|
|
1374
|
-
preToolUse: "custom",
|
|
1375
|
-
preToolUseObservational: "custom",
|
|
1376
|
-
postToolUse: "custom",
|
|
1377
|
-
postToolUseFailure: "custom",
|
|
1378
|
-
postToolBatch: "custom",
|
|
1379
|
-
userPromptSubmit: "custom",
|
|
1380
|
-
stop: "custom",
|
|
1381
|
-
preCompact: "custom",
|
|
1382
|
-
timeoutMs: "scalar",
|
|
1383
|
-
postCompact: "observe-void",
|
|
1384
|
-
stopFailure: "observe-void",
|
|
1385
|
-
permissionDenied: "observe-void",
|
|
1386
|
-
};
|
|
1387
|
-
const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
|
|
1388
|
-
export function composeHooks(deployment, task) {
|
|
1389
|
-
if (!deployment)
|
|
1390
|
-
return task;
|
|
1391
|
-
const d = deployment;
|
|
1392
|
-
const t = task;
|
|
1393
|
-
const out = {};
|
|
1394
|
-
const timeoutMs = d.timeoutMs ?? t.timeoutMs;
|
|
1395
|
-
if (timeoutMs !== undefined)
|
|
1396
|
-
out.timeoutMs = timeoutMs;
|
|
1397
|
-
if (d.preToolUse || t.preToolUse) {
|
|
1398
|
-
const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
|
|
1399
|
-
(t.preToolUse === undefined || t.preToolUseObservational === true);
|
|
1400
|
-
if (bothObservational)
|
|
1401
|
-
out.preToolUseObservational = true;
|
|
1402
|
-
out.preToolUse = async (toolName, input, ctx) => {
|
|
1403
|
-
let current = input;
|
|
1404
|
-
const contexts = [];
|
|
1405
|
-
let asked;
|
|
1406
|
-
let allowedFor;
|
|
1407
|
-
for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
|
|
1408
|
-
if (!slot)
|
|
1409
|
-
continue;
|
|
1410
|
-
const r = receiveHookResult(await slot(toolName, current, ctx));
|
|
1411
|
-
if (r === undefined || r === null || r === false)
|
|
1412
|
-
continue;
|
|
1413
|
-
if (isEngineRefusal(r))
|
|
1414
|
-
return r;
|
|
1415
|
-
if (r.additionalContext)
|
|
1416
|
-
contexts.push(r.additionalContext);
|
|
1417
|
-
const action = r.action;
|
|
1418
|
-
const updatedInput = r.updatedInput;
|
|
1419
|
-
switch (action) {
|
|
1420
|
-
case "deny":
|
|
1421
|
-
return { ...r, ...buildHookContextField(contexts) };
|
|
1422
|
-
case "ask":
|
|
1423
|
-
asked = r;
|
|
1424
|
-
break;
|
|
1425
|
-
case "allow":
|
|
1426
|
-
if (updatedInput !== undefined)
|
|
1427
|
-
current = updatedInput;
|
|
1428
|
-
allowedFor = { input: current };
|
|
1429
|
-
break;
|
|
1430
|
-
case undefined:
|
|
1431
|
-
if (updatedInput !== undefined)
|
|
1432
|
-
current = updatedInput;
|
|
1433
|
-
break;
|
|
1434
|
-
default: {
|
|
1435
|
-
const unread = action;
|
|
1436
|
-
void unread;
|
|
1437
|
-
return r;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (asked) {
|
|
1442
|
-
const askedUpdated = asked.updatedInput;
|
|
1443
|
-
return {
|
|
1444
|
-
...asked,
|
|
1445
|
-
...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
|
|
1446
|
-
...buildHookContextField(contexts),
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
|
|
1450
|
-
if (allowedFor !== undefined && allowedFor.input === current)
|
|
1451
|
-
return { action: "allow", ...carried };
|
|
1452
|
-
if (current !== input || contexts.length > 0)
|
|
1453
|
-
return carried;
|
|
1454
|
-
return undefined;
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
if (d.postToolUse || t.postToolUse) {
|
|
1458
|
-
out.postToolUse = async (toolName, input, output, ctx) => {
|
|
1459
|
-
const rd = d.postToolUse ? await d.postToolUse(toolName, input, output, ctx) : undefined;
|
|
1460
|
-
const rt = t.postToolUse ? await t.postToolUse(toolName, input, output, ctx) : undefined;
|
|
1461
|
-
if (!rd && !rt)
|
|
1462
|
-
return undefined;
|
|
1463
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1464
|
-
const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
|
|
1465
|
-
return {
|
|
1466
|
-
...(updatedOutput ? { updatedOutput } : {}),
|
|
1467
|
-
...buildHookContextField(contexts),
|
|
1468
|
-
};
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
if (d.postToolUseFailure || t.postToolUseFailure) {
|
|
1472
|
-
out.postToolUseFailure = async (toolName, input, failure, ctx) => {
|
|
1473
|
-
const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1474
|
-
const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1475
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1476
|
-
const additionalContext = buildHookContext(contexts);
|
|
1477
|
-
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
if (d.postToolBatch || t.postToolBatch) {
|
|
1481
|
-
out.postToolBatch = async (calls, meta, bctx) => {
|
|
1482
|
-
const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
|
|
1483
|
-
const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
|
|
1484
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1485
|
-
const additionalContext = buildHookContext(contexts);
|
|
1486
|
-
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
if (d.userPromptSubmit || t.userPromptSubmit) {
|
|
1490
|
-
out.userPromptSubmit = async (prompt, uctx) => {
|
|
1491
|
-
const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
|
|
1492
|
-
if (rd?.block)
|
|
1493
|
-
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1494
|
-
const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
|
|
1495
|
-
if (!rd && !rt)
|
|
1496
|
-
return undefined;
|
|
1497
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1498
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
if (d.stop || t.stop) {
|
|
1502
|
-
out.stop = async (sctx) => {
|
|
1503
|
-
const rd = d.stop ? await d.stop(sctx) : undefined;
|
|
1504
|
-
if (rd?.block)
|
|
1505
|
-
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1506
|
-
const rt = t.stop ? await t.stop(sctx) : undefined;
|
|
1507
|
-
if (!rd && !rt)
|
|
1508
|
-
return undefined;
|
|
1509
|
-
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1510
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
if (d.preCompact || t.preCompact) {
|
|
1514
|
-
out.preCompact = async (pctx) => {
|
|
1515
|
-
const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
|
|
1516
|
-
const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
|
|
1517
|
-
if (!rd && !rt)
|
|
1518
|
-
return undefined;
|
|
1519
|
-
const block = rd?.block ?? rt?.block;
|
|
1520
|
-
const instructions = [rd?.additionalInstructions, rt?.additionalInstructions].filter((c) => typeof c === "string" && c.length > 0);
|
|
1521
|
-
return {
|
|
1522
|
-
...(block !== undefined ? { block } : {}),
|
|
1523
|
-
...(instructions.length ? { additionalInstructions: instructions.join("\n") } : {}),
|
|
1813
|
+
compact_summary: clipDataText(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
|
|
1524
1814
|
};
|
|
1815
|
+
await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired, signal);
|
|
1525
1816
|
};
|
|
1526
1817
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
out[slot] = (ds ?? ts);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
return out;
|
|
1818
|
+
const events = Object.keys(translators);
|
|
1819
|
+
return {
|
|
1820
|
+
name: SETTINGS_HOOKS_REGISTRATION,
|
|
1821
|
+
uses: { events: events.flatMap((event) => SETTINGS_HOOK_STATIONS[event]), calls: [] },
|
|
1822
|
+
budgetMs,
|
|
1823
|
+
register(on) {
|
|
1824
|
+
for (const event of events)
|
|
1825
|
+
hangTranslator(on, event, translators);
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1541
1828
|
}
|
|
1542
1829
|
//# sourceMappingURL=hook-runner.js.map
|