agent-sanitizer 2.67.1 → 2.68.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/THREAT-MODEL.md +21 -21
- package/claude-hooks/lib/control-plane.mjs +66 -31
- package/claude-hooks/lib/env-config.mjs +2 -2
- package/claude-hooks/lib/hook-fault.mjs +11 -35
- package/claude-hooks/lib/hook-timing.mjs +26 -6
- package/claude-hooks/lib/redactor-client.mjs +23 -23
- package/claude-hooks/lib/secret-annotate.mjs +36 -66
- package/claude-hooks/plugin-hooks.mjs +1 -3
- package/claude-hooks/pretooluse-sanitize.mjs +0 -2
- package/claude-hooks/sanitize-output.mjs +27 -40
- package/claude-hooks/sanitize-user-prompt.mjs +2 -5
- package/claude-hooks/scan-invisible-chars.mjs +0 -2
- package/claude-hooks/scan-loaded-instructions.mjs +0 -2
- package/package.json +2 -2
- package/src/output.mjs +15 -15
- package/types/claude-hooks/lib/control-plane.d.mts +17 -8
- package/types/claude-hooks/lib/hook-fault.d.mts +6 -73
- package/types/claude-hooks/lib/hook-timing.d.mts +9 -4
- 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/sanitize-output.d.mts +1 -1
- package/types/output.d.mts +4 -5
|
@@ -170,10 +170,15 @@ export function startHookTimer(now?: () => number, cpuNow?: () => number): {
|
|
|
170
170
|
* sanitizer computing, and with `context.redactorMs` too it names the window the
|
|
171
171
|
* time went into ({@link attributeWait}), including `context.hostMs`'s host
|
|
172
172
|
* extensions — a caller that measured the first two but has no extensions to
|
|
173
|
-
* charge reports that window as zero, which is a measurement and not a shrug.
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
173
|
+
* charge reports that window as zero, which is a measurement and not a shrug.
|
|
174
|
+
* Such a caller is reported only when `cpuMs + redactorMs + hostMs` (an absent
|
|
175
|
+
* `hostMs` counting as 0) exceeds `thresholdMs`: a wall-clock overrun whose
|
|
176
|
+
* owned share is within the budget is the machine's and returns null, so the
|
|
177
|
+
* operator is never asked to report a cost the owner did not spend. Without the
|
|
178
|
+
* measurements the line says that it cannot tell, rather than asserting an
|
|
179
|
+
* attribution nothing measured: a wall-clock overrun on a loaded host is the
|
|
180
|
+
* common case, and blaming it on the sanitizer sends the operator hunting a
|
|
181
|
+
* per-call cost that does not exist.
|
|
177
182
|
*
|
|
178
183
|
* A CPU figure alone cannot pick a cause, so with only that the clause names
|
|
179
184
|
* candidates and commits to none. A hook that blocks on a dead socket inside a
|
|
@@ -55,9 +55,9 @@ export function classifySocket(socketPath: string, deps?: {
|
|
|
55
55
|
/**
|
|
56
56
|
* Open one connection, send `request`, resolve with the parsed response object
|
|
57
57
|
* (or null). Rejects on connect failure, a malformed/oversize/short frame, or an
|
|
58
|
-
* {error} response — every one of which the caller
|
|
58
|
+
* {error} response — every one of which the caller treats as a fault. A
|
|
59
59
|
* socket present but not owned by us, and a request the protocol does not
|
|
60
|
-
* accept,
|
|
60
|
+
* accept, throw WITHOUT dialing (the error carries no errno, so
|
|
61
61
|
* isRespawnable is false): we never dial into a squat, and an oversize request
|
|
62
62
|
* is not mistaken for a dead daemon worth respawning.
|
|
63
63
|
* @param {string} socketPath
|
|
@@ -105,7 +105,7 @@ export function waitForSocket(socketPath: string, { deadlineMs, stepMs, abandon
|
|
|
105
105
|
/**
|
|
106
106
|
* Redact `text` via the daemon. Returns the response object (`{text, found}` for
|
|
107
107
|
* plain, `{text, pairs, found}` / `{unmappable}` for map) or null when nothing was
|
|
108
|
-
* redacted (plain mode). Throws
|
|
108
|
+
* redacted (plain mode). Throws when the text cannot be vetted.
|
|
109
109
|
*
|
|
110
110
|
* `connect`/`spawn`/`waitForSocket` are injectable seams (default to the real
|
|
111
111
|
* implementations) so callers can stub the daemon in-process. `deadline` is the
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regex matching `value` tolerating invisible chars spliced between its
|
|
3
|
-
* characters (mirrors the engine's env-value regex). Code-point split so
|
|
4
|
-
* an astral character is escaped whole, not as two surrogate halves — and the
|
|
5
|
-
* `u` flag, which the astral `\u{…}` class members in {@link ENV_INVIS_RUN}
|
|
6
|
-
* require.
|
|
7
|
-
* Memoized per distinct value: {@link ENV_INVIS_RUN} renders ~435 code points
|
|
8
|
-
* as ~4 KB of source and is joined at EVERY interior gap, so a 20-char secret
|
|
9
|
-
* compiles a ~75 KB pattern — and `hasEnvBoundSecret` builds one per configured
|
|
10
|
-
* var on every PostToolUse output. Env values are stable for the process's
|
|
11
|
-
* lifetime, so the cache is bounded by the number of distinct values. Sharing an
|
|
12
|
-
* instance is safe because the regex carries no `g`/`y` flag, hence no
|
|
13
|
-
* `lastIndex` state to leak between calls.
|
|
14
|
-
* @param {string} value
|
|
15
|
-
* @returns {RegExp}
|
|
16
|
-
*/
|
|
17
|
-
export function envValueRegex(value: string): RegExp;
|
|
18
1
|
/**
|
|
19
2
|
* True when tool output contains the literal value of a configured env-bound
|
|
20
3
|
* secret. The shape-based secret hint can't match a prefix-less key or a host
|
|
21
4
|
* credential, so the pre-gate must also fire on the value itself — otherwise
|
|
22
5
|
* the engine's env-bound redaction never runs. Invisible-tolerant so a
|
|
23
6
|
* value with spliced Cf chars (which the daemon still redacts) trips it too.
|
|
7
|
+
*
|
|
8
|
+
* Both sides are stripped and compared as substrings, which is exactly
|
|
9
|
+
* equivalent: the value's visible characters appear contiguously in the
|
|
10
|
+
* stripped text iff they appear in the original separated only by invisibles.
|
|
11
|
+
* It is linear and independent of the value's length — a regex with an
|
|
12
|
+
* invisible class at every gap grows ~4 KB per character, and a long secret (a
|
|
13
|
+
* PEM key) overflows V8's regex size limit and throws on every output.
|
|
14
|
+
*
|
|
15
|
+
* Callers pass post-Layer-1 text: terminal escapes are already gone, so a value
|
|
16
|
+
* split by one is contiguous here. Raw text spliced with an escape is not
|
|
17
|
+
* matched.
|
|
24
18
|
* @param {string} text
|
|
25
19
|
* @param {NodeJS.ProcessEnv} [env]
|
|
26
20
|
* @returns {boolean}
|
|
@@ -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
|
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
|