@oh-my-pi/pi-coding-agent 16.1.11 → 16.1.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 +14 -0
- package/dist/cli.js +2770 -2770
- package/dist/types/export/share.d.ts +10 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +43 -9
- package/dist/types/utils/image-loading.d.ts +3 -4
- package/package.json +12 -12
- package/src/export/share.ts +198 -8
- package/src/main.ts +8 -0
- package/src/sdk.ts +2 -1
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +220 -71
- package/src/session/agent-session.ts +19 -29
- package/src/utils/image-loading.ts +3 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { AssistantMessage, Context, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { SessionContext } from "../session/session-context";
|
|
4
4
|
import { compileSecretRegex } from "./regex";
|
|
5
5
|
|
|
@@ -15,6 +15,9 @@ export interface SecretEntry {
|
|
|
15
15
|
flags?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue | undefined };
|
|
19
|
+
export type JsonRecord = { [key: string]: JsonValue | undefined };
|
|
20
|
+
|
|
18
21
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
22
|
// Deterministic replacement generation
|
|
20
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -85,26 +88,34 @@ export class SecretObfuscator {
|
|
|
85
88
|
|
|
86
89
|
constructor(entries: SecretEntry[]) {
|
|
87
90
|
let index = 0;
|
|
91
|
+
let hasRealSec = false;
|
|
88
92
|
for (const entry of entries) {
|
|
89
93
|
const mode = entry.mode ?? "obfuscate";
|
|
90
94
|
|
|
91
95
|
if (entry.type === "plain") {
|
|
92
96
|
if (mode === "obfuscate") {
|
|
97
|
+
if (entry.content.length < 8) {
|
|
98
|
+
// Tone down short plain secret obfuscation to avoid false matches on small words like "esp"
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
93
101
|
const placeholder = buildPlaceholder(index);
|
|
94
102
|
this.#plainMappings.set(entry.content, index);
|
|
95
103
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
96
104
|
this.#deobfuscateMap.set(placeholder, entry.content);
|
|
97
105
|
index++;
|
|
106
|
+
hasRealSec = true;
|
|
98
107
|
} else {
|
|
99
108
|
// replace mode
|
|
100
109
|
const replacement = entry.replacement ?? generateDeterministicReplacement(entry.content);
|
|
101
110
|
this.#replaceMappings.set(entry.content, replacement);
|
|
111
|
+
hasRealSec = true;
|
|
102
112
|
}
|
|
103
113
|
} else {
|
|
104
114
|
// regex type — compiled here, matches discovered during obfuscate()
|
|
105
115
|
try {
|
|
106
116
|
const regex = compileSecretRegex(entry.content, entry.flags);
|
|
107
117
|
this.#regexEntries.push({ regex, mode, replacement: entry.replacement });
|
|
118
|
+
hasRealSec = true;
|
|
108
119
|
} catch {
|
|
109
120
|
// Invalid regex — skip silently (validation happens at load time)
|
|
110
121
|
}
|
|
@@ -112,7 +123,7 @@ export class SecretObfuscator {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
this.#nextIndex = index;
|
|
115
|
-
this.#hasAny =
|
|
126
|
+
this.#hasAny = hasRealSec;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
hasSecrets(): boolean {
|
|
@@ -154,6 +165,10 @@ export class SecretObfuscator {
|
|
|
154
165
|
const replacement = entry.replacement ?? generateDeterministicReplacement(matchValue);
|
|
155
166
|
result = replaceAll(result, matchValue, replacement);
|
|
156
167
|
} else {
|
|
168
|
+
if (matchValue.length < 8) {
|
|
169
|
+
// Tone down short regex match obfuscation to avoid false matches on small words/fragments
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
157
172
|
// obfuscate mode — get or create stable index
|
|
158
173
|
let index = this.#findObfuscateIndex(matchValue);
|
|
159
174
|
if (index === undefined) {
|
|
@@ -174,30 +189,13 @@ export class SecretObfuscator {
|
|
|
174
189
|
/** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
|
|
175
190
|
deobfuscate(text: string): string {
|
|
176
191
|
if (!this.#hasAny || !text.includes("#")) return text;
|
|
177
|
-
return text.replace(PLACEHOLDER_RE, match =>
|
|
178
|
-
return this.#deobfuscateMap.get(match) ?? match;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Deep-walk an object, deobfuscating all string values. */
|
|
183
|
-
deobfuscateObject<T>(obj: T): T {
|
|
184
|
-
if (!this.#hasAny) return obj;
|
|
185
|
-
return deepWalkStrings(obj, s => this.deobfuscate(s));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Deep-walk an object, obfuscating all string values. */
|
|
189
|
-
obfuscateObject<T>(obj: T): T {
|
|
190
|
-
if (!this.#hasAny) return obj;
|
|
191
|
-
return deepWalkStrings(obj, s => this.obfuscate(s));
|
|
192
|
+
return text.replace(PLACEHOLDER_RE, match => this.#deobfuscateMap.get(match) ?? match);
|
|
192
193
|
}
|
|
193
|
-
|
|
194
194
|
/** Find the obfuscate index for a known secret value. */
|
|
195
195
|
#findObfuscateIndex(secret: string): number | undefined {
|
|
196
|
-
// Check plain mappings first
|
|
197
196
|
const plainIndex = this.#plainMappings.get(secret);
|
|
198
197
|
if (plainIndex !== undefined) return plainIndex;
|
|
199
198
|
|
|
200
|
-
// Check regex-discovered mappings
|
|
201
199
|
for (const [index, mapping] of this.#obfuscateMappings) {
|
|
202
200
|
if (mapping.secret === secret) return index;
|
|
203
201
|
}
|
|
@@ -205,47 +203,200 @@ export class SecretObfuscator {
|
|
|
205
203
|
}
|
|
206
204
|
}
|
|
207
205
|
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// Display restore (inbound, persisted/provider → local display)
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Restore secret placeholders for local display. Only message kinds the model
|
|
212
|
+
* itself authored from obfuscated context carry placeholders — assistant
|
|
213
|
+
* content and the LLM-written branch/compaction summaries. User, developer, and
|
|
214
|
+
* tool-result messages are persisted with their literal text, so a literal
|
|
215
|
+
* `#ABCD#` the operator typed must survive untouched; those roles are never
|
|
216
|
+
* walked.
|
|
217
|
+
*/
|
|
208
218
|
export function deobfuscateSessionContext(
|
|
209
219
|
sessionContext: SessionContext,
|
|
210
220
|
obfuscator: SecretObfuscator | undefined,
|
|
211
221
|
): SessionContext {
|
|
212
222
|
if (!obfuscator?.hasSecrets()) return sessionContext;
|
|
213
|
-
const messages = obfuscator
|
|
223
|
+
const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
|
|
214
224
|
return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
|
|
228
|
+
let changed = false;
|
|
229
|
+
const result = messages.map((message): AgentMessage => {
|
|
230
|
+
switch (message.role) {
|
|
231
|
+
case "assistant": {
|
|
232
|
+
const content = deobfuscateAssistantContent(obfuscator, message.content);
|
|
233
|
+
if (content === message.content) return message;
|
|
234
|
+
changed = true;
|
|
235
|
+
return { ...message, content };
|
|
236
|
+
}
|
|
237
|
+
case "branchSummary": {
|
|
238
|
+
const summary = obfuscator.deobfuscate(message.summary);
|
|
239
|
+
if (summary === message.summary) return message;
|
|
240
|
+
changed = true;
|
|
241
|
+
return { ...message, summary };
|
|
242
|
+
}
|
|
243
|
+
case "compactionSummary": {
|
|
244
|
+
const summary = obfuscator.deobfuscate(message.summary);
|
|
245
|
+
const shortSummary =
|
|
246
|
+
message.shortSummary === undefined ? undefined : obfuscator.deobfuscate(message.shortSummary);
|
|
247
|
+
const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
|
|
248
|
+
if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
|
|
249
|
+
return message;
|
|
250
|
+
}
|
|
251
|
+
changed = true;
|
|
252
|
+
return { ...message, summary, shortSummary, blocks };
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
return message;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return changed ? result : messages;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Restore placeholders in assistant content: visible text, thinking text, and
|
|
263
|
+
* tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
|
|
264
|
+
* are opaque provider-replay data and pass through byte-identical.
|
|
265
|
+
*/
|
|
266
|
+
export function deobfuscateAssistantContent(
|
|
267
|
+
obfuscator: SecretObfuscator,
|
|
268
|
+
content: AssistantMessage["content"],
|
|
269
|
+
): AssistantMessage["content"] {
|
|
270
|
+
if (!obfuscator.hasSecrets()) return content;
|
|
271
|
+
let changed = false;
|
|
272
|
+
const result = content.map((block): AssistantMessage["content"][number] => {
|
|
273
|
+
if (block.type === "text") {
|
|
274
|
+
const text = obfuscator.deobfuscate(block.text);
|
|
275
|
+
if (text === block.text) return block;
|
|
276
|
+
changed = true;
|
|
277
|
+
return { ...block, text };
|
|
278
|
+
}
|
|
279
|
+
if (block.type === "thinking") {
|
|
280
|
+
const thinking = obfuscator.deobfuscate(block.thinking);
|
|
281
|
+
if (thinking === block.thinking) return block;
|
|
282
|
+
changed = true;
|
|
283
|
+
return { ...block, thinking };
|
|
284
|
+
}
|
|
285
|
+
if (block.type === "toolCall") {
|
|
286
|
+
const args = deobfuscateToolArguments(obfuscator, block.arguments);
|
|
287
|
+
const intent = block.intent === undefined ? undefined : obfuscator.deobfuscate(block.intent);
|
|
288
|
+
const rawBlock = block.rawBlock === undefined ? undefined : obfuscator.deobfuscate(block.rawBlock);
|
|
289
|
+
if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
|
|
290
|
+
changed = true;
|
|
291
|
+
return { ...block, arguments: args, intent, rawBlock };
|
|
292
|
+
}
|
|
293
|
+
return block;
|
|
294
|
+
});
|
|
295
|
+
return changed ? result : content;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Restore placeholders inside a tool call's arguments. Arguments are arbitrary
|
|
300
|
+
* model-authored JSON, so tool-call arguments are the ONLY place a recursive
|
|
301
|
+
* JSON walk runs.
|
|
302
|
+
*/
|
|
303
|
+
export function deobfuscateToolArguments(
|
|
304
|
+
obfuscator: SecretObfuscator,
|
|
305
|
+
args: Record<string, unknown>,
|
|
306
|
+
): Record<string, unknown> {
|
|
307
|
+
if (!obfuscator.hasSecrets()) return args;
|
|
308
|
+
return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
|
|
312
|
+
export function obfuscateToolArguments(
|
|
313
|
+
obfuscator: SecretObfuscator,
|
|
314
|
+
args: Record<string, unknown>,
|
|
315
|
+
): Record<string, unknown> {
|
|
316
|
+
if (!obfuscator.hasSecrets()) return args;
|
|
317
|
+
return mapJsonStrings(args as JsonValue, s => obfuscator.obfuscate(s)) as Record<string, unknown>;
|
|
318
|
+
}
|
|
319
|
+
|
|
217
320
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
//
|
|
321
|
+
// Outbound obfuscation (local → provider)
|
|
219
322
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
323
|
|
|
221
|
-
|
|
324
|
+
type UserFacingMessage = Extract<Message, { role: "user" | "developer" | "toolResult" }>;
|
|
325
|
+
|
|
326
|
+
/** Obfuscate `text` blocks of a content array; image and other blocks pass through. */
|
|
327
|
+
function obfuscateTextBlocks(
|
|
328
|
+
obfuscator: SecretObfuscator,
|
|
329
|
+
content: (TextContent | ImageContent)[],
|
|
330
|
+
): (TextContent | ImageContent)[] {
|
|
331
|
+
let changed = false;
|
|
332
|
+
const result = content.map((block): TextContent | ImageContent => {
|
|
333
|
+
if (block.type !== "text") return block;
|
|
334
|
+
const text = obfuscator.obfuscate(block.text);
|
|
335
|
+
if (text === block.text) return block;
|
|
336
|
+
changed = true;
|
|
337
|
+
return { ...block, text };
|
|
338
|
+
});
|
|
339
|
+
return changed ? result : content;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Restore placeholders in `text` blocks of a content array; image and other blocks pass through. */
|
|
343
|
+
function deobfuscateTextBlocks(
|
|
344
|
+
obfuscator: SecretObfuscator,
|
|
345
|
+
content: (TextContent | ImageContent)[],
|
|
346
|
+
): (TextContent | ImageContent)[] {
|
|
347
|
+
let changed = false;
|
|
348
|
+
const result = content.map((block): TextContent | ImageContent => {
|
|
349
|
+
if (block.type !== "text") return block;
|
|
350
|
+
const text = obfuscator.deobfuscate(block.text);
|
|
351
|
+
if (text === block.text) return block;
|
|
352
|
+
changed = true;
|
|
353
|
+
return { ...block, text };
|
|
354
|
+
});
|
|
355
|
+
return changed ? result : content;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Redact secrets from outbound messages. Opt-in by origin: only user messages,
|
|
360
|
+
* tool results, and user-authored developer messages (e.g. `@file` mentions)
|
|
361
|
+
* can carry operator secrets. System prompts, tool schemas, and assistant
|
|
362
|
+
* output are author-controlled or model-generated and pass through untouched.
|
|
363
|
+
* Within a targeted message only `text` blocks are rewritten — inline image
|
|
364
|
+
* bytes are never walked.
|
|
365
|
+
*/
|
|
222
366
|
export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
|
|
223
|
-
|
|
367
|
+
if (!obfuscator.hasSecrets()) return messages;
|
|
368
|
+
let changed = false;
|
|
369
|
+
const result = messages.map((message): Message => {
|
|
370
|
+
if (
|
|
371
|
+
message.role !== "user" &&
|
|
372
|
+
message.role !== "toolResult" &&
|
|
373
|
+
!(message.role === "developer" && message.attribution === "user")
|
|
374
|
+
) {
|
|
375
|
+
return message;
|
|
376
|
+
}
|
|
377
|
+
const target = message as UserFacingMessage;
|
|
378
|
+
if (typeof target.content === "string") {
|
|
379
|
+
const content = obfuscator.obfuscate(target.content);
|
|
380
|
+
if (content === target.content) return message;
|
|
381
|
+
changed = true;
|
|
382
|
+
return { ...target, content } as Message;
|
|
383
|
+
}
|
|
384
|
+
const content = obfuscateTextBlocks(obfuscator, target.content);
|
|
385
|
+
if (content === target.content) return message;
|
|
386
|
+
changed = true;
|
|
387
|
+
return { ...target, content } as Message;
|
|
388
|
+
});
|
|
389
|
+
return changed ? result : messages;
|
|
224
390
|
}
|
|
225
391
|
|
|
226
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Redact outbound provider context. Only conversation messages are rewritten;
|
|
394
|
+
* the static system prompt and tool schemas pass through unchanged.
|
|
395
|
+
*/
|
|
227
396
|
export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
|
|
228
397
|
if (!obfuscator?.hasSecrets()) return context;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
systemPrompt: obfuscator.obfuscateObject(context.systemPrompt),
|
|
232
|
-
messages: obfuscator.obfuscateObject(context.messages),
|
|
233
|
-
tools: obfuscateProviderTools(obfuscator, context.tools),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
|
|
238
|
-
export function obfuscateProviderTools(
|
|
239
|
-
obfuscator: SecretObfuscator | undefined,
|
|
240
|
-
tools: Tool[] | undefined,
|
|
241
|
-
): Tool[] | undefined {
|
|
242
|
-
if (!tools || !obfuscator?.hasSecrets()) return tools;
|
|
243
|
-
return tools.map(tool => ({
|
|
244
|
-
...tool,
|
|
245
|
-
description: obfuscator.obfuscate(tool.description),
|
|
246
|
-
parameters: obfuscator.obfuscateObject(toolWireSchema(tool)),
|
|
247
|
-
customFormat: tool.customFormat ? obfuscator.obfuscateObject(tool.customFormat) : undefined,
|
|
248
|
-
}));
|
|
398
|
+
const messages = obfuscateMessages(obfuscator, context.messages);
|
|
399
|
+
return messages === context.messages ? context : { ...context, messages };
|
|
249
400
|
}
|
|
250
401
|
|
|
251
402
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -264,35 +415,33 @@ function replaceAll(text: string, search: string, replacement: string): string {
|
|
|
264
415
|
return result;
|
|
265
416
|
}
|
|
266
417
|
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Map every string in arbitrary JSON. Used ONLY for tool-call arguments, whose
|
|
420
|
+
* shape is model-authored and not known ahead of time. No other caller may walk
|
|
421
|
+
* untyped data: every message/content path is handled by a typed transformer.
|
|
422
|
+
*/
|
|
423
|
+
function mapJsonStrings(value: JsonValue, fn: (s: string) => string): JsonValue {
|
|
424
|
+
if (typeof value === "string") return fn(value);
|
|
425
|
+
if (Array.isArray(value)) {
|
|
273
426
|
let changed = false;
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
427
|
+
const out = value.map(item => {
|
|
428
|
+
const next = mapJsonStrings(item, fn);
|
|
429
|
+
if (next !== item) changed = true;
|
|
430
|
+
return next;
|
|
278
431
|
});
|
|
279
|
-
return
|
|
432
|
+
return changed ? out : value;
|
|
280
433
|
}
|
|
281
|
-
if (
|
|
434
|
+
if (value !== null && typeof value === "object") {
|
|
282
435
|
let changed = false;
|
|
283
|
-
const
|
|
284
|
-
for (const key of Object.keys(
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
436
|
+
const out: JsonRecord = {};
|
|
437
|
+
for (const key of Object.keys(value)) {
|
|
438
|
+
const item = value[key];
|
|
439
|
+
if (item === undefined) continue;
|
|
440
|
+
const next = mapJsonStrings(item, fn);
|
|
441
|
+
if (next !== item) changed = true;
|
|
442
|
+
out[key] = next;
|
|
289
443
|
}
|
|
290
|
-
return
|
|
444
|
+
return changed ? out : value;
|
|
291
445
|
}
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function isPlainRecord(obj: object): obj is Record<string, unknown> {
|
|
296
|
-
const prototype = Object.getPrototypeOf(obj);
|
|
297
|
-
return prototype === Object.prototype || prototype === null;
|
|
446
|
+
return value;
|
|
298
447
|
}
|
|
@@ -234,9 +234,10 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
|
|
|
234
234
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
235
235
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
236
236
|
import {
|
|
237
|
+
deobfuscateAssistantContent,
|
|
237
238
|
deobfuscateSessionContext,
|
|
239
|
+
obfuscateMessages,
|
|
238
240
|
obfuscateProviderContext,
|
|
239
|
-
obfuscateProviderTools,
|
|
240
241
|
type SecretObfuscator,
|
|
241
242
|
} from "../secrets/obfuscator";
|
|
242
243
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
@@ -2500,7 +2501,7 @@ export class AgentSession {
|
|
|
2500
2501
|
const obfuscator = this.#obfuscator;
|
|
2501
2502
|
if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
|
|
2502
2503
|
const message = event.message;
|
|
2503
|
-
const deobfuscatedContent = obfuscator
|
|
2504
|
+
const deobfuscatedContent = deobfuscateAssistantContent(obfuscator, message.content);
|
|
2504
2505
|
if (deobfuscatedContent !== message.content) {
|
|
2505
2506
|
displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
|
|
2506
2507
|
}
|
|
@@ -5232,28 +5233,17 @@ export class AgentSession {
|
|
|
5232
5233
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
5233
5234
|
}
|
|
5234
5235
|
|
|
5235
|
-
#obfuscateForProvider<T>(value: T): T {
|
|
5236
|
-
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
5237
|
-
return this.#obfuscator.obfuscateObject(value);
|
|
5238
|
-
}
|
|
5239
|
-
|
|
5240
5236
|
#obfuscateTextForProvider(text: string | undefined): string | undefined {
|
|
5241
5237
|
if (!text || !this.#obfuscator?.hasSecrets()) return text;
|
|
5242
5238
|
return this.#obfuscator.obfuscate(text);
|
|
5243
5239
|
}
|
|
5244
5240
|
|
|
5245
5241
|
#obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
|
|
5246
|
-
if (!this.#obfuscator?.hasSecrets()) return preparation;
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
? this.#obfuscator.obfuscate(preparation.previousSummary)
|
|
5252
|
-
: preparation.previousSummary,
|
|
5253
|
-
previousPreserveData: preparation.previousPreserveData
|
|
5254
|
-
? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
|
|
5255
|
-
: preparation.previousPreserveData,
|
|
5256
|
-
};
|
|
5242
|
+
if (!this.#obfuscator?.hasSecrets() || !preparation.previousSummary) return preparation;
|
|
5243
|
+
// `previousPreserveData` is opaque provider-replay state (e.g. OpenAI remote-compaction
|
|
5244
|
+
// `encrypted_content`); rewriting it would corrupt replay, so only the plaintext summary
|
|
5245
|
+
// is redacted.
|
|
5246
|
+
return { ...preparation, previousSummary: this.#obfuscator.obfuscate(preparation.previousSummary) };
|
|
5257
5247
|
}
|
|
5258
5248
|
|
|
5259
5249
|
#deobfuscateFromProvider(text: string): string {
|
|
@@ -5270,7 +5260,8 @@ export class AgentSession {
|
|
|
5270
5260
|
}
|
|
5271
5261
|
|
|
5272
5262
|
#convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
|
|
5273
|
-
|
|
5263
|
+
const converted = convertToLlm(messages);
|
|
5264
|
+
return this.#obfuscator?.hasSecrets() ? obfuscateMessages(this.#obfuscator, converted) : converted;
|
|
5274
5265
|
}
|
|
5275
5266
|
|
|
5276
5267
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
@@ -7865,8 +7856,8 @@ export class AgentSession {
|
|
|
7865
7856
|
compactionAbortController.signal,
|
|
7866
7857
|
{
|
|
7867
7858
|
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
7868
|
-
extraContext:
|
|
7869
|
-
remoteInstructions: this.#
|
|
7859
|
+
extraContext: compactionPrep.hookContext,
|
|
7860
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
7870
7861
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
7871
7862
|
},
|
|
7872
7863
|
);
|
|
@@ -8060,11 +8051,10 @@ export class AgentSession {
|
|
|
8060
8051
|
model,
|
|
8061
8052
|
this.#modelRegistry.resolver(model, this.sessionId),
|
|
8062
8053
|
{
|
|
8063
|
-
systemPrompt: this.#
|
|
8064
|
-
tools:
|
|
8065
|
-
this
|
|
8066
|
-
|
|
8067
|
-
),
|
|
8054
|
+
systemPrompt: this.#baseSystemPrompt,
|
|
8055
|
+
tools: this.#pruneToolDescriptions
|
|
8056
|
+
? stripToolDescriptions(this.agent.state.tools)
|
|
8057
|
+
: this.agent.state.tools,
|
|
8068
8058
|
customInstructions: this.#obfuscateTextForProvider(customInstructions),
|
|
8069
8059
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
8070
8060
|
initiatorOverride: "agent",
|
|
@@ -9752,8 +9742,8 @@ export class AgentSession {
|
|
|
9752
9742
|
autoCompactionSignal,
|
|
9753
9743
|
{
|
|
9754
9744
|
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
9755
|
-
extraContext:
|
|
9756
|
-
remoteInstructions: this.#
|
|
9745
|
+
extraContext: compactionPrep.hookContext,
|
|
9746
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
9757
9747
|
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
9758
9748
|
initiatorOverride: "agent",
|
|
9759
9749
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
@@ -11379,7 +11369,7 @@ export class AgentSession {
|
|
|
11379
11369
|
}
|
|
11380
11370
|
if (event.type === "done") {
|
|
11381
11371
|
assistantMessage = this.#obfuscator?.hasSecrets()
|
|
11382
|
-
? { ...event.message, content: this.#obfuscator
|
|
11372
|
+
? { ...event.message, content: deobfuscateAssistantContent(this.#obfuscator, event.message.content) }
|
|
11383
11373
|
: event.message;
|
|
11384
11374
|
break;
|
|
11385
11375
|
}
|
|
@@ -9,10 +9,9 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Ollama and its local-backend family decode image input through llama.cpp /
|
|
12
|
-
* `stb_image`, which is compiled without WebP support
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* automatic equivalent of `OMP_NO_WEBP=1`.
|
|
12
|
+
* `stb_image`, which is compiled without WebP support, so a WebP upload fails
|
|
13
|
+
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
|
14
|
+
* to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
|
|
16
15
|
*/
|
|
17
16
|
export function modelLacksWebpSupport(
|
|
18
17
|
model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
|
|
@@ -20,8 +19,6 @@ export function modelLacksWebpSupport(
|
|
|
20
19
|
if (!model) return false;
|
|
21
20
|
return (
|
|
22
21
|
model.imageInputDecoder === "stb" ||
|
|
23
|
-
model.api === "openai-codex-responses" ||
|
|
24
|
-
model.provider === "openai-codex" ||
|
|
25
22
|
model.provider === "ollama" ||
|
|
26
23
|
model.provider === "ollama-cloud" ||
|
|
27
24
|
model.provider === "llama.cpp" ||
|