agent-sanitizer 2.41.0 → 2.41.2
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.
|
@@ -386,6 +386,21 @@ export function lastStdinByteLength() {
|
|
|
386
386
|
return lastStdinBytes;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
/**
|
|
390
|
+
* A hook run that received no event at all: stdin closed with zero bytes. Its
|
|
391
|
+
* own type, not the `SyntaxError` an empty string gets from `JSON.parse`,
|
|
392
|
+
* because the two are different faults with different fixes — a malformed
|
|
393
|
+
* payload is a sender that sent something wrong, an empty one is a hook wired to
|
|
394
|
+
* a channel that sent nothing, and a caller that cannot tell them apart reports
|
|
395
|
+
* the wrong cause and offers remedies for content it never received.
|
|
396
|
+
*/
|
|
397
|
+
export class EmptyStdinError extends Error {
|
|
398
|
+
constructor() {
|
|
399
|
+
super("empty stdin: the hook received no payload");
|
|
400
|
+
this.name = "EmptyStdinError";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
389
404
|
/**
|
|
390
405
|
* @param {number} [maxBytes] cap before aborting (overridable for tests)
|
|
391
406
|
* @returns {Promise<any>}
|
|
@@ -393,6 +408,8 @@ export function lastStdinByteLength() {
|
|
|
393
408
|
export async function readStdinJson(maxBytes = MAX_STDIN_BYTES) {
|
|
394
409
|
const buf = await readAllBounded(process.stdin, maxBytes);
|
|
395
410
|
lastStdinBytes = buf.length;
|
|
411
|
+
// Before the parse, so the empty case never renders as malformed JSON.
|
|
412
|
+
if (buf.length === 0) throw new EmptyStdinError();
|
|
396
413
|
return JSON.parse(buf.toString());
|
|
397
414
|
}
|
|
398
415
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import {
|
|
22
22
|
emitHookResponse,
|
|
23
|
+
EmptyStdinError,
|
|
23
24
|
HookEvent,
|
|
24
25
|
isMain,
|
|
25
26
|
lazyImport,
|
|
@@ -82,6 +83,30 @@ function faultLine(ctx) {
|
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The parts for a run that received no event at all, or null when the fault is
|
|
88
|
+
* anything else. Zero bytes on stdin means no InstructionsLoaded payload was
|
|
89
|
+
* delivered, so no file was named and none went unscanned: the fault is in how
|
|
90
|
+
* the hook was INVOKED, not in a scan. Both postures render it the same way and
|
|
91
|
+
* neither arms the tool-call gate — that gate asks the user to clear something
|
|
92
|
+
* about this project's instruction files, and this fault says nothing about
|
|
93
|
+
* them, so arming it blocks the next tool call over a hook that was handed no
|
|
94
|
+
* event.
|
|
95
|
+
* @param {import("./lib/hook-fault.mjs").FaultContext} ctx
|
|
96
|
+
* @returns {import("./lib/hook-fault.mjs").FaultParts | null}
|
|
97
|
+
*/
|
|
98
|
+
function emptyPayloadParts(ctx) {
|
|
99
|
+
if (!(ctx.err instanceof EmptyStdinError)) return null;
|
|
100
|
+
return {
|
|
101
|
+
stderr:
|
|
102
|
+
`${HOOK_NAME} hook error: ${ctx.message}. No instruction file was named, so nothing ` +
|
|
103
|
+
"was scanned and nothing was left unguarded. Check how this hook is invoked: it reads " +
|
|
104
|
+
"its InstructionsLoaded event as JSON on stdin, and one wired to a channel that " +
|
|
105
|
+
"delivers nothing scans nothing for the whole session.\n",
|
|
106
|
+
exitCode: 1,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
// This hook's entry in the one posture table (lib/hook-fault.mjs). Like
|
|
86
111
|
// scan-invisible-chars it has no stdout verdict channel — InstructionsLoaded
|
|
87
112
|
// cannot block, and its exit code is ignored — so both arms are stated
|
|
@@ -90,15 +115,17 @@ function faultLine(ctx) {
|
|
|
90
115
|
registerFaultPolicy(HOOK_NAME, {
|
|
91
116
|
event: HookEvent.INSTRUCTIONS_LOADED,
|
|
92
117
|
guarded: "a loaded instruction file",
|
|
93
|
-
open: (ctx) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
open: (ctx) =>
|
|
119
|
+
emptyPayloadParts(ctx) ?? {
|
|
120
|
+
stderr: `${faultLine(ctx)} Passing through unguarded; set AGENT_SANITIZER_FAIL_OPEN=0 to arm the tool-call gate instead.\n`,
|
|
121
|
+
exitCode: 1,
|
|
122
|
+
},
|
|
123
|
+
closed: (ctx) =>
|
|
124
|
+
emptyPayloadParts(ctx) ?? {
|
|
125
|
+
stderr: `${faultLine(ctx)} Arming the tool-call gate (AGENT_SANITIZER_FAIL_OPEN=0).\n`,
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
armAlert: true,
|
|
128
|
+
},
|
|
102
129
|
});
|
|
103
130
|
|
|
104
131
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.41.
|
|
3
|
+
"version": "2.41.2",
|
|
4
4
|
"description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -485,6 +485,17 @@ export const DISABLED_HOOKS_ENV: "AGENT_SANITIZER_DISABLED_HOOKS";
|
|
|
485
485
|
* and take its own fail-closed output down with it.
|
|
486
486
|
*/
|
|
487
487
|
export const MAX_STDIN_BYTES: number;
|
|
488
|
+
/**
|
|
489
|
+
* A hook run that received no event at all: stdin closed with zero bytes. Its
|
|
490
|
+
* own type, not the `SyntaxError` an empty string gets from `JSON.parse`,
|
|
491
|
+
* because the two are different faults with different fixes — a malformed
|
|
492
|
+
* payload is a sender that sent something wrong, an empty one is a hook wired to
|
|
493
|
+
* a channel that sent nothing, and a caller that cannot tell them apart reports
|
|
494
|
+
* the wrong cause and offers remedies for content it never received.
|
|
495
|
+
*/
|
|
496
|
+
export class EmptyStdinError extends Error {
|
|
497
|
+
constructor();
|
|
498
|
+
}
|
|
488
499
|
/**
|
|
489
500
|
* The remedy {@link missingPackageMessage} states when the host does not supply
|
|
490
501
|
* one of its own. A host whose install has a specific entry point (a setup
|