@sema-agent/server 7.130.0 → 7.132.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.
Files changed (294) hide show
  1. package/MIGRATION.md +396 -0
  2. package/README.md +13 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +187 -50
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/audit.js +4 -1
  9. package/dist/auth-bridge.d.ts +4 -0
  10. package/dist/auth-bridge.js +10 -11
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/deployment-mods.d.ts +31 -0
  13. package/dist/boot/deployment-mods.js +26 -0
  14. package/dist/boot/execution-env.d.ts +1 -1
  15. package/dist/boot/execution-env.js +19 -6
  16. package/dist/boot/memory-boundary.d.ts +2 -2
  17. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  18. package/dist/boot/read-deny-anchor-audit.js +36 -0
  19. package/dist/boot/resolve-spec.d.ts +1 -5
  20. package/dist/boot/resolve-spec.js +56 -61
  21. package/dist/boot/runner-deps.d.ts +19 -8
  22. package/dist/boot/runner-deps.js +14 -2
  23. package/dist/boot/session-faces.d.ts +1 -7
  24. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  25. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  26. package/dist/boot/stage-03-store-backend.js +2 -2
  27. package/dist/boot/stage-04-stores.d.ts +1 -1
  28. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  29. package/dist/boot/stage-05-execution-env.js +7 -10
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +5 -4
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  33. package/dist/boot/stage-07-capability-layer.js +3 -3
  34. package/dist/boot/stage-08-reapers.d.ts +2 -2
  35. package/dist/boot/stage-09-leader.d.ts +3 -9
  36. package/dist/boot/stage-10-http-server.d.ts +3 -9
  37. package/dist/boot/stage-10-http-server.js +12 -10
  38. package/dist/boot/stores.d.ts +1 -1
  39. package/dist/boot/stores.js +5 -6
  40. package/dist/boot/web-admin.d.ts +0 -3
  41. package/dist/boot/web-admin.js +35 -35
  42. package/dist/brain.js +17 -22
  43. package/dist/budget.js +5 -4
  44. package/dist/caller-text-echo.d.ts +20 -0
  45. package/dist/caller-text-echo.js +24 -0
  46. package/dist/capabilities/brand-identity.d.ts +26 -0
  47. package/dist/capabilities/brand-identity.js +27 -0
  48. package/dist/capabilities/builtin-tools.js +2 -1
  49. package/dist/capabilities/center-plugins.js +1 -1
  50. package/dist/capabilities/center-prompts.d.ts +32 -59
  51. package/dist/capabilities/center-prompts.js +18 -32
  52. package/dist/capabilities/composite-members.d.ts +1 -0
  53. package/dist/capabilities/composite-members.js +1 -0
  54. package/dist/capabilities/engine-capabilities.js +2 -1
  55. package/dist/capabilities/hands-lane.d.ts +9 -6
  56. package/dist/capabilities/hands-lane.js +3 -3
  57. package/dist/capabilities/host-tool-names.d.ts +17 -0
  58. package/dist/capabilities/host-tool-names.js +15 -0
  59. package/dist/capabilities/mcp-probe.d.ts +14 -1
  60. package/dist/capabilities/mcp-probe.js +22 -133
  61. package/dist/capabilities/repo-tools.js +4 -3
  62. package/dist/capabilities/scenarios.d.ts +42 -47
  63. package/dist/capabilities/scenarios.js +19 -35
  64. package/dist/capabilities/select-environment-tool.js +2 -1
  65. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  66. package/dist/capabilities/send-user-file-tool.js +2 -2
  67. package/dist/capabilities/skills.d.ts +19 -4
  68. package/dist/capabilities/skills.js +30 -10
  69. package/dist/capabilities/tool-defer.js +2 -1
  70. package/dist/config-catalog.js +22 -11
  71. package/dist/config-invariants.js +2 -2
  72. package/dist/config-lkg.d.ts +9 -9
  73. package/dist/config-lkg.js +20 -4
  74. package/dist/config-provider.d.ts +3 -1
  75. package/dist/config-provider.js +14 -6
  76. package/dist/config-types.d.ts +13 -9
  77. package/dist/config.d.ts +2 -2
  78. package/dist/config.js +34 -20
  79. package/dist/device-ws-protocol.d.ts +1 -1
  80. package/dist/device-ws-protocol.js +1 -0
  81. package/dist/elicitation.d.ts +7 -0
  82. package/dist/elicitation.js +20 -5
  83. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  84. package/dist/env-name-allowlist-knobs.js +5 -4
  85. package/dist/exec-bounds.d.ts +15 -3
  86. package/dist/exec-bounds.js +4 -0
  87. package/dist/execution-lane-caps.d.ts +9 -2
  88. package/dist/execution-lane-caps.js +11 -0
  89. package/dist/hooks/classic-stations.d.ts +86 -0
  90. package/dist/hooks/classic-stations.js +27 -0
  91. package/dist/hooks/hook-llm.d.ts +8 -5
  92. package/dist/hooks/hook-llm.js +80 -37
  93. package/dist/hooks/hook-runner.d.ts +273 -75
  94. package/dist/hooks/hook-runner.js +293 -317
  95. package/dist/http/admission.d.ts +8 -2
  96. package/dist/http/admission.js +103 -59
  97. package/dist/http/approval-sweeps.js +59 -18
  98. package/dist/http/checkpoint-ctx.d.ts +36 -18
  99. package/dist/http/checkpoint-ctx.js +14 -10
  100. package/dist/http/claimed-run.d.ts +13 -0
  101. package/dist/http/claimed-run.js +15 -0
  102. package/dist/http/device-placement.js +5 -10
  103. package/dist/http/dispatch.d.ts +0 -4
  104. package/dist/http/dispatch.js +6 -10
  105. package/dist/http/durable-run-leg.d.ts +2 -1
  106. package/dist/http/durable-run-leg.js +5 -3
  107. package/dist/http/health-self-report.d.ts +7 -5
  108. package/dist/http/health-self-report.js +4 -18
  109. package/dist/http/principal-gate.d.ts +23 -6
  110. package/dist/http/principal-gate.js +24 -5
  111. package/dist/http/refusal-body.d.ts +21 -0
  112. package/dist/http/refusal-body.js +4 -0
  113. package/dist/http/request-guards.d.ts +27 -0
  114. package/dist/http/request-guards.js +24 -3
  115. package/dist/http/resume-legs.d.ts +25 -0
  116. package/dist/http/resume-legs.js +48 -26
  117. package/dist/http/routes/approvals-assistant.js +1 -1
  118. package/dist/http/routes/capabilities.js +7 -17
  119. package/dist/http/routes/diagnostics.js +1 -1
  120. package/dist/http/routes/rules.js +2 -2
  121. package/dist/http/routes/runs.js +13 -17
  122. package/dist/http/routes/session-sync.js +4 -2
  123. package/dist/http/routes/sessions.js +23 -11
  124. package/dist/http/routes/shared-memory.js +2 -13
  125. package/dist/http/routes/side-query.js +2 -3
  126. package/dist/http/routes/tasks.js +34 -19
  127. package/dist/http/scenario-read-face.d.ts +14 -0
  128. package/dist/http/scenario-read-face.js +5 -0
  129. package/dist/http/send.d.ts +13 -1
  130. package/dist/http/send.js +12 -1
  131. package/dist/http/server.d.ts +37 -23
  132. package/dist/http/server.js +30 -4
  133. package/dist/http/stored-body-replay.d.ts +10 -3
  134. package/dist/http/stored-body-replay.js +27 -6
  135. package/dist/http/submit-ctx.d.ts +25 -0
  136. package/dist/http/submit-ctx.js +39 -0
  137. package/dist/http/wire-types.d.ts +18 -5
  138. package/dist/index.d.ts +3 -2
  139. package/dist/index.js +3 -2
  140. package/dist/lane-home-probe.d.ts +43 -0
  141. package/dist/lane-home-probe.js +35 -0
  142. package/dist/limit-refusal.d.ts +47 -0
  143. package/dist/limit-refusal.js +12 -0
  144. package/dist/main.js +1 -1
  145. package/dist/mcp-assertion-header.d.ts +3 -11
  146. package/dist/mcp-assertion-header.js +3 -9
  147. package/dist/mcp-tool-faces.d.ts +5 -0
  148. package/dist/mcp-tool-faces.js +5 -1
  149. package/dist/memory-scope.d.ts +1 -1
  150. package/dist/model-entry-refusal.d.ts +1 -7
  151. package/dist/model-provider.d.ts +39 -12
  152. package/dist/model-provider.js +19 -2
  153. package/dist/model-route-endpoint.d.ts +52 -12
  154. package/dist/model-route-endpoint.js +48 -26
  155. package/dist/model-route-gap.d.ts +5 -4
  156. package/dist/model-route-gap.js +1 -1
  157. package/dist/model-select.d.ts +58 -0
  158. package/dist/model-select.js +31 -0
  159. package/dist/namespaced-rule-name.d.ts +7 -4
  160. package/dist/namespaced-rule-name.js +4 -3
  161. package/dist/observability/cost-taxonomy.d.ts +7 -7
  162. package/dist/observability/fail-open.d.ts +53 -12
  163. package/dist/observability/fail-open.js +47 -7
  164. package/dist/observability/metrics.js +3 -1
  165. package/dist/observability/prompt-manifest.d.ts +47 -82
  166. package/dist/observability/prompt-manifest.js +119 -17
  167. package/dist/observability/run-terminal.js +14 -6
  168. package/dist/observability/secret-env-scrub.d.ts +13 -1
  169. package/dist/observability/secret-env-scrub.js +13 -1
  170. package/dist/observability/tool-trace.d.ts +16 -21
  171. package/dist/observability/tool-trace.js +51 -42
  172. package/dist/per-task-image.d.ts +7 -1
  173. package/dist/per-task-image.js +3 -3
  174. package/dist/plain-string-record.d.ts +25 -0
  175. package/dist/plain-string-record.js +32 -0
  176. package/dist/plugins/blob-backend.d.ts +68 -13
  177. package/dist/plugins/blob-backend.js +53 -10
  178. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  179. package/dist/plugins/checkpoint-store-sql.js +39 -26
  180. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  181. package/dist/plugins/k8s-bg-scripts.js +22 -0
  182. package/dist/plugins/local-checkpoint-store.js +5 -2
  183. package/dist/plugins/local-session-store.js +3 -3
  184. package/dist/plugins/one-time-migrations.d.ts +147 -91
  185. package/dist/plugins/one-time-migrations.js +372 -208
  186. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  187. package/dist/plugins/permission-rule-store-sql.js +4 -18
  188. package/dist/plugins/pg-session-storage.d.ts +4 -5
  189. package/dist/plugins/pg-session-storage.js +7 -17
  190. package/dist/plugins/remote-env-adb.d.ts +6 -0
  191. package/dist/plugins/remote-env-adb.js +9 -4
  192. package/dist/plugins/remote-env-device.js +3 -0
  193. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  194. package/dist/plugins/remote-env-e2b.js +21 -9
  195. package/dist/plugins/remote-env-host.d.ts +9 -9
  196. package/dist/plugins/remote-env-host.js +10 -32
  197. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  198. package/dist/plugins/remote-env-k8s.js +265 -90
  199. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  200. package/dist/plugins/remote-env-local-docker.js +77 -9
  201. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  202. package/dist/plugins/remote-env-ssh.js +109 -3
  203. package/dist/plugins/remote-shell.d.ts +0 -1
  204. package/dist/plugins/remote-shell.js +1 -2
  205. package/dist/plugins/retention-store-sql.js +6 -2
  206. package/dist/plugins/schema-consistency.d.ts +54 -0
  207. package/dist/plugins/schema-consistency.js +52 -0
  208. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  209. package/dist/plugins/session-epoch-anchor.js +23 -0
  210. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  211. package/dist/plugins/shared-memory-store-sql.js +5 -16
  212. package/dist/plugins/sql-driver.d.ts +141 -0
  213. package/dist/plugins/sql-driver.js +115 -1
  214. package/dist/plugins/sql-errors.d.ts +41 -0
  215. package/dist/plugins/sql-errors.js +36 -0
  216. package/dist/plugins/store-backend.js +2 -1
  217. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  218. package/dist/plugins/tidb-session-storage.js +6 -16
  219. package/dist/plugins/tidb-session-store.js +1 -1
  220. package/dist/prompts-domain-validate.d.ts +17 -61
  221. package/dist/prompts-domain-validate.js +9 -119
  222. package/dist/question.d.ts +18 -9
  223. package/dist/question.js +21 -13
  224. package/dist/request-key-closure.d.ts +4 -4
  225. package/dist/request-key-closure.js +18 -13
  226. package/dist/rule-entry-list.d.ts +134 -24
  227. package/dist/rule-entry-list.js +162 -30
  228. package/dist/run-local.d.ts +3 -3
  229. package/dist/run-local.js +26 -25
  230. package/dist/runs.d.ts +48 -2
  231. package/dist/runs.js +61 -2
  232. package/dist/server-secret-env.d.ts +44 -18
  233. package/dist/server-secret-env.js +49 -8
  234. package/dist/session-incarnation.d.ts +15 -5
  235. package/dist/session-incarnation.js +16 -9
  236. package/dist/session-titler.js +12 -2
  237. package/dist/spec-fields.d.ts +34 -18
  238. package/dist/spec-fields.js +46 -53
  239. package/dist/sql-engine-posture.d.ts +29 -4
  240. package/dist/sql-engine-posture.js +36 -1
  241. package/dist/stored-request-hooks-m20.d.ts +23 -0
  242. package/dist/stored-request-hooks-m20.js +124 -0
  243. package/dist/stored-request-tool-axes-m19.d.ts +20 -5
  244. package/dist/stored-request-tool-axes-m19.js +30 -2
  245. package/dist/task-a2a.d.ts +9 -1
  246. package/dist/task-a2a.js +73 -47
  247. package/dist/task-mcp.d.ts +54 -102
  248. package/dist/task-mcp.js +194 -92
  249. package/dist/task-settings.d.ts +47 -25
  250. package/dist/task-settings.js +46 -61
  251. package/dist/text-bidi.d.ts +3 -46
  252. package/dist/text-bidi.js +3 -3
  253. package/dist/tool-approval.d.ts +1 -1
  254. package/dist/trace/core-keyset-guard.d.ts +24 -4
  255. package/dist/trace/engine-notice-wire.d.ts +11 -3
  256. package/dist/trace/engine-notice-wire.js +8 -1
  257. package/dist/trace/ledger-events.d.ts +3 -1
  258. package/dist/trace/ledger-sink.d.ts +29 -0
  259. package/dist/trace/ledger-sink.js +15 -1
  260. package/dist/trace/model-segment.js +1 -1
  261. package/dist/trace/project.d.ts +25 -6
  262. package/dist/trace/project.js +68 -22
  263. package/dist/trace/wire-projection-faces.d.ts +80 -2
  264. package/dist/trace/wire-projection-faces.js +23 -1
  265. package/dist/user-authored-text.d.ts +7 -0
  266. package/dist/user-authored-text.js +3 -0
  267. package/dist/web-admin-client/apply-effective.js +10 -5
  268. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  269. package/dist/web-admin-client/candidate-gate.js +3 -3
  270. package/dist/web-admin-client/http-client.js +0 -6
  271. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  272. package/dist/web-admin-client/read-warnings.js +10 -0
  273. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  274. package/dist/web-admin-client/skills-mcp.js +9 -6
  275. package/dist/web-admin-client/types.d.ts +3 -4
  276. package/dist/workspace-cache.d.ts +35 -0
  277. package/dist/workspace-cache.js +36 -0
  278. package/dist/workspace-space.d.ts +3 -2
  279. package/dist/workspace-space.js +2 -0
  280. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  281. package/package.json +4 -4
  282. package/scripts/drain-check-local.mjs +37 -3
  283. package/scripts/drain-check.mysql.sql +30 -2
  284. package/scripts/drain-check.pg.sql +29 -2
  285. package/dist/capabilities/memory-notice.d.ts +0 -138
  286. package/dist/capabilities/memory-notice.js +0 -64
  287. package/dist/capabilities/prompt.d.ts +0 -17
  288. package/dist/capabilities/prompt.js +0 -6
  289. package/dist/mcp-content-origin.d.ts +0 -34
  290. package/dist/mcp-content-origin.js +0 -10
  291. package/dist/tool-axis-words.d.ts +0 -27
  292. package/dist/tool-axis-words.js +0 -9
  293. package/dist/wire-name-bound.d.ts +0 -14
  294. package/dist/wire-name-bound.js +0 -13
@@ -1,10 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
2
3
  import vm from "node:vm";
3
4
  import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
4
- import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
5
- import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
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
+ import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
7
+ import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
8
+ import { describeToolCatalog } from "@sema-agent/core";
9
+ import { hangClassic } from "./classic-stations.js";
6
10
  import { boundedNoticeText } from "../fleet/fleet-bus.js";
7
11
  import { recordFailOpen } from "../observability/fail-open.js";
12
+ import { LIMIT_REASONS } from "../limit-refusal.js";
13
+ import { echoCallerText } from "../caller-text-echo.js";
8
14
  import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
9
15
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
10
16
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
@@ -25,26 +31,35 @@ const HOOK_ENV_ALLOWLIST = [
25
31
  "PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
26
32
  "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
27
33
  ];
28
- function hookEnv(ctx) {
29
- const env = { CLAUDE_PROJECT_DIR: ctx.cwd };
30
- for (const k of HOOK_ENV_ALLOWLIST) {
31
- const v = process.env[k];
32
- if (typeof v === "string")
33
- env[k] = v;
34
- }
35
- return { ...env, ...ctx.shellEnv };
34
+ function hookEnv(ctx, spawning) {
35
+ const base = spawning
36
+ ? hostSubprocessEnv({ site: "server.hooks.spawn-env", policy: [...HOOK_ENV_ALLOWLIST], allow: [] })
37
+ : composeHostSubprocessEnv({ policy: [...HOOK_ENV_ALLOWLIST], allow: [] });
38
+ return { ...base, CLAUDE_PROJECT_DIR: ctx.cwd, ...ctx.shellEnv };
39
+ }
40
+ export const SETTINGS_HOOK_STATIONS = {
41
+ PreToolUse: ["classic.PreToolUse"],
42
+ PostToolUse: ["classic.PostToolUse"],
43
+ PostToolUseFailure: ["classic.PostToolUseFailure"],
44
+ PostToolBatch: ["classic.PostToolBatch"],
45
+ UserPromptSubmit: ["classic.UserPromptSubmit"],
46
+ Stop: ["classic.Stop", "classic.SubagentStop"],
47
+ StopFailure: ["classic.StopFailure"],
48
+ PreCompact: ["classic.PreCompact"],
49
+ PostCompact: ["classic.PostCompact"],
50
+ };
51
+ function hangTranslator(on, event, translators) {
52
+ const translate = translators[event];
53
+ if (translate === undefined)
54
+ return;
55
+ for (const station of SETTINGS_HOOK_STATIONS[event])
56
+ hangClassic(on, station, translate);
57
+ }
58
+ export const SETTINGS_HOOKS_REGISTRATION = "settings.hooks";
59
+ export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
60
+ export function settingsHooksBudgetMs(config) {
61
+ return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
36
62
  }
37
- const LIT_EVENTS = [
38
- "PreToolUse",
39
- "PostToolUse",
40
- "PostToolUseFailure",
41
- "PostToolBatch",
42
- "UserPromptSubmit",
43
- "Stop",
44
- "StopFailure",
45
- "PreCompact",
46
- "PostCompact",
47
- ];
48
63
  const BRAIN_TO_TURN_ERROR = {
49
64
  auth: "authentication_failed",
50
65
  rate_limit: "rate_limit",
@@ -57,8 +72,25 @@ function toAssistantTurnError(errorKind) {
57
72
  return "unknown";
58
73
  return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
59
74
  }
75
+ const HOOK_ENTRY_TYPE_RUNS = {
76
+ command: true,
77
+ http: true,
78
+ prompt: true,
79
+ agent: true,
80
+ mcp_tool: false,
81
+ };
82
+ function isSupportedHookEntryType(t) {
83
+ return HOOK_ENTRY_TYPE_RUNS[t];
84
+ }
85
+ export const SUPPORTED_HOOK_ENTRY_TYPES = HOOK_ENTRY_TYPES.filter(isSupportedHookEntryType);
86
+ function isHookEventName(key) {
87
+ return HOOK_EVENTS.includes(key);
88
+ }
89
+ function isRunnableHookEntry(entry) {
90
+ return isSupportedHookEntryType(entry.type);
91
+ }
60
92
  export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
61
- export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
93
+ export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
62
94
  export const MATCHER_ECHO_CHARS = 200;
63
95
  const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
64
96
  function matcherReadsWireToolName(event) {
@@ -112,7 +144,7 @@ const MATCHER_EVALUATION = {
112
144
  regex: { regex: "budgeted", literal: "not_applicable" },
113
145
  invalid: { regex: "unreadable", literal: "not_applicable" },
114
146
  };
115
- function judgeMatcher(matcher, depth, budgetMs) {
147
+ function judgeMatcher(matcher, depth, ledger, judgesAlias) {
116
148
  if (matcher.length > MAX_MATCHER_EVAL_CHARS)
117
149
  return { kind: "unjudged", reason: "oversize" };
118
150
  const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
@@ -123,14 +155,34 @@ function judgeMatcher(matcher, depth, budgetMs) {
123
155
  return { kind: "invalid_regex" };
124
156
  case "inline":
125
157
  case "budgeted":
158
+ if (!judgesAlias)
159
+ return undefined;
126
160
  break;
127
161
  default: {
128
162
  const unreachable = evaluate;
129
163
  return unreachable;
130
164
  }
131
165
  }
132
- const selects = (name) => (evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs));
133
- const unanswered = (verdict) => (verdict === "over_budget" ? { kind: "over_budget" } : { kind: "invalid_regex" });
166
+ const selects = (name) => {
167
+ if (evaluate === "inline")
168
+ return hookMatcherSelects(matcher, name);
169
+ const startedAt = ledger.now();
170
+ const answer = matcherSelectsWithin(matcher, name, ledger.budgetMs);
171
+ ledger.spentMs += ledger.now() - startedAt;
172
+ if (answer === "over_budget" || answer === "invalid")
173
+ return answer;
174
+ return ledger.spentMs > ledger.totalMs ? "budget_exceeded" : answer;
175
+ };
176
+ const unanswered = (verdict) => {
177
+ switch (verdict) {
178
+ case "over_budget":
179
+ return { kind: "over_budget" };
180
+ case "budget_exceeded":
181
+ return { kind: "budget_exceeded" };
182
+ case "invalid":
183
+ return { kind: "invalid_regex" };
184
+ }
185
+ };
134
186
  for (const [alias, canonical] of engineToolAliases()) {
135
187
  const selectsAlias = selects(alias);
136
188
  if (typeof selectsAlias === "string")
@@ -145,50 +197,102 @@ function judgeMatcher(matcher, depth, budgetMs) {
145
197
  }
146
198
  return undefined;
147
199
  }
148
- export function buildToolMatcherRefusal(issue) {
200
+ const HOOKS_CAPS = {
201
+ matcher_groups: { measure: "entries", cap: MAX_HOOK_MATCHER_GROUPS, sentence: (event, count) => `settings.hooks.${event}: ${count} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` },
202
+ event_entries: { measure: "entries", cap: MAX_HOOK_ENTRIES_PER_EVENT, sentence: (event, count) => `settings.hooks.${event}: ${count} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` },
203
+ command: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
204
+ url: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
205
+ headers: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event, count) => `settings.hooks.${event}: ${count} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
206
+ header_chars: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` },
207
+ allowed_env_vars: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event) => `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
208
+ };
209
+ function hooksCapIssue(limit, event, path, count) {
210
+ const c = HOOKS_CAPS[limit];
211
+ return { reason: LIMIT_REASONS[c.measure], limit, event, path, count, cap: c.cap };
212
+ }
213
+ const matcherPath = (event, groupIndex) => ["hooks", event, groupIndex, "matcher"];
214
+ export function buildHooksRefusal(issue) {
215
+ const field = "settings.hooks";
149
216
  switch (issue.reason) {
150
217
  case "dead_alias":
151
218
  return {
152
219
  message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
153
- field: "settings.hooks",
154
- detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
220
+ field,
221
+ detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical, path: matcherPath(issue.event, issue.groupIndex) },
155
222
  };
156
223
  case "over_budget":
157
224
  return {
158
225
  message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
159
- field: "settings.hooks",
160
- detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
226
+ field,
227
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs, path: matcherPath(issue.event, issue.groupIndex) },
228
+ };
229
+ case "budget_exceeded":
230
+ return {
231
+ message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating this configuration's regular-expression matchers took longer than ${issue.totalMs} ms in total (very slow patterns) — write tool names as a list such as "Read|Edit", or simplify the patterns`,
232
+ field,
233
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, totalMs: issue.totalMs, path: matcherPath(issue.event, issue.groupIndex) },
161
234
  };
162
235
  case "invalid_regex":
163
236
  return {
164
237
  message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
165
- field: "settings.hooks",
166
- detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
238
+ field,
239
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, path: matcherPath(issue.event, issue.groupIndex) },
167
240
  };
241
+ case "unsupported_entry_type":
242
+ return {
243
+ message: `${hooksCoordinate(issue.path)}: this server does not run hook entries of type ${issue.type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`,
244
+ field,
245
+ detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), type: issue.type },
246
+ };
247
+ case "over_cap":
248
+ case "too_long": {
249
+ const cap = issue;
250
+ return { message: HOOKS_CAPS[cap.limit].sentence(cap.event, cap.count), field, detail: { reason: cap.reason, path: hooksOutgoingPath(cap.path), count: cap.count, cap: cap.cap } };
251
+ }
252
+ case "unnamed_refusal":
253
+ return { message: `${hooksCoordinate(issue.path)}: the hooks reader refused this configuration without naming a piece`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path) } };
168
254
  default: {
169
- const unreachable = issue;
170
- return unreachable;
255
+ const piece = issue;
256
+ const key = piece.key !== undefined ? hooksOutgoingKey(piece.key) : undefined;
257
+ return {
258
+ message: hooksPieceRefusal(piece),
259
+ field,
260
+ detail: { reason: piece.reason, path: hooksOutgoingPath(piece.path), ...(key !== undefined ? { key } : {}) },
261
+ };
171
262
  }
172
263
  }
173
264
  }
174
- export function toolMatcherIssue(config, depth, budget = {}) {
175
- const budgetMs = budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS;
265
+ export function hooksMatcherIssue(config, depth, budget = {}) {
266
+ const ledger = {
267
+ budgetMs: budget.budgetMs ?? ADMISSION_MATCHER_BUDGET.budgetMs,
268
+ totalMs: budget.totalMs ?? ADMISSION_MATCHER_BUDGET.totalMs,
269
+ now: budget.now ?? (() => performance.now()),
270
+ spentMs: 0,
271
+ };
272
+ const budgetMs = ledger.budgetMs;
176
273
  for (const [event, groups] of Object.entries(config)) {
177
- if (!matcherReadsWireToolName(event))
178
- continue;
274
+ const judgesAlias = matcherReadsWireToolName(event);
179
275
  for (const [groupIndex, group] of (groups ?? []).entries()) {
180
276
  if (group.matcher === undefined)
181
277
  continue;
182
- const verdict = judgeMatcher(group.matcher, depth, budgetMs);
278
+ const verdict = judgeMatcher(group.matcher, depth, ledger, judgesAlias);
183
279
  if (verdict === undefined)
184
280
  continue;
185
281
  switch (verdict.kind) {
186
282
  case "dead":
187
- return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
283
+ return { reason: "dead_alias", event, groupIndex, alias: verdict.alias, canonical: verdict.canonical };
188
284
  case "over_budget":
189
285
  return { reason: "over_budget", event, groupIndex, budgetMs };
286
+ case "budget_exceeded":
287
+ return { reason: "budget_exceeded", event, groupIndex, totalMs: ledger.totalMs };
190
288
  case "invalid_regex":
191
- return { reason: "invalid_regex", event, groupIndex, matcher: clip(group.matcher, MATCHER_ECHO_CHARS), compileError: clip(matcherCompileSentence(group.matcher), MATCHER_ECHO_CHARS) };
289
+ return {
290
+ reason: "invalid_regex",
291
+ event,
292
+ groupIndex,
293
+ matcher: echoCallerText(group.matcher, { maxChars: MATCHER_ECHO_CHARS }),
294
+ compileError: echoCallerText(matcherCompileSentence(group.matcher), { maxChars: MATCHER_ECHO_CHARS }),
295
+ };
192
296
  case "unjudged":
193
297
  recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
194
298
  break;
@@ -201,47 +305,95 @@ export function toolMatcherIssue(config, depth, budget = {}) {
201
305
  }
202
306
  return undefined;
203
307
  }
308
+ const HOOKS_PIECE_MEANING = {
309
+ "hooks-not-object": "`hooks` must be an object keyed by hook event name",
310
+ "unknown-event": "not a hook event name",
311
+ "event-not-array": "an event's value must be an array of matcher groups",
312
+ "matcher-invalid": "a matcher group must be an object with an optional string `matcher` and an array `hooks`",
313
+ "entry-invalid": "a hook entry must be an object whose `type` and fields are valid for that type",
314
+ "guard-outside-hooks": "a PreToolUse / PermissionRequest hook sits outside `hooks`",
315
+ "hooks-is-matcher": "`hooks` holds a matcher group or an entry directly; it must be an object keyed by hook event name",
316
+ "guard-under-unknown-event": "an unknown event name holds a PreToolUse / PermissionRequest hook",
317
+ "guard-event-not-array": "a PreToolUse / PermissionRequest event's value must be an array of matcher groups",
318
+ "guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
319
+ "guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
320
+ };
321
+ export function hooksOutgoingPath(path) {
322
+ return outgoingPath(path).map((seg) => (typeof seg === "string" ? echoCallerText(seg) : seg));
323
+ }
324
+ export function hooksCoordinate(path) {
325
+ return `settings.${formatConfigPath(hooksOutgoingPath(path))}`;
326
+ }
327
+ function hooksOutgoingKey(key) {
328
+ return echoCallerText(String(outgoingPath([key])[0] ?? ""));
329
+ }
330
+ function hooksPieceIssue(piece) {
331
+ return piece.field !== undefined ? { reason: piece.code, path: piece.path, key: piece.field } : { reason: piece.code, path: piece.path };
332
+ }
333
+ function hooksPieceRefusal(piece) {
334
+ const field = piece.key !== undefined ? ` (field: ${hooksOutgoingKey(piece.key)})` : "";
335
+ return `${hooksCoordinate(piece.path)}: ${piece.reason} — ${HOOKS_PIECE_MEANING[piece.reason]}${field}`;
336
+ }
337
+ function hooksRefused(issue, pieces) {
338
+ const error = buildHooksRefusal(issue).message;
339
+ return pieces === undefined ? { issue, error } : { issue, error, pieces };
340
+ }
204
341
  export function parseHooksConfig(raw) {
205
- const parsed = HooksConfig.safeParse(raw);
206
- if (!parsed.success) {
207
- const first = parsed.error.issues[0];
208
- return { error: `settings.hooks: ${first ? `${first.path.join(".")}: ${first.message}` : "invalid shape"}` };
342
+ const classified = classifyHooksConfig({ hooks: raw });
343
+ if (classified.outcome !== "loaded") {
344
+ const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
345
+ const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
346
+ return hooksRefused(first !== undefined ? hooksPieceIssue(first) : { reason: "unnamed_refusal", path: ["hooks"] }, pieces);
209
347
  }
210
- const config = parsed.data;
211
- for (const [event, groups] of Object.entries(config)) {
348
+ const loaded = classified.hooks;
349
+ if (loaded === undefined) {
350
+ return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
351
+ }
352
+ const config = {};
353
+ for (const key of Object.keys(loaded)) {
354
+ if (!isHookEventName(key)) {
355
+ return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
356
+ }
357
+ const event = key;
358
+ const groups = loaded[event];
212
359
  if (!groups)
213
360
  continue;
214
- if (groups.length > MAX_HOOK_MATCHER_GROUPS) {
215
- return { error: `settings.hooks.${event}: ${groups.length} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` };
216
- }
361
+ if (groups.length > MAX_HOOK_MATCHER_GROUPS)
362
+ return hooksRefused(hooksCapIssue("matcher_groups", event, ["hooks", event], groups.length));
217
363
  let entries = 0;
218
- for (const group of groups) {
219
- for (const entry of group.hooks) {
364
+ let firstPastCap;
365
+ const kept = [];
366
+ for (const [groupIndex, group] of groups.entries()) {
367
+ const hooks = [];
368
+ for (const [entryIndex, entry] of group.hooks.entries()) {
220
369
  entries++;
221
- if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS) {
222
- return { error: `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
223
- }
370
+ const at = ["hooks", event, groupIndex, "hooks", entryIndex];
371
+ if (entries === MAX_HOOK_ENTRIES_PER_EVENT + 1)
372
+ firstPastCap = at;
373
+ if (!isRunnableHookEntry(entry))
374
+ return hooksRefused({ reason: "unsupported_entry_type", path: [...at, "type"], type: entry.type });
375
+ if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS)
376
+ return hooksRefused(hooksCapIssue("command", event, [...at, "command"], entry.command.length));
224
377
  if (entry.type === "http") {
225
- if (entry.url.length > MAX_HOOK_COMMAND_CHARS) {
226
- return { error: `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
227
- }
378
+ if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
379
+ return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
228
380
  const headerEntries = Object.entries(entry.headers ?? {});
229
- if (headerEntries.length > MAX_HOOK_HTTP_HEADERS) {
230
- return { error: `settings.hooks.${event}: ${headerEntries.length} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
231
- }
381
+ if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
382
+ return hooksRefused(hooksCapIssue("headers", event, [...at, "headers"], headerEntries.length));
232
383
  const headerChars = headerEntries.reduce((n, [k, v]) => n + k.length + v.length, 0);
233
- if (headerChars > MAX_HOOK_COMMAND_CHARS) {
234
- return { error: `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` };
235
- }
236
- if ((entry.allowedEnvVars?.length ?? 0) > MAX_HOOK_HTTP_HEADERS) {
237
- return { error: `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
238
- }
384
+ if (headerChars > MAX_HOOK_COMMAND_CHARS)
385
+ return hooksRefused(hooksCapIssue("header_chars", event, [...at, "headers"], headerChars));
386
+ const envVars = entry.allowedEnvVars?.length ?? 0;
387
+ if (envVars > MAX_HOOK_HTTP_HEADERS)
388
+ return hooksRefused(hooksCapIssue("allowed_env_vars", event, [...at, "allowedEnvVars"], envVars));
239
389
  }
390
+ hooks.push(entry);
240
391
  }
392
+ kept.push(group.matcher !== undefined ? { matcher: group.matcher, hooks } : { hooks });
241
393
  }
242
- if (entries > MAX_HOOK_ENTRIES_PER_EVENT) {
243
- return { error: `settings.hooks.${event}: ${entries} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` };
244
- }
394
+ if (firstPastCap !== undefined)
395
+ return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
396
+ config[event] = kept;
245
397
  }
246
398
  return { config };
247
399
  }
@@ -331,61 +483,40 @@ function noteHookFailure(ctx, args) {
331
483
  });
332
484
  }
333
485
  export function buildHookContext(parts) {
334
- const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
486
+ const present = parts.filter((p) => p.length > 0);
335
487
  if (present.length === 0)
336
488
  return undefined;
337
489
  const kept = [];
338
490
  let used = 0;
339
491
  let truncating = false;
340
- let messages = 0;
341
492
  let omitted = 0;
342
493
  let droppedChars = 0;
343
494
  for (const part of present) {
344
- messages += part.messages;
345
- omitted += part.omitted;
346
- droppedChars += part.dropped;
347
495
  if (!truncating) {
348
496
  const sep = kept.length > 0 ? 1 : 0;
349
497
  const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
350
- if (room >= part.body.length) {
351
- if (part.body.length > 0) {
352
- kept.push(part.body);
353
- used += sep + part.body.length;
354
- }
498
+ if (room >= part.length) {
499
+ kept.push(part);
500
+ used += sep + part.length;
355
501
  continue;
356
502
  }
357
503
  truncating = true;
358
- if (part.messages === 1 && room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
359
- kept.push(part.body.slice(0, room));
504
+ if (room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
505
+ kept.push(part.slice(0, room));
360
506
  used += sep + room;
361
- droppedChars += part.body.length - room;
507
+ droppedChars += part.length - room;
362
508
  continue;
363
509
  }
364
510
  }
365
- omitted += part.messages - part.omitted;
366
- droppedChars += part.body.length;
511
+ omitted += 1;
512
+ droppedChars += part.length;
367
513
  }
368
514
  const body = kept.join("\n");
369
515
  if (droppedChars === 0)
370
516
  return body;
371
- const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${messages} hook messages omitted, ${droppedChars} characters dropped]`;
517
+ const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${present.length} hook messages omitted, ${droppedChars} characters dropped]`;
372
518
  return kept.length > 0 ? `${body}\n${marker}` : marker;
373
519
  }
374
- const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
375
- const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
376
- function readHookContextPart(part) {
377
- const m = HOOK_CONTEXT_MARKER_RE.exec(part);
378
- if (!m)
379
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
380
- const omitted = Number(m[1]);
381
- const messages = Number(m[2]);
382
- const dropped = Number(m[3]);
383
- const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
384
- if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
385
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
386
- }
387
- return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
388
- }
389
520
  function buildHookContextField(parts) {
390
521
  const text = buildHookContext(parts);
391
522
  return text === undefined ? {} : { additionalContext: text };
@@ -404,7 +535,7 @@ async function runCommandHook(entry, payload, ctx) {
404
535
  const sh = hostShell();
405
536
  child = spawn(sh.shell, [...sh.args, entry.command], {
406
537
  cwd: ctx.cwd,
407
- env: hookEnv(ctx),
538
+ env: hookEnv(ctx, true),
408
539
  stdio: ["pipe", "pipe", "pipe"],
409
540
  detached: true,
410
541
  });
@@ -467,7 +598,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
467
598
  const sh = hostShell();
468
599
  const child = spawn(sh.shell, [...sh.args, entry.command], {
469
600
  cwd: ctx.cwd,
470
- env: hookEnv(ctx),
601
+ env: hookEnv(ctx, true),
471
602
  stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
472
603
  detached: true,
473
604
  });
@@ -600,7 +731,7 @@ async function runLlmHook(entry, payload, ctx, extra) {
600
731
  const MAX_BLANKED_REFERENCES_LOGGED = 16;
601
732
  async function runHttpHook(entry, payload, ctx, event) {
602
733
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
603
- const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
734
+ const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx, false));
604
735
  if (interpolated.blanked.length > 0) {
605
736
  ctx.logger.warn("hook_http_header_env_blanked", {
606
737
  event,
@@ -712,6 +843,23 @@ function parseHookStdout(stdout) {
712
843
  return undefined;
713
844
  }
714
845
  }
846
+ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
847
+ switch (entry.type) {
848
+ case "http":
849
+ return runHttpHook(entry, payload, ctx, event);
850
+ case "prompt":
851
+ return runLlmHook(entry, payload, ctx, llmExtra);
852
+ case "agent":
853
+ return runLlmHook(entry, payload, ctx, { system: ccAgentHookSystemFor(event), event });
854
+ case "command":
855
+ return runCommandHook(entry, payload, ctx);
856
+ default: {
857
+ const unreachable = entry;
858
+ void unreachable;
859
+ throw new Error(`hook entry of an unsupported type reached the runner although parseHooksConfig admits only ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`);
860
+ }
861
+ }
862
+ }
715
863
  async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, deadlineAt, keyBase, llmExtra) {
716
864
  const results = [];
717
865
  let llmEntries = 0;
@@ -720,7 +868,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
720
868
  gi++;
721
869
  const m = hookMatcherMatches(group.matcher, matchValue);
722
870
  if (m === "invalid") {
723
- ctx.logger.warn("hook_matcher_invalid", { event, matcher: clip(group.matcher ?? "", 200) });
871
+ ctx.logger.warn("hook_matcher_invalid", { event, matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
724
872
  continue;
725
873
  }
726
874
  if (!m)
@@ -731,10 +879,6 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
731
879
  const onceKey = `${keyBase ?? event}:${gi}:${ei}`;
732
880
  if (entry.once && onceFired.has(onceKey))
733
881
  continue;
734
- if (entry.type !== "command" && entry.type !== "http" && entry.type !== "prompt" && entry.type !== "agent") {
735
- ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "type_not_supported" });
736
- continue;
737
- }
738
882
  if (entry.type === "prompt" || entry.type === "agent") {
739
883
  const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
740
884
  if (!carrier) {
@@ -771,9 +915,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
771
915
  onceFired.add(onceKey);
772
916
  if (entry.type === "prompt" || entry.type === "agent")
773
917
  llmEntries++;
774
- const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx, event)
775
- : entry.type === "prompt" || entry.type === "agent" ? await runLlmHook(entry, payload, ctx, entry.type === "prompt" ? llmExtra : { system: ccAgentHookSystemFor(event), event })
776
- : await runCommandHook(entry, payload, ctx);
918
+ const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra);
777
919
  if (run.spawnError) {
778
920
  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: clip(run.spawnError, 300) });
779
921
  if (!run.noticed)
@@ -828,12 +970,8 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
828
970
  }
829
971
  }
830
972
  }
831
- export function createTaskHooks(config, ctx) {
973
+ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
832
974
  const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
833
- const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
834
- if (dark.length > 0) {
835
- ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
836
- }
837
975
  const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
838
976
  if (shellOwned.length > 0) {
839
977
  ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
@@ -855,9 +993,11 @@ export function createTaskHooks(config, ctx) {
855
993
  return undefined;
856
994
  }
857
995
  const onceFired = new Set();
858
- const hooks = {};
996
+ const translators = {};
859
997
  if (pre) {
860
- const preToolUseHook = async (toolName, input, tctx) => {
998
+ translators.PreToolUse = async (e) => {
999
+ const toolName = e.tool;
1000
+ const input = e.input;
861
1001
  let currentInput = input;
862
1002
  let rewritten = false;
863
1003
  let asked;
@@ -870,18 +1010,18 @@ export function createTaskHooks(config, ctx) {
870
1010
  gi++;
871
1011
  const m = hookMatcherMatches(group.matcher, toolName);
872
1012
  if (m === "invalid") {
873
- ctx.logger.warn("hook_matcher_invalid", { event: "PreToolUse", matcher: clip(group.matcher ?? "", 200) });
1013
+ ctx.logger.warn("hook_matcher_invalid", { event: "PreToolUse", matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
874
1014
  continue;
875
1015
  }
876
1016
  if (!m)
877
1017
  continue;
878
1018
  const groupInput = currentInput;
879
1019
  const payload = {
880
- ...basePayload(ctx, tctx.permissionMode),
1020
+ ...basePayload(ctx, e.permissionMode),
881
1021
  hook_event_name: "PreToolUse",
882
1022
  tool_name: toolName,
883
1023
  tool_input: groupInput,
884
- tool_use_id: tctx.toolCallId,
1024
+ tool_use_id: e.tool_use_id,
885
1025
  };
886
1026
  const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
887
1027
  for (const { entry, run } of singles) {
@@ -964,21 +1104,23 @@ export function createTaskHooks(config, ctx) {
964
1104
  return carried;
965
1105
  return undefined;
966
1106
  };
967
- hooks.preToolUse = preToolUseHook;
968
1107
  }
969
1108
  if (post) {
970
- const postToolUseHook = async (toolName, input, output, tctx) => {
1109
+ translators.PostToolUse = async (e) => {
1110
+ const toolName = e.tool;
1111
+ const input = e.input;
1112
+ const output = e.output;
971
1113
  const contexts = [];
972
1114
  const responseText = output.content
973
1115
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
974
1116
  .join("");
975
1117
  const payload = {
976
- ...basePayload(ctx, tctx.permissionMode),
1118
+ ...basePayload(ctx, e.permissionMode),
977
1119
  hook_event_name: "PostToolUse",
978
1120
  tool_name: toolName,
979
1121
  tool_input: input,
980
1122
  tool_response: { isError: output.isError, text: clip(responseText, MAX_TOOL_RESPONSE_CHARS) },
981
- tool_use_id: tctx.toolCallId,
1123
+ tool_use_id: e.tool_use_id,
982
1124
  };
983
1125
  const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
984
1126
  for (const { entry, run } of singles) {
@@ -1016,17 +1158,18 @@ export function createTaskHooks(config, ctx) {
1016
1158
  const additionalContext = buildHookContext(contexts);
1017
1159
  return additionalContext === undefined ? undefined : { additionalContext };
1018
1160
  };
1019
- hooks.postToolUse = postToolUseHook;
1020
1161
  }
1021
1162
  if (postFailure) {
1022
- const postToolUseFailureHook = async (toolName, input, f, tctx) => {
1163
+ translators.PostToolUseFailure = async (e) => {
1164
+ const toolName = e.tool;
1165
+ const f = e.failure;
1023
1166
  const contexts = [];
1024
1167
  const payload = {
1025
- ...basePayload(ctx, tctx.permissionMode),
1168
+ ...basePayload(ctx, e.permissionMode),
1026
1169
  hook_event_name: "PostToolUseFailure",
1027
1170
  tool_name: toolName,
1028
- tool_input: input,
1029
- tool_use_id: tctx.toolCallId,
1171
+ tool_input: e.input,
1172
+ tool_use_id: e.tool_use_id,
1030
1173
  error: clip(f.error, MAX_TOOL_RESPONSE_CHARS),
1031
1174
  is_interrupt: f.isInterrupt,
1032
1175
  };
@@ -1062,15 +1205,14 @@ export function createTaskHooks(config, ctx) {
1062
1205
  const additionalContext = buildHookContext(contexts);
1063
1206
  return additionalContext === undefined ? undefined : { additionalContext };
1064
1207
  };
1065
- hooks.postToolUseFailure = postToolUseFailureHook;
1066
1208
  }
1067
1209
  if (postBatch) {
1068
- const postToolBatchHook = async (calls, _meta, bctx) => {
1210
+ translators.PostToolBatch = async (e) => {
1069
1211
  const contexts = [];
1070
1212
  const payload = {
1071
- ...basePayload(ctx, bctx?.permissionMode),
1213
+ ...basePayload(ctx, e.permissionMode),
1072
1214
  hook_event_name: "PostToolBatch",
1073
- tool_calls: calls.map((c) => ({
1215
+ tool_calls: e.batch.map((c) => ({
1074
1216
  tool_name: c.toolName,
1075
1217
  tool_input: boundedInputValue(c.input),
1076
1218
  tool_use_id: c.toolCallId,
@@ -1105,12 +1247,11 @@ export function createTaskHooks(config, ctx) {
1105
1247
  const additionalContext = buildHookContext(contexts);
1106
1248
  return additionalContext === undefined ? undefined : { additionalContext };
1107
1249
  };
1108
- hooks.postToolBatch = postToolBatchHook;
1109
1250
  }
1110
1251
  if (promptSubmit) {
1111
- const userPromptSubmitHook = async (prompt, uctx) => {
1252
+ translators.UserPromptSubmit = async (e) => {
1112
1253
  const contexts = [];
1113
- const payload = { ...basePayload(ctx, uctx?.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
1254
+ const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(e.prompt, MAX_TOOL_INPUT_CHARS) };
1114
1255
  const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1115
1256
  for (const { entry, run } of singles) {
1116
1257
  if (run.code === 2) {
@@ -1145,10 +1286,9 @@ export function createTaskHooks(config, ctx) {
1145
1286
  const additionalContext = buildHookContext(contexts);
1146
1287
  return additionalContext === undefined ? undefined : { additionalContext };
1147
1288
  };
1148
- hooks.userPromptSubmit = userPromptSubmitHook;
1149
1289
  }
1150
1290
  if (stopEntries) {
1151
- const stopHook = async (sctx) => {
1291
+ translators.Stop = async (sctx) => {
1152
1292
  const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1153
1293
  let llmExtra;
1154
1294
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
@@ -1237,10 +1377,9 @@ export function createTaskHooks(config, ctx) {
1237
1377
  ...buildHookContextField(contexts),
1238
1378
  };
1239
1379
  };
1240
- hooks.stop = stopHook;
1241
1380
  }
1242
1381
  if (stopFailureEntries) {
1243
- const stopFailureHook = async (fctx) => {
1382
+ translators.StopFailure = async (fctx) => {
1244
1383
  const turnError = toAssistantTurnError(fctx.errorKind);
1245
1384
  const payload = {
1246
1385
  ...basePayload(ctx, undefined),
@@ -1250,10 +1389,9 @@ export function createTaskHooks(config, ctx) {
1250
1389
  };
1251
1390
  await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
1252
1391
  };
1253
- hooks.stopFailure = stopFailureHook;
1254
1392
  }
1255
1393
  if (preCompactEntries) {
1256
- const preCompactHook = async (pctx) => {
1394
+ translators.PreCompact = async (pctx) => {
1257
1395
  const payload = {
1258
1396
  ...basePayload(ctx, undefined),
1259
1397
  hook_event_name: "PreCompact",
@@ -1284,10 +1422,9 @@ export function createTaskHooks(config, ctx) {
1284
1422
  }
1285
1423
  return undefined;
1286
1424
  };
1287
- hooks.preCompact = preCompactHook;
1288
1425
  }
1289
1426
  if (postCompactEntries) {
1290
- const postCompactHook = async (cctx) => {
1427
+ translators.PostCompact = async (cctx) => {
1291
1428
  const payload = {
1292
1429
  ...basePayload(ctx, undefined),
1293
1430
  hook_event_name: "PostCompact",
@@ -1296,177 +1433,16 @@ export function createTaskHooks(config, ctx) {
1296
1433
  };
1297
1434
  await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
1298
1435
  };
1299
- hooks.postCompact = postCompactHook;
1300
- }
1301
- return hooks;
1302
- }
1303
- const HOOK_SLOT_FOLD = {
1304
- preToolUse: "custom",
1305
- preToolUseObservational: "custom",
1306
- postToolUse: "custom",
1307
- postToolUseFailure: "custom",
1308
- postToolBatch: "custom",
1309
- userPromptSubmit: "custom",
1310
- stop: "custom",
1311
- preCompact: "custom",
1312
- timeoutMs: "scalar",
1313
- postCompact: "observe-void",
1314
- stopFailure: "observe-void",
1315
- permissionDenied: "observe-void",
1316
- };
1317
- const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
1318
- export function composeHooks(deployment, task) {
1319
- if (!deployment)
1320
- return task;
1321
- const d = deployment;
1322
- const t = task;
1323
- const out = {};
1324
- const timeoutMs = d.timeoutMs ?? t.timeoutMs;
1325
- if (timeoutMs !== undefined)
1326
- out.timeoutMs = timeoutMs;
1327
- if (d.preToolUse || t.preToolUse) {
1328
- const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
1329
- (t.preToolUse === undefined || t.preToolUseObservational === true);
1330
- if (bothObservational)
1331
- out.preToolUseObservational = true;
1332
- out.preToolUse = async (toolName, input, ctx) => {
1333
- let current = input;
1334
- const contexts = [];
1335
- let asked;
1336
- let allowedFor;
1337
- for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
1338
- if (!slot)
1339
- continue;
1340
- const r = receiveHookResult(await slot(toolName, current, ctx));
1341
- if (r === undefined || r === null || r === false)
1342
- continue;
1343
- if (isEngineRefusal(r))
1344
- return r;
1345
- if (r.additionalContext)
1346
- contexts.push(r.additionalContext);
1347
- const action = r.action;
1348
- const updatedInput = r.updatedInput;
1349
- switch (action) {
1350
- case "deny":
1351
- return { ...r, ...buildHookContextField(contexts) };
1352
- case "ask":
1353
- asked = r;
1354
- break;
1355
- case "allow":
1356
- if (updatedInput !== undefined)
1357
- current = updatedInput;
1358
- allowedFor = { input: current };
1359
- break;
1360
- case undefined:
1361
- if (updatedInput !== undefined)
1362
- current = updatedInput;
1363
- break;
1364
- default: {
1365
- const unread = action;
1366
- void unread;
1367
- return r;
1368
- }
1369
- }
1370
- }
1371
- if (asked) {
1372
- const askedUpdated = asked.updatedInput;
1373
- return {
1374
- ...asked,
1375
- ...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
1376
- ...buildHookContextField(contexts),
1377
- };
1378
- }
1379
- const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
1380
- if (allowedFor !== undefined && allowedFor.input === current)
1381
- return { action: "allow", ...carried };
1382
- if (current !== input || contexts.length > 0)
1383
- return carried;
1384
- return undefined;
1385
- };
1386
1436
  }
1387
- if (d.postToolUse || t.postToolUse) {
1388
- out.postToolUse = async (toolName, input, output, ctx) => {
1389
- const rd = d.postToolUse ? await d.postToolUse(toolName, input, output, ctx) : undefined;
1390
- const rt = t.postToolUse ? await t.postToolUse(toolName, input, output, ctx) : undefined;
1391
- if (!rd && !rt)
1392
- return undefined;
1393
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1394
- const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
1395
- return {
1396
- ...(updatedOutput ? { updatedOutput } : {}),
1397
- ...buildHookContextField(contexts),
1398
- };
1399
- };
1400
- }
1401
- if (d.postToolUseFailure || t.postToolUseFailure) {
1402
- out.postToolUseFailure = async (toolName, input, failure, ctx) => {
1403
- const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1404
- const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1405
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1406
- const additionalContext = buildHookContext(contexts);
1407
- return additionalContext === undefined ? undefined : { additionalContext };
1408
- };
1409
- }
1410
- if (d.postToolBatch || t.postToolBatch) {
1411
- out.postToolBatch = async (calls, meta, bctx) => {
1412
- const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
1413
- const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
1414
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1415
- const additionalContext = buildHookContext(contexts);
1416
- return additionalContext === undefined ? undefined : { additionalContext };
1417
- };
1418
- }
1419
- if (d.userPromptSubmit || t.userPromptSubmit) {
1420
- out.userPromptSubmit = async (prompt, uctx) => {
1421
- const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
1422
- if (rd?.block)
1423
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1424
- const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
1425
- if (!rd && !rt)
1426
- return undefined;
1427
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1428
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1429
- };
1430
- }
1431
- if (d.stop || t.stop) {
1432
- out.stop = async (sctx) => {
1433
- const rd = d.stop ? await d.stop(sctx) : undefined;
1434
- if (rd?.block)
1435
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1436
- const rt = t.stop ? await t.stop(sctx) : undefined;
1437
- if (!rd && !rt)
1438
- return undefined;
1439
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1440
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1441
- };
1442
- }
1443
- if (d.preCompact || t.preCompact) {
1444
- out.preCompact = async (pctx) => {
1445
- const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
1446
- const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
1447
- if (!rd && !rt)
1448
- return undefined;
1449
- const block = rd?.block ?? rt?.block;
1450
- const instructions = [rd?.additionalInstructions, rt?.additionalInstructions].filter((c) => typeof c === "string" && c.length > 0);
1451
- return {
1452
- ...(block !== undefined ? { block } : {}),
1453
- ...(instructions.length ? { additionalInstructions: instructions.join("\n") } : {}),
1454
- };
1455
- };
1456
- }
1457
- for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
1458
- const ds = d[slot];
1459
- const ts = t[slot];
1460
- if (ds && ts) {
1461
- out[slot] = async (...args) => {
1462
- await ds(...args);
1463
- await ts(...args);
1464
- };
1465
- }
1466
- else if (ds || ts) {
1467
- out[slot] = (ds ?? ts);
1468
- }
1469
- }
1470
- return out;
1437
+ const events = Object.keys(translators);
1438
+ return {
1439
+ name: SETTINGS_HOOKS_REGISTRATION,
1440
+ uses: { events: events.flatMap((event) => SETTINGS_HOOK_STATIONS[event]), calls: [] },
1441
+ budgetMs,
1442
+ register(on) {
1443
+ for (const event of events)
1444
+ hangTranslator(on, event, translators);
1445
+ },
1446
+ };
1471
1447
  }
1472
1448
  //# sourceMappingURL=hook-runner.js.map