@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12
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/CHANGELOG.md +13 -0
- package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
- package/dist/cli.js +10795 -10791
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +5 -52
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
- package/dist/types/discovery/contained-path.d.ts +2 -3
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +2 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +3 -1
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/turn-recovery.d.ts +2 -2
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/src/advisor/runtime.ts +12 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +64 -704
- package/src/config/settings.ts +7 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +7 -7
- package/src/discovery/contained-path.ts +2 -5
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/export/share.ts +2 -1
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/mcp/transports/header-policy.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/secrets/index.ts +6 -12
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +113 -7
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-handoff.ts +39 -20
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/turn-recovery.ts +21 -12
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -3000
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +77 -1820
- package/src/tools/shell-tokenize.ts +1 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import type { JsonRecord, JsonValue, SecretObfuscator } from "./obfuscator";
|
|
2
|
+
import {
|
|
3
|
+
lookupFriendlyPlaceholderAlias,
|
|
4
|
+
PLACEHOLDER_RE,
|
|
5
|
+
type RegexScanSegment,
|
|
6
|
+
type ReplaceRegexScan,
|
|
7
|
+
resumePlaceholderScanAfterRejectedCandidate,
|
|
8
|
+
} from "./placeholder";
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// Helpers
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
// Like the untracked walk, but threads a parallel `origin` tag string through:
|
|
15
|
+
// preserved placeholder spans keep their existing origin tag (so a
|
|
16
|
+
// same-call-fresh "F" placeholder is never relabeled prior-call "I", and vice
|
|
17
|
+
// versa), while `transform`'s output — always freshly generated or redacted
|
|
18
|
+
// content in both callers below — is tagged "I" (it must not be re-matched as
|
|
19
|
+
// though it arrived in the input, mirroring plain-secret replacement tagging).
|
|
20
|
+
export function transformOutsidePlaceholdersTracked(
|
|
21
|
+
text: string,
|
|
22
|
+
origin: string,
|
|
23
|
+
shouldSkipPlaceholder: (placeholder: string) => boolean,
|
|
24
|
+
transform: (chunk: string) => string,
|
|
25
|
+
preservePlaceholder?: (placeholder: string) => string,
|
|
26
|
+
): { text: string; origin: string } {
|
|
27
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
28
|
+
let result = "";
|
|
29
|
+
let resultOrigin = "";
|
|
30
|
+
let pendingIndex = 0;
|
|
31
|
+
for (;;) {
|
|
32
|
+
const match = PLACEHOLDER_RE.exec(text);
|
|
33
|
+
if (match === null) break;
|
|
34
|
+
if (!shouldSkipPlaceholder(match[0])) {
|
|
35
|
+
resumePlaceholderScanAfterRejectedCandidate(match);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const transformed = transform(text.slice(pendingIndex, match.index));
|
|
39
|
+
result += transformed;
|
|
40
|
+
resultOrigin += "I".repeat(transformed.length);
|
|
41
|
+
const preserved = preservePlaceholder ? preservePlaceholder(match[0]) : match[0];
|
|
42
|
+
result += preserved;
|
|
43
|
+
resultOrigin += origin.slice(match.index, match.index + match[0].length);
|
|
44
|
+
pendingIndex = match.index + match[0].length;
|
|
45
|
+
}
|
|
46
|
+
const trailing = transform(text.slice(pendingIndex));
|
|
47
|
+
result += trailing;
|
|
48
|
+
resultOrigin += "I".repeat(trailing.length);
|
|
49
|
+
return { text: result, origin: resultOrigin };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function trailingOutsidePreservedPlaceholderChunk(
|
|
53
|
+
text: string,
|
|
54
|
+
shouldPreservePlaceholder: (placeholder: string) => boolean,
|
|
55
|
+
): string {
|
|
56
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
57
|
+
let pendingIndex = 0;
|
|
58
|
+
let sawPlaceholder = false;
|
|
59
|
+
for (;;) {
|
|
60
|
+
const match = PLACEHOLDER_RE.exec(text);
|
|
61
|
+
if (match === null) break;
|
|
62
|
+
if (!shouldPreservePlaceholder(match[0])) {
|
|
63
|
+
resumePlaceholderScanAfterRejectedCandidate(match);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
sawPlaceholder = true;
|
|
67
|
+
pendingIndex = match.index + match[0].length;
|
|
68
|
+
}
|
|
69
|
+
return sawPlaceholder ? text.slice(pendingIndex) : "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildReplaceRegexScan(
|
|
73
|
+
text: string,
|
|
74
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
75
|
+
deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
|
|
76
|
+
): ReplaceRegexScan {
|
|
77
|
+
let scanText = "";
|
|
78
|
+
let cursor = 0;
|
|
79
|
+
const segments: RegexScanSegment[] = [];
|
|
80
|
+
const appendSegment = (
|
|
81
|
+
value: string,
|
|
82
|
+
textStart: number,
|
|
83
|
+
textEnd: number,
|
|
84
|
+
generatedPlaceholder: boolean,
|
|
85
|
+
recursive: boolean,
|
|
86
|
+
) => {
|
|
87
|
+
if (value.length === 0) return;
|
|
88
|
+
const scanStart = scanText.length;
|
|
89
|
+
scanText += value;
|
|
90
|
+
segments.push({
|
|
91
|
+
scanStart,
|
|
92
|
+
scanEnd: scanStart + value.length,
|
|
93
|
+
textStart,
|
|
94
|
+
textEnd,
|
|
95
|
+
generatedPlaceholder,
|
|
96
|
+
recursive,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const range of ranges) {
|
|
101
|
+
appendSegment(text.slice(cursor, range.start), cursor, range.start, false, false);
|
|
102
|
+
const placeholder = text.slice(range.start, range.end);
|
|
103
|
+
const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
|
|
104
|
+
appendSegment(mapping?.secret ?? placeholder, range.start, range.end, true, mapping?.recursive ?? false);
|
|
105
|
+
cursor = range.end;
|
|
106
|
+
}
|
|
107
|
+
appendSegment(text.slice(cursor), cursor, text.length, false, false);
|
|
108
|
+
|
|
109
|
+
return { text: scanText, segments };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function mapReplaceRegexMatch(
|
|
113
|
+
segments: ReadonlyArray<RegexScanSegment>,
|
|
114
|
+
scanStart: number,
|
|
115
|
+
scanEnd: number,
|
|
116
|
+
): {
|
|
117
|
+
start: number;
|
|
118
|
+
end: number;
|
|
119
|
+
recursive: boolean;
|
|
120
|
+
preserveGeneratedPlaceholders: boolean;
|
|
121
|
+
partialPlaceholderCut: boolean;
|
|
122
|
+
cutResumeIndex: number;
|
|
123
|
+
firstPlaceholderScanStart: number;
|
|
124
|
+
} {
|
|
125
|
+
const startSegment = findScanSegment(segments, scanStart);
|
|
126
|
+
const endSegment = findScanSegment(segments, scanEnd - 1);
|
|
127
|
+
const start = startSegment.generatedPlaceholder
|
|
128
|
+
? startSegment.textStart
|
|
129
|
+
: startSegment.textStart + (scanStart - startSegment.scanStart);
|
|
130
|
+
const end = endSegment.generatedPlaceholder
|
|
131
|
+
? endSegment.textEnd
|
|
132
|
+
: endSegment.textStart + (scanEnd - endSegment.scanStart);
|
|
133
|
+
// A match boundary that falls strictly inside a generated placeholder's
|
|
134
|
+
// expanded value cuts the underlying secret: the snap above pulls the span out
|
|
135
|
+
// to the whole `#…#` token, so the obfuscate path can leave it alone instead of
|
|
136
|
+
// consuming a partial placeholder expansion.
|
|
137
|
+
const partialPlaceholderCut =
|
|
138
|
+
(startSegment.generatedPlaceholder && scanStart > startSegment.scanStart) ||
|
|
139
|
+
(endSegment.generatedPlaceholder && scanEnd < endSegment.scanEnd);
|
|
140
|
+
let recursive = false;
|
|
141
|
+
let preserveGeneratedPlaceholders = false;
|
|
142
|
+
// When the match straddles a placeholder, resume scanning just past the last
|
|
143
|
+
// overlapping placeholder so trailing wholly-outside content (e.g. an 8-char
|
|
144
|
+
// run after the secret) still gets matched instead of being consumed by the
|
|
145
|
+
// straddling span. `firstPlaceholderScanStart` marks where the leading
|
|
146
|
+
// wholly-outside prefix ends, so a prefix that independently matches can be
|
|
147
|
+
// redacted on its own rather than skipped along with the cut span.
|
|
148
|
+
let cutResumeIndex = scanStart;
|
|
149
|
+
let firstPlaceholderScanStart = -1;
|
|
150
|
+
for (const segment of segments) {
|
|
151
|
+
if (segment.scanStart >= scanEnd || segment.scanEnd <= scanStart) continue;
|
|
152
|
+
recursive ||= segment.recursive;
|
|
153
|
+
preserveGeneratedPlaceholders ||= segment.generatedPlaceholder;
|
|
154
|
+
if (segment.generatedPlaceholder) {
|
|
155
|
+
if (firstPlaceholderScanStart === -1) firstPlaceholderScanStart = segment.scanStart;
|
|
156
|
+
if (segment.scanEnd > cutResumeIndex) cutResumeIndex = segment.scanEnd;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
start,
|
|
161
|
+
end,
|
|
162
|
+
recursive,
|
|
163
|
+
preserveGeneratedPlaceholders,
|
|
164
|
+
partialPlaceholderCut,
|
|
165
|
+
cutResumeIndex,
|
|
166
|
+
firstPlaceholderScanStart,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findScanSegment(segments: ReadonlyArray<RegexScanSegment>, scanIndex: number): RegexScanSegment {
|
|
171
|
+
for (const segment of segments) {
|
|
172
|
+
if (scanIndex >= segment.scanStart && scanIndex < segment.scanEnd) return segment;
|
|
173
|
+
}
|
|
174
|
+
throw new Error("regex match did not map to source text");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Extend a scan-space resume position past a consecutive run of generated
|
|
179
|
+
* placeholder segments starting exactly at it, with no raw gap in between. A
|
|
180
|
+
* cut-resolution resume point that happens to land precisely on the START of
|
|
181
|
+
* ANOTHER placeholder must not stop there and hand it to a fresh `regex.exec`
|
|
182
|
+
* attempt — the same content, scanned as an opaque adjacent placeholder run,
|
|
183
|
+
* must resolve identically whether the run's LEADING member is still raw text
|
|
184
|
+
* (this call is about to placeholder it) or is ALREADY a placeholder from a
|
|
185
|
+
* prior call or an earlier pass of this same call. Without this, a bounded
|
|
186
|
+
* regex whose reach spans two adjacent secrets plus trailing spillover bytes
|
|
187
|
+
* (e.g. `[A-Z]{9}` over `ABCDEFGH` + `SECRETUV` + `A`) resolves the leading
|
|
188
|
+
* secret as its own independent redaction on the FIRST obfuscate() call (a
|
|
189
|
+
* genuinely raw prefix gets its own match, then the discard for the rest
|
|
190
|
+
* resumes right after it), but on a LATER call — once that prefix is itself a
|
|
191
|
+
* placeholder — the very first match attempt starts already inside the
|
|
192
|
+
* placeholder run, cannot be prefix-narrowed at all, and its discard resume
|
|
193
|
+
* point lands mid-run instead of past it, exposing a shorter tail (`SECRETUV`
|
|
194
|
+
* + `A`) to a clean, un-cut match the first call never attempted. Chaining the
|
|
195
|
+
* resume point through every immediately-adjacent placeholder makes both
|
|
196
|
+
* calls land on the exact same next scan position.
|
|
197
|
+
*/
|
|
198
|
+
export function extendPastAdjacentPlaceholders(segments: ReadonlyArray<RegexScanSegment>, index: number): number {
|
|
199
|
+
let cursor = index;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const segment = segments.find(candidate => candidate.scanStart === cursor && candidate.generatedPlaceholder);
|
|
202
|
+
if (!segment) return cursor;
|
|
203
|
+
cursor = segment.scanEnd;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Apply a fixed custom replacement across a matched span while preserving any
|
|
208
|
+
// inner generated placeholders. Usually the replacement is the user's single
|
|
209
|
+
// redaction marker for the whole match, so emit it for the first non-empty
|
|
210
|
+
// surrounding chunk and drop later chunks. But bounded regexes can cut through
|
|
211
|
+
// an already-emitted marker on the trailing side (`X#…#RED` from
|
|
212
|
+
// `XSECRETUVREDACTED`), where dropping the later prefix would leave raw bytes
|
|
213
|
+
// (`ACTED`) to be consumed on the next pass. Promote later chunks that are a
|
|
214
|
+
// prefix of the replacement to the FULL marker so the first pass is already a
|
|
215
|
+
// fixed point. The reversible placeholder stays intact in its relative
|
|
216
|
+
// position.
|
|
217
|
+
export function redactWithFixedReplacementOutsidePlaceholders(
|
|
218
|
+
text: string,
|
|
219
|
+
origin: string,
|
|
220
|
+
replacement: string,
|
|
221
|
+
shouldPreservePlaceholder: (placeholder: string) => boolean,
|
|
222
|
+
): { text: string; origin: string } {
|
|
223
|
+
let emitted = false;
|
|
224
|
+
return transformOutsidePlaceholdersTracked(
|
|
225
|
+
text,
|
|
226
|
+
origin,
|
|
227
|
+
shouldPreservePlaceholder,
|
|
228
|
+
chunk => {
|
|
229
|
+
if (chunk.length === 0) return "";
|
|
230
|
+
if (!emitted) {
|
|
231
|
+
emitted = true;
|
|
232
|
+
return replacement;
|
|
233
|
+
}
|
|
234
|
+
return replacement.startsWith(chunk) ? replacement : "";
|
|
235
|
+
},
|
|
236
|
+
placeholder => placeholder,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function deobfuscateGeneratedPlaceholderRanges(
|
|
241
|
+
text: string,
|
|
242
|
+
start: number,
|
|
243
|
+
end: number,
|
|
244
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
245
|
+
deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
|
|
246
|
+
): { text: string; recursive: boolean } {
|
|
247
|
+
let result = "";
|
|
248
|
+
let cursor = start;
|
|
249
|
+
let recursive = false;
|
|
250
|
+
for (const range of ranges) {
|
|
251
|
+
if (range.end <= start || range.start >= end) continue;
|
|
252
|
+
const overlapStart = Math.max(range.start, start);
|
|
253
|
+
const overlapEnd = Math.min(range.end, end);
|
|
254
|
+
result += text.slice(cursor, overlapStart);
|
|
255
|
+
const placeholder = text.slice(overlapStart, overlapEnd);
|
|
256
|
+
const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
|
|
257
|
+
result += mapping?.secret ?? placeholder;
|
|
258
|
+
recursive ||= mapping?.recursive ?? false;
|
|
259
|
+
cursor = overlapEnd;
|
|
260
|
+
}
|
|
261
|
+
result += text.slice(cursor, end);
|
|
262
|
+
return { text: result, recursive };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Concatenate ONLY the deobfuscated placeholder ranges within [start, end),
|
|
266
|
+
// dropping the bytes that lie outside them. Used to test whether a regex match
|
|
267
|
+
// that straddles a prior-call placeholder would still match on the placeholder's
|
|
268
|
+
// own (expanded) secret value alone — i.e. the surrounding raw bytes are greedy
|
|
269
|
+
// spillover the match does not need, rather than content the match depends on.
|
|
270
|
+
export function placeholderInnerText(
|
|
271
|
+
text: string,
|
|
272
|
+
start: number,
|
|
273
|
+
end: number,
|
|
274
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
275
|
+
deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
|
|
276
|
+
): string {
|
|
277
|
+
let result = "";
|
|
278
|
+
for (const range of ranges) {
|
|
279
|
+
if (range.end <= start || range.start >= end) continue;
|
|
280
|
+
const overlapStart = Math.max(range.start, start);
|
|
281
|
+
const overlapEnd = Math.min(range.end, end);
|
|
282
|
+
const placeholder = text.slice(overlapStart, overlapEnd);
|
|
283
|
+
const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
|
|
284
|
+
result += mapping?.secret ?? placeholder;
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Concatenate the bytes of [start, end) that lie OUTSIDE the given (ascending,
|
|
290
|
+
// non-overlapping) placeholder ranges. Used to test whether a regex match that
|
|
291
|
+
// straddles a prior-call placeholder would still match on its surrounding bytes
|
|
292
|
+
// alone — i.e. those bytes are genuinely new content to redact rather than a
|
|
293
|
+
// match that only exists because the deobfuscated placeholder bridges them.
|
|
294
|
+
export function textOutsidePlaceholderRanges(
|
|
295
|
+
text: string,
|
|
296
|
+
start: number,
|
|
297
|
+
end: number,
|
|
298
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
299
|
+
): string {
|
|
300
|
+
let result = "";
|
|
301
|
+
let cursor = start;
|
|
302
|
+
for (const range of ranges) {
|
|
303
|
+
if (range.end <= start || range.start >= end) continue;
|
|
304
|
+
const overlapStart = Math.max(range.start, start);
|
|
305
|
+
const overlapEnd = Math.min(range.end, end);
|
|
306
|
+
result += text.slice(cursor, overlapStart);
|
|
307
|
+
cursor = overlapEnd;
|
|
308
|
+
}
|
|
309
|
+
result += text.slice(cursor, end);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Like `textOutsidePlaceholderRanges`, but tests each outside chunk against
|
|
314
|
+
// `regex` in its REAL context instead of on an isolated slice — tried in BOTH
|
|
315
|
+
// the literal `#…#` placeholder-token text AND the EXPANDED scan context
|
|
316
|
+
// (placeholder resolved to its secret value), since either can be the reason a
|
|
317
|
+
// chunk independently requires redaction:
|
|
318
|
+
// - Literal-token context matters when the placeholder TOKEN's own non-word
|
|
319
|
+
// boundary is what completes a boundary-sensitive pattern, e.g. a prefix
|
|
320
|
+
// "ABCDEFGH" next to a placeholder token matches `\b[A-Z]{8}\b` because the
|
|
321
|
+
// token's leading `#` is a non-word byte — but that boundary disappears
|
|
322
|
+
// once the placeholder expands into more `[A-Z]` bytes with no separator.
|
|
323
|
+
// - Expanded scan context matters when a lookbehind/lookahead only resolves
|
|
324
|
+
// once the neighboring placeholder is expanded, e.g. a prior plain
|
|
325
|
+
// placeholder for `ABCDEFGH` next to raw `SECRET`, matched by
|
|
326
|
+
// `(?<=ABCDEFGH)SECRET`: the literal placeholder token before `SECRET`
|
|
327
|
+
// never satisfies the lookbehind, so literal-context alone wrongly reports
|
|
328
|
+
// no independent match.
|
|
329
|
+
// A match only counts when it lies ENTIRELY within one outside chunk (in
|
|
330
|
+
// whichever context it was tested); a match that reaches into the
|
|
331
|
+
// placeholder itself is not evidence the outside chunk independently
|
|
332
|
+
// requires redaction.
|
|
333
|
+
export function outsidePlaceholderRangesAnyIndependentlyMatch(
|
|
334
|
+
text: string,
|
|
335
|
+
scanText: string,
|
|
336
|
+
segments: ReadonlyArray<RegexScanSegment>,
|
|
337
|
+
start: number,
|
|
338
|
+
end: number,
|
|
339
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
340
|
+
regex: RegExp,
|
|
341
|
+
): boolean {
|
|
342
|
+
// A text-space outside chunk lies entirely within one non-placeholder scan
|
|
343
|
+
// segment (placeholder ranges are exactly the gaps between such segments),
|
|
344
|
+
// so its scan-space span is a fixed offset from its text-space span.
|
|
345
|
+
const toScanSpace = (chunkStart: number, chunkEnd: number): [number, number] | undefined => {
|
|
346
|
+
for (const segment of segments) {
|
|
347
|
+
if (segment.generatedPlaceholder || segment.textStart > chunkStart || segment.textEnd < chunkEnd) continue;
|
|
348
|
+
const offset = segment.scanStart - segment.textStart;
|
|
349
|
+
return [chunkStart + offset, chunkEnd + offset];
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
};
|
|
353
|
+
const chunkIndependentlyMatches = (chunkStart: number, chunkEnd: number): boolean => {
|
|
354
|
+
if (chunkMatchesInSourceContext(text, chunkStart, chunkEnd, regex)) return true;
|
|
355
|
+
const scanSpan = toScanSpace(chunkStart, chunkEnd);
|
|
356
|
+
return scanSpan !== undefined && chunkMatchesInSourceContext(scanText, scanSpan[0], scanSpan[1], regex);
|
|
357
|
+
};
|
|
358
|
+
let cursor = start;
|
|
359
|
+
for (const range of ranges) {
|
|
360
|
+
if (range.end <= start || range.start >= end) continue;
|
|
361
|
+
const overlapStart = Math.max(range.start, start);
|
|
362
|
+
const overlapEnd = Math.min(range.end, end);
|
|
363
|
+
if (cursor < overlapStart && chunkIndependentlyMatches(cursor, overlapStart)) return true;
|
|
364
|
+
cursor = overlapEnd;
|
|
365
|
+
}
|
|
366
|
+
return cursor < end && chunkIndependentlyMatches(cursor, end);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Whether `regex` (global) has a match fully contained in [chunkStart, chunkEnd)
|
|
370
|
+
// when run against the full `text` — so lookbehind/lookahead see the actual
|
|
371
|
+
// surrounding bytes rather than an isolated slice's edges.
|
|
372
|
+
function chunkMatchesInSourceContext(text: string, chunkStart: number, chunkEnd: number, regex: RegExp): boolean {
|
|
373
|
+
regex.lastIndex = chunkStart;
|
|
374
|
+
for (;;) {
|
|
375
|
+
const found = regex.exec(text);
|
|
376
|
+
if (found === null || found.index >= chunkEnd) return false;
|
|
377
|
+
const matchEnd = found.index + found[0].length;
|
|
378
|
+
if (matchEnd <= chunkEnd) return true;
|
|
379
|
+
regex.lastIndex = found[0].length === 0 ? found.index + 1 : matchEnd;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function firstOutsidePlaceholderRange(
|
|
384
|
+
start: number,
|
|
385
|
+
end: number,
|
|
386
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
387
|
+
): { start: number; end: number } | undefined {
|
|
388
|
+
let cursor = start;
|
|
389
|
+
for (const range of ranges) {
|
|
390
|
+
if (range.end <= start || range.start >= end) continue;
|
|
391
|
+
const overlapStart = Math.max(range.start, start);
|
|
392
|
+
const overlapEnd = Math.min(range.end, end);
|
|
393
|
+
if (cursor < overlapStart) return { start: cursor, end: overlapStart };
|
|
394
|
+
cursor = overlapEnd;
|
|
395
|
+
}
|
|
396
|
+
return cursor < end ? { start: cursor, end } : undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function countOutsidePlaceholderRanges(
|
|
400
|
+
start: number,
|
|
401
|
+
end: number,
|
|
402
|
+
ranges: ReadonlyArray<{ start: number; end: number }>,
|
|
403
|
+
): number {
|
|
404
|
+
let count = 0;
|
|
405
|
+
let cursor = start;
|
|
406
|
+
for (const range of ranges) {
|
|
407
|
+
if (range.end <= start || range.start >= end) continue;
|
|
408
|
+
const overlapStart = Math.max(range.start, start);
|
|
409
|
+
const overlapEnd = Math.min(range.end, end);
|
|
410
|
+
if (cursor < overlapStart) count++;
|
|
411
|
+
cursor = overlapEnd;
|
|
412
|
+
}
|
|
413
|
+
if (cursor < end) count++;
|
|
414
|
+
return count;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function replaceRange(text: string, start: number, end: number, replacement: string): string {
|
|
418
|
+
return text.slice(0, start) + replacement + text.slice(end);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Deep-walk an object, transforming all string values. */
|
|
422
|
+
export function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
|
|
423
|
+
if (typeof obj === "string") {
|
|
424
|
+
return transform(obj) as unknown as T;
|
|
425
|
+
}
|
|
426
|
+
if (Array.isArray(obj)) {
|
|
427
|
+
let changed = false;
|
|
428
|
+
const result = obj.map(item => {
|
|
429
|
+
const transformed = deepWalkStrings(item, transform);
|
|
430
|
+
if (transformed !== item) changed = true;
|
|
431
|
+
return transformed;
|
|
432
|
+
});
|
|
433
|
+
return (changed ? result : obj) as unknown as T;
|
|
434
|
+
}
|
|
435
|
+
if (obj !== null && typeof obj === "object" && isPlainRecord(obj)) {
|
|
436
|
+
let changed = false;
|
|
437
|
+
const result: Record<string, unknown> = {};
|
|
438
|
+
for (const key of Object.keys(obj)) {
|
|
439
|
+
const value = (obj as Record<string, unknown>)[key];
|
|
440
|
+
const transformed = deepWalkStrings(value, transform);
|
|
441
|
+
if (transformed !== value) changed = true;
|
|
442
|
+
result[key] = transformed;
|
|
443
|
+
}
|
|
444
|
+
return (changed ? result : obj) as T;
|
|
445
|
+
}
|
|
446
|
+
return obj;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isPlainRecord(obj: object): obj is Record<string, unknown> {
|
|
450
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
451
|
+
return prototype === Object.prototype || prototype === null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function collectJsonRegexSecretValues(obfuscator: SecretObfuscator, value: JsonValue): Set<string> {
|
|
455
|
+
const values = new Set<string>();
|
|
456
|
+
const collect = (item: JsonValue): void => {
|
|
457
|
+
if (typeof item === "string") {
|
|
458
|
+
for (const secretValue of obfuscator.collectRegexSecretValuesForObfuscation(item)) {
|
|
459
|
+
values.add(secretValue);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (Array.isArray(item)) {
|
|
464
|
+
for (const child of item) collect(child);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (item !== null && typeof item === "object") {
|
|
468
|
+
for (const child of Object.values(item)) {
|
|
469
|
+
if (child !== undefined) collect(child);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
collect(value);
|
|
474
|
+
return values;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Map every string in arbitrary JSON. Used ONLY for tool-call arguments, whose
|
|
479
|
+
* shape is model-authored and not known ahead of time. No other caller may walk
|
|
480
|
+
* untyped data: every message/content path is handled by a typed transformer.
|
|
481
|
+
*/
|
|
482
|
+
export function mapJsonStrings(value: JsonValue, fn: (s: string) => string): JsonValue {
|
|
483
|
+
if (typeof value === "string") return fn(value);
|
|
484
|
+
if (Array.isArray(value)) {
|
|
485
|
+
let changed = false;
|
|
486
|
+
const out = value.map(item => {
|
|
487
|
+
const next = mapJsonStrings(item, fn);
|
|
488
|
+
if (next !== item) changed = true;
|
|
489
|
+
return next;
|
|
490
|
+
});
|
|
491
|
+
return changed ? out : value;
|
|
492
|
+
}
|
|
493
|
+
if (value !== null && typeof value === "object") {
|
|
494
|
+
let changed = false;
|
|
495
|
+
const out: JsonRecord = {};
|
|
496
|
+
for (const key of Object.keys(value)) {
|
|
497
|
+
const item = value[key];
|
|
498
|
+
if (item === undefined) continue;
|
|
499
|
+
const next = mapJsonStrings(item, fn);
|
|
500
|
+
if (next !== item) changed = true;
|
|
501
|
+
out[key] = next;
|
|
502
|
+
}
|
|
503
|
+
return changed ? out : value;
|
|
504
|
+
}
|
|
505
|
+
return value;
|
|
506
|
+
}
|