agent-sanitizer 2.0.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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Pure offset/text machinery for mapping between a file's on-disk bytes and
3
+ * the sanitized view the model reads (Layer 1 invisible/ANSI stripping, then
4
+ * Layer 4 secret redaction). No I/O — consumed by `./rehydrate.mjs`,
5
+ * which owns file access, the injected redactor, and policy.
6
+ *
7
+ * Coordinate spaces, disk → view:
8
+ * disk — the file's real bytes
9
+ * cleaned — disk minus the runs Layer 1 deleted (`alignDeletions` recovers
10
+ * them; a run at `start` sits immediately before cleaned[start])
11
+ * view — cleaned with each secret replaced by its [REDACTED…]
12
+ * placeholder (`pairs` from the injected redactor’s map mode)
13
+ */
14
+ /**
15
+ * Non-overlapping occurrence indices of `needle` in `haystack`.
16
+ * @param {string} haystack
17
+ * @param {string} needle
18
+ * @returns {number[]}
19
+ */
20
+ export function occurrences(haystack: string, needle: string): number[];
21
+ /**
22
+ * Count of ALL matches of `needle` in `haystack`, including self-overlapping
23
+ * ones (stepping by 1, not by the needle length). `occurrences` deliberately
24
+ * steps by the needle length so it never reports overlapping spans — correct
25
+ * for splicing, but it undercounts a self-overlapping needle (e.g. "aa" in
26
+ * "aaa" is one non-overlapping match yet two overlapping ones). Ambiguity
27
+ * gating must use THIS count: an old_string that overlaps itself has more than
28
+ * one anchor a human (or the real Edit tool) could mean, so it is ambiguous even
29
+ * when `occurrences` reports a single non-overlapping match.
30
+ * @param {string} haystack
31
+ * @param {string} needle
32
+ * @returns {number}
33
+ */
34
+ export function overlapAwareCount(haystack: string, needle: string): number;
35
+ /**
36
+ * The character runs Layer 1 deleted, located by greedy subsequence alignment
37
+ * (stripping only deletes, so `cleaned` is always a subsequence of `content`).
38
+ * Throws if the subsequence property does not hold — the caller fails closed.
39
+ * @param {string} content disk bytes
40
+ * @param {string} cleaned Layer-1 view of the same bytes
41
+ * @returns {{start: number, deleted: string}[]}
42
+ */
43
+ export function alignDeletions(content: string, cleaned: string): {
44
+ start: number;
45
+ deleted: string;
46
+ }[];
47
+ /**
48
+ * Re-express each pair's `start` from a Unicode code-point offset — what the
49
+ * redactor's map mode emits (Python indexes strings by code point) — to a
50
+ * UTF-16 code-unit offset into `text`, the basis every other function here uses
51
+ * (JS `indexOf`/`slice`/`.length` count UTF-16 units). The two are identical for
52
+ * BMP-only text and diverge only when an astral character (e.g. an emoji)
53
+ * precedes a placeholder, where the code-point offset undercounts by one per
54
+ * astral char. `pair.start` is compared against UTF-16 view offsets throughout,
55
+ * so this conversion MUST run once at ingestion or an astral-preceded
56
+ * placeholder mis-anchors the edit onto the wrong bytes.
57
+ * @param {string} text the redacted view text the offsets index into
58
+ * @param {{placeholder: string, original: string, start: number}[]} pairs
59
+ * @returns {{placeholder: string, original: string, start: number}[]}
60
+ */
61
+ export function pairsToUtf16(text: string, pairs: {
62
+ placeholder: string;
63
+ original: string;
64
+ start: number;
65
+ }[]): {
66
+ placeholder: string;
67
+ original: string;
68
+ start: number;
69
+ }[];
70
+ /**
71
+ * Resolve view span [viewStart, viewEnd) to its on-disk text and the redaction
72
+ * pairs it wholly contains, mapping across placeholder expansion (view →
73
+ * cleaned) and stripped invisible runs (cleaned → disk). Null when a boundary
74
+ * cuts through a placeholder. `invisibleBytes` counts stripped characters
75
+ * inside the span (replaced along with it); runs at the boundaries stay
76
+ * outside and are preserved. `cleanedText` is the span's Layer-1 view — the
77
+ * caller MUST verify that re-cleaning `diskText` reproduces it before acting:
78
+ * greedy alignment is ambiguous when a deleted run's edge character equals the
79
+ * adjacent kept character (an ANSI sequence ending in `m` before a kept `m`),
80
+ * and a mis-attributed run would mis-anchor the edit.
81
+ * @param {string} content disk file content
82
+ * @param {string} cleaned Layer-1 view of `content`
83
+ * @param {{text: string, pairs: {placeholder: string, original: string, start: number}[]}} view
84
+ * @param {{start: number, deleted: string}[]} deletions
85
+ * @param {number} viewStart
86
+ * @param {number} viewEnd
87
+ */
88
+ export function resolveSpan(content: string, cleaned: string, view: {
89
+ text: string;
90
+ pairs: {
91
+ placeholder: string;
92
+ original: string;
93
+ start: number;
94
+ }[];
95
+ }, deletions: {
96
+ start: number;
97
+ deleted: string;
98
+ }[], viewStart: number, viewEnd: number): {
99
+ diskText: string;
100
+ cleanedText: string;
101
+ invisibleBytes: number;
102
+ pairs: {
103
+ placeholder: string;
104
+ original: string;
105
+ start: number;
106
+ }[];
107
+ } | null;
108
+ /**
109
+ * All occurrences of any needle in `text`, ordered by position. Placeholder
110
+ * texts never substring-overlap one another (each ends in "]" right after its
111
+ * distinguishing label), so the sorted matches are non-overlapping.
112
+ * @param {string} text
113
+ * @param {string[]} needles
114
+ * @returns {{text: string, index: number}[]}
115
+ */
116
+ export function orderedMatches(text: string, needles: string[]): {
117
+ text: string;
118
+ index: number;
119
+ }[];
120
+ /**
121
+ * On-disk [start, end) span of every redaction pair, mapped from its view
122
+ * offset through placeholder expansion (view → cleaned) and stripped invisible
123
+ * runs (cleaned → disk). A run abutting the secret stays outside its span (it
124
+ * was never part of the secret); interior runs are included. Callers use these
125
+ * to detect an edit whose on-disk footprint intrudes into bytes the model was
126
+ * never shown.
127
+ * @param {{pairs: {placeholder: string, original: string, start: number}[]}} view
128
+ * @param {{start: number, deleted: string}[]} deletions
129
+ * @returns {{start: number, end: number}[]}
130
+ */
131
+ export function pairDiskSpans(view: {
132
+ pairs: {
133
+ placeholder: string;
134
+ original: string;
135
+ start: number;
136
+ }[];
137
+ }, deletions: {
138
+ start: number;
139
+ deleted: string;
140
+ }[]): {
141
+ start: number;
142
+ end: number;
143
+ }[];
144
+ /**
145
+ * Substitute the placeholders in a model-authored new_string with the secrets
146
+ * they stand for. Resolution, strictest first: if the new placeholder
147
+ * sequence equals the matched span's, map 1:1 by position; otherwise each
148
+ * placeholder text must name a single distinct secret within the span. A
149
+ * placeholder naming a secret outside the span, or one whose text also
150
+ * appears literally in the matched file text, is unresolvable → deny.
151
+ * @param {string} oldS matched old_string (≡ the view span text)
152
+ * @param {string} newS model-authored replacement
153
+ * @param {{placeholder: string, original: string, start: number}[]} spanPairs
154
+ * @param {{placeholder: string, original: string, start: number}[]} filePairs
155
+ * @returns {{text: string, secrets: string[]} | {deny: string}}
156
+ */
157
+ export function rehydrateNewString(oldS: string, newS: string, spanPairs: {
158
+ placeholder: string;
159
+ original: string;
160
+ start: number;
161
+ }[], filePairs: {
162
+ placeholder: string;
163
+ original: string;
164
+ start: number;
165
+ }[]): {
166
+ text: string;
167
+ secrets: string[];
168
+ } | {
169
+ deny: string;
170
+ };