agent-sanitizer 2.8.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,9 +19,29 @@ export function buildPreToolUseResponse(input: any, rehydrate?: (tool: string, t
19
19
  * fail-closed posture holds even when the adapter never loaded.
20
20
  * @param {import("agent-control-plane-core").ToolCallEvent} event
21
21
  * @param {(tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>} [rehydrate]
22
+ * @param {{ messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>, gates?: HostGate[] }} [opts]
23
+ * messages are merged over the defaults, so a partial table is supported
22
24
  * @returns {Promise<import("agent-control-plane-core").Verdict>}
23
25
  */
24
- export function judgePreToolUseSanitize(event: import("agent-control-plane-core").ToolCallEvent, rehydrate?: (tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>): Promise<import("agent-control-plane-core").Verdict>;
26
+ export function judgePreToolUseSanitize(event: import("agent-control-plane-core").ToolCallEvent, rehydrate?: (tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>, opts?: {
27
+ messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>;
28
+ gates?: HostGate[];
29
+ }): Promise<import("agent-control-plane-core").Verdict>;
30
+ /**
31
+ * The dependency-load failure hiding behind a hook error, or "". A binding that
32
+ * never loaded surfaces at use time as a bare TypeError ("X is not a function")
33
+ * that names neither the package nor the remedy; when any lazily-loaded package
34
+ * has a recorded load error, name it — the failed set is derived from the
35
+ * loader's own records, so a future dependency is covered without editing a list
36
+ * here. An error already reporting a missing package (missingPackageError's
37
+ * `DEP_UNAVAILABLE` tag) gets no second copy.
38
+ * @param {unknown} err
39
+ * @param {string} [remedy] what a reader should run; hosts pass their own
40
+ * @param {() => string[]} [failedPackages]
41
+ * @param {(pkg: string) => unknown} [loadErrorFor]
42
+ * @returns {string}
43
+ */
44
+ export function depLoadHint(err: unknown, remedy?: string, failedPackages?: () => string[], loadErrorFor?: (pkg: string) => unknown): string;
25
45
  /**
26
46
  * The fail-closed hookSpecificOutput fields for a hook-level failure, chosen by
27
47
  * WHICH failure it was. Corrupt/unparsable INPUT (`parsedOk` false — a JSON parse
@@ -32,16 +52,64 @@ export function judgePreToolUseSanitize(event: import("agent-control-plane-core"
32
52
  * so it ASKS to keep a human in the loop rather than hard-block on infrastructure.
33
53
  * @param {boolean} parsedOk whether the input parsed before the failure
34
54
  * @param {unknown} err
55
+ * @param {{ messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>, hint?: string }} [opts]
35
56
  * @returns {Record<string, unknown>}
36
57
  */
37
- export function failClosedFields(parsedOk: boolean, err: unknown): Record<string, unknown>;
58
+ export function failClosedFields(parsedOk: boolean, err: unknown, opts?: {
59
+ messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>;
60
+ hint?: string;
61
+ }): Record<string, unknown>;
38
62
  /**
39
63
  * The hook's CLI: parse → judge → render, with this hook's fail-closed posture.
40
64
  * Exported so a bundle entry (which must claim the CLI slot before this module
41
65
  * loads) can run the exact same wiring instead of duplicating the onError
42
66
  * posture.
67
+ * @param {{
68
+ * messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>,
69
+ * gates?: HostGate[],
70
+ * }} [opts]
43
71
  * @returns {Promise<void>}
44
72
  */
45
- export function cliMain(): Promise<void>;
73
+ export function cliMain(opts?: {
74
+ messages?: Partial<typeof PRE_TOOL_USE_MESSAGES>;
75
+ gates?: HostGate[];
76
+ }): Promise<void>;
77
+ /**
78
+ * A host-supplied deny gate: given the PreToolUse input, the reason this call
79
+ * must be blocked, or null to let the pipeline continue. Hosts use these for
80
+ * policy the package has no view of (a required workflow step, a project-local
81
+ * rule); the package ships none.
82
+ * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string })
83
+ * => string | null | undefined} HostGate
84
+ */
85
+ /**
86
+ * The reasons this hook emits, as a table a host overrides. A host that knows
87
+ * which of ITS files wires the adapter, and what a reader should do about a
88
+ * failure, can say so — the package cannot, since it has no idea where it is
89
+ * installed.
90
+ * @type {Readonly<{
91
+ * unknownEvent: string,
92
+ * failed: (cause: string) => string,
93
+ * unparsable: (cause: string) => string,
94
+ * remedy: string,
95
+ * }>}
96
+ */
97
+ export const PRE_TOOL_USE_MESSAGES: Readonly<{
98
+ unknownEvent: string;
99
+ failed: (cause: string) => string;
100
+ unparsable: (cause: string) => string;
101
+ remedy: string;
102
+ }>;
103
+ /**
104
+ * A host-supplied deny gate: given the PreToolUse input, the reason this call
105
+ * must be blocked, or null to let the pipeline continue. Hosts use these for
106
+ * policy the package has no view of (a required workflow step, a project-local
107
+ * rule); the package ships none.
108
+ */
109
+ export type HostGate = (input: {
110
+ tool_name: string | null;
111
+ tool_input: any;
112
+ session_id?: string;
113
+ }) => string | null | undefined;
46
114
  declare const rehydrateRedacted: typeof import("agent-sanitizer/rehydrate").rehydrateRedacted;
47
115
  export {};
@@ -30,6 +30,11 @@
30
30
  * @property {(record: { tool: string | null, modified: boolean, output: unknown, context?: string }) => Promise<void> | void} [audit]
31
31
  * Awaited once per judged event that carried a tool response, with the output
32
32
  * the model will actually see.
33
+ * @property {string} [remedy]
34
+ * What a reader should run when the sanitizer's own bindings are what is
35
+ * missing. This hook's host channel is `ext`, where the other two gates use a
36
+ * frozen message table; either way it is one channel per gate, so a host
37
+ * cannot supply its wording somewhere the fail-closed context never reads.
33
38
  */
34
39
  /**
35
40
  * Run Layers 1-4 over a single text blob, delegated to the package's output seam
@@ -118,12 +123,17 @@ export function failClosedReplacement(input: any, message: string): any;
118
123
  */
119
124
  export function sanitizerDepsLoaded(): boolean;
120
125
  /**
121
- * The model-facing note for a fail-closed emission, with the missing-dependency
122
- * remedy appended when the sanitizer's bindings are the thing that is absent.
126
+ * The model-facing note for a fail-closed emission. When the sanitizer's own
127
+ * bindings are what is absent — a broken INSTALL rather than a broken hook, and
128
+ * otherwise invisible because every later tool call then fails closed with no
129
+ * stated cause — the recorded loader error and its remedy ride along. The text
130
+ * comes from missingPackageMessage so this hook, the PreToolUse gate and the
131
+ * prompt gate cannot drift apart on what a missing dependency reads like.
123
132
  * @param {() => boolean} [depsLoaded] injectable seam for testing
133
+ * @param {string} [remedy] what a reader should run; hosts pass their own
124
134
  * @returns {string}
125
135
  */
126
- export function failClosedContext(depsLoaded?: () => boolean): string;
136
+ export function failClosedContext(depsLoaded?: () => boolean, remedy?: string): string;
127
137
  /**
128
138
  * Emit a fail-closed PostToolUse response, robust to the suppression itself
129
139
  * throwing. The shape-matching replacement walks `input.tool_response` and the
@@ -137,9 +147,10 @@ export function failClosedContext(depsLoaded?: () => boolean): string;
137
147
  * @param {any} input parsed hook input, or undefined if parsing threw
138
148
  * @param {string} message
139
149
  * @param {(fields: Record<string, unknown>) => void} [emit]
150
+ * @param {string} [remedy] what a reader should run; hosts pass their own
140
151
  * @returns {void}
141
152
  */
142
- export function emitFailClosed(input: any, message: string, emit?: (fields: Record<string, unknown>) => void): void;
153
+ export function emitFailClosed(input: any, message: string, emit?: (fields: Record<string, unknown>) => void, remedy?: string): void;
143
154
  /**
144
155
  * Run the sanitization pipeline over a tool output and return the contract-
145
156
  * shaped verdict fields — `mutated_output` (the shape-matching sanitized value)
@@ -261,4 +272,11 @@ export type SanitizeExtensions = {
261
272
  output: unknown;
262
273
  context?: string;
263
274
  }) => Promise<void> | void) | undefined;
275
+ /**
276
+ * What a reader should run when the sanitizer's own bindings are what is
277
+ * missing. This hook's host channel is `ext`, where the other two gates use a
278
+ * frozen message table; either way it is one channel per gate, so a host
279
+ * cannot supply its wording somewhere the fail-closed context never reads.
280
+ */
281
+ remedy?: string | undefined;
264
282
  };
@@ -8,16 +8,41 @@
8
8
  * @param {import("agent-control-plane-core").ToolCallEvent} event
9
9
  * @param {((s: string) => string) | null} [strip] the ANSI stripper (defaults
10
10
  * to the package's stripAnsiFully; injectable so the fail-closed path is testable)
11
+ * @param {Partial<typeof USER_PROMPT_MESSAGES>} [overrides] reason overrides,
12
+ * merged over the defaults so a partial table can never leave a field unset
11
13
  * @returns {import("agent-control-plane-core").Verdict}
12
14
  */
13
- export function judgeSanitizeUserPrompt(event: import("agent-control-plane-core").ToolCallEvent, strip?: ((s: string) => string) | null): import("agent-control-plane-core").Verdict;
15
+ export function judgeSanitizeUserPrompt(event: import("agent-control-plane-core").ToolCallEvent, strip?: ((s: string) => string) | null, overrides?: Partial<typeof USER_PROMPT_MESSAGES>): import("agent-control-plane-core").Verdict;
14
16
  /**
15
17
  * @param {() => Promise<any> | any} read
16
18
  * @param {(chunk: string) => void} write
17
19
  * @param {((s: string) => string) | null} [strip] the ANSI stripper (defaults
18
20
  * to the package's stripAnsiFully; injectable so the fail-closed path is testable)
21
+ * @param {Partial<typeof USER_PROMPT_MESSAGES>} [overrides] reason overrides,
22
+ * merged over the defaults so a partial table can never leave a field unset
19
23
  * @returns {Promise<void>}
20
24
  */
21
- export function main(read: () => Promise<any> | any, write: (chunk: string) => void, strip?: ((s: string) => string) | null): Promise<void>;
25
+ export function main(read: () => Promise<any> | any, write: (chunk: string) => void, strip?: ((s: string) => string) | null, overrides?: Partial<typeof USER_PROMPT_MESSAGES>): Promise<void>;
22
26
  /** @type {typeof import("agent-sanitizer/prompt").classifyPrompt} */
23
27
  export let classifyPrompt: typeof import("agent-sanitizer/prompt").classifyPrompt;
28
+ /**
29
+ * The reasons this gate emits, as a table a host overrides. A host that knows
30
+ * which of ITS files wires the adapter, and what a reader should do about a
31
+ * failure, can say so — the package cannot, since it has no idea where it is
32
+ * installed. Every field is a plain string or a string-returning function, so
33
+ * an override is auditable next to the default it replaces.
34
+ * @type {Readonly<{
35
+ * unknownEvent: string,
36
+ * blockContext: string,
37
+ * sgrNote: string,
38
+ * hookFailed: (cause: string) => string,
39
+ * remedy: string,
40
+ * }>}
41
+ */
42
+ export const USER_PROMPT_MESSAGES: Readonly<{
43
+ unknownEvent: string;
44
+ blockContext: string;
45
+ sgrNote: string;
46
+ hookFailed: (cause: string) => string;
47
+ remedy: string;
48
+ }>;
@@ -42,9 +42,9 @@ export function scanFile(filePath: string): Array<{
42
42
  }>;
43
43
  import { ALERT_FILE } from "./lib/invisible-alert.mjs";
44
44
  import { ALERT_ACK_FILE } from "./lib/invisible-alert.mjs";
45
- export const LONG_RUN_RE: RegExp;
46
- export const LONG_RUN_THRESHOLD: 10;
47
- export const TOTAL_INVISIBLE_THRESHOLD: 30;
45
+ export let LONG_RUN_RE: RegExp;
46
+ export let LONG_RUN_THRESHOLD: 10;
47
+ export let TOTAL_INVISIBLE_THRESHOLD: 30;
48
48
  /**
49
49
  * @param {Array<{
50
50
  * file: string,