@namzu/sdk 62.14.0 → 63.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +3 -0
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/eval/compare.d.ts +54 -0
- package/dist/eval/compare.d.ts.map +1 -1
- package/dist/eval/compare.js +146 -1
- package/dist/eval/compare.js.map +1 -1
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +46 -6
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/index.d.ts +6 -2
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +3 -1
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/report-stats.d.ts +6 -0
- package/dist/eval/report-stats.d.ts.map +1 -1
- package/dist/eval/report-stats.js +10 -0
- package/dist/eval/report-stats.js.map +1 -1
- package/dist/eval/run-options.d.ts +25 -0
- package/dist/eval/run-options.d.ts.map +1 -1
- package/dist/eval/run-options.js +36 -0
- package/dist/eval/run-options.js.map +1 -1
- package/dist/eval/run-summary.d.ts +8 -0
- package/dist/eval/run-summary.d.ts.map +1 -1
- package/dist/eval/run-summary.js +13 -3
- package/dist/eval/run-summary.js.map +1 -1
- package/dist/eval/settings.d.ts +74 -0
- package/dist/eval/settings.d.ts.map +1 -0
- package/dist/eval/settings.js +161 -0
- package/dist/eval/settings.js.map +1 -0
- package/dist/eval/split.d.ts +48 -0
- package/dist/eval/split.d.ts.map +1 -0
- package/dist/eval/split.js +41 -0
- package/dist/eval/split.js.map +1 -0
- package/dist/eval/types.d.ts +12 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +2 -1
- package/dist/execution/local.js.map +1 -1
- package/dist/peers/envelope.d.ts +11 -0
- package/dist/peers/envelope.d.ts.map +1 -1
- package/dist/peers/envelope.js +29 -0
- package/dist/peers/envelope.js.map +1 -1
- package/dist/peers/index.d.ts +3 -3
- package/dist/peers/index.d.ts.map +1 -1
- package/dist/peers/index.js +2 -2
- package/dist/peers/index.js.map +1 -1
- package/dist/peers/protocol.d.ts +26 -0
- package/dist/peers/protocol.d.ts.map +1 -1
- package/dist/peers/protocol.js +22 -2
- package/dist/peers/protocol.js.map +1 -1
- package/dist/peers/record.d.ts +2 -0
- package/dist/peers/record.d.ts.map +1 -1
- package/dist/plugin/hook-context.d.ts +30 -0
- package/dist/plugin/hook-context.d.ts.map +1 -0
- package/dist/plugin/hook-context.js +51 -0
- package/dist/plugin/hook-context.js.map +1 -0
- package/dist/plugin/index.d.ts +2 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -0
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +9 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +13 -0
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +87 -12
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +224 -32
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/process/session-child.d.ts +21 -0
- package/dist/process/session-child.d.ts.map +1 -0
- package/dist/process/session-child.js +32 -0
- package/dist/process/session-child.js.map +1 -0
- package/dist/public-runtime.d.ts +4 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +2 -3
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +2 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +13 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +3 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/system-events.d.ts +3 -3
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +2 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +5 -2
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/sandbox/terminal.d.ts.map +1 -1
- package/dist/sandbox/terminal.js +2 -1
- package/dist/sandbox/terminal.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +7 -1
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +37 -10
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/connector/mcp/stdio.ts +3 -0
- package/src/eval/compare.ts +245 -2
- package/src/eval/experiment.ts +58 -4
- package/src/eval/index.ts +32 -1
- package/src/eval/report-stats.ts +11 -0
- package/src/eval/run-options.ts +51 -0
- package/src/eval/run-summary.ts +19 -1
- package/src/eval/settings.ts +225 -0
- package/src/eval/split.ts +83 -0
- package/src/eval/types.ts +12 -0
- package/src/execution/local.ts +2 -1
- package/src/peers/envelope.ts +36 -0
- package/src/peers/index.ts +3 -1
- package/src/peers/protocol.ts +24 -2
- package/src/plugin/hook-context.ts +58 -0
- package/src/plugin/index.ts +9 -0
- package/src/plugin/lifecycle.ts +13 -0
- package/src/plugin/shell-hook.ts +290 -43
- package/src/process/session-child.ts +38 -0
- package/src/public-runtime.ts +27 -0
- package/src/public-types.ts +2 -0
- package/src/runtime/query/executor/tool-call-admission.ts +2 -4
- package/src/runtime/query/executor.ts +15 -3
- package/src/runtime/query/index.ts +3 -3
- package/src/runtime/system-events.ts +2 -1
- package/src/sandbox/provider/local.ts +5 -1
- package/src/sandbox/terminal.ts +3 -1
- package/src/tools/command-shell.ts +7 -1
- package/src/types/message/index.ts +1 -0
- package/src/types/plugin/index.ts +48 -16
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* `permission_mode`. The Namzu names stay, so no existing script changes.
|
|
23
23
|
* The same facts go out as
|
|
24
24
|
* same facts as `NAMZU_HOOK_EVENT`, `NAMZU_SESSION_ID`, `NAMZU_TURN_ID`,
|
|
25
|
-
* `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH
|
|
25
|
+
* `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH`;
|
|
26
|
+
* `NAMZU_PROJECT_DIR` is the folder the session was started for.
|
|
27
|
+
* `session_start` also sends `source` (`startup` or `resume`) and `session_end`
|
|
28
|
+
* `reason` (`exit` or `replaced`).
|
|
26
29
|
*
|
|
27
30
|
* `session_start` and `session_end` run outside any turn, so they carry no
|
|
28
31
|
* `turn_id` on stdin and no `NAMZU_TURN_ID` in the environment — not even
|
|
@@ -32,8 +35,8 @@
|
|
|
32
35
|
* Its answer is the exit code and, on exit `0`, an optional JSON object on
|
|
33
36
|
* stdout. The exit codes:
|
|
34
37
|
*
|
|
35
|
-
* - `0` — carry on. On `user_prompt_submit`,
|
|
36
|
-
* context for the model.
|
|
38
|
+
* - `0` — carry on. On `user_prompt_submit`, `subagent_start` and
|
|
39
|
+
* `session_start`, plain (non-JSON) stdout becomes context for the model.
|
|
37
40
|
* - `2` — **block**, read per event: before a tool (`pre_tool_use`) the call is
|
|
38
41
|
* skipped and the model is told why, with the hook's stderr as the reason;
|
|
39
42
|
* on `user_prompt_submit` the turn ends; on `turn_end` and `subagent_stop`
|
|
@@ -68,7 +71,9 @@
|
|
|
68
71
|
* `permissionDecisionReason`, `updatedInput` and `additionalContext` for
|
|
69
72
|
* `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
|
|
70
73
|
* `post_tool_use`, `post_tool_use_failure`, `turn_end`, `subagent_stop`,
|
|
71
|
-
* `session_start
|
|
74
|
+
* `session_start` (context for the whole session), `notification`;
|
|
75
|
+
* `updatedToolOutput` for `post_tool_use` (the text the model reads instead
|
|
76
|
+
* of the tool's result); and for `permission_request` a `decision`
|
|
72
77
|
* `{ behavior: "allow" }` or `{ behavior: "deny", message, interrupt }`.
|
|
73
78
|
*
|
|
74
79
|
* ## What a hook may decide, and what it may not
|
|
@@ -117,8 +122,10 @@ interface ShellHookEntryBase {
|
|
|
117
122
|
* What this applies to. On a tool event (and `permission_request`) the tool
|
|
118
123
|
* name; on `notification` the notification type. `*` or absent matches
|
|
119
124
|
* everything; a list of names (`a|b`, `a,b`, each optionally ending in `*`
|
|
120
|
-
* as a prefix) matches those
|
|
121
|
-
*
|
|
125
|
+
* as a prefix) matches those without regard to case, and on a tool event
|
|
126
|
+
* also by the names skills write (`Bash`, `Edit`); anything else is a
|
|
127
|
+
* regular expression tested against the name (and, on a tool event, the
|
|
128
|
+
* names skills write for that tool).
|
|
122
129
|
*/
|
|
123
130
|
readonly matcher?: string;
|
|
124
131
|
/** Deadline in milliseconds; default `DEFAULT_SHELL_HOOK_TIMEOUT_MS`, capped at `MAX_SHELL_HOOK_TIMEOUT_MS`. */
|
|
@@ -138,10 +145,19 @@ interface ShellHookEntryBase {
|
|
|
138
145
|
* What the hook's own failure does: it could not start, it timed out, it
|
|
139
146
|
* exited with a code other than `0` or `2`, or it printed JSON that is not
|
|
140
147
|
* valid. `continue` (the default) reports it and lets the action go ahead;
|
|
141
|
-
* `block` counts it as exit `2`, so the action the event guards is refused
|
|
148
|
+
* `block` counts it as exit `2`, so the action the event guards is refused;
|
|
149
|
+
* on `post_tool_use` and `post_tool_use_failure`, where the call has already
|
|
150
|
+
* run, it withholds the result from the model instead (set it on a hook that
|
|
151
|
+
* redacts).
|
|
142
152
|
* Not read on a `prompt` hook or an `async` command.
|
|
143
153
|
*/
|
|
144
154
|
readonly onFailure?: ShellHookOnFailure;
|
|
155
|
+
/**
|
|
156
|
+
* Where the entry was declared, as a label a host shows (a file's path).
|
|
157
|
+
* It has no effect on how the hook runs, and two entries that differ only
|
|
158
|
+
* here are the same entry.
|
|
159
|
+
*/
|
|
160
|
+
readonly source?: string;
|
|
145
161
|
}
|
|
146
162
|
/** One shell command at one event. */
|
|
147
163
|
export interface ShellHookEntry extends ShellHookEntryBase {
|
|
@@ -202,10 +218,29 @@ export declare const MAX_SHELL_HOOK_TIMEOUT_MS: number;
|
|
|
202
218
|
* matches exactly, each part optionally ending in `*` as a prefix (the
|
|
203
219
|
* spelling this adapter has always taken). Anything else is a regular
|
|
204
220
|
* expression, tested unanchored against the name; one that does not compile
|
|
205
|
-
* matches nothing.
|
|
206
|
-
*
|
|
221
|
+
* matches nothing. A matcher on an event that has no name (`turn_start`)
|
|
222
|
+
* matches nothing unless it is `*`.
|
|
223
|
+
*
|
|
224
|
+
* Names in a list compare without regard to case. With `tool: true` (the
|
|
225
|
+
* event's name is a tool: the tool events and `permission_request`) they also
|
|
226
|
+
* take the names skills write, the way an entry's `if` does: `Bash` matches
|
|
227
|
+
* the tool `bash`, `MultiEdit` matches `edit`, and a prefix such as `Web*`
|
|
228
|
+
* matches `web_fetch` by its written name too. Where the name is not a tool
|
|
229
|
+
* (a notification's type, an agent's name, a session's `source` or `reason`)
|
|
230
|
+
* no tool alias applies, since those words are not tool names; case is still
|
|
231
|
+
* ignored there, because a guard that silently never runs over a capital
|
|
232
|
+
* letter is worse than one that also runs for a name differing only in case.
|
|
233
|
+
*
|
|
234
|
+
* A list part ending in `.*` is also tried as a regular expression.
|
|
235
|
+
*
|
|
236
|
+
* A regular expression is tested, case-sensitively as written, against the
|
|
237
|
+
* real name and, with `tool: true`, against each name skills write for that
|
|
238
|
+
* tool, so one written for those names still fires. It never matches less
|
|
239
|
+
* than it did against the real name alone.
|
|
207
240
|
*/
|
|
208
|
-
export declare function shellHookMatches(matcher: string | undefined, toolName: string | undefined
|
|
241
|
+
export declare function shellHookMatches(matcher: string | undefined, toolName: string | undefined, options?: {
|
|
242
|
+
readonly tool?: boolean;
|
|
243
|
+
}): boolean;
|
|
209
244
|
/**
|
|
210
245
|
* A hook's `if` against one tool call: `Tool` or `Tool(pattern)`. The tool
|
|
211
246
|
* part is case-insensitive and takes the names skills write (`Bash`,
|
|
@@ -213,13 +248,23 @@ export declare function shellHookMatches(matcher: string | undefined, toolName:
|
|
|
213
248
|
* there; for a tool with no command line the filter cannot be read, which
|
|
214
249
|
* runs the hook.
|
|
215
250
|
*/
|
|
216
|
-
export declare function shellHookIfMatches(filter: string | undefined, toolName: string | undefined, toolInput: unknown): boolean;
|
|
251
|
+
export declare function shellHookIfMatches(filter: string | undefined, toolName: string | undefined, toolInput: unknown, pathFilter?: ShellHookPathFilter): boolean;
|
|
252
|
+
/**
|
|
253
|
+
* A host's reading of a path pattern in a hook's `if`: whether the call's path
|
|
254
|
+
* is named by `pattern` the way the host's permission rules read it. `tool` is
|
|
255
|
+
* the filter's tool (`edit`, `read`), `callTool` the tool being called.
|
|
256
|
+
* `undefined` means the host does not read this tool's paths, and the
|
|
257
|
+
* filter is then read as it is for any other tool.
|
|
258
|
+
*/
|
|
259
|
+
export type ShellHookPathFilter = (tool: string, pattern: string, callTool: string, toolInput: unknown) => boolean | undefined;
|
|
217
260
|
/** What one hook run came back with, before it is mapped to a verdict. */
|
|
218
261
|
export interface ShellHookOutcome {
|
|
219
262
|
readonly exitCode: number | null;
|
|
220
263
|
readonly timedOut: boolean;
|
|
221
264
|
readonly stdout: string;
|
|
222
265
|
readonly stderr: string;
|
|
266
|
+
/** Set when the hook printed more on stdout than is kept (`MAX_CAPTURE_BYTES`). */
|
|
267
|
+
readonly stdoutTruncated?: boolean;
|
|
223
268
|
/** Set when the process could not be started at all. */
|
|
224
269
|
readonly spawnError?: string;
|
|
225
270
|
}
|
|
@@ -230,6 +275,12 @@ export interface ShellHookInput {
|
|
|
230
275
|
readonly sessionId: string;
|
|
231
276
|
/** The turn the hook fired in. Absent on `session_start` and `session_end`. */
|
|
232
277
|
readonly turnId?: string;
|
|
278
|
+
/** Why the session started, on `session_start`. Sent as `source`. */
|
|
279
|
+
readonly sessionSource?: string;
|
|
280
|
+
/** Why the session closed, on `session_end`. Sent as `reason`. */
|
|
281
|
+
readonly endReason?: string;
|
|
282
|
+
/** The folder the session was started for. Sent as `NAMZU_PROJECT_DIR`; absent, `cwd`. */
|
|
283
|
+
readonly projectDir?: string;
|
|
233
284
|
/** The delegating session, on `subagent_start` and `subagent_stop`. */
|
|
234
285
|
readonly parentSessionId?: string;
|
|
235
286
|
/** The parent turn whose tool call spawned the child, on `subagent_start` and `subagent_stop`. */
|
|
@@ -299,11 +350,35 @@ export interface ShellHookOptions {
|
|
|
299
350
|
readonly transcriptPath?: (sessionId: string) => string | undefined;
|
|
300
351
|
/** The permission mode in force now, sent as `permission_mode`. Called at each run. */
|
|
301
352
|
readonly permissionMode?: () => string | undefined;
|
|
353
|
+
/**
|
|
354
|
+
* The folder the session was started for, sent to every command as
|
|
355
|
+
* `NAMZU_PROJECT_DIR`. Absent, `cwd`.
|
|
356
|
+
*/
|
|
357
|
+
readonly projectDir?: string;
|
|
358
|
+
/** How a path pattern in an entry's `if` is read. Absent, such a filter runs the hook. */
|
|
359
|
+
readonly pathFilter?: ShellHookPathFilter;
|
|
360
|
+
/**
|
|
361
|
+
* A deadline, in milliseconds, for entries of an event that set no
|
|
362
|
+
* `timeoutMs` of their own (an entry's own is honoured as written, up to
|
|
363
|
+
* the cap). Absent, or an event not listed, the ordinary default applies.
|
|
364
|
+
*/
|
|
365
|
+
readonly defaultTimeoutMs?: {
|
|
366
|
+
readonly [event in ShellHookEvent]?: number;
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Called when a hook of this adapter ran out its deadline and was ended.
|
|
370
|
+
* `hook` is the command, URL or prompt text the entry names.
|
|
371
|
+
*/
|
|
372
|
+
readonly onTimeout?: (info: {
|
|
373
|
+
readonly event: ShellHookEvent;
|
|
374
|
+
readonly hook: string;
|
|
375
|
+
readonly timeoutMs: number;
|
|
376
|
+
}) => void;
|
|
302
377
|
}
|
|
303
378
|
/** The deadline for `prompt` hooks when they name none. */
|
|
304
379
|
export declare const DEFAULT_PROMPT_HOOK_TIMEOUT_MS = 30000;
|
|
305
380
|
/** One entry as a hook definition the lifecycle manager accepts. */
|
|
306
|
-
export declare function createShellHook(event: ShellHookEvent,
|
|
381
|
+
export declare function createShellHook(event: ShellHookEvent, configured: ShellHookHandler, options: ShellHookOptions): PluginHookDefinition;
|
|
307
382
|
/**
|
|
308
383
|
* Register every entry on the manager, in file order within an event. The
|
|
309
384
|
* manager starts all of an event's hooks together and reads their answers in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/shell-hook.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"shell-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/shell-hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* `permission_mode`. The Namzu names stay, so no existing script changes.
|
|
23
23
|
* The same facts go out as
|
|
24
24
|
* same facts as `NAMZU_HOOK_EVENT`, `NAMZU_SESSION_ID`, `NAMZU_TURN_ID`,
|
|
25
|
-
* `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH
|
|
25
|
+
* `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH`;
|
|
26
|
+
* `NAMZU_PROJECT_DIR` is the folder the session was started for.
|
|
27
|
+
* `session_start` also sends `source` (`startup` or `resume`) and `session_end`
|
|
28
|
+
* `reason` (`exit` or `replaced`).
|
|
26
29
|
*
|
|
27
30
|
* `session_start` and `session_end` run outside any turn, so they carry no
|
|
28
31
|
* `turn_id` on stdin and no `NAMZU_TURN_ID` in the environment — not even
|
|
@@ -32,8 +35,8 @@
|
|
|
32
35
|
* Its answer is the exit code and, on exit `0`, an optional JSON object on
|
|
33
36
|
* stdout. The exit codes:
|
|
34
37
|
*
|
|
35
|
-
* - `0` — carry on. On `user_prompt_submit`,
|
|
36
|
-
* context for the model.
|
|
38
|
+
* - `0` — carry on. On `user_prompt_submit`, `subagent_start` and
|
|
39
|
+
* `session_start`, plain (non-JSON) stdout becomes context for the model.
|
|
37
40
|
* - `2` — **block**, read per event: before a tool (`pre_tool_use`) the call is
|
|
38
41
|
* skipped and the model is told why, with the hook's stderr as the reason;
|
|
39
42
|
* on `user_prompt_submit` the turn ends; on `turn_end` and `subagent_stop`
|
|
@@ -68,7 +71,9 @@
|
|
|
68
71
|
* `permissionDecisionReason`, `updatedInput` and `additionalContext` for
|
|
69
72
|
* `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
|
|
70
73
|
* `post_tool_use`, `post_tool_use_failure`, `turn_end`, `subagent_stop`,
|
|
71
|
-
* `session_start
|
|
74
|
+
* `session_start` (context for the whole session), `notification`;
|
|
75
|
+
* `updatedToolOutput` for `post_tool_use` (the text the model reads instead
|
|
76
|
+
* of the tool's result); and for `permission_request` a `decision`
|
|
72
77
|
* `{ behavior: "allow" }` or `{ behavior: "deny", message, interrupt }`.
|
|
73
78
|
*
|
|
74
79
|
* ## What a hook may decide, and what it may not
|
|
@@ -101,10 +106,12 @@
|
|
|
101
106
|
import { spawn } from 'node:child_process';
|
|
102
107
|
import { decodedCommands, decomposeCommandLine } from '../authorization/command-line.js';
|
|
103
108
|
import { SKILL_TOOL_NAME_ALIASES, permissionPatternToRegExpSource, } from '../authorization/skill-grant.js';
|
|
109
|
+
import { withSessionChildMarker } from '../process/session-child.js';
|
|
104
110
|
import { spawnHostShell } from '../tools/command-shell.js';
|
|
105
111
|
import { RENAMED_PLUGIN_HOOK_EVENTS } from '../types/plugin/index.js';
|
|
106
112
|
import { asPluginId } from '../utils/id.js';
|
|
107
113
|
import { NOOP_LOGGER } from '../utils/log/create-logger.js';
|
|
114
|
+
import { boundHookReplacement } from './hook-context.js';
|
|
108
115
|
import { postHookJson } from './shell-hook-http.js';
|
|
109
116
|
export const SHELL_HOOK_EVENTS = [
|
|
110
117
|
'user_prompt_submit',
|
|
@@ -131,6 +138,51 @@ export const DEFAULT_SHELL_HOOK_TIMEOUT_MS = 600_000;
|
|
|
131
138
|
export const MAX_SHELL_HOOK_TIMEOUT_MS = 10 * 60_000;
|
|
132
139
|
/** Captured output bound per stream. */
|
|
133
140
|
const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
141
|
+
/**
|
|
142
|
+
* How a skill writes each name in `SKILL_TOOL_NAME_ALIASES` (the alias table
|
|
143
|
+
* is keyed lower-case). A regular-expression matcher is also tested against
|
|
144
|
+
* these, so `^(Edit|Write)$` written for those names still fires.
|
|
145
|
+
*/
|
|
146
|
+
const WRITTEN_TOOL_NAMES = Object.freeze({
|
|
147
|
+
read: 'Read',
|
|
148
|
+
write: 'Write',
|
|
149
|
+
edit: 'Edit',
|
|
150
|
+
multiedit: 'MultiEdit',
|
|
151
|
+
bash: 'Bash',
|
|
152
|
+
grep: 'Grep',
|
|
153
|
+
glob: 'Glob',
|
|
154
|
+
ls: 'LS',
|
|
155
|
+
webfetch: 'WebFetch',
|
|
156
|
+
websearch: 'WebSearch',
|
|
157
|
+
skill: 'Skill',
|
|
158
|
+
askuserquestion: 'AskUserQuestion',
|
|
159
|
+
task: 'Task',
|
|
160
|
+
agent: 'Agent',
|
|
161
|
+
lsp: 'LSP',
|
|
162
|
+
toolsearch: 'ToolSearch',
|
|
163
|
+
bashoutput: 'BashOutput',
|
|
164
|
+
taskoutput: 'TaskOutput',
|
|
165
|
+
killshell: 'KillShell',
|
|
166
|
+
killbash: 'KillBash',
|
|
167
|
+
taskstop: 'TaskStop',
|
|
168
|
+
taskcreate: 'TaskCreate',
|
|
169
|
+
taskupdate: 'TaskUpdate',
|
|
170
|
+
tasklist: 'TaskList',
|
|
171
|
+
});
|
|
172
|
+
/** The names a skill writes for the tool called `toolName` here. */
|
|
173
|
+
function writtenNamesFor(toolName) {
|
|
174
|
+
const lower = toolName.toLowerCase();
|
|
175
|
+
const names = [];
|
|
176
|
+
for (const [written, tool] of Object.entries(SKILL_TOOL_NAME_ALIASES)) {
|
|
177
|
+
if (tool !== lower)
|
|
178
|
+
continue;
|
|
179
|
+
names.push(written);
|
|
180
|
+
const spelled = WRITTEN_TOOL_NAMES[written];
|
|
181
|
+
if (spelled !== undefined)
|
|
182
|
+
names.push(spelled);
|
|
183
|
+
}
|
|
184
|
+
return names;
|
|
185
|
+
}
|
|
134
186
|
/**
|
|
135
187
|
* `matcher` against a name (a tool's, or a notification's type).
|
|
136
188
|
*
|
|
@@ -138,22 +190,56 @@ const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
|
138
190
|
* matches exactly, each part optionally ending in `*` as a prefix (the
|
|
139
191
|
* spelling this adapter has always taken). Anything else is a regular
|
|
140
192
|
* expression, tested unanchored against the name; one that does not compile
|
|
141
|
-
* matches nothing.
|
|
142
|
-
*
|
|
193
|
+
* matches nothing. A matcher on an event that has no name (`turn_start`)
|
|
194
|
+
* matches nothing unless it is `*`.
|
|
195
|
+
*
|
|
196
|
+
* Names in a list compare without regard to case. With `tool: true` (the
|
|
197
|
+
* event's name is a tool: the tool events and `permission_request`) they also
|
|
198
|
+
* take the names skills write, the way an entry's `if` does: `Bash` matches
|
|
199
|
+
* the tool `bash`, `MultiEdit` matches `edit`, and a prefix such as `Web*`
|
|
200
|
+
* matches `web_fetch` by its written name too. Where the name is not a tool
|
|
201
|
+
* (a notification's type, an agent's name, a session's `source` or `reason`)
|
|
202
|
+
* no tool alias applies, since those words are not tool names; case is still
|
|
203
|
+
* ignored there, because a guard that silently never runs over a capital
|
|
204
|
+
* letter is worse than one that also runs for a name differing only in case.
|
|
205
|
+
*
|
|
206
|
+
* A list part ending in `.*` is also tried as a regular expression.
|
|
207
|
+
*
|
|
208
|
+
* A regular expression is tested, case-sensitively as written, against the
|
|
209
|
+
* real name and, with `tool: true`, against each name skills write for that
|
|
210
|
+
* tool, so one written for those names still fires. It never matches less
|
|
211
|
+
* than it did against the real name alone.
|
|
143
212
|
*/
|
|
144
|
-
export function shellHookMatches(matcher, toolName) {
|
|
213
|
+
export function shellHookMatches(matcher, toolName, options = {}) {
|
|
145
214
|
if (matcher === undefined || matcher.trim() === '' || matcher.trim() === '*')
|
|
146
215
|
return true;
|
|
147
216
|
if (toolName === undefined)
|
|
148
217
|
return false;
|
|
218
|
+
const tool = options.tool === true;
|
|
149
219
|
const parts = matcher.split(/[|,]/).map((part) => part.trim());
|
|
150
220
|
if (parts.every((part) => /^[A-Za-z0-9_.:-]+\*?$/.test(part))) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
221
|
+
const subject = toolName.toLowerCase();
|
|
222
|
+
const listed = parts.some((candidate) => {
|
|
223
|
+
const prefix = candidate.endsWith('*');
|
|
224
|
+
const written = (prefix ? candidate.slice(0, -1) : candidate).toLowerCase();
|
|
225
|
+
const spellings = [written];
|
|
226
|
+
if (tool) {
|
|
227
|
+
const alias = SKILL_TOOL_NAME_ALIASES[written];
|
|
228
|
+
if (alias !== undefined)
|
|
229
|
+
spellings.push(alias);
|
|
230
|
+
}
|
|
231
|
+
return spellings.some((name) => (prefix ? subject.startsWith(name) : subject === name));
|
|
232
|
+
});
|
|
233
|
+
// `Bash.*` reads as the prefix `bash.` above, which no tool has. A part
|
|
234
|
+
// ending in `.*` is also a regular expression, so it is tried as one.
|
|
235
|
+
if (listed || !matcher.includes('.*'))
|
|
236
|
+
return listed;
|
|
154
237
|
}
|
|
155
238
|
try {
|
|
156
|
-
|
|
239
|
+
const expression = new RegExp(matcher);
|
|
240
|
+
if (expression.test(toolName))
|
|
241
|
+
return true;
|
|
242
|
+
return tool && writtenNamesFor(toolName).some((name) => expression.test(name));
|
|
157
243
|
}
|
|
158
244
|
catch {
|
|
159
245
|
return false;
|
|
@@ -166,7 +252,7 @@ export function shellHookMatches(matcher, toolName) {
|
|
|
166
252
|
* there; for a tool with no command line the filter cannot be read, which
|
|
167
253
|
* runs the hook.
|
|
168
254
|
*/
|
|
169
|
-
export function shellHookIfMatches(filter, toolName, toolInput) {
|
|
255
|
+
export function shellHookIfMatches(filter, toolName, toolInput, pathFilter) {
|
|
170
256
|
if (filter === undefined || filter.trim() === '')
|
|
171
257
|
return true;
|
|
172
258
|
if (toolName === undefined)
|
|
@@ -176,10 +262,18 @@ export function shellHookIfMatches(filter, toolName, toolInput) {
|
|
|
176
262
|
return true;
|
|
177
263
|
const written = (shape[1] ?? '').toLowerCase();
|
|
178
264
|
const tool = SKILL_TOOL_NAME_ALIASES[written] ?? written;
|
|
265
|
+
const specifier = shape[2]?.trim();
|
|
266
|
+
const hasPattern = specifier !== undefined && specifier !== '' && specifier.replaceAll('*', '') !== '';
|
|
267
|
+
// A path pattern is the permission rules' own: the host that has them says
|
|
268
|
+
// whether this call's path is named. It decides for the tools it knows.
|
|
269
|
+
if (hasPattern && pathFilter !== undefined) {
|
|
270
|
+
const answer = pathFilter(tool, specifier, toolName, toolInput);
|
|
271
|
+
if (answer !== undefined)
|
|
272
|
+
return answer;
|
|
273
|
+
}
|
|
179
274
|
if (tool !== toolName.toLowerCase())
|
|
180
275
|
return false;
|
|
181
|
-
|
|
182
|
-
if (specifier === undefined || specifier === '' || specifier.replaceAll('*', '') === '')
|
|
276
|
+
if (!hasPattern)
|
|
183
277
|
return true;
|
|
184
278
|
if (tool !== 'bash')
|
|
185
279
|
return true;
|
|
@@ -214,6 +308,7 @@ export function shellHookPayload(input) {
|
|
|
214
308
|
...(input.transcriptPath !== undefined ? { transcript_path: input.transcriptPath } : {}),
|
|
215
309
|
...(input.permissionMode !== undefined ? { permission_mode: input.permissionMode } : {}),
|
|
216
310
|
...(input.turnId !== undefined ? { turn_id: input.turnId } : {}),
|
|
311
|
+
...(input.sessionSource !== undefined ? { source: input.sessionSource } : {}),
|
|
217
312
|
...(input.parentSessionId !== undefined ? { parent_session_id: input.parentSessionId } : {}),
|
|
218
313
|
...(input.parentTurnId !== undefined ? { parent_turn_id: input.parentTurnId } : {}),
|
|
219
314
|
...(input.agentType !== undefined
|
|
@@ -240,7 +335,11 @@ export function shellHookPayload(input) {
|
|
|
240
335
|
})),
|
|
241
336
|
}
|
|
242
337
|
: {}),
|
|
243
|
-
...(input.reason !== undefined
|
|
338
|
+
...(input.reason !== undefined
|
|
339
|
+
? { reason: input.reason }
|
|
340
|
+
: input.endReason !== undefined
|
|
341
|
+
? { reason: input.endReason }
|
|
342
|
+
: {}),
|
|
244
343
|
...(input.event === 'turn_failure' && input.error !== undefined ? { error: input.error } : {}),
|
|
245
344
|
...(input.notification !== undefined
|
|
246
345
|
? {
|
|
@@ -266,6 +365,7 @@ export function runShellHook(entry, input, signal) {
|
|
|
266
365
|
...inherited,
|
|
267
366
|
NAMZU_HOOK_EVENT: input.event,
|
|
268
367
|
NAMZU_SESSION_ID: input.sessionId,
|
|
368
|
+
NAMZU_PROJECT_DIR: input.projectDir ?? input.cwd,
|
|
269
369
|
...(input.turnId !== undefined ? { NAMZU_TURN_ID: input.turnId } : {}),
|
|
270
370
|
...(input.toolName !== undefined ? { NAMZU_TOOL_NAME: input.toolName } : {}),
|
|
271
371
|
...(toolPath !== undefined ? { NAMZU_TOOL_PATH: toolPath } : {}),
|
|
@@ -282,7 +382,7 @@ export function runShellHook(entry, input, signal) {
|
|
|
282
382
|
? // The exec form: no shell reads `command` or any argument.
|
|
283
383
|
spawn(entry.command, [...entry.args], {
|
|
284
384
|
cwd: input.cwd,
|
|
285
|
-
env,
|
|
385
|
+
env: withSessionChildMarker(env),
|
|
286
386
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
287
387
|
detached,
|
|
288
388
|
})
|
|
@@ -311,16 +411,26 @@ export function runShellHook(entry, input, signal) {
|
|
|
311
411
|
}
|
|
312
412
|
let stdout = '';
|
|
313
413
|
let stderr = '';
|
|
414
|
+
let stdoutTruncated = false;
|
|
314
415
|
let timedOut = false;
|
|
315
416
|
let settled = false;
|
|
316
|
-
const capture = (current, chunk) =>
|
|
317
|
-
|
|
318
|
-
|
|
417
|
+
const capture = (current, chunk) => {
|
|
418
|
+
if (current.length >= MAX_CAPTURE_BYTES)
|
|
419
|
+
return { text: current, cut: chunk.length > 0 };
|
|
420
|
+
const joined = current + chunk.toString('utf8');
|
|
421
|
+
return {
|
|
422
|
+
text: joined.slice(0, MAX_CAPTURE_BYTES),
|
|
423
|
+
cut: joined.length > MAX_CAPTURE_BYTES,
|
|
424
|
+
};
|
|
425
|
+
};
|
|
319
426
|
child.stdout?.on('data', (chunk) => {
|
|
320
|
-
|
|
427
|
+
const next = capture(stdout, chunk);
|
|
428
|
+
stdout = next.text;
|
|
429
|
+
if (next.cut)
|
|
430
|
+
stdoutTruncated = true;
|
|
321
431
|
});
|
|
322
432
|
child.stderr?.on('data', (chunk) => {
|
|
323
|
-
stderr = capture(stderr, chunk);
|
|
433
|
+
stderr = capture(stderr, chunk).text;
|
|
324
434
|
});
|
|
325
435
|
const finish = (outcome) => {
|
|
326
436
|
if (settled)
|
|
@@ -355,11 +465,18 @@ export function runShellHook(entry, input, signal) {
|
|
|
355
465
|
timedOut,
|
|
356
466
|
stdout,
|
|
357
467
|
stderr,
|
|
468
|
+
...(stdoutTruncated ? { stdoutTruncated } : {}),
|
|
358
469
|
spawnError: error.message,
|
|
359
470
|
});
|
|
360
471
|
});
|
|
361
472
|
child.on('close', (code) => {
|
|
362
|
-
finish({
|
|
473
|
+
finish({
|
|
474
|
+
exitCode: code,
|
|
475
|
+
timedOut,
|
|
476
|
+
stdout,
|
|
477
|
+
stderr,
|
|
478
|
+
...(stdoutTruncated ? { stdoutTruncated } : {}),
|
|
479
|
+
});
|
|
363
480
|
});
|
|
364
481
|
child.on('exit', () => {
|
|
365
482
|
// After a kill, a survivor that still holds the pipes must not hold
|
|
@@ -518,6 +635,12 @@ function readShellHookJson(event, stdout) {
|
|
|
518
635
|
reading.updatedInput = specific.updatedInput;
|
|
519
636
|
}
|
|
520
637
|
}
|
|
638
|
+
if (event === 'post_tool_use' && specific.updatedToolOutput !== undefined) {
|
|
639
|
+
if (typeof specific.updatedToolOutput !== 'string') {
|
|
640
|
+
return { invalid: 'hookSpecificOutput: `updatedToolOutput` must be a string' };
|
|
641
|
+
}
|
|
642
|
+
reading.replacement = boundHookReplacement(specific.updatedToolOutput);
|
|
643
|
+
}
|
|
521
644
|
if (event === 'permission_request') {
|
|
522
645
|
const decision = specific.decision;
|
|
523
646
|
if (!isRecord(decision)) {
|
|
@@ -552,9 +675,25 @@ export function shellHookVerdict(event, entry, outcome, log = NOOP_LOGGER) {
|
|
|
552
675
|
};
|
|
553
676
|
// A failure of the hook itself is "carry on" unless the entry asked for
|
|
554
677
|
// it to count as a refusal, which is exit 2 with the failure as the reason.
|
|
555
|
-
const failed = (reason
|
|
556
|
-
|
|
557
|
-
|
|
678
|
+
const failed = (reason, shown = reason) => {
|
|
679
|
+
if (entry.onFailure !== 'block' || entry.type === 'prompt' || isAsyncShellHook(entry)) {
|
|
680
|
+
return { action: 'continue' };
|
|
681
|
+
}
|
|
682
|
+
// The call has run and a refusal cannot undo it, but the hook may have
|
|
683
|
+
// been the one thing keeping something out of the result (a redaction),
|
|
684
|
+
// so a failed guard withholds the result instead of passing it on. The
|
|
685
|
+
// kernel marks the record as it does for any replaced result.
|
|
686
|
+
if (event === 'post_tool_use' || event === 'post_tool_use_failure') {
|
|
687
|
+
const line = `A hook guarding this result failed, so the result is not shown (${shown}). The tool call itself has already run.`;
|
|
688
|
+
return {
|
|
689
|
+
action: 'replace',
|
|
690
|
+
output: line,
|
|
691
|
+
content: [{ type: 'text', text: line }],
|
|
692
|
+
disclose: true,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return shellHookVerdict(event, { ...entry, onFailure: 'continue' }, { exitCode: 2, timedOut: false, stdout: '', stderr: `hook failed: ${reason}` }, log);
|
|
696
|
+
};
|
|
558
697
|
if (outcome.spawnError !== undefined) {
|
|
559
698
|
log.warn('hook could not start', {
|
|
560
699
|
...attrs,
|
|
@@ -602,12 +741,17 @@ export function shellHookVerdict(event, entry, outcome, log = NOOP_LOGGER) {
|
|
|
602
741
|
}
|
|
603
742
|
// Exit 0.
|
|
604
743
|
const text = outcome.stdout.trim();
|
|
605
|
-
const reading =
|
|
744
|
+
const reading = outcome.stdoutTruncated === true && text.startsWith('{')
|
|
745
|
+
? {
|
|
746
|
+
invalid: `answer is too large: the hook printed more than the ${MAX_CAPTURE_BYTES} characters of stdout that are read`,
|
|
747
|
+
}
|
|
748
|
+
: readShellHookJson(event, outcome.stdout);
|
|
606
749
|
if (reading === undefined) {
|
|
607
750
|
// On the prompt event, what the hook printed is context for the
|
|
608
751
|
// model — the branch, the time, a ticket. Everywhere else stdout is
|
|
609
752
|
// the operator's to read and the model's business not at all.
|
|
610
|
-
if ((event === 'user_prompt_submit' || event === 'subagent_start'
|
|
753
|
+
if ((event === 'user_prompt_submit' || event === 'subagent_start' || event === 'session_start') &&
|
|
754
|
+
text.length > 0)
|
|
611
755
|
return { action: 'annotate', text };
|
|
612
756
|
return { action: 'continue' };
|
|
613
757
|
}
|
|
@@ -616,7 +760,9 @@ export function shellHookVerdict(event, entry, outcome, log = NOOP_LOGGER) {
|
|
|
616
760
|
...attrs,
|
|
617
761
|
'namzu.hook.problem': reading.invalid,
|
|
618
762
|
});
|
|
619
|
-
|
|
763
|
+
// The parser's own detail can quote a piece of the answer; the line the
|
|
764
|
+
// model reads keeps only the lead.
|
|
765
|
+
return failed(reading.invalid, reading.invalid.split(' (')[0]);
|
|
620
766
|
}
|
|
621
767
|
if (!reading.suppressOutput) {
|
|
622
768
|
log.debug('hook output', {
|
|
@@ -667,11 +813,34 @@ export function shellHookVerdict(event, entry, outcome, log = NOOP_LOGGER) {
|
|
|
667
813
|
const context = [reading.block, reading.context].filter(Boolean).join('\n');
|
|
668
814
|
return context.length > 0 ? { action: 'annotate', text: context } : { action: 'continue' };
|
|
669
815
|
}
|
|
816
|
+
case 'session_start': {
|
|
817
|
+
// Nothing here can be refused. What the hook said for the model becomes
|
|
818
|
+
// the session's context; a stop request and a warning still count.
|
|
819
|
+
return reading.context !== undefined
|
|
820
|
+
? { action: 'annotate', text: reading.context }
|
|
821
|
+
: {
|
|
822
|
+
action: 'continue',
|
|
823
|
+
...(reading.halt ? { halt: reading.halt } : {}),
|
|
824
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
670
827
|
case 'post_tool_use':
|
|
671
828
|
case 'post_tool_use_failure': {
|
|
672
829
|
// The call has run, so a "block" cannot undo it: its reason is what the
|
|
673
830
|
// model is told about the result.
|
|
674
831
|
const context = [reading.block, reading.context].filter(Boolean).join('\n');
|
|
832
|
+
// A replacement is the hook's text in place of the tool's: the record
|
|
833
|
+
// says so beside it (the kernel adds the mark).
|
|
834
|
+
if (reading.replacement !== undefined) {
|
|
835
|
+
return {
|
|
836
|
+
action: 'replace',
|
|
837
|
+
output: reading.replacement,
|
|
838
|
+
disclose: true,
|
|
839
|
+
...(context.length > 0 ? { context } : {}),
|
|
840
|
+
...(reading.halt ? { halt: reading.halt } : {}),
|
|
841
|
+
...(reading.notice ? { notice: reading.notice } : {}),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
675
844
|
return {
|
|
676
845
|
action: 'continue',
|
|
677
846
|
...(context.length > 0 ? { context } : {}),
|
|
@@ -817,8 +986,17 @@ function readJudgement(reply) {
|
|
|
817
986
|
}
|
|
818
987
|
}
|
|
819
988
|
/** One entry as a hook definition the lifecycle manager accepts. */
|
|
820
|
-
export function createShellHook(event,
|
|
989
|
+
export function createShellHook(event, configured, options) {
|
|
821
990
|
const log = options.log ?? NOOP_LOGGER;
|
|
991
|
+
const eventDefault = options.defaultTimeoutMs?.[event];
|
|
992
|
+
const entry = configured.timeoutMs === undefined && eventDefault !== undefined
|
|
993
|
+
? { ...configured, timeoutMs: eventDefault }
|
|
994
|
+
: configured;
|
|
995
|
+
const reportTimeout = (timedOut) => {
|
|
996
|
+
if (!timedOut)
|
|
997
|
+
return;
|
|
998
|
+
options.onTimeout?.({ event, hook: describeShellHook(entry), timeoutMs: hookTimeout(entry) });
|
|
999
|
+
};
|
|
822
1000
|
// `once` is per definition, and a definition is per session: the manager
|
|
823
1001
|
// is built for one session and its hooks are attached to it.
|
|
824
1002
|
let spent = false;
|
|
@@ -831,14 +1009,23 @@ export function createShellHook(event, entry, options) {
|
|
|
831
1009
|
if (spent)
|
|
832
1010
|
return { action: 'continue' };
|
|
833
1011
|
// A notification is matched on its type; every other event on the tool.
|
|
1012
|
+
const namesATool = !(event === 'notification' ||
|
|
1013
|
+
event === 'subagent_start' ||
|
|
1014
|
+
event === 'subagent_stop' ||
|
|
1015
|
+
event === 'session_start' ||
|
|
1016
|
+
event === 'session_end');
|
|
834
1017
|
const subject = event === 'notification'
|
|
835
1018
|
? context.notification?.type
|
|
836
1019
|
: event === 'subagent_start' || event === 'subagent_stop'
|
|
837
1020
|
? context.agentType
|
|
838
|
-
:
|
|
839
|
-
|
|
1021
|
+
: event === 'session_start'
|
|
1022
|
+
? context.source
|
|
1023
|
+
: event === 'session_end'
|
|
1024
|
+
? context.endReason
|
|
1025
|
+
: context.toolName;
|
|
1026
|
+
if (!shellHookMatches(entry.matcher, subject, { tool: namesATool }))
|
|
840
1027
|
return { action: 'continue' };
|
|
841
|
-
if (!shellHookIfMatches(entry.if, context.toolName, context.toolInput)) {
|
|
1028
|
+
if (!shellHookIfMatches(entry.if, context.toolName, context.toolInput, options.pathFilter)) {
|
|
842
1029
|
return { action: 'continue' };
|
|
843
1030
|
}
|
|
844
1031
|
if (entry.once === true)
|
|
@@ -848,7 +1035,10 @@ export function createShellHook(event, entry, options) {
|
|
|
848
1035
|
const hookInput = {
|
|
849
1036
|
event,
|
|
850
1037
|
cwd: options.cwd,
|
|
1038
|
+
...(options.projectDir !== undefined ? { projectDir: options.projectDir } : {}),
|
|
851
1039
|
sessionId: String(context.sessionId),
|
|
1040
|
+
...(context.source !== undefined ? { sessionSource: context.source } : {}),
|
|
1041
|
+
...(context.endReason !== undefined ? { endReason: context.endReason } : {}),
|
|
852
1042
|
...(context.turnId !== undefined ? { turnId: String(context.turnId) } : {}),
|
|
853
1043
|
...(context.parentSessionId !== undefined
|
|
854
1044
|
? { parentSessionId: String(context.parentSessionId) }
|
|
@@ -889,6 +1079,7 @@ export function createShellHook(event, entry, options) {
|
|
|
889
1079
|
timeoutMs: hookTimeout(entry),
|
|
890
1080
|
...(context.signal !== undefined ? { signal: context.signal } : {}),
|
|
891
1081
|
});
|
|
1082
|
+
reportTimeout(response.timedOut);
|
|
892
1083
|
const ok = response.status !== null && response.status >= 200 && response.status < 300;
|
|
893
1084
|
return shellHookVerdict(event, entry, {
|
|
894
1085
|
exitCode: ok ? 0 : (response.status ?? null),
|
|
@@ -912,6 +1103,7 @@ export function createShellHook(event, entry, options) {
|
|
|
912
1103
|
return { action: 'continue' };
|
|
913
1104
|
}
|
|
914
1105
|
const outcome = await runShellHook(entry, hookInput, context.signal);
|
|
1106
|
+
reportTimeout(outcome.timedOut);
|
|
915
1107
|
return shellHookVerdict(event, entry, outcome, log);
|
|
916
1108
|
},
|
|
917
1109
|
};
|