@sema-agent/server 7.132.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 +266 -0
- package/README.md +8 -9
- package/README.zh-CN.md +7 -6
- package/USAGE.md +174 -55
- 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 +79 -19
- package/dist/approval-card.js +44 -13
- 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 +27 -10
- package/dist/audit.js +12 -16
- package/dist/bake-runner/main.js +3 -2
- package/dist/boot/deferred-sandbox-path-env.js +1 -1
- package/dist/boot/execution-env.js +9 -7
- package/dist/boot/execution-lane.js +11 -0
- package/dist/boot/leader.js +2 -0
- 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/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +21 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +60 -81
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/session-faces.d.ts +1 -0
- package/dist/boot/session-faces.js +4 -2
- package/dist/boot/stage-02-web-admin-pull.js +4 -1
- package/dist/boot/stage-06-runners.d.ts +2 -0
- package/dist/boot/stage-06-runners.js +6 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -0
- package/dist/boot/stage-07-capability-layer.js +24 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -0
- package/dist/boot/stage-10-http-server.d.ts +3 -0
- package/dist/boot/stage-10-http-server.js +5 -6
- package/dist/boot/stores.js +2 -2
- package/dist/boot/web-admin.d.ts +4 -0
- package/dist/boot/web-admin.js +151 -63
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +2 -1
- package/dist/brain.js +8 -3
- package/dist/breaker-key.d.ts +6 -0
- package/dist/breaker-key.js +29 -0
- package/dist/caller-text-echo.d.ts +9 -6
- package/dist/caller-text-echo.js +8 -11
- package/dist/capabilities/center-plugins.d.ts +7 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/execution-lane.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +21 -0
- package/dist/capabilities/hands-lane.js +10 -0
- package/dist/capabilities/mcp-probe.d.ts +36 -4
- package/dist/capabilities/mcp-probe.js +27 -4
- package/dist/capabilities/scenarios.d.ts +63 -7
- package/dist/capabilities/scenarios.js +39 -7
- 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 +43 -28
- 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-provider.d.ts +6 -1
- package/dist/config-provider.js +6 -2
- package/dist/config-types.d.ts +71 -14
- package/dist/config-types.js +8 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +121 -59
- 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 -23
- package/dist/elicitation.js +3 -2
- package/dist/entry-problems.d.ts +34 -0
- package/dist/entry-problems.js +14 -0
- package/dist/execution-lane-caps.d.ts +49 -10
- package/dist/execution-lane-caps.js +43 -4
- 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 +10 -5
- package/dist/hooks/classic-stations.js +4 -4
- package/dist/hooks/command-spawn.d.ts +57 -0
- package/dist/hooks/command-spawn.js +139 -0
- package/dist/hooks/hook-llm.js +2 -2
- package/dist/hooks/hook-runner.d.ts +127 -25
- package/dist/hooks/hook-runner.js +668 -287
- 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.js +103 -60
- package/dist/http/approval-sweeps.js +55 -18
- package/dist/http/caller-trust.d.ts +77 -0
- package/dist/http/caller-trust.js +143 -0
- package/dist/http/checkpoint-ctx.d.ts +16 -16
- package/dist/http/checkpoint-ctx.js +13 -10
- package/dist/http/claimed-run.d.ts +4 -3
- package/dist/http/device-placement.d.ts +2 -4
- package/dist/http/device-placement.js +2 -1
- package/dist/http/dispatch.js +10 -4
- package/dist/http/durable-run-leg.d.ts +4 -32
- package/dist/http/durable-run-leg.js +34 -10
- package/dist/http/owner-access.d.ts +56 -0
- package/dist/http/owner-access.js +42 -0
- package/dist/http/principal-gate.d.ts +48 -23
- package/dist/http/principal-gate.js +39 -12
- package/dist/http/readiness.d.ts +11 -5
- package/dist/http/readiness.js +10 -3
- package/dist/http/request-guards.d.ts +16 -23
- package/dist/http/request-guards.js +30 -15
- 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 +3 -4
- package/dist/http/resume-legs.js +126 -69
- 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 +20 -4
- 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 +5 -1
- 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 +33 -106
- package/dist/http/routes/session-sync.js +15 -28
- package/dist/http/routes/sessions.js +30 -40
- package/dist/http/routes/side-query.js +6 -4
- package/dist/http/routes/tasks.js +50 -14
- package/dist/http/routes/workflows.d.ts +351 -0
- package/dist/http/routes/workflows.js +153 -119
- package/dist/http/send.d.ts +24 -3
- package/dist/http/send.js +14 -2
- package/dist/http/server.d.ts +17 -18
- package/dist/http/server.js +15 -13
- package/dist/http/stored-body-replay.js +17 -4
- package/dist/http/submit-ctx.js +4 -3
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +5 -4
- package/dist/http/wire-types.d.ts +10 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/lane-home-probe.d.ts +78 -9
- package/dist/lane-home-probe.js +78 -12
- package/dist/leader/endpoint.d.ts +4 -1
- package/dist/leader/endpoint.js +11 -10
- package/dist/limit-refusal.d.ts +17 -5
- package/dist/limit-refusal.js +4 -1
- package/dist/mcp-assertion-header.d.ts +31 -10
- package/dist/mcp-assertion-header.js +10 -3
- package/dist/mcp-tool-faces.js +1 -3
- package/dist/memory-project-layer.d.ts +37 -0
- package/dist/memory-project-layer.js +46 -0
- package/dist/memory-scope.d.ts +23 -16
- 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 +5 -0
- package/dist/model-provider.js +4 -2
- package/dist/model-route-endpoint.d.ts +8 -11
- package/dist/model-route-endpoint.js +4 -7
- 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/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +18 -4
- package/dist/observability/fail-open.d.ts +35 -9
- package/dist/observability/fail-open.js +35 -9
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/prompt-manifest.d.ts +3 -17
- package/dist/observability/prompt-manifest.js +45 -42
- package/dist/observability/tool-trace.js +9 -9
- 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/permission-rule-list.d.ts +1 -1
- package/dist/permission-rule-list.js +2 -2
- 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 +38 -25
- package/dist/plugins/blob-backend.js +29 -19
- 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 +14 -0
- package/dist/plugins/checkpoint-store-sql.js +20 -2
- 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/local-checkpoint-store.d.ts +2 -0
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/local-session-store.d.ts +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 +110 -5
- package/dist/plugins/one-time-migrations.js +320 -8
- package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
- package/dist/plugins/org-delivery-quota-store.js +228 -0
- package/dist/plugins/pg-pool.js +39 -7
- package/dist/plugins/pg-session-storage.d.ts +17 -4
- package/dist/plugins/pg-session-storage.js +29 -6
- package/dist/plugins/remote-env-adb.d.ts +6 -12
- package/dist/plugins/remote-env-adb.js +12 -2
- package/dist/plugins/remote-env-k8s.d.ts +8 -12
- package/dist/plugins/remote-env-k8s.js +23 -5
- package/dist/plugins/remote-env-local-docker.d.ts +10 -15
- package/dist/plugins/remote-env-local-docker.js +12 -9
- package/dist/plugins/remote-env-ssh.d.ts +10 -16
- package/dist/plugins/remote-env-ssh.js +17 -11
- package/dist/plugins/run-store-sql.d.ts +16 -14
- package/dist/plugins/run-store-sql.js +22 -2
- 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 +32 -6
- package/dist/plugins/session-epoch-anchor.js +2 -3
- package/dist/plugins/session-store.d.ts +18 -8
- package/dist/plugins/session-store.js +15 -4
- package/dist/plugins/sql-driver.d.ts +74 -11
- package/dist/plugins/sql-driver.js +210 -12
- package/dist/plugins/sql-errors.d.ts +23 -0
- package/dist/plugins/sql-errors.js +21 -0
- package/dist/plugins/store-backend.d.ts +36 -1
- package/dist/plugins/store-backend.js +40 -14
- 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 +3 -2
- package/dist/plugins/tidb-session-storage.js +2 -2
- package/dist/plugins/tidb-session-store.d.ts +24 -4
- package/dist/plugins/tidb-session-store.js +28 -4
- 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/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 +7 -0
- package/dist/request-key-closure.js +9 -1
- package/dist/request-value-closure.d.ts +5 -0
- package/dist/request-value-closure.js +11 -3
- package/dist/rule-entry-list.d.ts +26 -2
- package/dist/rule-entry-list.js +72 -12
- 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 +36 -6
- package/dist/runs.d.ts +10 -9
- package/dist/runs.js +21 -3
- 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/session-error-wire.d.ts +19 -2
- package/dist/session-error-wire.js +5 -1
- 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 +15 -4
- package/dist/spec-fields.js +21 -2
- package/dist/sql-engine-posture.d.ts +3 -1
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-mcp.js +7 -66
- package/dist/task-settings.d.ts +89 -88
- package/dist/task-settings.js +71 -63
- package/dist/text-bidi.d.ts +0 -4
- package/dist/text-bidi.js +1 -3
- package/dist/tighten-only-seats.d.ts +150 -0
- package/dist/tighten-only-seats.js +196 -0
- package/dist/tool-approval.d.ts +303 -87
- package/dist/tool-approval.js +623 -239
- package/dist/trace/core-keyset-guard.d.ts +104 -22
- package/dist/trace/exit-seat.d.ts +28 -0
- package/dist/trace/exit-seat.js +76 -0
- package/dist/trace/ledger-events.d.ts +3 -2
- package/dist/trace/project.d.ts +80 -12
- package/dist/trace/project.js +41 -28
- 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 +298 -2
- package/dist/trace/wire-projection-faces.js +154 -13
- 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/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/read-face.js +5 -9
- package/dist/web-admin-client/skills-mcp.js +4 -5
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -9
- package/package.json +7 -3
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
|
|
3
3
|
import vm from "node:vm";
|
|
4
|
-
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
|
+
import { hostShell, resolveHostShell, adoptHostForegroundChild, killTreeSoft, killTreeHard } from "../plugins/host-platform.js";
|
|
5
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";
|
|
6
6
|
import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
|
|
7
7
|
import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
|
|
8
8
|
import { describeToolCatalog } from "@sema-agent/core";
|
|
9
9
|
import { hangClassic } from "./classic-stations.js";
|
|
10
|
-
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
10
|
+
import { MAX_HOOK_NOTICE_TEXT_CHARS, boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
11
11
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
12
12
|
import { LIMIT_REASONS } from "../limit-refusal.js";
|
|
13
|
-
import { echoCallerText } from "../caller-text-echo.js";
|
|
13
|
+
import { clipDataText, echoCallerText } from "../caller-text-echo.js";
|
|
14
14
|
import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
|
|
15
|
+
import { canonicalJson } from "../canonical-json.js";
|
|
15
16
|
import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
|
|
16
17
|
import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
|
|
17
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";
|
|
18
21
|
export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
|
|
19
22
|
export const MAX_HOOK_COMMAND_CHARS = 8_192;
|
|
20
23
|
export const MAX_HOOK_TIMEOUT_SECONDS = 600;
|
|
@@ -31,11 +34,16 @@ const HOOK_ENV_ALLOWLIST = [
|
|
|
31
34
|
"PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
|
|
32
35
|
"LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
|
|
33
36
|
];
|
|
34
|
-
function
|
|
35
|
-
|
|
37
|
+
function hookBaseEnv(spawning) {
|
|
38
|
+
return spawning
|
|
36
39
|
? hostSubprocessEnv({ site: "server.hooks.spawn-env", policy: [...HOOK_ENV_ALLOWLIST], allow: [] })
|
|
37
40
|
: composeHostSubprocessEnv({ policy: [...HOOK_ENV_ALLOWLIST], allow: [] });
|
|
38
|
-
|
|
41
|
+
}
|
|
42
|
+
function hookEnvOverBase(layer, spawning) {
|
|
43
|
+
return { ...hookBaseEnv(spawning), ...layer };
|
|
44
|
+
}
|
|
45
|
+
function hookEnv(ctx) {
|
|
46
|
+
return hookEnvOverBase(composeHookEnvLayer(ctx.shellEnv, ctx.cwd, undefined), false);
|
|
39
47
|
}
|
|
40
48
|
export const SETTINGS_HOOK_STATIONS = {
|
|
41
49
|
PreToolUse: ["classic.PreToolUse"],
|
|
@@ -60,6 +68,7 @@ export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
|
|
|
60
68
|
export function settingsHooksBudgetMs(config) {
|
|
61
69
|
return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
|
|
62
70
|
}
|
|
71
|
+
export const HOOK_KILL_GRACE_MS = 1_000;
|
|
63
72
|
const BRAIN_TO_TURN_ERROR = {
|
|
64
73
|
auth: "authentication_failed",
|
|
65
74
|
rate_limit: "rate_limit",
|
|
@@ -92,6 +101,10 @@ function isRunnableHookEntry(entry) {
|
|
|
92
101
|
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
93
102
|
export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
|
|
94
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;
|
|
95
108
|
const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
|
|
96
109
|
function matcherReadsWireToolName(event) {
|
|
97
110
|
return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
|
|
@@ -144,6 +157,10 @@ const MATCHER_EVALUATION = {
|
|
|
144
157
|
regex: { regex: "budgeted", literal: "not_applicable" },
|
|
145
158
|
invalid: { regex: "unreadable", literal: "not_applicable" },
|
|
146
159
|
};
|
|
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
|
+
}
|
|
147
164
|
function judgeMatcher(matcher, depth, ledger, judgesAlias) {
|
|
148
165
|
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
149
166
|
return { kind: "unjudged", reason: "oversize" };
|
|
@@ -165,9 +182,9 @@ function judgeMatcher(matcher, depth, ledger, judgesAlias) {
|
|
|
165
182
|
}
|
|
166
183
|
const selects = (name) => {
|
|
167
184
|
if (evaluate === "inline")
|
|
168
|
-
return
|
|
185
|
+
return matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
|
|
169
186
|
const startedAt = ledger.now();
|
|
170
|
-
const answer =
|
|
187
|
+
const answer = matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
|
|
171
188
|
ledger.spentMs += ledger.now() - startedAt;
|
|
172
189
|
if (answer === "over_budget" || answer === "invalid")
|
|
173
190
|
return answer;
|
|
@@ -205,6 +222,13 @@ const HOOKS_CAPS = {
|
|
|
205
222
|
headers: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event, count) => `settings.hooks.${event}: ${count} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
|
|
206
223
|
header_chars: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` },
|
|
207
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)",
|
|
208
232
|
};
|
|
209
233
|
function hooksCapIssue(limit, event, path, count) {
|
|
210
234
|
const c = HOOKS_CAPS[limit];
|
|
@@ -251,6 +275,8 @@ export function buildHooksRefusal(issue) {
|
|
|
251
275
|
}
|
|
252
276
|
case "unnamed_refusal":
|
|
253
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 } };
|
|
254
280
|
default: {
|
|
255
281
|
const piece = issue;
|
|
256
282
|
const key = piece.key !== undefined ? hooksOutgoingKey(piece.key) : undefined;
|
|
@@ -350,6 +376,7 @@ export function parseHooksConfig(raw) {
|
|
|
350
376
|
return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
|
|
351
377
|
}
|
|
352
378
|
const config = {};
|
|
379
|
+
const rawMember = (node, key) => node !== null && typeof node === "object" && Object.hasOwn(node, key) ? node[key] : undefined;
|
|
353
380
|
for (const key of Object.keys(loaded)) {
|
|
354
381
|
if (!isHookEventName(key)) {
|
|
355
382
|
return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
|
|
@@ -363,17 +390,39 @@ export function parseHooksConfig(raw) {
|
|
|
363
390
|
let entries = 0;
|
|
364
391
|
let firstPastCap;
|
|
365
392
|
const kept = [];
|
|
393
|
+
const rawGroups = rawMember(raw, event);
|
|
366
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" });
|
|
367
400
|
const hooks = [];
|
|
368
|
-
for (const [entryIndex,
|
|
401
|
+
for (const [entryIndex, contractEntry] of group.hooks.entries()) {
|
|
369
402
|
entries++;
|
|
370
403
|
const at = ["hooks", event, groupIndex, "hooks", entryIndex];
|
|
371
404
|
if (entries === MAX_HOOK_ENTRIES_PER_EVENT + 1)
|
|
372
405
|
firstPastCap = at;
|
|
373
|
-
if (!isRunnableHookEntry(
|
|
374
|
-
return hooksRefused({ reason: "unsupported_entry_type", path: [...at, "type"], type:
|
|
375
|
-
|
|
376
|
-
|
|
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)));
|
|
425
|
+
}
|
|
377
426
|
if (entry.type === "http") {
|
|
378
427
|
if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
|
|
379
428
|
return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
|
|
@@ -389,7 +438,7 @@ export function parseHooksConfig(raw) {
|
|
|
389
438
|
}
|
|
390
439
|
hooks.push(entry);
|
|
391
440
|
}
|
|
392
|
-
kept.push(group.matcher !== undefined ? { matcher: group.matcher,
|
|
441
|
+
kept.push({ ...(group.matcher !== undefined ? { matcher: group.matcher } : {}), ...(source !== undefined ? { source } : {}), hooks });
|
|
393
442
|
}
|
|
394
443
|
if (firstPastCap !== undefined)
|
|
395
444
|
return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
|
|
@@ -398,7 +447,18 @@ export function parseHooksConfig(raw) {
|
|
|
398
447
|
return { config };
|
|
399
448
|
}
|
|
400
449
|
export function hookMatcherMatches(matcher, value) {
|
|
401
|
-
|
|
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
|
+
}
|
|
402
462
|
}
|
|
403
463
|
export function hookIfMatches(cond, payload) {
|
|
404
464
|
if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
|
|
@@ -411,10 +471,20 @@ export function hookIfMatches(cond, payload) {
|
|
|
411
471
|
if (!m)
|
|
412
472
|
return "unevaluable";
|
|
413
473
|
const nameOk = hookMatcherMatches(m[1].trim(), toolName);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
}
|
|
418
488
|
const spec = m[2];
|
|
419
489
|
if (spec === undefined || spec === "*" || spec === "")
|
|
420
490
|
return true;
|
|
@@ -433,8 +503,11 @@ export function hookIfMatches(cond, payload) {
|
|
|
433
503
|
const MAX_LLM_ENTRIES_PER_EVENT = 4;
|
|
434
504
|
const DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS = 30;
|
|
435
505
|
const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
|
|
436
|
-
function
|
|
437
|
-
|
|
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 }) });
|
|
438
511
|
}
|
|
439
512
|
function hookDisplayName(entry) {
|
|
440
513
|
if (typeof entry.statusMessage === "string" && entry.statusMessage.length > 0)
|
|
@@ -463,7 +536,7 @@ function emitHookNotice(ctx, n) {
|
|
|
463
536
|
}
|
|
464
537
|
catch (e) {
|
|
465
538
|
recordFailOpen("server.hooks.notice-sink-threw", `kind=${n.kind} event=${n.event}`);
|
|
466
|
-
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 }) });
|
|
467
540
|
}
|
|
468
541
|
}
|
|
469
542
|
function noteHookFailure(ctx, args) {
|
|
@@ -482,6 +555,147 @@ function noteHookFailure(ctx, args) {
|
|
|
482
555
|
...(args.detail !== undefined ? { detail: boundedNoticeText(args.detail) } : {}),
|
|
483
556
|
});
|
|
484
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
|
+
}
|
|
485
699
|
export function buildHookContext(parts) {
|
|
486
700
|
const present = parts.filter((p) => p.length > 0);
|
|
487
701
|
if (present.length === 0)
|
|
@@ -521,24 +735,32 @@ function buildHookContextField(parts) {
|
|
|
521
735
|
const text = buildHookContext(parts);
|
|
522
736
|
return text === undefined ? {} : { additionalContext: text };
|
|
523
737
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
756
|
}
|
|
757
|
+
if (halt.aborted)
|
|
758
|
+
return { code: null, stdout: "", stderr: "", timedOut: false, aborted: {} };
|
|
531
759
|
return new Promise((resolve) => {
|
|
532
760
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
533
761
|
let child;
|
|
534
762
|
try {
|
|
535
|
-
|
|
536
|
-
child = spawn(sh.shell, [...sh.args, entry.command], {
|
|
537
|
-
cwd: ctx.cwd,
|
|
538
|
-
env: hookEnv(ctx, true),
|
|
539
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
540
|
-
detached: true,
|
|
541
|
-
});
|
|
763
|
+
child = spawnPlannedCommand(plan, ctx, ["pipe", "pipe", "pipe"]);
|
|
542
764
|
}
|
|
543
765
|
catch (e) {
|
|
544
766
|
resolve({ code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) });
|
|
@@ -549,16 +771,20 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
549
771
|
let stderr = "";
|
|
550
772
|
let timedOut = false;
|
|
551
773
|
let settled = false;
|
|
774
|
+
let sent;
|
|
552
775
|
const settle = (code) => {
|
|
553
776
|
if (settled)
|
|
554
777
|
return;
|
|
555
778
|
settled = true;
|
|
556
779
|
clearTimeout(timer);
|
|
780
|
+
halt.removeEventListener("abort", onHalt);
|
|
557
781
|
ticket?.release();
|
|
558
|
-
resolve({ code, stdout, stderr, timedOut });
|
|
782
|
+
resolve({ code, stdout, stderr, timedOut, ...(sent !== undefined ? { aborted: { signal: sent } } : {}) });
|
|
559
783
|
};
|
|
560
784
|
const timer = setTimeout(() => {
|
|
561
785
|
timedOut = true;
|
|
786
|
+
if (sent !== undefined)
|
|
787
|
+
sent = "SIGKILL";
|
|
562
788
|
try {
|
|
563
789
|
if (child.pid)
|
|
564
790
|
process.kill(-child.pid, "SIGKILL");
|
|
@@ -570,6 +796,31 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
570
796
|
}
|
|
571
797
|
settle(null);
|
|
572
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 });
|
|
573
824
|
child.stdout?.on("data", (d) => {
|
|
574
825
|
if (stdout.length < MAX_HOOK_OUTPUT_BYTES)
|
|
575
826
|
stdout += d.toString("utf8").slice(0, MAX_HOOK_OUTPUT_BYTES - stdout.length);
|
|
@@ -580,6 +831,7 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
580
831
|
});
|
|
581
832
|
child.on("error", (e) => {
|
|
582
833
|
clearTimeout(timer);
|
|
834
|
+
halt.removeEventListener("abort", onHalt);
|
|
583
835
|
if (!settled) {
|
|
584
836
|
settled = true;
|
|
585
837
|
resolve({ code: null, stdout, stderr, timedOut, spawnError: String(e) });
|
|
@@ -590,18 +842,18 @@ async function runCommandHook(entry, payload, ctx) {
|
|
|
590
842
|
child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
|
|
591
843
|
});
|
|
592
844
|
}
|
|
593
|
-
async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
845
|
+
async function fireAsyncCommandHook(entry, payload, ctx, event, halt) {
|
|
594
846
|
try {
|
|
595
847
|
const rewake = entry.asyncRewake === true;
|
|
596
848
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
597
|
-
await
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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"]);
|
|
605
857
|
const ticket = adoptHostForegroundChild(child);
|
|
606
858
|
let killedByTimeout = false;
|
|
607
859
|
const timer = setTimeout(() => {
|
|
@@ -627,7 +879,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
627
879
|
}
|
|
628
880
|
child.on("error", (e) => {
|
|
629
881
|
clearTimeout(timer);
|
|
630
|
-
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 }) });
|
|
631
883
|
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
632
884
|
});
|
|
633
885
|
child.on("close", (code) => {
|
|
@@ -650,14 +902,14 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
650
902
|
ctx.logger.info("hook_async_rewake_delivered", { event, chars: text.length });
|
|
651
903
|
else
|
|
652
904
|
ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "no_live_stream" });
|
|
653
|
-
}, (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 }) }));
|
|
654
906
|
});
|
|
655
907
|
child.stdin?.on("error", () => { });
|
|
656
908
|
child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
|
|
657
909
|
child.unref();
|
|
658
910
|
}
|
|
659
911
|
catch (e) {
|
|
660
|
-
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 }) });
|
|
661
913
|
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
662
914
|
}
|
|
663
915
|
}
|
|
@@ -682,14 +934,19 @@ function noteAsyncOutcome(entry, ctx, event, payload, outcome) {
|
|
|
682
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)" } : {}),
|
|
683
935
|
});
|
|
684
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
|
+
}
|
|
685
943
|
async function runLlmHook(entry, payload, ctx, extra) {
|
|
686
944
|
const call = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
|
|
687
945
|
if (!call)
|
|
688
946
|
return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: `no ${entry.type} hook carrier wired on this deployment` };
|
|
689
947
|
const defSec = entry.type === "prompt" ? DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS : DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS;
|
|
690
948
|
const timeoutMs = Math.min(entry.timeout ?? defSec, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
691
|
-
const
|
|
692
|
-
const substituted = entry.prompt.split("$ARGUMENTS").join(args);
|
|
949
|
+
const substituted = entry.prompt.split("$ARGUMENTS").join(llmHookArguments(payload).text);
|
|
693
950
|
const prompt = extra?.transcript !== undefined ? `${extra.transcript}\n\n---\n\n${wrapCondition("Stop", substituted)}` : substituted;
|
|
694
951
|
let hardTopTimer;
|
|
695
952
|
const hardTop = new Promise((r) => {
|
|
@@ -719,9 +976,9 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
719
976
|
if (!res.ok) {
|
|
720
977
|
return res.code === "hard_timeout"
|
|
721
978
|
? { code: null, stdout: "", stderr: "", timedOut: true, ...(noticed ? { noticed } : {}) }
|
|
722
|
-
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError:
|
|
979
|
+
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: res.error, ...(noticed ? { noticed } : {}) };
|
|
723
980
|
}
|
|
724
|
-
return { code: 0, stdout:
|
|
981
|
+
return { code: 0, stdout: clipDataText(res.text, 1024 * 1024), stderr: "", timedOut: false };
|
|
725
982
|
}
|
|
726
983
|
finally {
|
|
727
984
|
if (hardTopTimer !== undefined)
|
|
@@ -731,13 +988,15 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
731
988
|
const MAX_BLANKED_REFERENCES_LOGGED = 16;
|
|
732
989
|
async function runHttpHook(entry, payload, ctx, event) {
|
|
733
990
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
734
|
-
const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx
|
|
991
|
+
const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
|
|
735
992
|
if (interpolated.blanked.length > 0) {
|
|
736
993
|
ctx.logger.warn("hook_http_header_env_blanked", {
|
|
737
994
|
event,
|
|
738
995
|
url: buildDisplaySafeUrl(entry.url),
|
|
739
996
|
blankedCount: interpolated.blanked.length,
|
|
740
|
-
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 })),
|
|
741
1000
|
});
|
|
742
1001
|
}
|
|
743
1002
|
const headers = { "content-type": "application/json", ...interpolated.headers };
|
|
@@ -775,7 +1034,7 @@ async function runHttpHook(entry, payload, ctx, event) {
|
|
|
775
1034
|
if (trimmed === "")
|
|
776
1035
|
return { code: 0, stdout: "{}", stderr: "", timedOut: false };
|
|
777
1036
|
if (!parseHookStdout(body)) {
|
|
778
|
-
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 }) });
|
|
779
1038
|
noteHookFailure(ctx, {
|
|
780
1039
|
event: eventOfPayload(payload),
|
|
781
1040
|
entry,
|
|
@@ -784,7 +1043,7 @@ async function runHttpHook(entry, payload, ctx, event) {
|
|
|
784
1043
|
stderr: trimmed,
|
|
785
1044
|
toolName: toolNameOf(payload),
|
|
786
1045
|
});
|
|
787
|
-
return { code: 0, stdout: "", stderr: "", timedOut: false };
|
|
1046
|
+
return { code: 0, stdout: "", stderr: "", timedOut: false, badJson: true };
|
|
788
1047
|
}
|
|
789
1048
|
return { code: 0, stdout: body, stderr: "", timedOut: false };
|
|
790
1049
|
}
|
|
@@ -820,14 +1079,16 @@ function boundedToolInputPayload(payload) {
|
|
|
820
1079
|
const bounded = boundedInputValue(p.tool_input);
|
|
821
1080
|
return bounded === p.tool_input ? payload : { ...p, tool_input: bounded };
|
|
822
1081
|
}
|
|
823
|
-
function readHookStdout(
|
|
1082
|
+
function readHookStdout(ran, event, ctx, payload, ledger) {
|
|
1083
|
+
const { run, entry } = ran;
|
|
824
1084
|
const out = parseHookStdout(run.stdout);
|
|
825
1085
|
if (out !== undefined)
|
|
826
1086
|
return out;
|
|
827
1087
|
const t = run.stdout.trim();
|
|
828
1088
|
if (t.startsWith("{") || t.startsWith("[")) {
|
|
829
|
-
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 }) });
|
|
830
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");
|
|
831
1092
|
}
|
|
832
1093
|
return undefined;
|
|
833
1094
|
}
|
|
@@ -843,7 +1104,7 @@ function parseHookStdout(stdout) {
|
|
|
843
1104
|
return undefined;
|
|
844
1105
|
}
|
|
845
1106
|
}
|
|
846
|
-
function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
1107
|
+
function runRunnableEntry(entry, payload, ctx, event, llmExtra, halt) {
|
|
847
1108
|
switch (entry.type) {
|
|
848
1109
|
case "http":
|
|
849
1110
|
return runHttpHook(entry, payload, ctx, event);
|
|
@@ -852,7 +1113,7 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
|
852
1113
|
case "agent":
|
|
853
1114
|
return runLlmHook(entry, payload, ctx, { system: ccAgentHookSystemFor(event), event });
|
|
854
1115
|
case "command":
|
|
855
|
-
return runCommandHook(entry, payload, ctx);
|
|
1116
|
+
return runCommandHook(entry, payload, ctx, halt);
|
|
856
1117
|
default: {
|
|
857
1118
|
const unreachable = entry;
|
|
858
1119
|
void unreachable;
|
|
@@ -860,79 +1121,162 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
|
860
1121
|
}
|
|
861
1122
|
}
|
|
862
1123
|
}
|
|
863
|
-
|
|
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) {
|
|
864
1128
|
const results = [];
|
|
865
1129
|
let llmEntries = 0;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if (
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (entry.type === "command" && entry.shell === "powershell") {
|
|
894
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "powershell_not_supported" });
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
if (entry.if !== undefined) {
|
|
898
|
-
const iv = hookIfMatches(entry.if, payload);
|
|
899
|
-
if (iv !== true) {
|
|
900
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: iv === "unevaluable" ? "if_unevaluable" : "if_not_matched", if: clip(entry.if, 200) });
|
|
901
|
-
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;
|
|
902
1157
|
}
|
|
903
1158
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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" };
|
|
909
1169
|
}
|
|
910
|
-
if (
|
|
911
|
-
ctx.logger.warn("
|
|
912
|
-
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" };
|
|
913
1173
|
}
|
|
1174
|
+
}
|
|
1175
|
+
if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
|
|
914
1176
|
if (entry.once)
|
|
915
1177
|
onceFired.add(onceKey);
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
+
}
|
|
930
1250
|
}
|
|
931
|
-
results.push({ entry, run });
|
|
932
1251
|
}
|
|
933
1252
|
}
|
|
934
1253
|
return results;
|
|
935
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
|
+
}
|
|
936
1280
|
function basePayload(ctx, permissionMode) {
|
|
937
1281
|
return {
|
|
938
1282
|
session_id: ctx.sessionId,
|
|
@@ -944,27 +1288,20 @@ function basePayload(ctx, permissionMode) {
|
|
|
944
1288
|
function ignoreMatchers(groups) {
|
|
945
1289
|
return groups.map((g) => (g.matcher === undefined ? g : { ...g, matcher: undefined }));
|
|
946
1290
|
}
|
|
947
|
-
async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired) {
|
|
948
|
-
const
|
|
949
|
-
|
|
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;
|
|
950
1296
|
if (run.code !== 0) {
|
|
951
|
-
ctx
|
|
952
|
-
noteHookFailure(ctx, {
|
|
953
|
-
event,
|
|
954
|
-
entry,
|
|
955
|
-
reason: "exit_nonzero",
|
|
956
|
-
exitCode: run.code,
|
|
957
|
-
stderr: run.stderr,
|
|
958
|
-
toolName: toolNameOf(payload),
|
|
959
|
-
...(run.code === 2 ? { detail: `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` } : {}),
|
|
960
|
-
});
|
|
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);
|
|
961
1298
|
continue;
|
|
962
1299
|
}
|
|
963
|
-
const out = readHookStdout(
|
|
1300
|
+
const out = readHookStdout(ran, event, ctx, payload, ledger);
|
|
964
1301
|
if (!out)
|
|
965
1302
|
continue;
|
|
966
1303
|
if (typeof out.systemMessage === "string")
|
|
967
|
-
ctx
|
|
1304
|
+
logHookSystemMessage(ctx, event, out.systemMessage);
|
|
968
1305
|
if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
|
|
969
1306
|
ctx.logger.warn("hook_result_unsupported", { event });
|
|
970
1307
|
}
|
|
@@ -995,98 +1332,129 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
995
1332
|
const onceFired = new Set();
|
|
996
1333
|
const translators = {};
|
|
997
1334
|
if (pre) {
|
|
998
|
-
translators.PreToolUse = async (e) => {
|
|
1335
|
+
translators.PreToolUse = async (e, signal) => {
|
|
999
1336
|
const toolName = e.tool;
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1337
|
+
const ledger = createUnrunLedger(ctx, "PreToolUse", toolName);
|
|
1338
|
+
const deadlineFor = createLaneClocks();
|
|
1339
|
+
let currentInput = e.input;
|
|
1002
1340
|
let rewritten = false;
|
|
1003
1341
|
let asked;
|
|
1004
1342
|
let allowedFor;
|
|
1005
1343
|
let unknownDecision = false;
|
|
1006
|
-
const contexts = [];
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
};
|
|
1026
|
-
const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
|
|
1027
|
-
for (const { entry, run } of singles) {
|
|
1028
|
-
if (run.code === 2) {
|
|
1029
|
-
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
|
|
1030
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
1031
|
-
}
|
|
1032
|
-
if (run.code !== 0) {
|
|
1033
|
-
ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
|
|
1034
|
-
noteHookFailure(ctx, { event: "PreToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
const out = readHookStdout(run, entry, "PreToolUse", ctx, payload);
|
|
1038
|
-
if (!out)
|
|
1039
|
-
continue;
|
|
1040
|
-
if (typeof out.systemMessage === "string")
|
|
1041
|
-
ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
|
|
1042
|
-
if (out.continue === false) {
|
|
1043
|
-
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
1044
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
1045
|
-
}
|
|
1046
|
-
if (out.decision === "block") {
|
|
1047
|
-
const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1048
|
-
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
1049
|
-
}
|
|
1050
|
-
const hso = out.hookSpecificOutput;
|
|
1051
|
-
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
|
|
1052
|
-
const h = hso;
|
|
1053
|
-
if (typeof h.additionalContext === "string")
|
|
1054
|
-
contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1055
|
-
const decision = h.permissionDecision;
|
|
1056
|
-
if (decision === "deny") {
|
|
1057
|
-
const reason = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1058
|
-
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() } };
|
|
1059
1363
|
}
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1364
|
+
if (run.code !== 0) {
|
|
1365
|
+
noteExitNonzero(ctx, ledger, "PreToolUse", ran, toolName);
|
|
1366
|
+
continue;
|
|
1062
1367
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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() } };
|
|
1071
1376
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
+
}
|
|
1080
1420
|
}
|
|
1081
1421
|
}
|
|
1082
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
|
+
}
|
|
1083
1448
|
}
|
|
1084
1449
|
if (asked !== undefined) {
|
|
1450
|
+
return { action: "ask", message: asked, ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
|
|
1451
|
+
}
|
|
1452
|
+
if (ledger.managedWithoutVerdict) {
|
|
1085
1453
|
return {
|
|
1086
1454
|
action: "ask",
|
|
1087
|
-
message:
|
|
1455
|
+
message: "a managed PreToolUse hook did not run to a verdict — approval required",
|
|
1088
1456
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
1089
|
-
...
|
|
1457
|
+
...contextField(),
|
|
1090
1458
|
};
|
|
1091
1459
|
}
|
|
1092
1460
|
if (unknownDecision) {
|
|
@@ -1094,19 +1462,19 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1094
1462
|
action: "ask",
|
|
1095
1463
|
message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
|
|
1096
1464
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
1097
|
-
...
|
|
1465
|
+
...contextField(),
|
|
1098
1466
|
};
|
|
1099
1467
|
}
|
|
1100
|
-
const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...
|
|
1468
|
+
const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
|
|
1101
1469
|
if (allowedFor !== undefined && allowedFor.input === currentInput)
|
|
1102
1470
|
return { action: "allow", ...carried };
|
|
1103
|
-
if (rewritten || contexts.length > 0)
|
|
1471
|
+
if (rewritten || HOOK_PHASES.some((p) => contexts[p].length > 0))
|
|
1104
1472
|
return carried;
|
|
1105
1473
|
return undefined;
|
|
1106
1474
|
};
|
|
1107
1475
|
}
|
|
1108
1476
|
if (post) {
|
|
1109
|
-
translators.PostToolUse = async (e) => {
|
|
1477
|
+
translators.PostToolUse = async (e, signal) => {
|
|
1110
1478
|
const toolName = e.tool;
|
|
1111
1479
|
const input = e.input;
|
|
1112
1480
|
const output = e.output;
|
|
@@ -1119,37 +1487,38 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1119
1487
|
hook_event_name: "PostToolUse",
|
|
1120
1488
|
tool_name: toolName,
|
|
1121
1489
|
tool_input: input,
|
|
1122
|
-
tool_response: { isError: output.isError, text:
|
|
1490
|
+
tool_response: { isError: output.isError, text: clipDataText(responseText, MAX_TOOL_RESPONSE_CHARS) },
|
|
1123
1491
|
tool_use_id: e.tool_use_id,
|
|
1124
1492
|
};
|
|
1125
|
-
const
|
|
1126
|
-
|
|
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;
|
|
1127
1497
|
if (run.code === 2) {
|
|
1128
|
-
const fb =
|
|
1498
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1129
1499
|
if (fb)
|
|
1130
1500
|
contexts.push(fb);
|
|
1131
1501
|
continue;
|
|
1132
1502
|
}
|
|
1133
1503
|
if (run.code !== 0) {
|
|
1134
|
-
ctx
|
|
1135
|
-
noteHookFailure(ctx, { event: "PostToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
1504
|
+
noteExitNonzero(ctx, ledger, "PostToolUse", ran, toolName);
|
|
1136
1505
|
continue;
|
|
1137
1506
|
}
|
|
1138
|
-
const out = readHookStdout(
|
|
1507
|
+
const out = readHookStdout(ran, "PostToolUse", ctx, payload, ledger);
|
|
1139
1508
|
if (!out)
|
|
1140
1509
|
continue;
|
|
1141
1510
|
if (typeof out.systemMessage === "string")
|
|
1142
|
-
ctx
|
|
1511
|
+
logHookSystemMessage(ctx, "PostToolUse", out.systemMessage);
|
|
1143
1512
|
if (out.continue === false) {
|
|
1144
1513
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
|
|
1145
1514
|
}
|
|
1146
1515
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1147
|
-
contexts.push(
|
|
1516
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1148
1517
|
const hso = out.hookSpecificOutput;
|
|
1149
1518
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUse") {
|
|
1150
1519
|
const h = hso;
|
|
1151
1520
|
if (typeof h.additionalContext === "string")
|
|
1152
|
-
contexts.push(
|
|
1521
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1153
1522
|
if (h.updatedMCPToolOutput !== undefined) {
|
|
1154
1523
|
ctx.logger.warn("hook_updated_mcp_output_unsupported", { event: "PostToolUse" });
|
|
1155
1524
|
}
|
|
@@ -1160,7 +1529,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1160
1529
|
};
|
|
1161
1530
|
}
|
|
1162
1531
|
if (postFailure) {
|
|
1163
|
-
translators.PostToolUseFailure = async (e) => {
|
|
1532
|
+
translators.PostToolUseFailure = async (e, signal) => {
|
|
1164
1533
|
const toolName = e.tool;
|
|
1165
1534
|
const f = e.failure;
|
|
1166
1535
|
const contexts = [];
|
|
@@ -1170,36 +1539,37 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1170
1539
|
tool_name: toolName,
|
|
1171
1540
|
tool_input: e.input,
|
|
1172
1541
|
tool_use_id: e.tool_use_id,
|
|
1173
|
-
error:
|
|
1542
|
+
error: clipDataText(f.error, MAX_TOOL_RESPONSE_CHARS),
|
|
1174
1543
|
is_interrupt: f.isInterrupt,
|
|
1175
1544
|
};
|
|
1176
|
-
const
|
|
1177
|
-
|
|
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;
|
|
1178
1549
|
if (run.code === 2) {
|
|
1179
|
-
const fb =
|
|
1550
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1180
1551
|
if (fb)
|
|
1181
1552
|
contexts.push(fb);
|
|
1182
1553
|
continue;
|
|
1183
1554
|
}
|
|
1184
1555
|
if (run.code !== 0) {
|
|
1185
|
-
ctx
|
|
1186
|
-
noteHookFailure(ctx, { event: "PostToolUseFailure", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
1556
|
+
noteExitNonzero(ctx, ledger, "PostToolUseFailure", ran, toolName);
|
|
1187
1557
|
continue;
|
|
1188
1558
|
}
|
|
1189
|
-
const out = readHookStdout(
|
|
1559
|
+
const out = readHookStdout(ran, "PostToolUseFailure", ctx, payload, ledger);
|
|
1190
1560
|
if (!out)
|
|
1191
1561
|
continue;
|
|
1192
1562
|
if (typeof out.systemMessage === "string")
|
|
1193
|
-
ctx
|
|
1563
|
+
logHookSystemMessage(ctx, "PostToolUseFailure", out.systemMessage);
|
|
1194
1564
|
if (out.continue === false)
|
|
1195
1565
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
|
|
1196
1566
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1197
|
-
contexts.push(
|
|
1567
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1198
1568
|
const hso = out.hookSpecificOutput;
|
|
1199
1569
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUseFailure") {
|
|
1200
1570
|
const h = hso;
|
|
1201
1571
|
if (typeof h.additionalContext === "string")
|
|
1202
|
-
contexts.push(
|
|
1572
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1203
1573
|
}
|
|
1204
1574
|
}
|
|
1205
1575
|
const additionalContext = buildHookContext(contexts);
|
|
@@ -1207,7 +1577,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1207
1577
|
};
|
|
1208
1578
|
}
|
|
1209
1579
|
if (postBatch) {
|
|
1210
|
-
translators.PostToolBatch = async (e) => {
|
|
1580
|
+
translators.PostToolBatch = async (e, signal) => {
|
|
1211
1581
|
const contexts = [];
|
|
1212
1582
|
const payload = {
|
|
1213
1583
|
...basePayload(ctx, e.permissionMode),
|
|
@@ -1216,31 +1586,32 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1216
1586
|
tool_name: c.toolName,
|
|
1217
1587
|
tool_input: boundedInputValue(c.input),
|
|
1218
1588
|
tool_use_id: c.toolCallId,
|
|
1219
|
-
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) } : {}) },
|
|
1220
1590
|
})),
|
|
1221
1591
|
};
|
|
1222
|
-
const
|
|
1223
|
-
|
|
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;
|
|
1224
1596
|
if (run.code === 2) {
|
|
1225
|
-
const fb =
|
|
1597
|
+
const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
1226
1598
|
if (fb)
|
|
1227
1599
|
contexts.push(fb);
|
|
1228
1600
|
continue;
|
|
1229
1601
|
}
|
|
1230
1602
|
if (run.code !== 0) {
|
|
1231
|
-
ctx
|
|
1232
|
-
noteHookFailure(ctx, { event: "PostToolBatch", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1603
|
+
noteExitNonzero(ctx, ledger, "PostToolBatch", ran);
|
|
1233
1604
|
continue;
|
|
1234
1605
|
}
|
|
1235
|
-
const out = readHookStdout(
|
|
1606
|
+
const out = readHookStdout(ran, "PostToolBatch", ctx, payload, ledger);
|
|
1236
1607
|
if (!out)
|
|
1237
1608
|
continue;
|
|
1238
1609
|
if (typeof out.systemMessage === "string")
|
|
1239
|
-
ctx
|
|
1610
|
+
logHookSystemMessage(ctx, "PostToolBatch", out.systemMessage);
|
|
1240
1611
|
if (out.continue === false)
|
|
1241
1612
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
|
|
1242
1613
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
1243
|
-
contexts.push(
|
|
1614
|
+
contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
|
|
1244
1615
|
if (out.hookSpecificOutput !== undefined)
|
|
1245
1616
|
ctx.logger.warn("hook_result_unsupported", { event: "PostToolBatch" });
|
|
1246
1617
|
}
|
|
@@ -1249,38 +1620,39 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1249
1620
|
};
|
|
1250
1621
|
}
|
|
1251
1622
|
if (promptSubmit) {
|
|
1252
|
-
translators.UserPromptSubmit = async (e) => {
|
|
1623
|
+
translators.UserPromptSubmit = async (e, signal) => {
|
|
1253
1624
|
const contexts = [];
|
|
1254
|
-
const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt:
|
|
1255
|
-
const
|
|
1256
|
-
|
|
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;
|
|
1257
1630
|
if (run.code === 2) {
|
|
1258
|
-
const reason =
|
|
1631
|
+
const reason = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
|
|
1259
1632
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1260
1633
|
}
|
|
1261
1634
|
if (run.code !== 0) {
|
|
1262
|
-
ctx
|
|
1263
|
-
noteHookFailure(ctx, { event: "UserPromptSubmit", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1635
|
+
noteExitNonzero(ctx, ledger, "UserPromptSubmit", ran);
|
|
1264
1636
|
continue;
|
|
1265
1637
|
}
|
|
1266
|
-
const out = readHookStdout(
|
|
1638
|
+
const out = readHookStdout(ran, "UserPromptSubmit", ctx, payload, ledger);
|
|
1267
1639
|
if (!out)
|
|
1268
1640
|
continue;
|
|
1269
1641
|
if (typeof out.systemMessage === "string")
|
|
1270
|
-
ctx
|
|
1642
|
+
logHookSystemMessage(ctx, "UserPromptSubmit", out.systemMessage);
|
|
1271
1643
|
if (out.continue === false) {
|
|
1272
|
-
const reason =
|
|
1644
|
+
const reason = clipDataText(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
1273
1645
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1274
1646
|
}
|
|
1275
1647
|
if (out.decision === "block") {
|
|
1276
|
-
const reason =
|
|
1648
|
+
const reason = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a UserPromptSubmit hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1277
1649
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
1278
1650
|
}
|
|
1279
1651
|
const hso = out.hookSpecificOutput;
|
|
1280
1652
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "UserPromptSubmit") {
|
|
1281
1653
|
const h = hso;
|
|
1282
1654
|
if (typeof h.additionalContext === "string")
|
|
1283
|
-
contexts.push(
|
|
1655
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1284
1656
|
}
|
|
1285
1657
|
}
|
|
1286
1658
|
const additionalContext = buildHookContext(contexts);
|
|
@@ -1288,7 +1660,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1288
1660
|
};
|
|
1289
1661
|
}
|
|
1290
1662
|
if (stopEntries) {
|
|
1291
|
-
translators.Stop = async (sctx) => {
|
|
1663
|
+
translators.Stop = async (sctx, signal) => {
|
|
1292
1664
|
const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
|
|
1293
1665
|
let llmExtra;
|
|
1294
1666
|
const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
|
|
@@ -1303,7 +1675,7 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1303
1675
|
branch = await sctx.getBranch?.();
|
|
1304
1676
|
}
|
|
1305
1677
|
catch (e) {
|
|
1306
|
-
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 }) });
|
|
1307
1679
|
}
|
|
1308
1680
|
const rendered = renderBranchTranscript(branch);
|
|
1309
1681
|
if (rendered.sufficient) {
|
|
@@ -1316,22 +1688,29 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1316
1688
|
}
|
|
1317
1689
|
}
|
|
1318
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
|
+
}
|
|
1319
1698
|
const effectiveGroups = llmExtra || !wantsPrompt
|
|
1320
1699
|
? stopGroups
|
|
1321
1700
|
: stopGroups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => h.type !== "prompt") })).filter((g) => g.hooks.length > 0);
|
|
1322
|
-
const singles = await
|
|
1701
|
+
const singles = await runEventLanes("Stop", effectiveGroups, "", payload, ctx, onceFired, signal, ledger, llmExtra);
|
|
1323
1702
|
let block;
|
|
1324
1703
|
let sawContinueFalse = false;
|
|
1325
1704
|
const contexts = [];
|
|
1326
|
-
for (const
|
|
1705
|
+
for (const ran of singles) {
|
|
1706
|
+
const { entry, run } = ran;
|
|
1327
1707
|
if (run.code === 2) {
|
|
1328
1708
|
if (block === undefined)
|
|
1329
|
-
block =
|
|
1709
|
+
block = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a Stop hook";
|
|
1330
1710
|
continue;
|
|
1331
1711
|
}
|
|
1332
1712
|
if (run.code !== 0) {
|
|
1333
|
-
ctx
|
|
1334
|
-
noteHookFailure(ctx, { event: "Stop", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1713
|
+
noteExitNonzero(ctx, ledger, "Stop", ran);
|
|
1335
1714
|
continue;
|
|
1336
1715
|
}
|
|
1337
1716
|
if (llmExtra && entry.type === "prompt" && parseCcVerdict(run.stdout)) {
|
|
@@ -1339,33 +1718,34 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1339
1718
|
if (v.ok)
|
|
1340
1719
|
continue;
|
|
1341
1720
|
if (v.impossible) {
|
|
1342
|
-
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 }) });
|
|
1343
1722
|
continue;
|
|
1344
1723
|
}
|
|
1345
1724
|
if (block === undefined)
|
|
1346
|
-
block =
|
|
1725
|
+
block = clipDataText(v.reason?.trim() || "stop condition not satisfied", MAX_HOOK_FEEDBACK_CHARS);
|
|
1347
1726
|
continue;
|
|
1348
1727
|
}
|
|
1349
|
-
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);
|
|
1350
1729
|
if (!out) {
|
|
1351
1730
|
if (llmExtra && entry.type === "prompt") {
|
|
1352
|
-
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 }) });
|
|
1353
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");
|
|
1354
1734
|
}
|
|
1355
1735
|
continue;
|
|
1356
1736
|
}
|
|
1357
1737
|
if (typeof out.systemMessage === "string")
|
|
1358
|
-
ctx
|
|
1738
|
+
logHookSystemMessage(ctx, "Stop", out.systemMessage);
|
|
1359
1739
|
if (out.continue === false)
|
|
1360
1740
|
sawContinueFalse = true;
|
|
1361
1741
|
if (out.decision === "block" && block === undefined) {
|
|
1362
|
-
block =
|
|
1742
|
+
block = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a Stop hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1363
1743
|
}
|
|
1364
1744
|
const hso = out.hookSpecificOutput;
|
|
1365
1745
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "Stop") {
|
|
1366
1746
|
const h = hso;
|
|
1367
1747
|
if (typeof h.additionalContext === "string")
|
|
1368
|
-
contexts.push(
|
|
1748
|
+
contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1369
1749
|
}
|
|
1370
1750
|
}
|
|
1371
1751
|
if (sawContinueFalse)
|
|
@@ -1379,59 +1759,60 @@ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
|
|
|
1379
1759
|
};
|
|
1380
1760
|
}
|
|
1381
1761
|
if (stopFailureEntries) {
|
|
1382
|
-
translators.StopFailure = async (fctx) => {
|
|
1762
|
+
translators.StopFailure = async (fctx, signal) => {
|
|
1383
1763
|
const turnError = toAssistantTurnError(fctx.errorKind);
|
|
1384
1764
|
const payload = {
|
|
1385
1765
|
...basePayload(ctx, undefined),
|
|
1386
1766
|
hook_event_name: "StopFailure",
|
|
1387
1767
|
error: turnError,
|
|
1388
|
-
...(fctx.error ? { error_details:
|
|
1768
|
+
...(fctx.error ? { error_details: clipDataText(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
|
|
1389
1769
|
};
|
|
1390
|
-
await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
|
|
1770
|
+
await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired, signal);
|
|
1391
1771
|
};
|
|
1392
1772
|
}
|
|
1393
1773
|
if (preCompactEntries) {
|
|
1394
|
-
translators.PreCompact = async (pctx) => {
|
|
1774
|
+
translators.PreCompact = async (pctx, signal) => {
|
|
1395
1775
|
const payload = {
|
|
1396
1776
|
...basePayload(ctx, undefined),
|
|
1397
1777
|
hook_event_name: "PreCompact",
|
|
1398
1778
|
trigger: pctx.trigger,
|
|
1399
1779
|
custom_instructions: pctx.customInstructions ?? null,
|
|
1400
1780
|
};
|
|
1401
|
-
const
|
|
1402
|
-
|
|
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;
|
|
1403
1785
|
if (run.code === 2) {
|
|
1404
|
-
return { block:
|
|
1786
|
+
return { block: clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
|
|
1405
1787
|
}
|
|
1406
1788
|
if (run.code !== 0) {
|
|
1407
|
-
ctx
|
|
1408
|
-
noteHookFailure(ctx, { event: "PreCompact", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1789
|
+
noteExitNonzero(ctx, ledger, "PreCompact", ran);
|
|
1409
1790
|
continue;
|
|
1410
1791
|
}
|
|
1411
|
-
const out = readHookStdout(
|
|
1792
|
+
const out = readHookStdout(ran, "PreCompact", ctx, payload, ledger);
|
|
1412
1793
|
if (!out)
|
|
1413
1794
|
continue;
|
|
1414
1795
|
if (typeof out.systemMessage === "string")
|
|
1415
|
-
ctx
|
|
1796
|
+
logHookSystemMessage(ctx, "PreCompact", out.systemMessage);
|
|
1416
1797
|
if (out.continue === false) {
|
|
1417
1798
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
|
|
1418
1799
|
}
|
|
1419
1800
|
if (out.decision === "block") {
|
|
1420
|
-
return { block:
|
|
1801
|
+
return { block: clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
|
|
1421
1802
|
}
|
|
1422
1803
|
}
|
|
1423
1804
|
return undefined;
|
|
1424
1805
|
};
|
|
1425
1806
|
}
|
|
1426
1807
|
if (postCompactEntries) {
|
|
1427
|
-
translators.PostCompact = async (cctx) => {
|
|
1808
|
+
translators.PostCompact = async (cctx, signal) => {
|
|
1428
1809
|
const payload = {
|
|
1429
1810
|
...basePayload(ctx, undefined),
|
|
1430
1811
|
hook_event_name: "PostCompact",
|
|
1431
1812
|
trigger: cctx.trigger,
|
|
1432
|
-
compact_summary:
|
|
1813
|
+
compact_summary: clipDataText(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
|
|
1433
1814
|
};
|
|
1434
|
-
await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
|
|
1815
|
+
await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired, signal);
|
|
1435
1816
|
};
|
|
1436
1817
|
}
|
|
1437
1818
|
const events = Object.keys(translators);
|