@wrongstack/core 0.313.0 → 0.316.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/legacy-journal-import.d.ts +0 -6
- package/dist/chronicle/metrics-ingest.d.ts +14 -0
- package/dist/chronicle/metrics-schema.d.ts +10 -1
- package/dist/chronicle/metrics-store.d.ts +20 -2
- package/dist/chronicle/project-server.js +98 -4874
- package/dist/chronicle/sink.d.ts +13 -0
- package/dist/chronicle/sqlite-journal.d.ts +20 -2
- package/dist/chunk-25LSLC3G.js +334 -0
- package/dist/chunk-27QIURLN.js +1073 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3AGWVSS7.js +4608 -0
- package/dist/chunk-3BDV7RL3.js +211 -0
- package/dist/chunk-3ECENVHL.js +325 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-45WI72ZB.js +16884 -0
- package/dist/chunk-4BMVPOHO.js +3748 -0
- package/dist/chunk-4I7HGVVA.js +363 -0
- package/dist/chunk-4LCZVJVF.js +317 -0
- package/dist/chunk-4SJNS7OB.js +217 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5BMIE7QG.js +46 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-5YPSXQXM.js +1443 -0
- package/dist/chunk-6FHWXLPT.js +10 -0
- package/dist/chunk-6H5GEOET.js +2406 -0
- package/dist/chunk-6NWEVFEL.js +151 -0
- package/dist/chunk-AYW5YINX.js +58 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-B7F4WVP4.js +65 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-BPBKKBUK.js +2085 -0
- package/dist/chunk-BYD345OX.js +589 -0
- package/dist/chunk-C27KDLOX.js +279 -0
- package/dist/chunk-CU546ESW.js +597 -0
- package/dist/chunk-DLHHXUAM.js +172 -0
- package/dist/chunk-DOTHQSJ2.js +429 -0
- package/dist/chunk-DYLPMV4Q.js +711 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-F3JWNST7.js +361 -0
- package/dist/chunk-FC2ZGRF3.js +140 -0
- package/dist/chunk-FCBTO76Z.js +5786 -0
- package/dist/chunk-FHZVFYSL.js +396 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FWNLEZYG.js +259 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GDQ7CZRQ.js +116 -0
- package/dist/chunk-GJJGJB2R.js +1374 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-HI3JGZR2.js +188 -0
- package/dist/chunk-HMLIGAU7.js +395 -0
- package/dist/chunk-HXKZ7OBQ.js +303 -0
- package/dist/chunk-I7DALG7J.js +292 -0
- package/dist/chunk-IRUTEHCE.js +960 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-J2WRWR37.js +1104 -0
- package/dist/chunk-JFPJ6LLQ.js +129 -0
- package/dist/chunk-JMULFFLN.js +9 -0
- package/dist/chunk-JO4MGB6Z.js +1264 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K6BXGITA.js +931 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KZMSZV32.js +187 -0
- package/dist/chunk-LA7CLDRW.js +129 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LSMMOS2Z.js +119 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LXWCMQNJ.js +789 -0
- package/dist/chunk-M27LIEKV.js +2829 -0
- package/dist/chunk-MJPHJRAL.js +663 -0
- package/dist/chunk-MTL4O5YR.js +228 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NHJ75HXY.js +1 -0
- package/dist/chunk-NIOAGFPB.js +1510 -0
- package/dist/chunk-NS52ISWS.js +355 -0
- package/dist/chunk-NSOFPKGT.js +5352 -0
- package/dist/chunk-NVIZG63R.js +757 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-OOYZPOXM.js +308 -0
- package/dist/chunk-OSGLYW5J.js +28 -0
- package/dist/chunk-PFCMSSUF.js +1640 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PGRVXIMY.js +158 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-PISBFBJN.js +109 -0
- package/dist/chunk-POOOIWE5.js +9 -0
- package/dist/chunk-PQNS3U7Z.js +1240 -0
- package/dist/chunk-QSTSJOCK.js +402 -0
- package/dist/chunk-RFQMOKWK.js +282 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RTOTNK3P.js +484 -0
- package/dist/chunk-RXKSDUAU.js +1172 -0
- package/dist/chunk-S45JPKQN.js +3317 -0
- package/dist/chunk-SAFHACFA.js +152 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TCCHINEF.js +655 -0
- package/dist/chunk-THEGTRAG.js +316 -0
- package/dist/chunk-TR4LQE7L.js +2256 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TWJMGXCI.js +900 -0
- package/dist/chunk-U45AXT5D.js +1023 -0
- package/dist/chunk-U4FWBSO3.js +509 -0
- package/dist/chunk-UBGDX6FI.js +147 -0
- package/dist/chunk-UJILKYES.js +75 -0
- package/dist/chunk-ULMAQV3T.js +234 -0
- package/dist/chunk-VVBP6KGT.js +9 -0
- package/dist/chunk-W5VITDM2.js +168 -0
- package/dist/chunk-WAHQVNWT.js +194 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WGGYRBXH.js +5434 -0
- package/dist/chunk-WLYKU45P.js +5770 -0
- package/dist/chunk-WP7BVANJ.js +53 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XF4VUMKW.js +231 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-Y5YXDM7D.js +74 -0
- package/dist/chunk-YOPAUVU4.js +2588 -0
- package/dist/chunk-YPT4A46F.js +362 -0
- package/dist/chunk-YWXT3JNM.js +425 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZHZ2Y2DH.js +1643 -0
- package/dist/chunk-ZT7JNIGX.js +152 -0
- package/dist/coordination/agent-monitor.d.ts +9 -1
- package/dist/coordination/agent-subagent-runner.d.ts +1 -1
- package/dist/coordination/agents/agent-prompts.d.ts +8 -0
- package/dist/coordination/agents/index.d.ts +33 -1
- package/dist/coordination/agents/index.js +184 -6269
- package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
- package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
- package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
- package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
- package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
- package/dist/coordination/agents/types.d.ts +13 -2
- package/dist/coordination/brain-monitor.d.ts +30 -7
- package/dist/coordination/collab-debug.d.ts +1 -1
- package/dist/coordination/collab-pause.d.ts +1 -1
- package/dist/coordination/delegate-tool.d.ts +7 -5
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
- package/dist/coordination/director-options.d.ts +7 -0
- package/dist/coordination/director-spawn-model.d.ts +17 -0
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/director.d.ts +12 -0
- package/dist/coordination/dispatcher.d.ts +8 -0
- package/dist/coordination/fleet-bus.d.ts +35 -4
- package/dist/coordination/fleet-supervisor.d.ts +10 -2
- package/dist/coordination/fleet.d.ts +16 -7
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.js +402 -34324
- package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
- package/dist/coordination/mailbox-http-router.d.ts +4 -3
- package/dist/coordination/mailbox-http-validation.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +48 -3276
- package/dist/coordination/mailbox-types.d.ts +24 -6
- package/dist/coordination/model-tier-leader.d.ts +122 -0
- package/dist/coordination/model-tier.d.ts +122 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
- package/dist/coordination/origin-session.d.ts +16 -0
- package/dist/coordination/provider-status-tracker.d.ts +41 -5
- package/dist/coordination/session-note-hub.d.ts +44 -0
- package/dist/coordination/session-note-tool.d.ts +10 -0
- package/dist/core/agent.d.ts +15 -2
- package/dist/core/context.d.ts +22 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +239 -12360
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/core/request-conversation-binding.d.ts +37 -0
- package/dist/core/session-notes.d.ts +25 -0
- package/dist/core/system-prompt-builder.d.ts +11 -2
- package/dist/core/system-prompt-variants.d.ts +78 -0
- package/dist/design/index.js +50 -1087
- package/dist/execution/auto-compaction-middleware.d.ts +11 -0
- package/dist/execution/autonomy-brain.d.ts +2 -1
- package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
- package/dist/execution/compaction-core.d.ts +4 -4
- package/dist/execution/eternal-autonomy.d.ts +1 -1
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1615 -21895
- package/dist/execution/model-runtime.d.ts +0 -7
- package/dist/execution/parallel-eternal-engine.d.ts +7 -7
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +114 -5139
- package/dist/hq/persistence/event-log.d.ts +0 -1
- package/dist/hq/persistence.d.ts +4 -4
- package/dist/hq/protocol.js +21 -668
- package/dist/hq/publisher.d.ts +2 -0
- package/dist/index.d.ts +14 -11
- package/dist/index.js +1502 -92655
- package/dist/infrastructure/index.d.ts +4 -4
- package/dist/infrastructure/index.js +504 -5230
- package/dist/infrastructure/token-counter.d.ts +9 -0
- package/dist/kernel/events/agent-events.d.ts +77 -3
- package/dist/kernel/events/brain-events.d.ts +12 -0
- package/dist/kernel/events/provider-events.d.ts +22 -1
- package/dist/kernel/events/session-events.d.ts +14 -0
- package/dist/kernel/events/tool-events.d.ts +13 -0
- package/dist/kernel/index.js +14 -992
- package/dist/mailbox-attach.d.ts +1 -1
- package/dist/models/index.js +41 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/event-bridge.d.ts +25 -5
- package/dist/observability/index.d.ts +9 -8
- package/dist/observability/index.js +477 -631
- package/dist/observability/tool-usage-source.d.ts +65 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +600 -14835
- package/dist/plugins/auto-review-plugin.d.ts +3 -2
- package/dist/plugins/review-finding-commands.d.ts +0 -8
- package/dist/plugins/review-finding-store.js +22 -0
- package/dist/plugins/review-finding-types.js +19 -0
- package/dist/plugins/review-report-integration.js +21 -0
- package/dist/plugins/review-report-store.js +20 -0
- package/dist/plugins/review-report-types.js +13 -0
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry/tool-registry.d.ts +59 -9
- package/dist/registry-7N4O3E7W.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/security/capabilities.d.ts +6 -0
- package/dist/security/file-permissions.d.ts +1 -1
- package/dist/security/index.js +161 -2235
- package/dist/security/permission-policy.d.ts +12 -1
- package/dist/security/secret-scrubber.d.ts +15 -0
- package/dist/session-catalog/index.d.ts +1 -0
- package/dist/session-catalog/index.js +41 -2103
- package/dist/session-catalog/project-server.js +59 -1435
- package/dist/session-catalog/protocol.d.ts +32 -1
- package/dist/session-catalog/registry.d.ts +17 -1
- package/dist/session-catalog/session-agents.d.ts +56 -0
- package/dist/session-catalog/store-rebuild.d.ts +14 -0
- package/dist/session-catalog/store-schema.d.ts +26 -0
- package/dist/session-catalog/store.d.ts +27 -1
- package/dist/session-note-attach.d.ts +9 -0
- package/dist/skills/index.js +35 -1578
- package/dist/statusline/index.d.ts +43 -0
- package/dist/statusline/index.js +102 -0
- package/dist/storage/board-store-port.d.ts +2 -2
- package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
- package/dist/storage/config-loader.d.ts +3 -2
- package/dist/storage/file-session-writer.d.ts +4 -2
- package/dist/storage/goal-coordination.d.ts +6 -1
- package/dist/storage/goal-kanban.d.ts +4 -1
- package/dist/storage/index.d.ts +12 -4
- package/dist/storage/index.js +198 -15601
- package/dist/storage/input-history-store.d.ts +2 -4
- package/dist/storage/memory-curator.d.ts +133 -0
- package/dist/storage/session-agent-attribution.d.ts +39 -0
- package/dist/storage/session-doctor.d.ts +109 -0
- package/dist/storage/session-event-bridge.d.ts +5 -7
- package/dist/storage/session-outcome.d.ts +42 -0
- package/dist/storage/session-recovery.d.ts +55 -0
- package/dist/storage/session-store/load-session-data.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +0 -2
- package/dist/storage/session-store/types.d.ts +8 -0
- package/dist/storage/session-store.d.ts +3 -3
- package/dist/storage/session-summary-tracker.d.ts +8 -1
- package/dist/storage/session-write-buffer.d.ts +18 -0
- package/dist/storage/session-writer-truncate.d.ts +2 -0
- package/dist/tasking/index.js +10 -676
- package/dist/tools/fallback-manage-tools.d.ts +6 -4
- package/dist/tools/index.js +52 -9590
- package/dist/tools/model-tier-set-tool.d.ts +66 -0
- package/dist/types/agent-bridge.d.ts +2 -1
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/mcp-features.d.ts +7 -0
- package/dist/types/config/model-tiers.d.ts +148 -0
- package/dist/types/config/root.d.ts +9 -0
- package/dist/types/config/tools.d.ts +73 -0
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/context.d.ts +2 -0
- package/dist/types/default-config.d.ts +6 -0
- package/dist/types/errors.d.ts +20 -1
- package/dist/types/index.d.ts +11 -9
- package/dist/types/index.js +131 -1342
- package/dist/types/multi-agent.d.ts +43 -0
- package/dist/types/plugin.d.ts +1 -1
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/provider.d.ts +15 -0
- package/dist/types/runtime-capability-manifest.d.ts +1 -1
- package/dist/types/secret-scrubber.d.ts +15 -0
- package/dist/types/session-markers.d.ts +37 -10
- package/dist/types/session-markers.js +8 -144
- package/dist/types/session-timeline.d.ts +197 -0
- package/dist/types/session-timeline.js +12 -0
- package/dist/types/session.d.ts +172 -11
- package/dist/types/system-prompt.d.ts +27 -0
- package/dist/utils/config-backup.d.ts +9 -1
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +271 -5685
- package/dist/utils/message-invariants.d.ts +4 -1
- package/dist/utils/regex-guard.d.ts +1 -1
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/utils/socket-path.d.ts +1 -1
- package/dist/utils/token-estimate.d.ts +6 -2
- package/dist/utils/wstack-paths.d.ts +2 -0
- package/dist/wiring/proxy-rewrite.d.ts +46 -0
- package/dist/wiring/proxy-rewrite.js +24 -121
- package/dist/worktree/index.js +6 -887
- package/instructions/agents/explore-companion.md +45 -18
- package/instructions/agents/memory-curator.md +34 -22
- package/instructions/coordination/director-preamble.md +12 -7
- package/instructions/coordination/subagent-baseline.md +26 -5
- package/instructions/leader-after-task.md +4 -1
- package/instructions/llm/chimera-review.md +30 -9
- package/instructions/llm/memory-curator.md +31 -0
- package/instructions/sections/tool/delegation-compact.md +2 -0
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/sections/tool/mailbox-compact.md +5 -1
- package/instructions/sections/tool/mailbox-full.md +3 -0
- package/instructions/sections/tool/session-note-compact.md +3 -0
- package/instructions/sections/tool/session-note-full.md +15 -0
- package/instructions/system-lite.md +9 -4
- package/instructions/system-pro.md +66 -143
- package/instructions/system.md +73 -175
- package/package.json +32 -4
- package/dist/coordination/agents.d.ts +0 -8
- package/dist/coordination/transport.d.ts +0 -2
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNotPrivateHost
|
|
3
|
+
} from "./chunk-LA7CLDRW.js";
|
|
4
|
+
import {
|
|
5
|
+
buildChildEnv
|
|
6
|
+
} from "./chunk-6NWEVFEL.js";
|
|
7
|
+
import {
|
|
8
|
+
toErrorMessage
|
|
9
|
+
} from "./chunk-TBRL2RCC.js";
|
|
10
|
+
|
|
11
|
+
// src/hooks/shell-executor.ts
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
14
|
+
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
15
|
+
var ALLOWED_SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
16
|
+
// POSIX shells + Windows shells
|
|
17
|
+
"bash",
|
|
18
|
+
"sh",
|
|
19
|
+
"dash",
|
|
20
|
+
"zsh",
|
|
21
|
+
"fish",
|
|
22
|
+
"pwsh",
|
|
23
|
+
"powershell",
|
|
24
|
+
"cmd",
|
|
25
|
+
// Script interpreters — hooks are routinely small node/python scripts
|
|
26
|
+
"node",
|
|
27
|
+
"deno",
|
|
28
|
+
"bun",
|
|
29
|
+
"npx",
|
|
30
|
+
"npm",
|
|
31
|
+
"pnpm",
|
|
32
|
+
"yarn",
|
|
33
|
+
"python",
|
|
34
|
+
"python3",
|
|
35
|
+
"perl",
|
|
36
|
+
"ruby",
|
|
37
|
+
// Utilities
|
|
38
|
+
"echo",
|
|
39
|
+
"cat",
|
|
40
|
+
"grep",
|
|
41
|
+
"sed",
|
|
42
|
+
"awk",
|
|
43
|
+
"find",
|
|
44
|
+
"sort",
|
|
45
|
+
"uniq",
|
|
46
|
+
"wc",
|
|
47
|
+
"head",
|
|
48
|
+
"tail",
|
|
49
|
+
"cut",
|
|
50
|
+
"tr",
|
|
51
|
+
"tee",
|
|
52
|
+
"xargs",
|
|
53
|
+
"printf",
|
|
54
|
+
"test",
|
|
55
|
+
"expr",
|
|
56
|
+
// File operations
|
|
57
|
+
"ls",
|
|
58
|
+
"stat",
|
|
59
|
+
"touch",
|
|
60
|
+
"mkdir",
|
|
61
|
+
"rm",
|
|
62
|
+
"cp",
|
|
63
|
+
"mv",
|
|
64
|
+
"chmod",
|
|
65
|
+
"chown",
|
|
66
|
+
// Network (read-only)
|
|
67
|
+
"curl",
|
|
68
|
+
"wget",
|
|
69
|
+
// Git (common operations)
|
|
70
|
+
"git",
|
|
71
|
+
"diff",
|
|
72
|
+
"merge",
|
|
73
|
+
// Process
|
|
74
|
+
"ps",
|
|
75
|
+
"kill",
|
|
76
|
+
"pgrep",
|
|
77
|
+
"pkill",
|
|
78
|
+
// Text processing
|
|
79
|
+
"jq",
|
|
80
|
+
"yq",
|
|
81
|
+
// System info
|
|
82
|
+
"uname",
|
|
83
|
+
"hostname",
|
|
84
|
+
"whoami",
|
|
85
|
+
"date"
|
|
86
|
+
]);
|
|
87
|
+
function isAbsoluteCommandPath(p) {
|
|
88
|
+
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
89
|
+
}
|
|
90
|
+
function isCommandAllowed(command) {
|
|
91
|
+
const argv = command.trim().split(/\s+/);
|
|
92
|
+
if (argv.length === 0 || argv[0].length === 0) return null;
|
|
93
|
+
const executable = argv[0];
|
|
94
|
+
if (isAbsoluteCommandPath(executable)) return argv;
|
|
95
|
+
const commandName = /[\\/]/.test(executable) ? executable.split(/[\\/]/).pop() ?? executable : executable;
|
|
96
|
+
if (ALLOWED_SHELL_COMMANDS.has(commandName)) return argv;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function killProcessTree(child) {
|
|
100
|
+
const pid = child.pid;
|
|
101
|
+
if (typeof pid === "number") {
|
|
102
|
+
if (process.platform === "win32") {
|
|
103
|
+
try {
|
|
104
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
105
|
+
stdio: "ignore",
|
|
106
|
+
windowsHide: true
|
|
107
|
+
});
|
|
108
|
+
killer.unref();
|
|
109
|
+
return;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(-pid, "SIGKILL");
|
|
115
|
+
return;
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
child.kill("SIGKILL");
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runShellHook(spec, input, logger) {
|
|
126
|
+
const result = await runShellHookDetailed(spec, input, logger);
|
|
127
|
+
if (result.outcome && "action" in result.outcome) {
|
|
128
|
+
if (result.outcome.action === "deny") {
|
|
129
|
+
return { decision: "block", reason: result.outcome.reason };
|
|
130
|
+
}
|
|
131
|
+
if (result.outcome.action === "mutate") {
|
|
132
|
+
return { decision: "allow", modifiedInput: result.outcome.input };
|
|
133
|
+
}
|
|
134
|
+
return { decision: "allow" };
|
|
135
|
+
}
|
|
136
|
+
return result.outcome;
|
|
137
|
+
}
|
|
138
|
+
async function runShellHookDetailed(spec, input, logger, options = {}) {
|
|
139
|
+
const timeoutMs = Math.max(1, Math.min(spec.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10 * 6e4));
|
|
140
|
+
const argv = isCommandAllowed(spec.command);
|
|
141
|
+
if (!argv) {
|
|
142
|
+
logger?.warn?.(`hook rejected: command not in allowlist: ${spec.command}`);
|
|
143
|
+
return {
|
|
144
|
+
outcome: null,
|
|
145
|
+
failure: { kind: "rejected", message: "command is not in the hook allowlist" }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return await new Promise((resolve) => {
|
|
149
|
+
let settled = false;
|
|
150
|
+
let timer;
|
|
151
|
+
const done = (v) => {
|
|
152
|
+
if (settled) return;
|
|
153
|
+
settled = true;
|
|
154
|
+
if (timer) clearTimeout(timer);
|
|
155
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
156
|
+
resolve(v);
|
|
157
|
+
};
|
|
158
|
+
let child;
|
|
159
|
+
try {
|
|
160
|
+
const cmd = argv[0];
|
|
161
|
+
const args = argv.slice(1);
|
|
162
|
+
child = spawn(cmd, args, {
|
|
163
|
+
cwd: input.cwd,
|
|
164
|
+
env: buildChildEnv(),
|
|
165
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
166
|
+
windowsHide: true,
|
|
167
|
+
detached: process.platform !== "win32"
|
|
168
|
+
});
|
|
169
|
+
} catch (err2) {
|
|
170
|
+
logger?.warn?.(`hook spawn failed: ${toErrorMessage(err2)}`);
|
|
171
|
+
return resolve({
|
|
172
|
+
outcome: null,
|
|
173
|
+
failure: { kind: "error", message: `spawn failed: ${toErrorMessage(err2)}` }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const onAbort = () => {
|
|
177
|
+
killProcessTree(child);
|
|
178
|
+
done({ outcome: null, failure: { kind: "aborted", message: "hook invocation aborted" } });
|
|
179
|
+
};
|
|
180
|
+
if (options.signal?.aborted) {
|
|
181
|
+
onAbort();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
185
|
+
timer = setTimeout(() => {
|
|
186
|
+
logger?.warn?.(`hook command timed out after ${timeoutMs}ms: ${spec.command}`);
|
|
187
|
+
killProcessTree(child);
|
|
188
|
+
done({
|
|
189
|
+
outcome: null,
|
|
190
|
+
failure: { kind: "timeout", message: `timed out after ${timeoutMs}ms` }
|
|
191
|
+
});
|
|
192
|
+
}, timeoutMs);
|
|
193
|
+
let out = "";
|
|
194
|
+
let err = "";
|
|
195
|
+
let outTrunc = false;
|
|
196
|
+
child.stdout?.on("data", (d) => {
|
|
197
|
+
if (outTrunc) return;
|
|
198
|
+
out += d.toString("utf8");
|
|
199
|
+
if (Buffer.byteLength(out, "utf8") > MAX_OUTPUT_BYTES) {
|
|
200
|
+
out = out.slice(0, MAX_OUTPUT_BYTES);
|
|
201
|
+
outTrunc = true;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
child.stderr?.on("data", (d) => {
|
|
205
|
+
if (err.length < MAX_OUTPUT_BYTES) err += d.toString("utf8");
|
|
206
|
+
});
|
|
207
|
+
child.on("error", (e) => {
|
|
208
|
+
logger?.warn?.(`hook command error: ${e instanceof Error ? e.message : String(e)}`);
|
|
209
|
+
done({
|
|
210
|
+
outcome: null,
|
|
211
|
+
failure: { kind: "error", message: e instanceof Error ? e.message : String(e) }
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
child.on("close", (code) => {
|
|
215
|
+
if (code === 2) {
|
|
216
|
+
const reason = (err.trim() || out.trim() || "blocked by hook").slice(0, 2e3);
|
|
217
|
+
return done({ outcome: { action: "deny", reason } });
|
|
218
|
+
}
|
|
219
|
+
if (code !== 0) {
|
|
220
|
+
return done({
|
|
221
|
+
outcome: null,
|
|
222
|
+
failure: {
|
|
223
|
+
kind: "exit",
|
|
224
|
+
message: (err.trim() || `hook exited with code ${String(code)}`).slice(0, 2e3)
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (!out.trim()) return done({ outcome: null });
|
|
229
|
+
const parsed = parseHookOutcome(out);
|
|
230
|
+
if (!parsed) {
|
|
231
|
+
return done({
|
|
232
|
+
outcome: null,
|
|
233
|
+
failure: { kind: "invalid_output", message: "hook stdout was not valid outcome JSON" }
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
done({ outcome: parsed });
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
child.stdin?.end(`${JSON.stringify(input)}
|
|
240
|
+
`);
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function parseHookOutcome(stdout) {
|
|
246
|
+
const trimmed = stdout.trim();
|
|
247
|
+
if (trimmed?.[0] !== "{") return null;
|
|
248
|
+
try {
|
|
249
|
+
const obj = JSON.parse(trimmed);
|
|
250
|
+
const outcome = {};
|
|
251
|
+
const common = {
|
|
252
|
+
...typeof obj["additionalContext"] === "string" ? { additionalContext: obj["additionalContext"] } : {},
|
|
253
|
+
...obj["contextAs"] === "inline" || obj["contextAs"] === "separate" ? { contextAs: obj["contextAs"] } : {}
|
|
254
|
+
};
|
|
255
|
+
if (obj["action"] === "allow" || obj["action"] === "deny" || obj["action"] === "mutate") {
|
|
256
|
+
if (obj["action"] === "deny") {
|
|
257
|
+
if (typeof obj["reason"] !== "string" || !obj["reason"].trim()) return null;
|
|
258
|
+
return { action: "deny", reason: obj["reason"], ...common };
|
|
259
|
+
}
|
|
260
|
+
if (obj["action"] === "mutate") {
|
|
261
|
+
if (!obj["input"] || typeof obj["input"] !== "object" || Array.isArray(obj["input"]))
|
|
262
|
+
return null;
|
|
263
|
+
return {
|
|
264
|
+
action: "mutate",
|
|
265
|
+
input: obj["input"],
|
|
266
|
+
...typeof obj["reason"] === "string" ? { reason: obj["reason"] } : {},
|
|
267
|
+
...common
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { action: "allow", ...common };
|
|
271
|
+
}
|
|
272
|
+
if (obj["decision"] === "block" || obj["decision"] === "allow") {
|
|
273
|
+
outcome.decision = obj["decision"];
|
|
274
|
+
}
|
|
275
|
+
if (typeof obj["reason"] === "string") outcome.reason = obj["reason"];
|
|
276
|
+
if (typeof obj["additionalContext"] === "string") {
|
|
277
|
+
outcome.additionalContext = obj["additionalContext"];
|
|
278
|
+
}
|
|
279
|
+
if (obj["modifiedInput"] && typeof obj["modifiedInput"] === "object") {
|
|
280
|
+
outcome.modifiedInput = obj["modifiedInput"];
|
|
281
|
+
}
|
|
282
|
+
if (obj["contextAs"] === "inline" || obj["contextAs"] === "separate") {
|
|
283
|
+
outcome.contextAs = obj["contextAs"];
|
|
284
|
+
}
|
|
285
|
+
return outcome;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/hooks/http-executor.ts
|
|
292
|
+
var DEFAULT_TIMEOUT_MS2 = 5e3;
|
|
293
|
+
var MAX_OUTPUT_BYTES2 = 64 * 1024;
|
|
294
|
+
async function isAllowedUrl(raw) {
|
|
295
|
+
let url;
|
|
296
|
+
try {
|
|
297
|
+
url = new URL(raw);
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
if (url.protocol === "https:") {
|
|
302
|
+
try {
|
|
303
|
+
await assertNotPrivateHost(url.hostname);
|
|
304
|
+
return true;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (url.protocol !== "http:") return false;
|
|
310
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
311
|
+
}
|
|
312
|
+
async function runHttpHookDetailed(spec, input, logger, options = {}) {
|
|
313
|
+
if (!await isAllowedUrl(spec.url)) {
|
|
314
|
+
return {
|
|
315
|
+
outcome: null,
|
|
316
|
+
failure: {
|
|
317
|
+
kind: "rejected",
|
|
318
|
+
message: "HTTP hooks require HTTPS, except for loopback HTTP endpoints"
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const timeoutMs = Math.max(1, Math.min(spec.timeoutMs ?? DEFAULT_TIMEOUT_MS2, 10 * 6e4));
|
|
323
|
+
const timeoutController = new AbortController();
|
|
324
|
+
const timer = setTimeout(() => timeoutController.abort(new Error("hook timeout")), timeoutMs);
|
|
325
|
+
timer.unref?.();
|
|
326
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutController.signal]) : timeoutController.signal;
|
|
327
|
+
try {
|
|
328
|
+
const response = await fetch(spec.url, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { ...spec.headers ?? {}, "content-type": "application/json" },
|
|
331
|
+
body: JSON.stringify(input),
|
|
332
|
+
redirect: "error",
|
|
333
|
+
signal
|
|
334
|
+
});
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
return {
|
|
337
|
+
outcome: null,
|
|
338
|
+
failure: { kind: "error", message: `HTTP hook returned ${response.status}` }
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const text = await response.text();
|
|
342
|
+
if (Buffer.byteLength(text, "utf8") > MAX_OUTPUT_BYTES2) {
|
|
343
|
+
return {
|
|
344
|
+
outcome: null,
|
|
345
|
+
failure: { kind: "invalid_output", message: "HTTP hook response exceeded 64 KiB" }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (!text.trim()) return { outcome: null };
|
|
349
|
+
const outcome = parseHookOutcome(text);
|
|
350
|
+
return outcome ? { outcome } : {
|
|
351
|
+
outcome: null,
|
|
352
|
+
failure: {
|
|
353
|
+
kind: "invalid_output",
|
|
354
|
+
message: "HTTP hook response was not valid outcome JSON"
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const timedOut = timeoutController.signal.aborted && !options.signal?.aborted;
|
|
359
|
+
const kind = timedOut ? "timeout" : options.signal?.aborted ? "aborted" : "error";
|
|
360
|
+
const message = timedOut ? `timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
|
|
361
|
+
logger?.warn?.(`HTTP hook failed: ${message}`);
|
|
362
|
+
return { outcome: null, failure: { kind, message } };
|
|
363
|
+
} finally {
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/hooks/registry.ts
|
|
369
|
+
function entryControls(event, fallbackName, options, legacyStage) {
|
|
370
|
+
return {
|
|
371
|
+
name: options.name?.trim() || fallbackName,
|
|
372
|
+
timeoutMs: options.timeoutMs,
|
|
373
|
+
failurePolicy: options.failurePolicy ?? "open",
|
|
374
|
+
// Legacy PreToolUse hooks may rewrite input, so preserve their old
|
|
375
|
+
// behavior unless the registration explicitly opts into final validation.
|
|
376
|
+
stage: event === "PreToolUse" ? options.stage ?? legacyStage : "validate",
|
|
377
|
+
policy: options.policy === true,
|
|
378
|
+
// `background` only applies to PostToolUse — PreToolUse hooks always block.
|
|
379
|
+
background: event === "PostToolUse" && options.background === true
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
var HookRegistry = class _HookRegistry {
|
|
383
|
+
entries = [];
|
|
384
|
+
/** Register an in-process hook. Returns an unsubscribe function. */
|
|
385
|
+
registerInProcess(event, matcher, hook, owner, options = {}) {
|
|
386
|
+
const entry = {
|
|
387
|
+
kind: "inprocess",
|
|
388
|
+
event,
|
|
389
|
+
matcher: matcher ?? "*",
|
|
390
|
+
hook,
|
|
391
|
+
owner,
|
|
392
|
+
...entryControls(
|
|
393
|
+
event,
|
|
394
|
+
owner ? `${owner}:${event}` : `inprocess:${event}`,
|
|
395
|
+
options,
|
|
396
|
+
"mutate"
|
|
397
|
+
)
|
|
398
|
+
};
|
|
399
|
+
this.entries.push(entry);
|
|
400
|
+
return () => this.remove(entry);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Register a single shell hook. The hook is owned by the runtime (no plugin
|
|
404
|
+
* name) — it survives plugin uninstalls. Returns an unsubscribe function.
|
|
405
|
+
*/
|
|
406
|
+
registerShell(event, hook) {
|
|
407
|
+
const entry = {
|
|
408
|
+
kind: "shell",
|
|
409
|
+
event,
|
|
410
|
+
matcher: hook.matcher ?? "*",
|
|
411
|
+
command: hook.command,
|
|
412
|
+
...entryControls(event, hook.command, hook, "validate")
|
|
413
|
+
};
|
|
414
|
+
this.entries.push(entry);
|
|
415
|
+
return () => this.remove(entry);
|
|
416
|
+
}
|
|
417
|
+
/** Register a native HTTP hook. */
|
|
418
|
+
registerHttp(event, hook) {
|
|
419
|
+
const entry = {
|
|
420
|
+
kind: "http",
|
|
421
|
+
event,
|
|
422
|
+
matcher: hook.matcher ?? "*",
|
|
423
|
+
url: hook.url,
|
|
424
|
+
headers: hook.headers,
|
|
425
|
+
...entryControls(event, hook.url, hook, "validate")
|
|
426
|
+
};
|
|
427
|
+
this.entries.push(entry);
|
|
428
|
+
return () => this.remove(entry);
|
|
429
|
+
}
|
|
430
|
+
/** Bulk-load command/HTTP hooks from a `config.hooks` map. */
|
|
431
|
+
loadShellHooks(hooks, options = {}) {
|
|
432
|
+
if (!hooks) return;
|
|
433
|
+
for (const [event, list] of Object.entries(hooks)) {
|
|
434
|
+
for (const h of list ?? []) {
|
|
435
|
+
if (options.policyOnly && h.policy !== true) continue;
|
|
436
|
+
if (h.type === "http") {
|
|
437
|
+
if (h.url) this.registerHttp(event, h);
|
|
438
|
+
} else if (h.command) {
|
|
439
|
+
this.registerShell(event, h);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Atomically replace the shell-hook set: drop every currently-registered
|
|
446
|
+
* shell entry, then load the supplied `config.hooks` map. In-process
|
|
447
|
+
* hooks (and their plugin owners) are untouched — only shell entries
|
|
448
|
+
* are touched, because they are the only entries sourced from
|
|
449
|
+
* `config.hooks`.
|
|
450
|
+
*
|
|
451
|
+
* Designed for hot-reload via `ConfigStore.watch`: when the operator
|
|
452
|
+
* edits `config.json` and the store fires its watcher, the CLI calls
|
|
453
|
+
* this with the new `config.hooks` value. Idempotent — calling twice
|
|
454
|
+
* with the same map yields the same registry state (callers should still
|
|
455
|
+
* guard at the change-detection layer to avoid wasted work).
|
|
456
|
+
*/
|
|
457
|
+
replaceShellHooks(hooks, options = {}) {
|
|
458
|
+
const replacement = new _HookRegistry();
|
|
459
|
+
replacement.loadShellHooks(hooks, options);
|
|
460
|
+
const configuredEntries = replacement.entries.slice();
|
|
461
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
462
|
+
const e = this.entries[i];
|
|
463
|
+
if (e && e.kind !== "inprocess") this.entries.splice(i, 1);
|
|
464
|
+
}
|
|
465
|
+
this.entries.push(...configuredEntries);
|
|
466
|
+
}
|
|
467
|
+
/** All entries registered for an event, in registration order. */
|
|
468
|
+
list(event) {
|
|
469
|
+
return this.entries.filter((e) => e.event === event);
|
|
470
|
+
}
|
|
471
|
+
/** True when any entry is registered for the event. */
|
|
472
|
+
has(event) {
|
|
473
|
+
return this.entries.some((e) => e.event === event);
|
|
474
|
+
}
|
|
475
|
+
/** Every entry currently registered (across all events). */
|
|
476
|
+
all() {
|
|
477
|
+
return this.entries.slice();
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Drop every in-process hook whose `owner` matches. Used by the plugin
|
|
481
|
+
* loader during teardown as a belt-and-braces backstop for the per-call
|
|
482
|
+
* unsubscribe functions pushed onto `pluginCleanupFns` — if a plugin
|
|
483
|
+
* `setup()` throws partway through after registering some hooks, the
|
|
484
|
+
* remaining unsubscribes may never run, and the registry would otherwise
|
|
485
|
+
* hold dangling references to a torn-down plugin's closures.
|
|
486
|
+
*
|
|
487
|
+
* Returns the number of hooks actually removed (useful for tests).
|
|
488
|
+
* Shell hooks are owned by the runtime, not a plugin, so they're never
|
|
489
|
+
* drained by owner.
|
|
490
|
+
*/
|
|
491
|
+
drainByOwner(owner) {
|
|
492
|
+
let removed = 0;
|
|
493
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
494
|
+
const e = this.entries[i];
|
|
495
|
+
if (e && e.kind === "inprocess" && e.owner === owner) {
|
|
496
|
+
this.entries.splice(i, 1);
|
|
497
|
+
removed++;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return removed;
|
|
501
|
+
}
|
|
502
|
+
/** Count of in-process hooks currently registered by `owner`. */
|
|
503
|
+
countByOwner(owner) {
|
|
504
|
+
let n = 0;
|
|
505
|
+
for (const e of this.entries) {
|
|
506
|
+
if (e.kind === "inprocess" && e.owner === owner) n++;
|
|
507
|
+
}
|
|
508
|
+
return n;
|
|
509
|
+
}
|
|
510
|
+
/** Drop every registered hook (used in teardown / tests). */
|
|
511
|
+
clear() {
|
|
512
|
+
this.entries.length = 0;
|
|
513
|
+
}
|
|
514
|
+
remove(entry) {
|
|
515
|
+
const i = this.entries.indexOf(entry);
|
|
516
|
+
if (i >= 0) this.entries.splice(i, 1);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
function hookMatcherMatches(matcher, toolName) {
|
|
520
|
+
if (!matcher || matcher === "*") return true;
|
|
521
|
+
if (toolName === void 0) return true;
|
|
522
|
+
const target = toolName.toLowerCase();
|
|
523
|
+
return matcher.split("|").map((s) => s.trim().toLowerCase()).filter(Boolean).includes(target);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/hooks/runner.ts
|
|
527
|
+
var DEFAULT_TIMEOUT_MS3 = 5e3;
|
|
528
|
+
var DEFAULT_BACKGROUND_TIMEOUT_MS = 6e4;
|
|
529
|
+
var MAX_TIMEOUT_MS = 10 * 6e4;
|
|
530
|
+
function hookTimeoutMs(entry) {
|
|
531
|
+
return entry.timeoutMs ?? (entry.background ? DEFAULT_BACKGROUND_TIMEOUT_MS : DEFAULT_TIMEOUT_MS3);
|
|
532
|
+
}
|
|
533
|
+
function contextFromOutcome(outcome) {
|
|
534
|
+
return {
|
|
535
|
+
...outcome.additionalContext ? { additionalContext: outcome.additionalContext } : {},
|
|
536
|
+
...outcome.contextAs ? { contextAs: outcome.contextAs } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function normalizePreToolOutcome(outcome) {
|
|
540
|
+
const context = contextFromOutcome(outcome);
|
|
541
|
+
if ("action" in outcome) return { ...outcome, ...context };
|
|
542
|
+
if (outcome.decision === "block") {
|
|
543
|
+
return {
|
|
544
|
+
action: "deny",
|
|
545
|
+
reason: outcome.reason?.trim() || "blocked by PreToolUse hook",
|
|
546
|
+
...context
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (outcome.modifiedInput && typeof outcome.modifiedInput === "object") {
|
|
550
|
+
return {
|
|
551
|
+
action: "mutate",
|
|
552
|
+
input: outcome.modifiedInput,
|
|
553
|
+
...outcome.reason ? { reason: outcome.reason } : {},
|
|
554
|
+
...context
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return { action: "allow", ...context };
|
|
558
|
+
}
|
|
559
|
+
function malformedOutcome(outcome) {
|
|
560
|
+
if (!("action" in outcome)) return void 0;
|
|
561
|
+
if (outcome.action === "allow") return void 0;
|
|
562
|
+
if (outcome.action === "deny") {
|
|
563
|
+
return typeof outcome.reason === "string" && outcome.reason.trim() ? void 0 : "deny outcome requires a non-empty reason";
|
|
564
|
+
}
|
|
565
|
+
if (outcome.action === "mutate") {
|
|
566
|
+
return outcome.input && typeof outcome.input === "object" && !Array.isArray(outcome.input) ? void 0 : "mutate outcome requires an object input";
|
|
567
|
+
}
|
|
568
|
+
return `unknown hook action: ${String(outcome.action)}`;
|
|
569
|
+
}
|
|
570
|
+
var HookRunner = class {
|
|
571
|
+
constructor(opts) {
|
|
572
|
+
this.opts = opts;
|
|
573
|
+
this.registry = opts.registry;
|
|
574
|
+
}
|
|
575
|
+
opts;
|
|
576
|
+
registry;
|
|
577
|
+
/**
|
|
578
|
+
* Background hooks are advisory and must never form an unbounded work queue.
|
|
579
|
+
* Keep at most one invocation running plus the latest pending payload for
|
|
580
|
+
* each registered hook.
|
|
581
|
+
*/
|
|
582
|
+
backgroundRuns = /* @__PURE__ */ new Map();
|
|
583
|
+
/** Cheap guard so callers can skip building payloads when nothing listens. */
|
|
584
|
+
has(event) {
|
|
585
|
+
return this.registry.has(event);
|
|
586
|
+
}
|
|
587
|
+
async preToolUse(toolName, toolInput, env, toolMeta) {
|
|
588
|
+
const entries = this.matching("PreToolUse", toolName);
|
|
589
|
+
if (entries.length === 0) return {};
|
|
590
|
+
const declared = {
|
|
591
|
+
...toolMeta?.capabilities ? { toolCapabilities: toolMeta.capabilities } : {},
|
|
592
|
+
...toolMeta?.mutating === void 0 ? {} : { toolMutating: toolMeta.mutating }
|
|
593
|
+
};
|
|
594
|
+
let current = toolInput;
|
|
595
|
+
let mutated = false;
|
|
596
|
+
const contexts = [];
|
|
597
|
+
let contextAs;
|
|
598
|
+
const collectContext = (outcome) => {
|
|
599
|
+
if (outcome.additionalContext) contexts.push(outcome.additionalContext);
|
|
600
|
+
if (outcome.contextAs === "separate") contextAs = "separate";
|
|
601
|
+
else if (outcome.contextAs === "inline" && contextAs === void 0) contextAs = "inline";
|
|
602
|
+
};
|
|
603
|
+
const resultContext = () => ({
|
|
604
|
+
...contexts.length > 0 ? { additionalContext: contexts.join("\n") } : {},
|
|
605
|
+
...contextAs ? { contextAs } : {}
|
|
606
|
+
});
|
|
607
|
+
const mutators = entries.filter((entry) => entry.stage === "mutate");
|
|
608
|
+
const validators = entries.filter((entry) => entry.stage === "validate");
|
|
609
|
+
for (const entry of mutators) {
|
|
610
|
+
const payload = {
|
|
611
|
+
event: "PreToolUse",
|
|
612
|
+
toolName,
|
|
613
|
+
toolInput: current,
|
|
614
|
+
...declared,
|
|
615
|
+
...this.base(env)
|
|
616
|
+
};
|
|
617
|
+
const outcome = await this.invoke(entry, payload, env);
|
|
618
|
+
if (!outcome) continue;
|
|
619
|
+
const normalized = normalizePreToolOutcome(outcome);
|
|
620
|
+
collectContext(normalized);
|
|
621
|
+
if (normalized.action === "deny") {
|
|
622
|
+
return { block: true, reason: normalized.reason, ...resultContext() };
|
|
623
|
+
}
|
|
624
|
+
if (normalized.action === "mutate") {
|
|
625
|
+
current = normalized.input;
|
|
626
|
+
mutated = true;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const entry of validators) {
|
|
630
|
+
const payload = {
|
|
631
|
+
event: "PreToolUse",
|
|
632
|
+
toolName,
|
|
633
|
+
toolInput: current,
|
|
634
|
+
...declared,
|
|
635
|
+
...this.base(env)
|
|
636
|
+
};
|
|
637
|
+
const outcome = await this.invoke(entry, payload, env);
|
|
638
|
+
if (!outcome) continue;
|
|
639
|
+
const normalized = normalizePreToolOutcome(outcome);
|
|
640
|
+
collectContext(normalized);
|
|
641
|
+
if (normalized.action === "deny") {
|
|
642
|
+
return { block: true, reason: normalized.reason, ...resultContext() };
|
|
643
|
+
}
|
|
644
|
+
if (normalized.action === "mutate") {
|
|
645
|
+
const reason = `PreToolUse validator "${entry.name}" attempted to mutate tool arguments`;
|
|
646
|
+
this.opts.logger?.warn?.(reason);
|
|
647
|
+
if (entry.failurePolicy === "closed") {
|
|
648
|
+
return { block: true, reason, ...resultContext() };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { ...mutated ? { input: current } : {}, ...resultContext() };
|
|
653
|
+
}
|
|
654
|
+
async postToolUse(toolName, toolInput, result, env) {
|
|
655
|
+
const payload = {
|
|
656
|
+
event: "PostToolUse",
|
|
657
|
+
toolName,
|
|
658
|
+
toolInput,
|
|
659
|
+
toolResult: result,
|
|
660
|
+
...this.base(env)
|
|
661
|
+
};
|
|
662
|
+
const entries = this.matching("PostToolUse", toolName);
|
|
663
|
+
if (entries.length === 0) return {};
|
|
664
|
+
const foreground = [];
|
|
665
|
+
for (const entry of entries) {
|
|
666
|
+
if (entry.background) {
|
|
667
|
+
this.scheduleBackground(entry, payload, env);
|
|
668
|
+
} else {
|
|
669
|
+
foreground.push(entry);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (foreground.length === 0) return {};
|
|
673
|
+
const results = await Promise.allSettled(
|
|
674
|
+
foreground.map((entry) => this.invoke(entry, payload, env))
|
|
675
|
+
);
|
|
676
|
+
const parts = [];
|
|
677
|
+
let separate = false;
|
|
678
|
+
for (const result2 of results) {
|
|
679
|
+
if (result2.status !== "fulfilled" || !result2.value?.additionalContext) continue;
|
|
680
|
+
parts.push(result2.value.additionalContext);
|
|
681
|
+
if (result2.value.contextAs === "separate") separate = true;
|
|
682
|
+
}
|
|
683
|
+
return parts.length ? { additionalContext: parts.join("\n"), contextAs: separate ? "separate" : "inline" } : {};
|
|
684
|
+
}
|
|
685
|
+
async userPromptSubmit(prompt, env) {
|
|
686
|
+
const entries = this.matching("UserPromptSubmit", void 0);
|
|
687
|
+
if (entries.length === 0) return {};
|
|
688
|
+
const payload = { event: "UserPromptSubmit", prompt, ...this.base(env) };
|
|
689
|
+
const parts = [];
|
|
690
|
+
for (const entry of entries) {
|
|
691
|
+
const outcome = await this.invoke(entry, payload, env);
|
|
692
|
+
if (!outcome) continue;
|
|
693
|
+
const denied = "action" in outcome ? outcome.action === "deny" : outcome.decision === "block";
|
|
694
|
+
if (denied) {
|
|
695
|
+
const reason = "reason" in outcome ? outcome.reason : void 0;
|
|
696
|
+
return { block: true, reason: reason ?? "Blocked by UserPromptSubmit hook" };
|
|
697
|
+
}
|
|
698
|
+
if (outcome.additionalContext) parts.push(outcome.additionalContext);
|
|
699
|
+
}
|
|
700
|
+
return parts.length ? { additionalContext: parts.join("\n") } : {};
|
|
701
|
+
}
|
|
702
|
+
async sessionStart(env) {
|
|
703
|
+
const payload = { event: "SessionStart", ...this.base(env) };
|
|
704
|
+
return {
|
|
705
|
+
additionalContext: await this.collectContext("SessionStart", void 0, payload, env)
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async stop(env) {
|
|
709
|
+
const payload = { event: "Stop", ...this.base(env) };
|
|
710
|
+
await this.collectContext("Stop", void 0, payload, env);
|
|
711
|
+
}
|
|
712
|
+
// ── internals ──────────────────────────────────────────────────────
|
|
713
|
+
base(env) {
|
|
714
|
+
const sessionId = this.opts.sessionId?.();
|
|
715
|
+
return sessionId ? { cwd: env.cwd, sessionId } : { cwd: env.cwd };
|
|
716
|
+
}
|
|
717
|
+
matching(event, toolName) {
|
|
718
|
+
return this.registry.list(event).filter((e) => hookMatcherMatches(e.matcher, toolName));
|
|
719
|
+
}
|
|
720
|
+
scheduleBackground(entry, payload, env) {
|
|
721
|
+
const running = this.backgroundRuns.get(entry);
|
|
722
|
+
if (running !== void 0) {
|
|
723
|
+
running.pending = { payload, env };
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const state = { pending: null };
|
|
727
|
+
this.backgroundRuns.set(entry, state);
|
|
728
|
+
void (async () => {
|
|
729
|
+
let next = { payload, env };
|
|
730
|
+
while (next !== null) {
|
|
731
|
+
try {
|
|
732
|
+
await this.invoke(entry, next.payload, next.env);
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
next = state.pending;
|
|
736
|
+
state.pending = null;
|
|
737
|
+
}
|
|
738
|
+
})().finally(() => {
|
|
739
|
+
this.backgroundRuns.delete(entry);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async invoke(entry, payload, env) {
|
|
743
|
+
const allowNonPolicy = this.opts.allowNonPolicy ?? true;
|
|
744
|
+
if (!allowNonPolicy && !entry.policy) return null;
|
|
745
|
+
let result;
|
|
746
|
+
if (entry.kind === "inprocess") {
|
|
747
|
+
result = await this.invokeInProcess(entry, payload, env);
|
|
748
|
+
} else {
|
|
749
|
+
result = entry.kind === "shell" ? await runShellHookDetailed(
|
|
750
|
+
{ command: entry.command, timeoutMs: hookTimeoutMs(entry) },
|
|
751
|
+
payload,
|
|
752
|
+
this.opts.logger,
|
|
753
|
+
{ signal: env.signal }
|
|
754
|
+
) : await runHttpHookDetailed(
|
|
755
|
+
{ url: entry.url, headers: entry.headers, timeoutMs: hookTimeoutMs(entry) },
|
|
756
|
+
payload,
|
|
757
|
+
this.opts.logger,
|
|
758
|
+
{ signal: env.signal }
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (!result.failure && result.outcome) {
|
|
762
|
+
const malformed = malformedOutcome(result.outcome);
|
|
763
|
+
if (malformed) {
|
|
764
|
+
result = {
|
|
765
|
+
outcome: null,
|
|
766
|
+
failure: { kind: "invalid_output", message: malformed }
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (!result.failure) return result.outcome;
|
|
771
|
+
this.opts.logger?.warn?.(
|
|
772
|
+
`${payload.event} ${entry.background ? "background " : ""}hook "${entry.name}" failed (${result.failure.kind}): ${result.failure.message}`
|
|
773
|
+
);
|
|
774
|
+
return this.failureOutcome(entry, payload, result.failure);
|
|
775
|
+
}
|
|
776
|
+
async invokeInProcess(entry, payload, env) {
|
|
777
|
+
const timeoutMs = Math.max(1, Math.min(hookTimeoutMs(entry), MAX_TIMEOUT_MS));
|
|
778
|
+
const controller = new AbortController();
|
|
779
|
+
let timedOut = false;
|
|
780
|
+
const onParentAbort = () => controller.abort(env.signal?.reason);
|
|
781
|
+
if (env.signal?.aborted) {
|
|
782
|
+
return { outcome: null, failure: { kind: "aborted", message: "hook invocation aborted" } };
|
|
783
|
+
}
|
|
784
|
+
env.signal?.addEventListener("abort", onParentAbort, { once: true });
|
|
785
|
+
let timer;
|
|
786
|
+
const timeout = new Promise((_, reject) => {
|
|
787
|
+
timer = setTimeout(() => {
|
|
788
|
+
timedOut = true;
|
|
789
|
+
controller.abort(new Error("hook timeout"));
|
|
790
|
+
reject(new Error(`timed out after ${timeoutMs}ms`));
|
|
791
|
+
}, timeoutMs);
|
|
792
|
+
timer.unref?.();
|
|
793
|
+
});
|
|
794
|
+
const runtime = {
|
|
795
|
+
signal: controller.signal,
|
|
796
|
+
deadlineAt: Date.now() + timeoutMs
|
|
797
|
+
};
|
|
798
|
+
try {
|
|
799
|
+
const invocation = Promise.resolve().then(() => entry.hook(payload, runtime));
|
|
800
|
+
const outcome = await Promise.race([invocation, timeout]);
|
|
801
|
+
if (outcome === void 0) return { outcome: null };
|
|
802
|
+
if (!outcome || typeof outcome !== "object" || Array.isArray(outcome)) {
|
|
803
|
+
return {
|
|
804
|
+
outcome: null,
|
|
805
|
+
failure: {
|
|
806
|
+
kind: "invalid_output",
|
|
807
|
+
message: "in-process hook returned a non-object outcome"
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return { outcome };
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const aborted = env.signal?.aborted === true;
|
|
814
|
+
return {
|
|
815
|
+
outcome: null,
|
|
816
|
+
failure: {
|
|
817
|
+
kind: timedOut ? "timeout" : aborted ? "aborted" : "error",
|
|
818
|
+
message: toErrorMessage(err)
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
} finally {
|
|
822
|
+
if (timer) clearTimeout(timer);
|
|
823
|
+
env.signal?.removeEventListener("abort", onParentAbort);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
failureOutcome(entry, payload, failure) {
|
|
827
|
+
if (entry.failurePolicy !== "closed") return null;
|
|
828
|
+
const reason = `${payload.event} policy hook "${entry.name}" failed (${failure.kind}); the action was denied by fail-closed policy. This is not an approval request.`;
|
|
829
|
+
if (payload.event === "PreToolUse") return { action: "deny", reason };
|
|
830
|
+
if (payload.event === "UserPromptSubmit") return { decision: "block", reason };
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
async collectContext(event, toolName, payload, env) {
|
|
834
|
+
const entries = this.matching(event, toolName);
|
|
835
|
+
if (entries.length === 0) return void 0;
|
|
836
|
+
const results = await Promise.allSettled(
|
|
837
|
+
entries.map((entry) => this.invoke(entry, payload, env))
|
|
838
|
+
);
|
|
839
|
+
const parts = [];
|
|
840
|
+
for (const result of results) {
|
|
841
|
+
if (result.status === "fulfilled" && result.value?.additionalContext) {
|
|
842
|
+
parts.push(result.value.additionalContext);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return parts.length ? parts.join("\n") : void 0;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/hooks/shell-hooks-equal.ts
|
|
850
|
+
function stableJson(value) {
|
|
851
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
852
|
+
if (value && typeof value === "object") {
|
|
853
|
+
const entries = Object.entries(value).filter(([, child]) => child !== void 0).sort(([a], [b]) => a.localeCompare(b));
|
|
854
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`).join(",")}}`;
|
|
855
|
+
}
|
|
856
|
+
return JSON.stringify(value) ?? "null";
|
|
857
|
+
}
|
|
858
|
+
function shellHooksEqual(a, b) {
|
|
859
|
+
if (a === b) return true;
|
|
860
|
+
if (!a || !b) return !a && !b;
|
|
861
|
+
const aKeys = Object.keys(a);
|
|
862
|
+
const bKeys = Object.keys(b);
|
|
863
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
864
|
+
for (const k of aKeys) {
|
|
865
|
+
const event = k;
|
|
866
|
+
const aList = a[event];
|
|
867
|
+
const bList = b[event];
|
|
868
|
+
if (aList === bList) continue;
|
|
869
|
+
if (!aList || !bList) return false;
|
|
870
|
+
if (aList.length !== bList.length) return false;
|
|
871
|
+
for (let i = 0; i < aList.length; i++) {
|
|
872
|
+
const x = aList[i];
|
|
873
|
+
const y = bList[i];
|
|
874
|
+
if (!x || !y) return !x && !y;
|
|
875
|
+
if (stableJson(x) !== stableJson(y)) return false;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
function countShellHooks(hooks) {
|
|
881
|
+
if (!hooks) return 0;
|
|
882
|
+
let n = 0;
|
|
883
|
+
for (const list of Object.values(hooks)) {
|
|
884
|
+
if (list) n += list.length;
|
|
885
|
+
}
|
|
886
|
+
return n;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export {
|
|
890
|
+
runShellHook,
|
|
891
|
+
runShellHookDetailed,
|
|
892
|
+
parseHookOutcome,
|
|
893
|
+
runHttpHookDetailed,
|
|
894
|
+
HookRegistry,
|
|
895
|
+
hookMatcherMatches,
|
|
896
|
+
HookRunner,
|
|
897
|
+
shellHooksEqual,
|
|
898
|
+
countShellHooks
|
|
899
|
+
};
|
|
900
|
+
//# sourceMappingURL=chunk-TWJMGXCI.js.map
|