agent-sanitizer 2.66.7 → 2.67.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * True when `tool` is one of {@link FILE_WRITE_TOOLS}.
3
+ * @param {unknown} tool
4
+ * @returns {boolean}
5
+ */
6
+ export function isFileWriteTool(tool: unknown): boolean;
7
+ /**
8
+ * A field spec, parsed: `{ key }` for a top-level field, or `{ arr, sub }` for
9
+ * `sub` on every element of the array at `arr`.
10
+ * @param {string} spec
11
+ * @returns {{ key: string } | { arr: string, sub: string }}
12
+ */
13
+ export function parseFieldSpec(spec: string): {
14
+ key: string;
15
+ } | {
16
+ arr: string;
17
+ sub: string;
18
+ };
19
+ /**
20
+ * Every value the field `spec` addresses in `input`: one for a top-level
21
+ * field, one per array element for a nested one. `null` when the array a
22
+ * nested spec names is absent, not an array, or empty.
23
+ * @param {string} spec
24
+ * @param {any} input
25
+ * @returns {unknown[] | null}
26
+ */
27
+ export function fieldValues(spec: string, input: any): unknown[] | null;
28
+ /**
29
+ * The file-editing tools whose input fields are the bytes persisted to disk, as
30
+ * ONE table, and the reader for the field specs it holds.
31
+ *
32
+ * PROBLEM CLASS — the set of file-writing tools restated per consumer. The
33
+ * fault carve-out that refuses a placeholder-bearing write, the advisories that
34
+ * stay silent on rehydrated tools, the authored-content sanitizer and the
35
+ * rehydration layer each kept their own list. A tool added to one list (a new
36
+ * write-shaped tool, or a renamed field) left the others silently stale, which
37
+ * is a placeholder persisted over a secret on disk. Every consumer reads this
38
+ * table instead.
39
+ *
40
+ * Package-free and dependency-free by construction: the PreToolUse fault path
41
+ * imports it relatively and must work exactly when the agent-sanitizer package
42
+ * failed to load.
43
+ *
44
+ * Each entry:
45
+ * - `authored` — the fields the model writes, which Layer 3 sanitizes.
46
+ * - `placeholders` — every field a redaction placeholder can sit in.
47
+ * - `rehydrates` — whether the rehydration layer re-anchors the call onto
48
+ * disk bytes (`false`: it refuses a placeholder-bearing call instead).
49
+ *
50
+ * A field spec is `key`, or `key[].sub` for `sub` on every element of the
51
+ * array at `key` (MultiEdit batches its writes as `edits[]`).
52
+ */
53
+ /** @typedef {{ authored: readonly string[], placeholders: readonly string[], rehydrates: boolean }} FileWriteTool */
54
+ /**
55
+ * Null-prototype: the tool name comes from an untrusted payload, and on a plain
56
+ * object literal `TABLE["constructor"]` answers a truthy inherited value.
57
+ * @type {Readonly<Record<string, FileWriteTool>>}
58
+ */
59
+ export const FILE_WRITE_TOOLS: Readonly<Record<string, FileWriteTool>>;
60
+ export type FileWriteTool = {
61
+ authored: readonly string[];
62
+ placeholders: readonly string[];
63
+ rehydrates: boolean;
64
+ };
package/types/gates.d.mts CHANGED
@@ -30,7 +30,7 @@ export function needsUrlScan(text: string): boolean;
30
30
  */
31
31
  export function isHtmlShaped(text: string): boolean;
32
32
  /**
33
- * True when either pre-gate alternation shape-matches `text`, or the same text
33
+ * True when any pre-gate arm shape-matches `text`, or the same text
34
34
  * with every `\n` removed (the view the engine's `_redact_cross_line` scans, so
35
35
  * this stays a superset of the redactor). Two literals OR'd so neither grows
36
36
  * into a polynomial-backtracking shape (see SECRET_HINT).
@@ -38,20 +38,6 @@ export function isHtmlShaped(text: string): boolean;
38
38
  * @returns {boolean}
39
39
  */
40
40
  export function matchesSecretHint(text: string): boolean;
41
- /**
42
- * Cheap, dependency-free pre-gates shared by the HTML layer (Layers 2 & 3) and
43
- * re-exported from both the package root and the `./html` subpath.
44
- *
45
- * These are pulled out of `html.mjs` so the package root can re-export them
46
- * without dragging in the heavy remark/rehype/unified graph: a static
47
- * `export … from "./html.mjs"` would eagerly evaluate that ~200ms module on
48
- * every root import, defeating the lazy-load design. This module imports
49
- * nothing, so re-exporting it is free.
50
- *
51
- * INVARIANT: every gate is a SUPERSET of its parser (a narrower one silently
52
- * turns its layer off; a needless fire only costs a parse), checked by
53
- * test/gates-superset.test.mjs.
54
- */
55
41
  /**
56
42
  * Matches every point where the HTML tokenizer leaves the data state: `<` then
57
43
  * an ASCII letter, `/`, `!` or `?`. No closing `>` on purpose: a comment holding
@@ -55,14 +55,12 @@ export function walkContextGlobs(globs: string[], cwd: string, exclude?: (entry:
55
55
  * Expand `globs` (relative to `cwd`) to absolute file paths, skipping
56
56
  * `node_modules`. The glob set is the caller's instruction-file convention.
57
57
  *
58
- * Containment is enforced per match (see {@link keepContained}): a match whose
59
- * glob pattern itself escapes `cwd` — via `..` or an absolute-path glob
58
+ * Containment is enforced per match (see {@link assertContained}): a match
59
+ * whose glob pattern itself escapes `cwd` — via `..` or an absolute-path glob
60
60
  * outside the tree — THROWS, since reaching outside the tree is a caller
61
- * misconfiguration. A match that lexically stays inside `cwd` but resolves
62
- * (via an in-tree symlink) to a target outside the tree, or that simply
63
- * cannot be resolved (a dangling symlink or unreadable entry inside the
64
- * tree), is SKIPPED, so one bad symlink never aborts scanning the rest of the
65
- * project.
61
+ * misconfiguration. A match that lexically stays inside `cwd` is RETURNED even
62
+ * when an in-tree symlink resolves it outside the tree or to nothing, so the
63
+ * reader decides what an unreadable one means.
66
64
  *
67
65
  * `exclude` prunes the walk via {@link walkContextGlobs}, so it is handed one
68
66
  * root-relative, `/`-separated path per entry. It is composed with, never
@@ -78,21 +76,32 @@ export function findInstructionFiles(globs: string[], { cwd, exclude }?: {
78
76
  exclude?: (entry: string) => boolean;
79
77
  }): string[];
80
78
  /**
81
- * Scan every instruction file matched by `globs` and return only those with
82
- * findings, each path reported relative to `cwd`. Unreadable/missing files are
83
- * skipped. Pure scan — no mutation; pair with {@link cleanFile} to strip.
79
+ * Scan every instruction file matched by `globs`. `findings` lists only the
80
+ * files with findings; `skipped` lists every file that exists but could not be
81
+ * read, so a caller never reports an unread file as clean. A missing file
82
+ * (ENOENT, a dangling symlink) is in neither: no bytes of it can load. Paths
83
+ * are relative to `cwd`. Pure scan — no mutation; pair with {@link cleanFile}.
84
84
  * @param {string[]} globs
85
85
  * @param {{ cwd?: string, exclude?: (entry: string) => boolean }} [options]
86
86
  * `exclude` is forwarded to {@link findInstructionFiles}
87
- * @returns {Array<{ file: string, findings: ReturnType<typeof scanText> }>}
87
+ * @returns {{
88
+ * findings: Array<{ file: string, findings: ReturnType<typeof scanText> }>,
89
+ * skipped: Array<{ file: string, reason: string }>,
90
+ * }}
88
91
  */
89
92
  export function scanInstructionFiles(globs: string[], { cwd, exclude }?: {
90
93
  cwd?: string;
91
94
  exclude?: (entry: string) => boolean;
92
- }): Array<{
93
- file: string;
94
- findings: ReturnType<typeof scanText>;
95
- }>;
95
+ }): {
96
+ findings: Array<{
97
+ file: string;
98
+ findings: ReturnType<typeof scanText>;
99
+ }>;
100
+ skipped: Array<{
101
+ file: string;
102
+ reason: string;
103
+ }>;
104
+ };
96
105
  /**
97
106
  * Strip payload-capable invisible characters from `absPath` in place. Returns
98
107
  * `true` when the file's bytes actually changed (a payload {@link scanText}
@@ -151,4 +160,4 @@ export function cleanFile(absPath: string, { root, lstat }?: {
151
160
  lstat?: (path: string) => import("node:fs").Stats;
152
161
  }): boolean;
153
162
  export { contextScanExclude } from "./repo-scope.mjs";
154
- export { ancestorInstructionFiles, announcedByInstructionsLoaded, CLAUDE_CONTEXT_KINDS, CLAUDE_CONTEXT_SUBDIRS, CLAUDE_DIR_INSTRUCTION_FILES, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, CLAUDE_MEMORY_FILES, contextScopeContradiction, excludeFromContextScan, USER_GLOBAL_EVENT_NAMED_GLOBS } from "./claude-context.mjs";
163
+ export { ancestorInstructionFiles, announcedByInstructionsLoaded, CLAUDE_CONTEXT_KINDS, CLAUDE_CONTEXT_SUBDIRS, CLAUDE_DIR_INSTRUCTION_FILES, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, CLAUDE_MEMORY_FILES, classifyReadFailure, contextScopeContradiction, excludeFromContextScan, USER_GLOBAL_EVENT_NAMED_GLOBS } from "./claude-context.mjs";
@@ -37,6 +37,23 @@ export function isInsideDir(dir: string, file: string): boolean;
37
37
  * @returns {string}
38
38
  */
39
39
  export function displayUntrusted(text: string): string;
40
+ /**
41
+ * PROBLEM CLASS — how a failed instruction-file read is classified. Every
42
+ * scanner reads this one bucketing; nothing re-derives it from an errno.
43
+ *
44
+ * A read failure is never swallowed into an empty findings list — that turns
45
+ * "we could not read this file" into "this file is fine". Three buckets:
46
+ *
47
+ * - SKIPPED — the file exists and this uid cannot read it (EACCES, EISDIR,
48
+ * ELOOP…). Unvetted context, which the caller must report.
49
+ * - ABSENT — ENOENT. The path resolves to nothing, and the host loads
50
+ * instruction files through the same open, so no bytes can reach the model.
51
+ * - THROWN — no errno at all, i.e. a bug. Nothing can be trusted, so the
52
+ * error propagates to the caller's failure posture.
53
+ * @param {unknown} err
54
+ * @returns {"absent" | "skipped"} never returns for a non-errno throw
55
+ */
56
+ export function classifyReadFailure(err: unknown): "absent" | "skipped";
40
57
  /**
41
58
  * The one directory no instruction-file walk ever descends into. Its own
42
59
  * function so the name is spelled once, and so the two predicates that need it
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The message of a caught value, which is `unknown` under strict mode.
3
+ *
4
+ * PROBLEM CLASS — rendering a caught error as text. The library's warnings and
5
+ * every hook's failure reason read this one definition, so a wrapped failure
6
+ * reads "outer: root" everywhere rather than "outer" in some copies.
7
+ * Dependency-free: the hooks import it relatively and must keep it when the
8
+ * agent-sanitizer package failed to load.
9
+ */
10
+ /**
11
+ * Message from a caught value, with one level of cause chain appended when the
12
+ * cause is itself an Error.
13
+ * @param {unknown} err
14
+ * @returns {string}
15
+ */
16
+ export function errMessage(err: unknown): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * True when `tool` is one of {@link FILE_WRITE_TOOLS}.
3
+ * @param {unknown} tool
4
+ * @returns {boolean}
5
+ */
6
+ export function isFileWriteTool(tool: unknown): boolean;
7
+ /**
8
+ * A field spec, parsed: `{ key }` for a top-level field, or `{ arr, sub }` for
9
+ * `sub` on every element of the array at `arr`.
10
+ * @param {string} spec
11
+ * @returns {{ key: string } | { arr: string, sub: string }}
12
+ */
13
+ export function parseFieldSpec(spec: string): {
14
+ key: string;
15
+ } | {
16
+ arr: string;
17
+ sub: string;
18
+ };
19
+ /**
20
+ * Every value the field `spec` addresses in `input`: one for a top-level
21
+ * field, one per array element for a nested one. `null` when the array a
22
+ * nested spec names is absent, not an array, or empty.
23
+ * @param {string} spec
24
+ * @param {any} input
25
+ * @returns {unknown[] | null}
26
+ */
27
+ export function fieldValues(spec: string, input: any): unknown[] | null;
28
+ /**
29
+ * The file-editing tools whose input fields are the bytes persisted to disk, as
30
+ * ONE table, and the reader for the field specs it holds.
31
+ *
32
+ * PROBLEM CLASS — the set of file-writing tools restated per consumer. The
33
+ * fault carve-out that refuses a placeholder-bearing write, the advisories that
34
+ * stay silent on rehydrated tools, the authored-content sanitizer and the
35
+ * rehydration layer each kept their own list. A tool added to one list (a new
36
+ * write-shaped tool, or a renamed field) left the others silently stale, which
37
+ * is a placeholder persisted over a secret on disk. Every consumer reads this
38
+ * table instead.
39
+ *
40
+ * Package-free and dependency-free by construction: the PreToolUse fault path
41
+ * imports it relatively and must work exactly when the agent-sanitizer package
42
+ * failed to load.
43
+ *
44
+ * Each entry:
45
+ * - `authored` — the fields the model writes, which Layer 3 sanitizes.
46
+ * - `placeholders` — every field a redaction placeholder can sit in.
47
+ * - `rehydrates` — whether the rehydration layer re-anchors the call onto
48
+ * disk bytes (`false`: it refuses a placeholder-bearing call instead).
49
+ *
50
+ * A field spec is `key`, or `key[].sub` for `sub` on every element of the
51
+ * array at `key` (MultiEdit batches its writes as `edits[]`).
52
+ */
53
+ /** @typedef {{ authored: readonly string[], placeholders: readonly string[], rehydrates: boolean }} FileWriteTool */
54
+ /**
55
+ * Null-prototype: the tool name comes from an untrusted payload, and on a plain
56
+ * object literal `TABLE["constructor"]` answers a truthy inherited value.
57
+ * @type {Readonly<Record<string, FileWriteTool>>}
58
+ */
59
+ export const FILE_WRITE_TOOLS: Readonly<Record<string, FileWriteTool>>;
60
+ export type FileWriteTool = {
61
+ authored: readonly string[];
62
+ placeholders: readonly string[];
63
+ rehydrates: boolean;
64
+ };
@@ -55,14 +55,12 @@ export function walkContextGlobs(globs: string[], cwd: string, exclude?: (entry:
55
55
  * Expand `globs` (relative to `cwd`) to absolute file paths, skipping
56
56
  * `node_modules`. The glob set is the caller's instruction-file convention.
57
57
  *
58
- * Containment is enforced per match (see {@link keepContained}): a match whose
59
- * glob pattern itself escapes `cwd` — via `..` or an absolute-path glob
58
+ * Containment is enforced per match (see {@link assertContained}): a match
59
+ * whose glob pattern itself escapes `cwd` — via `..` or an absolute-path glob
60
60
  * outside the tree — THROWS, since reaching outside the tree is a caller
61
- * misconfiguration. A match that lexically stays inside `cwd` but resolves
62
- * (via an in-tree symlink) to a target outside the tree, or that simply
63
- * cannot be resolved (a dangling symlink or unreadable entry inside the
64
- * tree), is SKIPPED, so one bad symlink never aborts scanning the rest of the
65
- * project.
61
+ * misconfiguration. A match that lexically stays inside `cwd` is RETURNED even
62
+ * when an in-tree symlink resolves it outside the tree or to nothing, so the
63
+ * reader decides what an unreadable one means.
66
64
  *
67
65
  * `exclude` prunes the walk via {@link walkContextGlobs}, so it is handed one
68
66
  * root-relative, `/`-separated path per entry. It is composed with, never
@@ -78,21 +76,32 @@ export function findInstructionFiles(globs: string[], { cwd, exclude }?: {
78
76
  exclude?: (entry: string) => boolean;
79
77
  }): string[];
80
78
  /**
81
- * Scan every instruction file matched by `globs` and return only those with
82
- * findings, each path reported relative to `cwd`. Unreadable/missing files are
83
- * skipped. Pure scan — no mutation; pair with {@link cleanFile} to strip.
79
+ * Scan every instruction file matched by `globs`. `findings` lists only the
80
+ * files with findings; `skipped` lists every file that exists but could not be
81
+ * read, so a caller never reports an unread file as clean. A missing file
82
+ * (ENOENT, a dangling symlink) is in neither: no bytes of it can load. Paths
83
+ * are relative to `cwd`. Pure scan — no mutation; pair with {@link cleanFile}.
84
84
  * @param {string[]} globs
85
85
  * @param {{ cwd?: string, exclude?: (entry: string) => boolean }} [options]
86
86
  * `exclude` is forwarded to {@link findInstructionFiles}
87
- * @returns {Array<{ file: string, findings: ReturnType<typeof scanText> }>}
87
+ * @returns {{
88
+ * findings: Array<{ file: string, findings: ReturnType<typeof scanText> }>,
89
+ * skipped: Array<{ file: string, reason: string }>,
90
+ * }}
88
91
  */
89
92
  export function scanInstructionFiles(globs: string[], { cwd, exclude }?: {
90
93
  cwd?: string;
91
94
  exclude?: (entry: string) => boolean;
92
- }): Array<{
93
- file: string;
94
- findings: ReturnType<typeof scanText>;
95
- }>;
95
+ }): {
96
+ findings: Array<{
97
+ file: string;
98
+ findings: ReturnType<typeof scanText>;
99
+ }>;
100
+ skipped: Array<{
101
+ file: string;
102
+ reason: string;
103
+ }>;
104
+ };
96
105
  /**
97
106
  * Strip payload-capable invisible characters from `absPath` in place. Returns
98
107
  * `true` when the file's bytes actually changed (a payload {@link scanText}
@@ -151,4 +160,4 @@ export function cleanFile(absPath: string, { root, lstat }?: {
151
160
  lstat?: (path: string) => import("node:fs").Stats;
152
161
  }): boolean;
153
162
  export { contextScanExclude } from "./repo-scope.mjs";
154
- export { ancestorInstructionFiles, announcedByInstructionsLoaded, CLAUDE_CONTEXT_KINDS, CLAUDE_CONTEXT_SUBDIRS, CLAUDE_DIR_INSTRUCTION_FILES, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, CLAUDE_MEMORY_FILES, contextScopeContradiction, excludeFromContextScan, USER_GLOBAL_EVENT_NAMED_GLOBS } from "./claude-context.mjs";
163
+ export { ancestorInstructionFiles, announcedByInstructionsLoaded, CLAUDE_CONTEXT_KINDS, CLAUDE_CONTEXT_SUBDIRS, CLAUDE_DIR_INSTRUCTION_FILES, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, CLAUDE_MEMORY_FILES, classifyReadFailure, contextScopeContradiction, excludeFromContextScan, USER_GLOBAL_EVENT_NAMED_GLOBS } from "./claude-context.mjs";