@sema-agent/server 7.131.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  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/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
@@ -1,11 +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
5
  import { classifyHooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_ENTRY_TYPES, HOOK_EVENTS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
5
6
  import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
6
- import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
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";
7
10
  import { boundedNoticeText } from "../fleet/fleet-bus.js";
8
11
  import { recordFailOpen } from "../observability/fail-open.js";
12
+ import { LIMIT_REASONS } from "../limit-refusal.js";
13
+ import { echoCallerText } from "../caller-text-echo.js";
9
14
  import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
10
15
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
11
16
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
@@ -26,26 +31,35 @@ const HOOK_ENV_ALLOWLIST = [
26
31
  "PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
27
32
  "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
28
33
  ];
29
- function hookEnv(ctx) {
30
- const env = { CLAUDE_PROJECT_DIR: ctx.cwd };
31
- for (const k of HOOK_ENV_ALLOWLIST) {
32
- const v = process.env[k];
33
- if (typeof v === "string")
34
- env[k] = v;
35
- }
36
- return { ...env, ...ctx.shellEnv };
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;
37
62
  }
38
- const LIT_EVENTS = [
39
- "PreToolUse",
40
- "PostToolUse",
41
- "PostToolUseFailure",
42
- "PostToolBatch",
43
- "UserPromptSubmit",
44
- "Stop",
45
- "StopFailure",
46
- "PreCompact",
47
- "PostCompact",
48
- ];
49
63
  const BRAIN_TO_TURN_ERROR = {
50
64
  auth: "authentication_failed",
51
65
  rate_limit: "rate_limit",
@@ -76,7 +90,7 @@ function isRunnableHookEntry(entry) {
76
90
  return isSupportedHookEntryType(entry.type);
77
91
  }
78
92
  export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
79
- export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
93
+ export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
80
94
  export const MATCHER_ECHO_CHARS = 200;
81
95
  const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
82
96
  function matcherReadsWireToolName(event) {
@@ -130,7 +144,7 @@ const MATCHER_EVALUATION = {
130
144
  regex: { regex: "budgeted", literal: "not_applicable" },
131
145
  invalid: { regex: "unreadable", literal: "not_applicable" },
132
146
  };
133
- function judgeMatcher(matcher, depth, budgetMs) {
147
+ function judgeMatcher(matcher, depth, ledger, judgesAlias) {
134
148
  if (matcher.length > MAX_MATCHER_EVAL_CHARS)
135
149
  return { kind: "unjudged", reason: "oversize" };
136
150
  const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
@@ -141,14 +155,34 @@ function judgeMatcher(matcher, depth, budgetMs) {
141
155
  return { kind: "invalid_regex" };
142
156
  case "inline":
143
157
  case "budgeted":
158
+ if (!judgesAlias)
159
+ return undefined;
144
160
  break;
145
161
  default: {
146
162
  const unreachable = evaluate;
147
163
  return unreachable;
148
164
  }
149
165
  }
150
- const selects = (name) => (evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs));
151
- 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
+ };
152
186
  for (const [alias, canonical] of engineToolAliases()) {
153
187
  const selectsAlias = selects(alias);
154
188
  if (typeof selectsAlias === "string")
@@ -163,50 +197,102 @@ function judgeMatcher(matcher, depth, budgetMs) {
163
197
  }
164
198
  return undefined;
165
199
  }
166
- 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";
167
216
  switch (issue.reason) {
168
217
  case "dead_alias":
169
218
  return {
170
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}"`,
171
- field: "settings.hooks",
172
- 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) },
173
222
  };
174
223
  case "over_budget":
175
224
  return {
176
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`,
177
- field: "settings.hooks",
178
- 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) },
179
234
  };
180
235
  case "invalid_regex":
181
236
  return {
182
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"`,
183
- field: "settings.hooks",
184
- 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) },
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 },
185
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) } };
186
254
  default: {
187
- const unreachable = issue;
188
- 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
+ };
189
262
  }
190
263
  }
191
264
  }
192
- export function toolMatcherIssue(config, depth, budget = {}) {
193
- 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;
194
273
  for (const [event, groups] of Object.entries(config)) {
195
- if (!matcherReadsWireToolName(event))
196
- continue;
274
+ const judgesAlias = matcherReadsWireToolName(event);
197
275
  for (const [groupIndex, group] of (groups ?? []).entries()) {
198
276
  if (group.matcher === undefined)
199
277
  continue;
200
- const verdict = judgeMatcher(group.matcher, depth, budgetMs);
278
+ const verdict = judgeMatcher(group.matcher, depth, ledger, judgesAlias);
201
279
  if (verdict === undefined)
202
280
  continue;
203
281
  switch (verdict.kind) {
204
282
  case "dead":
205
- return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
283
+ return { reason: "dead_alias", event, groupIndex, alias: verdict.alias, canonical: verdict.canonical };
206
284
  case "over_budget":
207
285
  return { reason: "over_budget", event, groupIndex, budgetMs };
286
+ case "budget_exceeded":
287
+ return { reason: "budget_exceeded", event, groupIndex, totalMs: ledger.totalMs };
208
288
  case "invalid_regex":
209
- 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
+ };
210
296
  case "unjudged":
211
297
  recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
212
298
  break;
@@ -232,74 +318,81 @@ const HOOKS_PIECE_MEANING = {
232
318
  "guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
233
319
  "guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
234
320
  };
321
+ export function hooksOutgoingPath(path) {
322
+ return outgoingPath(path).map((seg) => (typeof seg === "string" ? echoCallerText(seg) : seg));
323
+ }
235
324
  export function hooksCoordinate(path) {
236
- return `settings.${formatConfigPath(outgoingPath(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 };
237
332
  }
238
333
  function hooksPieceRefusal(piece) {
239
- const field = piece.field !== undefined ? ` (field: ${piece.field})` : "";
240
- return `${hooksCoordinate(piece.path)}: ${piece.code} — ${HOOKS_PIECE_MEANING[piece.code]}${field}`;
334
+ const field = piece.key !== undefined ? ` (field: ${hooksOutgoingKey(piece.key)})` : "";
335
+ return `${hooksCoordinate(piece.path)}: ${piece.reason} — ${HOOKS_PIECE_MEANING[piece.reason]}${field}`;
241
336
  }
242
- function unsupportedHookEntryTypeRefusal(path, type) {
243
- return `${hooksCoordinate(path)}: this server does not run hook entries of type ${type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`;
337
+ function hooksRefused(issue, pieces) {
338
+ const error = buildHooksRefusal(issue).message;
339
+ return pieces === undefined ? { issue, error } : { issue, error, pieces };
244
340
  }
245
341
  export function parseHooksConfig(raw) {
246
342
  const classified = classifyHooksConfig({ hooks: raw });
247
343
  if (classified.outcome !== "loaded") {
248
344
  const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
249
345
  const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
250
- return { error: first !== undefined ? hooksPieceRefusal(first) : `${hooksCoordinate(["hooks"])}: the hooks reader refused this configuration without naming a piece`, pieces };
346
+ return hooksRefused(first !== undefined ? hooksPieceIssue(first) : { reason: "unnamed_refusal", path: ["hooks"] }, pieces);
251
347
  }
252
348
  const loaded = classified.hooks;
253
349
  if (loaded === undefined) {
254
- return { error: hooksPieceRefusal({ code: "hooks-not-object", path: ["hooks"] }) };
350
+ return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
255
351
  }
256
352
  const config = {};
257
353
  for (const key of Object.keys(loaded)) {
258
354
  if (!isHookEventName(key)) {
259
- return { error: hooksPieceRefusal({ code: "unknown-event", path: ["hooks", key] }) };
355
+ return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
260
356
  }
261
357
  const event = key;
262
358
  const groups = loaded[event];
263
359
  if (!groups)
264
360
  continue;
265
- if (groups.length > MAX_HOOK_MATCHER_GROUPS) {
266
- return { error: `settings.hooks.${event}: ${groups.length} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` };
267
- }
361
+ if (groups.length > MAX_HOOK_MATCHER_GROUPS)
362
+ return hooksRefused(hooksCapIssue("matcher_groups", event, ["hooks", event], groups.length));
268
363
  let entries = 0;
364
+ let firstPastCap;
269
365
  const kept = [];
270
366
  for (const [groupIndex, group] of groups.entries()) {
271
367
  const hooks = [];
272
368
  for (const [entryIndex, entry] of group.hooks.entries()) {
273
369
  entries++;
274
- if (!isRunnableHookEntry(entry)) {
275
- return { error: unsupportedHookEntryTypeRefusal(["hooks", event, groupIndex, "hooks", entryIndex, "type"], entry.type) };
276
- }
277
- if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS) {
278
- return { error: `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
279
- }
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));
280
377
  if (entry.type === "http") {
281
- if (entry.url.length > MAX_HOOK_COMMAND_CHARS) {
282
- return { error: `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
283
- }
378
+ if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
379
+ return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
284
380
  const headerEntries = Object.entries(entry.headers ?? {});
285
- if (headerEntries.length > MAX_HOOK_HTTP_HEADERS) {
286
- return { error: `settings.hooks.${event}: ${headerEntries.length} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
287
- }
381
+ if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
382
+ return hooksRefused(hooksCapIssue("headers", event, [...at, "headers"], headerEntries.length));
288
383
  const headerChars = headerEntries.reduce((n, [k, v]) => n + k.length + v.length, 0);
289
- if (headerChars > MAX_HOOK_COMMAND_CHARS) {
290
- return { error: `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` };
291
- }
292
- if ((entry.allowedEnvVars?.length ?? 0) > MAX_HOOK_HTTP_HEADERS) {
293
- return { error: `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
294
- }
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));
295
389
  }
296
390
  hooks.push(entry);
297
391
  }
298
392
  kept.push(group.matcher !== undefined ? { matcher: group.matcher, hooks } : { hooks });
299
393
  }
300
- if (entries > MAX_HOOK_ENTRIES_PER_EVENT) {
301
- return { error: `settings.hooks.${event}: ${entries} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` };
302
- }
394
+ if (firstPastCap !== undefined)
395
+ return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
303
396
  config[event] = kept;
304
397
  }
305
398
  return { config };
@@ -390,61 +483,40 @@ function noteHookFailure(ctx, args) {
390
483
  });
391
484
  }
392
485
  export function buildHookContext(parts) {
393
- const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
486
+ const present = parts.filter((p) => p.length > 0);
394
487
  if (present.length === 0)
395
488
  return undefined;
396
489
  const kept = [];
397
490
  let used = 0;
398
491
  let truncating = false;
399
- let messages = 0;
400
492
  let omitted = 0;
401
493
  let droppedChars = 0;
402
494
  for (const part of present) {
403
- messages += part.messages;
404
- omitted += part.omitted;
405
- droppedChars += part.dropped;
406
495
  if (!truncating) {
407
496
  const sep = kept.length > 0 ? 1 : 0;
408
497
  const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
409
- if (room >= part.body.length) {
410
- if (part.body.length > 0) {
411
- kept.push(part.body);
412
- used += sep + part.body.length;
413
- }
498
+ if (room >= part.length) {
499
+ kept.push(part);
500
+ used += sep + part.length;
414
501
  continue;
415
502
  }
416
503
  truncating = true;
417
- if (part.messages === 1 && room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
418
- kept.push(part.body.slice(0, room));
504
+ if (room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
505
+ kept.push(part.slice(0, room));
419
506
  used += sep + room;
420
- droppedChars += part.body.length - room;
507
+ droppedChars += part.length - room;
421
508
  continue;
422
509
  }
423
510
  }
424
- omitted += part.messages - part.omitted;
425
- droppedChars += part.body.length;
511
+ omitted += 1;
512
+ droppedChars += part.length;
426
513
  }
427
514
  const body = kept.join("\n");
428
515
  if (droppedChars === 0)
429
516
  return body;
430
- 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]`;
431
518
  return kept.length > 0 ? `${body}\n${marker}` : marker;
432
519
  }
433
- const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
434
- const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
435
- function readHookContextPart(part) {
436
- const m = HOOK_CONTEXT_MARKER_RE.exec(part);
437
- if (!m)
438
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
439
- const omitted = Number(m[1]);
440
- const messages = Number(m[2]);
441
- const dropped = Number(m[3]);
442
- const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
443
- if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
444
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
445
- }
446
- return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
447
- }
448
520
  function buildHookContextField(parts) {
449
521
  const text = buildHookContext(parts);
450
522
  return text === undefined ? {} : { additionalContext: text };
@@ -463,7 +535,7 @@ async function runCommandHook(entry, payload, ctx) {
463
535
  const sh = hostShell();
464
536
  child = spawn(sh.shell, [...sh.args, entry.command], {
465
537
  cwd: ctx.cwd,
466
- env: hookEnv(ctx),
538
+ env: hookEnv(ctx, true),
467
539
  stdio: ["pipe", "pipe", "pipe"],
468
540
  detached: true,
469
541
  });
@@ -526,7 +598,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
526
598
  const sh = hostShell();
527
599
  const child = spawn(sh.shell, [...sh.args, entry.command], {
528
600
  cwd: ctx.cwd,
529
- env: hookEnv(ctx),
601
+ env: hookEnv(ctx, true),
530
602
  stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
531
603
  detached: true,
532
604
  });
@@ -659,7 +731,7 @@ async function runLlmHook(entry, payload, ctx, extra) {
659
731
  const MAX_BLANKED_REFERENCES_LOGGED = 16;
660
732
  async function runHttpHook(entry, payload, ctx, event) {
661
733
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
662
- const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
734
+ const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx, false));
663
735
  if (interpolated.blanked.length > 0) {
664
736
  ctx.logger.warn("hook_http_header_env_blanked", {
665
737
  event,
@@ -796,7 +868,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
796
868
  gi++;
797
869
  const m = hookMatcherMatches(group.matcher, matchValue);
798
870
  if (m === "invalid") {
799
- 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 }) });
800
872
  continue;
801
873
  }
802
874
  if (!m)
@@ -898,12 +970,8 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
898
970
  }
899
971
  }
900
972
  }
901
- export function createTaskHooks(config, ctx) {
973
+ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
902
974
  const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
903
- const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
904
- if (dark.length > 0) {
905
- ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
906
- }
907
975
  const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
908
976
  if (shellOwned.length > 0) {
909
977
  ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
@@ -925,9 +993,11 @@ export function createTaskHooks(config, ctx) {
925
993
  return undefined;
926
994
  }
927
995
  const onceFired = new Set();
928
- const hooks = {};
996
+ const translators = {};
929
997
  if (pre) {
930
- const preToolUseHook = async (toolName, input, tctx) => {
998
+ translators.PreToolUse = async (e) => {
999
+ const toolName = e.tool;
1000
+ const input = e.input;
931
1001
  let currentInput = input;
932
1002
  let rewritten = false;
933
1003
  let asked;
@@ -940,18 +1010,18 @@ export function createTaskHooks(config, ctx) {
940
1010
  gi++;
941
1011
  const m = hookMatcherMatches(group.matcher, toolName);
942
1012
  if (m === "invalid") {
943
- 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 }) });
944
1014
  continue;
945
1015
  }
946
1016
  if (!m)
947
1017
  continue;
948
1018
  const groupInput = currentInput;
949
1019
  const payload = {
950
- ...basePayload(ctx, tctx.permissionMode),
1020
+ ...basePayload(ctx, e.permissionMode),
951
1021
  hook_event_name: "PreToolUse",
952
1022
  tool_name: toolName,
953
1023
  tool_input: groupInput,
954
- tool_use_id: tctx.toolCallId,
1024
+ tool_use_id: e.tool_use_id,
955
1025
  };
956
1026
  const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
957
1027
  for (const { entry, run } of singles) {
@@ -1034,21 +1104,23 @@ export function createTaskHooks(config, ctx) {
1034
1104
  return carried;
1035
1105
  return undefined;
1036
1106
  };
1037
- hooks.preToolUse = preToolUseHook;
1038
1107
  }
1039
1108
  if (post) {
1040
- 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;
1041
1113
  const contexts = [];
1042
1114
  const responseText = output.content
1043
1115
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
1044
1116
  .join("");
1045
1117
  const payload = {
1046
- ...basePayload(ctx, tctx.permissionMode),
1118
+ ...basePayload(ctx, e.permissionMode),
1047
1119
  hook_event_name: "PostToolUse",
1048
1120
  tool_name: toolName,
1049
1121
  tool_input: input,
1050
1122
  tool_response: { isError: output.isError, text: clip(responseText, MAX_TOOL_RESPONSE_CHARS) },
1051
- tool_use_id: tctx.toolCallId,
1123
+ tool_use_id: e.tool_use_id,
1052
1124
  };
1053
1125
  const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1054
1126
  for (const { entry, run } of singles) {
@@ -1086,17 +1158,18 @@ export function createTaskHooks(config, ctx) {
1086
1158
  const additionalContext = buildHookContext(contexts);
1087
1159
  return additionalContext === undefined ? undefined : { additionalContext };
1088
1160
  };
1089
- hooks.postToolUse = postToolUseHook;
1090
1161
  }
1091
1162
  if (postFailure) {
1092
- const postToolUseFailureHook = async (toolName, input, f, tctx) => {
1163
+ translators.PostToolUseFailure = async (e) => {
1164
+ const toolName = e.tool;
1165
+ const f = e.failure;
1093
1166
  const contexts = [];
1094
1167
  const payload = {
1095
- ...basePayload(ctx, tctx.permissionMode),
1168
+ ...basePayload(ctx, e.permissionMode),
1096
1169
  hook_event_name: "PostToolUseFailure",
1097
1170
  tool_name: toolName,
1098
- tool_input: input,
1099
- tool_use_id: tctx.toolCallId,
1171
+ tool_input: e.input,
1172
+ tool_use_id: e.tool_use_id,
1100
1173
  error: clip(f.error, MAX_TOOL_RESPONSE_CHARS),
1101
1174
  is_interrupt: f.isInterrupt,
1102
1175
  };
@@ -1132,15 +1205,14 @@ export function createTaskHooks(config, ctx) {
1132
1205
  const additionalContext = buildHookContext(contexts);
1133
1206
  return additionalContext === undefined ? undefined : { additionalContext };
1134
1207
  };
1135
- hooks.postToolUseFailure = postToolUseFailureHook;
1136
1208
  }
1137
1209
  if (postBatch) {
1138
- const postToolBatchHook = async (calls, _meta, bctx) => {
1210
+ translators.PostToolBatch = async (e) => {
1139
1211
  const contexts = [];
1140
1212
  const payload = {
1141
- ...basePayload(ctx, bctx?.permissionMode),
1213
+ ...basePayload(ctx, e.permissionMode),
1142
1214
  hook_event_name: "PostToolBatch",
1143
- tool_calls: calls.map((c) => ({
1215
+ tool_calls: e.batch.map((c) => ({
1144
1216
  tool_name: c.toolName,
1145
1217
  tool_input: boundedInputValue(c.input),
1146
1218
  tool_use_id: c.toolCallId,
@@ -1175,12 +1247,11 @@ export function createTaskHooks(config, ctx) {
1175
1247
  const additionalContext = buildHookContext(contexts);
1176
1248
  return additionalContext === undefined ? undefined : { additionalContext };
1177
1249
  };
1178
- hooks.postToolBatch = postToolBatchHook;
1179
1250
  }
1180
1251
  if (promptSubmit) {
1181
- const userPromptSubmitHook = async (prompt, uctx) => {
1252
+ translators.UserPromptSubmit = async (e) => {
1182
1253
  const contexts = [];
1183
- 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) };
1184
1255
  const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1185
1256
  for (const { entry, run } of singles) {
1186
1257
  if (run.code === 2) {
@@ -1215,10 +1286,9 @@ export function createTaskHooks(config, ctx) {
1215
1286
  const additionalContext = buildHookContext(contexts);
1216
1287
  return additionalContext === undefined ? undefined : { additionalContext };
1217
1288
  };
1218
- hooks.userPromptSubmit = userPromptSubmitHook;
1219
1289
  }
1220
1290
  if (stopEntries) {
1221
- const stopHook = async (sctx) => {
1291
+ translators.Stop = async (sctx) => {
1222
1292
  const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1223
1293
  let llmExtra;
1224
1294
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
@@ -1307,10 +1377,9 @@ export function createTaskHooks(config, ctx) {
1307
1377
  ...buildHookContextField(contexts),
1308
1378
  };
1309
1379
  };
1310
- hooks.stop = stopHook;
1311
1380
  }
1312
1381
  if (stopFailureEntries) {
1313
- const stopFailureHook = async (fctx) => {
1382
+ translators.StopFailure = async (fctx) => {
1314
1383
  const turnError = toAssistantTurnError(fctx.errorKind);
1315
1384
  const payload = {
1316
1385
  ...basePayload(ctx, undefined),
@@ -1320,10 +1389,9 @@ export function createTaskHooks(config, ctx) {
1320
1389
  };
1321
1390
  await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
1322
1391
  };
1323
- hooks.stopFailure = stopFailureHook;
1324
1392
  }
1325
1393
  if (preCompactEntries) {
1326
- const preCompactHook = async (pctx) => {
1394
+ translators.PreCompact = async (pctx) => {
1327
1395
  const payload = {
1328
1396
  ...basePayload(ctx, undefined),
1329
1397
  hook_event_name: "PreCompact",
@@ -1354,10 +1422,9 @@ export function createTaskHooks(config, ctx) {
1354
1422
  }
1355
1423
  return undefined;
1356
1424
  };
1357
- hooks.preCompact = preCompactHook;
1358
1425
  }
1359
1426
  if (postCompactEntries) {
1360
- const postCompactHook = async (cctx) => {
1427
+ translators.PostCompact = async (cctx) => {
1361
1428
  const payload = {
1362
1429
  ...basePayload(ctx, undefined),
1363
1430
  hook_event_name: "PostCompact",
@@ -1366,177 +1433,16 @@ export function createTaskHooks(config, ctx) {
1366
1433
  };
1367
1434
  await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
1368
1435
  };
1369
- hooks.postCompact = postCompactHook;
1370
- }
1371
- return hooks;
1372
- }
1373
- const HOOK_SLOT_FOLD = {
1374
- preToolUse: "custom",
1375
- preToolUseObservational: "custom",
1376
- postToolUse: "custom",
1377
- postToolUseFailure: "custom",
1378
- postToolBatch: "custom",
1379
- userPromptSubmit: "custom",
1380
- stop: "custom",
1381
- preCompact: "custom",
1382
- timeoutMs: "scalar",
1383
- postCompact: "observe-void",
1384
- stopFailure: "observe-void",
1385
- permissionDenied: "observe-void",
1386
- };
1387
- const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
1388
- export function composeHooks(deployment, task) {
1389
- if (!deployment)
1390
- return task;
1391
- const d = deployment;
1392
- const t = task;
1393
- const out = {};
1394
- const timeoutMs = d.timeoutMs ?? t.timeoutMs;
1395
- if (timeoutMs !== undefined)
1396
- out.timeoutMs = timeoutMs;
1397
- if (d.preToolUse || t.preToolUse) {
1398
- const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
1399
- (t.preToolUse === undefined || t.preToolUseObservational === true);
1400
- if (bothObservational)
1401
- out.preToolUseObservational = true;
1402
- out.preToolUse = async (toolName, input, ctx) => {
1403
- let current = input;
1404
- const contexts = [];
1405
- let asked;
1406
- let allowedFor;
1407
- for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
1408
- if (!slot)
1409
- continue;
1410
- const r = receiveHookResult(await slot(toolName, current, ctx));
1411
- if (r === undefined || r === null || r === false)
1412
- continue;
1413
- if (isEngineRefusal(r))
1414
- return r;
1415
- if (r.additionalContext)
1416
- contexts.push(r.additionalContext);
1417
- const action = r.action;
1418
- const updatedInput = r.updatedInput;
1419
- switch (action) {
1420
- case "deny":
1421
- return { ...r, ...buildHookContextField(contexts) };
1422
- case "ask":
1423
- asked = r;
1424
- break;
1425
- case "allow":
1426
- if (updatedInput !== undefined)
1427
- current = updatedInput;
1428
- allowedFor = { input: current };
1429
- break;
1430
- case undefined:
1431
- if (updatedInput !== undefined)
1432
- current = updatedInput;
1433
- break;
1434
- default: {
1435
- const unread = action;
1436
- void unread;
1437
- return r;
1438
- }
1439
- }
1440
- }
1441
- if (asked) {
1442
- const askedUpdated = asked.updatedInput;
1443
- return {
1444
- ...asked,
1445
- ...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
1446
- ...buildHookContextField(contexts),
1447
- };
1448
- }
1449
- const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
1450
- if (allowedFor !== undefined && allowedFor.input === current)
1451
- return { action: "allow", ...carried };
1452
- if (current !== input || contexts.length > 0)
1453
- return carried;
1454
- return undefined;
1455
- };
1456
- }
1457
- if (d.postToolUse || t.postToolUse) {
1458
- out.postToolUse = async (toolName, input, output, ctx) => {
1459
- const rd = d.postToolUse ? await d.postToolUse(toolName, input, output, ctx) : undefined;
1460
- const rt = t.postToolUse ? await t.postToolUse(toolName, input, output, ctx) : undefined;
1461
- if (!rd && !rt)
1462
- return undefined;
1463
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1464
- const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
1465
- return {
1466
- ...(updatedOutput ? { updatedOutput } : {}),
1467
- ...buildHookContextField(contexts),
1468
- };
1469
- };
1470
- }
1471
- if (d.postToolUseFailure || t.postToolUseFailure) {
1472
- out.postToolUseFailure = async (toolName, input, failure, ctx) => {
1473
- const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1474
- const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1475
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1476
- const additionalContext = buildHookContext(contexts);
1477
- return additionalContext === undefined ? undefined : { additionalContext };
1478
- };
1479
1436
  }
1480
- if (d.postToolBatch || t.postToolBatch) {
1481
- out.postToolBatch = async (calls, meta, bctx) => {
1482
- const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
1483
- const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
1484
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1485
- const additionalContext = buildHookContext(contexts);
1486
- return additionalContext === undefined ? undefined : { additionalContext };
1487
- };
1488
- }
1489
- if (d.userPromptSubmit || t.userPromptSubmit) {
1490
- out.userPromptSubmit = async (prompt, uctx) => {
1491
- const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
1492
- if (rd?.block)
1493
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1494
- const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
1495
- if (!rd && !rt)
1496
- return undefined;
1497
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1498
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1499
- };
1500
- }
1501
- if (d.stop || t.stop) {
1502
- out.stop = async (sctx) => {
1503
- const rd = d.stop ? await d.stop(sctx) : undefined;
1504
- if (rd?.block)
1505
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1506
- const rt = t.stop ? await t.stop(sctx) : undefined;
1507
- if (!rd && !rt)
1508
- return undefined;
1509
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1510
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1511
- };
1512
- }
1513
- if (d.preCompact || t.preCompact) {
1514
- out.preCompact = async (pctx) => {
1515
- const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
1516
- const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
1517
- if (!rd && !rt)
1518
- return undefined;
1519
- const block = rd?.block ?? rt?.block;
1520
- const instructions = [rd?.additionalInstructions, rt?.additionalInstructions].filter((c) => typeof c === "string" && c.length > 0);
1521
- return {
1522
- ...(block !== undefined ? { block } : {}),
1523
- ...(instructions.length ? { additionalInstructions: instructions.join("\n") } : {}),
1524
- };
1525
- };
1526
- }
1527
- for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
1528
- const ds = d[slot];
1529
- const ts = t[slot];
1530
- if (ds && ts) {
1531
- out[slot] = async (...args) => {
1532
- await ds(...args);
1533
- await ts(...args);
1534
- };
1535
- }
1536
- else if (ds || ts) {
1537
- out[slot] = (ds ?? ts);
1538
- }
1539
- }
1540
- 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
+ };
1541
1447
  }
1542
1448
  //# sourceMappingURL=hook-runner.js.map