agent-sanitizer 2.67.2 → 2.69.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/README.md +8 -6
- package/THREAT-MODEL.md +52 -39
- package/claude-hooks/lib/env-config.mjs +2 -2
- package/claude-hooks/lib/hook-fault.mjs +13 -35
- package/claude-hooks/lib/hook-io.mjs +84 -9
- package/claude-hooks/lib/hook-timing.mjs +26 -6
- package/claude-hooks/lib/invisible-alert.mjs +6 -3
- package/claude-hooks/lib/redactor-client.mjs +23 -23
- package/claude-hooks/lib/secret-annotate.mjs +36 -66
- package/claude-hooks/lib/secret-drop-guard.mjs +136 -29
- package/claude-hooks/plugin-hooks.mjs +30 -27
- package/claude-hooks/pretooluse-sanitize.mjs +288 -64
- package/claude-hooks/sanitize-output.mjs +27 -40
- package/claude-hooks/sanitize-user-prompt.mjs +28 -17
- package/claude-hooks/scan-invisible-chars.mjs +86 -35
- package/claude-hooks/scan-loaded-instructions.mjs +20 -16
- package/package.json +1 -1
- package/src/html.mjs +17 -17
- package/src/output.mjs +20 -19
- package/src/rehydrate.mjs +36 -9
- package/types/claude-hooks/lib/hook-fault.d.mts +8 -73
- package/types/claude-hooks/lib/hook-io.d.mts +32 -6
- package/types/claude-hooks/lib/hook-timing.d.mts +9 -4
- package/types/claude-hooks/lib/invisible-alert.d.mts +6 -3
- package/types/claude-hooks/lib/redactor-client.d.mts +3 -3
- package/types/claude-hooks/lib/secret-annotate.d.mts +11 -17
- package/types/claude-hooks/lib/secret-drop-guard.d.mts +77 -9
- package/types/claude-hooks/pretooluse-sanitize.d.mts +28 -10
- package/types/claude-hooks/sanitize-output.d.mts +1 -1
- package/types/claude-hooks/sanitize-user-prompt.d.mts +3 -2
- package/types/claude-hooks/scan-invisible-chars.d.mts +3 -3
- package/types/output.d.mts +4 -5
- package/types/rehydrate.d.mts +8 -1
|
@@ -29,13 +29,14 @@
|
|
|
29
29
|
* @param {boolean} [secretsOn] the run's secret opt-in (decided once by
|
|
30
30
|
* buildPreToolUseResponse; a direct caller reads the env); only with it on
|
|
31
31
|
* does the context claim the restored bytes were redacted
|
|
32
|
-
* @returns {{ updatedInput: any, context: string } | { deny: string } | null}
|
|
32
|
+
* @returns {{ updatedInput: any, context: string } | { deny: string, fault?: string } | null}
|
|
33
33
|
*/
|
|
34
34
|
export function rehydrateLayer2(tool: string, toolInput: any, secretsOn?: boolean): {
|
|
35
35
|
updatedInput: any;
|
|
36
36
|
context: string;
|
|
37
37
|
} | {
|
|
38
38
|
deny: string;
|
|
39
|
+
fault?: string;
|
|
39
40
|
} | null;
|
|
40
41
|
/**
|
|
41
42
|
* The declared stage chain after the gate. Every entry states the two properties
|
|
@@ -53,9 +54,12 @@ export function rehydrateLayer2(tool: string, toolInput: any, secretsOn?: boolea
|
|
|
53
54
|
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
|
54
55
|
* @param {boolean} [secretsOn] the run's secret opt-in (see
|
|
55
56
|
* buildPreToolUseResponse, which decides it once and passes it here)
|
|
57
|
+
* @param {FaultLedger} [ledger] where a fault deny the open posture lets
|
|
58
|
+
* through leaves its warning; required as soon as a stage reports a fault,
|
|
59
|
+
* since a warning with nowhere to go would let the call through silently
|
|
56
60
|
* @returns {import("./lib/layer-pipeline.mjs").Layer[]}
|
|
57
61
|
*/
|
|
58
|
-
export function preToolUseLayers(rehydrate: ((tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>) | null, env?: NodeJS.ProcessEnv | Record<string, string | undefined>, secretsOn?: boolean): import("./lib/layer-pipeline.mjs").Layer[];
|
|
62
|
+
export function preToolUseLayers(rehydrate: ((tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>) | null, env?: NodeJS.ProcessEnv | Record<string, string | undefined>, secretsOn?: boolean, ledger?: FaultLedger): import("./lib/layer-pipeline.mjs").Layer[];
|
|
59
63
|
/**
|
|
60
64
|
* Compose the five stages. Returns the `hookSpecificOutput` fields to emit, or
|
|
61
65
|
* null for a clean no-op. Throws only if a stage's engine throws; the caller
|
|
@@ -79,9 +83,11 @@ export function buildPreToolUseResponse(input: any, rehydrate?: (tool: string, t
|
|
|
79
83
|
* ToolCallEvent and returns a Verdict, so a non-Claude host can run the same
|
|
80
84
|
* sanitization pipeline through its own adapter. The wired Claude CLI below
|
|
81
85
|
* routes through this judge and renders the Verdict with the Claude adapter; on
|
|
82
|
-
* any throw (a control-plane package-load failure
|
|
83
|
-
*
|
|
84
|
-
* caller's fault posture holds even when the
|
|
86
|
+
* any throw (a control-plane package-load failure, or an event the adapter
|
|
87
|
+
* could not classify) it falls back to hookFailureFields — a native response
|
|
88
|
+
* that needs no package — so the caller's fault posture holds even when the
|
|
89
|
+
* adapter never loaded. A host calling this directly renders its throws the
|
|
90
|
+
* same way.
|
|
85
91
|
* @param {import("agent-control-plane-core").ToolCallEvent} event
|
|
86
92
|
* @param {(tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>} [rehydrate]
|
|
87
93
|
* @param {{
|
|
@@ -147,8 +153,8 @@ export function failClosedFields(parsedOk: boolean, err: unknown, opts?: {
|
|
|
147
153
|
}): Record<string, unknown>;
|
|
148
154
|
/**
|
|
149
155
|
* True when a faulting PreToolUse call is the one case the OPEN posture must
|
|
150
|
-
*
|
|
151
|
-
* placeholder, matched by the real grammar (containsPlaceholder) so
|
|
156
|
+
* not pass on the first try: a write-shaped tool whose input carries a
|
|
157
|
+
* redaction placeholder, matched by the real grammar (containsPlaceholder) so
|
|
152
158
|
* hint-prefixed prose such as `[REDACTED…]` is not mistaken for one. Such a
|
|
153
159
|
* placeholder stands for a secret the sanitizer redacted out of the model's
|
|
154
160
|
* view; with the sanitizer down, rehydration cannot translate it back, so
|
|
@@ -159,7 +165,7 @@ export function failClosedFields(parsedOk: boolean, err: unknown, opts?: {
|
|
|
159
165
|
*
|
|
160
166
|
* Deliberately NOT exhaustive over every clobber path: Bash can also write a
|
|
161
167
|
* placeholder to disk (`>`, `tee`, `sed -i`, a heredoc), but command strings
|
|
162
|
-
* mention "[REDACTED" benignly far too often for
|
|
168
|
+
* mention "[REDACTED" benignly far too often for a refusal to hold precision
|
|
163
169
|
* there. That is an accepted gap, named in THREAT-MODEL.md's carve-out.
|
|
164
170
|
* @param {unknown} input raw parsed PreToolUse payload (undefined if unparsed)
|
|
165
171
|
* @returns {boolean}
|
|
@@ -172,8 +178,9 @@ export function hintedWriteFault(input: unknown): boolean;
|
|
|
172
178
|
* fail-CLOSED verdict of {@link failClosedFields} when the caller set
|
|
173
179
|
* AGENT_SANITIZER_FAIL_OPEN=0. The open default has ONE carve-out, declared in
|
|
174
180
|
* this hook's fault policy below: a write-shaped call whose input carries a
|
|
175
|
-
* `[REDACTED…]` placeholder
|
|
176
|
-
* {@link hintedWriteFault}) — pass `input` so the policy
|
|
181
|
+
* `[REDACTED…]` placeholder is refused once, and its exact re-issue proceeds
|
|
182
|
+
* with a warning (see {@link hintedWriteFault}) — pass `input` so the policy
|
|
183
|
+
* can see it.
|
|
177
184
|
*
|
|
178
185
|
* The posture covers this hook's own failures, whatever their cause. What it
|
|
179
186
|
* does NOT cover is the verdict of a sanitizer that ran: a payload
|
|
@@ -239,6 +246,17 @@ export const PRE_TOOL_USE_MESSAGES: Readonly<{
|
|
|
239
246
|
remedy: string;
|
|
240
247
|
}>;
|
|
241
248
|
export const REDACTION_HINT: "[REDACTED";
|
|
249
|
+
/**
|
|
250
|
+
* What the stage table hands back besides the pipeline's own result: the
|
|
251
|
+
* warnings of fault denies the open posture let through (the pipeline drops a
|
|
252
|
+
* stage's note when the input is unchanged, and these calls proceed unchanged),
|
|
253
|
+
* and the re-issue confirmations they rode on, consumed only once the call is
|
|
254
|
+
* known to proceed.
|
|
255
|
+
*/
|
|
256
|
+
export type FaultLedger = {
|
|
257
|
+
warnings: string[];
|
|
258
|
+
released: string[];
|
|
259
|
+
};
|
|
242
260
|
/**
|
|
243
261
|
* A host-supplied deny gate: given the PreToolUse input, the reason this call
|
|
244
262
|
* must be blocked, or null to let the pipeline continue. Hosts use these for
|
|
@@ -177,7 +177,7 @@ export function emitFailClosed(input: any, message: string, emit?: (fields: Reco
|
|
|
177
177
|
* fail-OPEN by default — a warning context and NO `updatedToolOutput`, leaving
|
|
178
178
|
* the original tool output in the model's view — or the fail-closed
|
|
179
179
|
* suppression of {@link emitFailClosed} when the caller set
|
|
180
|
-
* AGENT_SANITIZER_FAIL_OPEN=0
|
|
180
|
+
* AGENT_SANITIZER_FAIL_OPEN=0.
|
|
181
181
|
*
|
|
182
182
|
* This is the hook where the two postures diverge the most, so state the open
|
|
183
183
|
* one plainly: several of these layers throw on inputs an attacker composes (a
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Judge a normalized prompt-submit event. Agent-agnostic: consumes the
|
|
3
3
|
* control-plane ToolCallEvent and returns a Verdict, so the same prompt gate
|
|
4
4
|
* renders through any agent adapter, not just Claude's. Throws (into the
|
|
5
|
-
* calling hook's catch) when the sanitizer package never loaded
|
|
6
|
-
* cannot classify takes the hook's
|
|
5
|
+
* calling hook's catch) when the sanitizer package never loaded or the payload
|
|
6
|
+
* is no recognizable event, so a prompt it cannot classify takes the hook's
|
|
7
|
+
* fault posture rather than a silent ALLOW.
|
|
7
8
|
* @param {import("agent-control-plane-core").ToolCallEvent} event
|
|
8
9
|
* @param {((s: string) => string) | null} [strip] the ANSI stripper (defaults
|
|
9
10
|
* to the package's stripAnsiFully; injectable so the fault path is testable)
|
|
@@ -28,9 +28,9 @@ export function scanProject(dir?: string): {
|
|
|
28
28
|
absent: string[];
|
|
29
29
|
};
|
|
30
30
|
/**
|
|
31
|
-
* The report for targets the scan could not read
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* The report for targets the scan could not read: a SessionStart warning under
|
|
32
|
+
* the open posture, and the alert the PreToolUse gate surfaces under the closed
|
|
33
|
+
* one — either way an incomplete scan reaches the operator rather than silence.
|
|
34
34
|
* @param {Array<{file: string, reason: string}>} skipped
|
|
35
35
|
* @returns {string}
|
|
36
36
|
*/
|
package/types/output.d.mts
CHANGED
|
@@ -55,11 +55,10 @@ export function deleteVerbatimSpans(text: string, spans: string[]): {
|
|
|
55
55
|
*/
|
|
56
56
|
/**
|
|
57
57
|
* Run the configured layers over a single text blob. Layer 1 always runs; the
|
|
58
|
-
* rest are opt-in via `options`. Layer 4 (`redact`)
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* 5, below) — a redactor failure there fails the whole call closed too.
|
|
58
|
+
* rest are opt-in via `options`. A Layer 4 (`redact`) redactor that throws is
|
|
59
|
+
* rethrown wrapped, so the caller answers it under its fault posture rather
|
|
60
|
+
* than receiving unvetted text as vetted. The same holds for Layer 4's re-scan
|
|
61
|
+
* after a Layer-5 span deletion (see Layer 5, below).
|
|
63
62
|
* `reveal` is the pre-Layer-2 text, present only when the HTML splice removed
|
|
64
63
|
* bytes, so a caller can persist what was hidden for later inspection (see
|
|
65
64
|
* {@link applyMarkdownPipeline}); the field is omitted otherwise, and also when
|
package/types/rehydrate.d.mts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* there is nothing to do. Throws only on internal error (the caller fails
|
|
9
9
|
* closed).
|
|
10
10
|
*
|
|
11
|
+
* A deny carrying `fault` is not a verdict on the input but this module being
|
|
12
|
+
* unable to do its job (an unreadable target, a redactor map that fails
|
|
13
|
+
* validation): `fault` states what proceeding un-rehydrated would write, and the
|
|
14
|
+
* caller's failure posture decides whether the deny stands. A deny without it
|
|
15
|
+
* is a verdict and stands in every posture.
|
|
16
|
+
*
|
|
11
17
|
* `io` is the injected I/O (file read + redactor map/plain). `hint` is the
|
|
12
18
|
* redaction-placeholder prefix (defaults to {@link DEFAULT_HINT}); override it
|
|
13
19
|
* only if the injected redactor emits a different placeholder shape.
|
|
@@ -15,7 +21,7 @@
|
|
|
15
21
|
* @param {any} toolInput
|
|
16
22
|
* @param {RehydrateIo} io
|
|
17
23
|
* @param {{ hint?: string }} [options]
|
|
18
|
-
* @returns {Promise<{updatedInput: any, context: string} | {deny: string} | null>}
|
|
24
|
+
* @returns {Promise<{updatedInput: any, context: string} | {deny: string, fault?: string} | null>}
|
|
19
25
|
*/
|
|
20
26
|
export function rehydrateRedacted(tool: string, toolInput: any, io: RehydrateIo, { hint }?: {
|
|
21
27
|
hint?: string;
|
|
@@ -24,6 +30,7 @@ export function rehydrateRedacted(tool: string, toolInput: any, io: RehydrateIo,
|
|
|
24
30
|
context: string;
|
|
25
31
|
} | {
|
|
26
32
|
deny: string;
|
|
33
|
+
fault?: string;
|
|
27
34
|
} | null>;
|
|
28
35
|
export const DEFAULT_HINT: "[REDACTED";
|
|
29
36
|
/**
|