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.
- package/LICENSE +202 -0
- package/README.md +217 -0
- package/SECURITY.md +31 -0
- package/THREAT-MODEL.md +173 -0
- package/bin/sanitize-cli.mjs +423 -0
- package/package.json +157 -0
- package/src/cf-charset.mjs +43 -0
- package/src/confusables.mjs +199 -0
- package/src/gates.mjs +71 -0
- package/src/html.mjs +2233 -0
- package/src/index.mjs +166 -0
- package/src/instructions.mjs +530 -0
- package/src/invisible.mjs +976 -0
- package/src/joining-type.mjs +616 -0
- package/src/layer1.mjs +177 -0
- package/src/output.mjs +788 -0
- package/src/prompt.mjs +154 -0
- package/src/rehydrate.mjs +646 -0
- package/src/standardized-variants.mjs +1335 -0
- package/src/view-map.mjs +354 -0
- package/types/cf-charset.d.mts +14 -0
- package/types/confusables.d.mts +85 -0
- package/types/gates.d.mts +37 -0
- package/types/html.d.mts +117 -0
- package/types/index.d.mts +33 -0
- package/types/instructions.d.mts +137 -0
- package/types/invisible.d.mts +98 -0
- package/types/joining-type.d.mts +22 -0
- package/types/layer1.d.mts +35 -0
- package/types/output.d.mts +195 -0
- package/types/prompt.d.mts +28 -0
- package/types/rehydrate.d.mts +54 -0
- package/types/standardized-variants.d.mts +22 -0
- package/types/view-map.d.mts +170 -0
|
@@ -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
|
+
};
|