@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.
@@ -1,6 +1,6 @@
1
1
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
2
2
  import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
3
- import type { SecretObfuscator } from "../secrets/obfuscator";
3
+ import { type SecretObfuscator } from "../secrets/obfuscator";
4
4
  import type { SessionManager } from "../session/session-manager";
5
5
  import { type SessionData } from "./html";
6
6
  export { DEFAULT_SHARE_URL };
@@ -12,10 +12,15 @@ export interface ShareSessionOptions {
12
12
  /** Agent state for system prompt + tool descriptions in the snapshot. */
13
13
  state?: AgentState;
14
14
  /**
15
- * Redacts the snapshot before sealing: deep-walks every string (entries,
16
- * header, system prompt, tool descriptions) through the obfuscator, so
17
- * secrets that landed in persisted entries (tool outputs reading .env,
18
- * etc.) never leave the machine. Pass undefined to skip.
15
+ * Redacts the snapshot before sealing via a typed, per-field walk over the
16
+ * session (header title/cwd, system prompt, tool descriptions, entry summaries,
17
+ * labels, and message text including tool-result output and `@file` mentions),
18
+ * so secrets that landed in persisted entries (tool outputs reading .env, etc.)
19
+ * never leave the machine. Inline image bytes are preserved (size-trimmed
20
+ * separately); opaque provider-replay blobs (`providerPayload`,
21
+ * `redactedThinking`, `compaction.preserveData`) and untyped extension payloads
22
+ * (`details`/`data`/`outputSchema`) are dropped rather than walked. Pass
23
+ * undefined to skip redaction entirely.
19
24
  */
20
25
  obfuscator?: SecretObfuscator;
21
26
  }
@@ -1,5 +1,5 @@
1
1
  import type { SecretEntry } from "./obfuscator";
2
- export { deobfuscateSessionContext, obfuscateMessages, obfuscateProviderContext, obfuscateProviderTools, type SecretEntry, SecretObfuscator, } from "./obfuscator";
2
+ export { deobfuscateSessionContext, deobfuscateToolArguments, obfuscateMessages, obfuscateProviderContext, type SecretEntry, SecretObfuscator, } from "./obfuscator";
3
3
  /**
4
4
  * Load secrets from project-local and global secrets.yml files.
5
5
  * Project-local entries override global entries with matching content.
@@ -1,4 +1,5 @@
1
- import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Context, Message } from "@oh-my-pi/pi-ai";
2
3
  import type { SessionContext } from "../session/session-context";
3
4
  export interface SecretEntry {
4
5
  type: "plain" | "regex";
@@ -7,6 +8,12 @@ export interface SecretEntry {
7
8
  replacement?: string;
8
9
  flags?: string;
9
10
  }
11
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
12
+ [key: string]: JsonValue | undefined;
13
+ };
14
+ export type JsonRecord = {
15
+ [key: string]: JsonValue | undefined;
16
+ };
10
17
  export declare class SecretObfuscator {
11
18
  #private;
12
19
  constructor(entries: SecretEntry[]);
@@ -15,15 +22,42 @@ export declare class SecretObfuscator {
15
22
  obfuscate(text: string): string;
16
23
  /** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
17
24
  deobfuscate(text: string): string;
18
- /** Deep-walk an object, deobfuscating all string values. */
19
- deobfuscateObject<T>(obj: T): T;
20
- /** Deep-walk an object, obfuscating all string values. */
21
- obfuscateObject<T>(obj: T): T;
22
25
  }
26
+ /**
27
+ * Restore secret placeholders for local display. Only message kinds the model
28
+ * itself authored from obfuscated context carry placeholders — assistant
29
+ * content and the LLM-written branch/compaction summaries. User, developer, and
30
+ * tool-result messages are persisted with their literal text, so a literal
31
+ * `#ABCD#` the operator typed must survive untouched; those roles are never
32
+ * walked.
33
+ */
23
34
  export declare function deobfuscateSessionContext(sessionContext: SessionContext, obfuscator: SecretObfuscator | undefined): SessionContext;
24
- /** Obfuscate all string content in LLM messages (for outbound interception). */
35
+ export declare function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[];
36
+ /**
37
+ * Restore placeholders in assistant content: visible text, thinking text, and
38
+ * tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
39
+ * are opaque provider-replay data and pass through byte-identical.
40
+ */
41
+ export declare function deobfuscateAssistantContent(obfuscator: SecretObfuscator, content: AssistantMessage["content"]): AssistantMessage["content"];
42
+ /**
43
+ * Restore placeholders inside a tool call's arguments. Arguments are arbitrary
44
+ * model-authored JSON, so tool-call arguments are the ONLY place a recursive
45
+ * JSON walk runs.
46
+ */
47
+ export declare function deobfuscateToolArguments(obfuscator: SecretObfuscator, args: Record<string, unknown>): Record<string, unknown>;
48
+ /** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
49
+ export declare function obfuscateToolArguments(obfuscator: SecretObfuscator, args: Record<string, unknown>): Record<string, unknown>;
50
+ /**
51
+ * Redact secrets from outbound messages. Opt-in by origin: only user messages,
52
+ * tool results, and user-authored developer messages (e.g. `@file` mentions)
53
+ * can carry operator secrets. System prompts, tool schemas, and assistant
54
+ * output are author-controlled or model-generated and pass through untouched.
55
+ * Within a targeted message only `text` blocks are rewritten — inline image
56
+ * bytes are never walked.
57
+ */
25
58
  export declare function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[];
26
- /** Obfuscate provider request context without walking live tool schema instances. */
59
+ /**
60
+ * Redact outbound provider context. Only conversation messages are rewritten;
61
+ * the static system prompt and tool schemas pass through unchanged.
62
+ */
27
63
  export declare function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context;
28
- /** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
29
- export declare function obfuscateProviderTools(obfuscator: SecretObfuscator | undefined, tools: Tool[] | undefined): Tool[] | undefined;
@@ -4,10 +4,9 @@ export declare const MAX_IMAGE_INPUT_BYTES: number;
4
4
  export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: Set<string>;
5
5
  /**
6
6
  * Ollama and its local-backend family decode image input through llama.cpp /
7
- * `stb_image`, which is compiled without WebP support. The first-party Codex
8
- * Responses backend also rejects WebP `input_image.image_url` data URLs. Detect
9
- * those models so the resize pipeline encodes to PNG/JPEG instead — the
10
- * automatic equivalent of `OMP_NO_WEBP=1`.
7
+ * `stb_image`, which is compiled without WebP support, so a WebP upload fails
8
+ * with an opaque HTTP 400. Detect those models so the resize pipeline encodes
9
+ * to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
11
10
  */
12
11
  export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined): boolean;
13
12
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.1.11",
4
+ "version": "16.1.12",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.11",
52
- "@oh-my-pi/omp-stats": "16.1.11",
53
- "@oh-my-pi/pi-agent-core": "16.1.11",
54
- "@oh-my-pi/pi-ai": "16.1.11",
55
- "@oh-my-pi/pi-catalog": "16.1.11",
56
- "@oh-my-pi/pi-mnemopi": "16.1.11",
57
- "@oh-my-pi/pi-natives": "16.1.11",
58
- "@oh-my-pi/pi-tui": "16.1.11",
59
- "@oh-my-pi/pi-utils": "16.1.11",
60
- "@oh-my-pi/pi-wire": "16.1.11",
61
- "@oh-my-pi/snapcompact": "16.1.11",
51
+ "@oh-my-pi/hashline": "16.1.12",
52
+ "@oh-my-pi/omp-stats": "16.1.12",
53
+ "@oh-my-pi/pi-agent-core": "16.1.12",
54
+ "@oh-my-pi/pi-ai": "16.1.12",
55
+ "@oh-my-pi/pi-catalog": "16.1.12",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.12",
57
+ "@oh-my-pi/pi-natives": "16.1.12",
58
+ "@oh-my-pi/pi-tui": "16.1.12",
59
+ "@oh-my-pi/pi-utils": "16.1.12",
60
+ "@oh-my-pi/pi-wire": "16.1.12",
61
+ "@oh-my-pi/snapcompact": "16.1.12",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -19,13 +19,16 @@
19
19
  import * as fs from "node:fs/promises";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
- import type { AgentState } from "@oh-my-pi/pi-agent-core";
22
+ import type { AgentMessage, AgentState } from "@oh-my-pi/pi-agent-core";
23
+ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
23
24
  import { $which, logger } from "@oh-my-pi/pi-utils";
24
25
  import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
25
26
  import { $ } from "bun";
26
- import type { SecretObfuscator } from "../secrets/obfuscator";
27
+ import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
28
+ import type { SessionEntry, SessionHeader } from "../session/session-entries";
27
29
  import type { SessionManager } from "../session/session-manager";
28
- import { buildSessionData, type SessionData } from "./html";
30
+ import type { OutputMeta } from "../tools/output-meta";
31
+ import { buildSessionData, type SessionData, type SubSession } from "./html";
29
32
 
30
33
  export { DEFAULT_SHARE_URL };
31
34
 
@@ -53,10 +56,15 @@ export interface ShareSessionOptions {
53
56
  /** Agent state for system prompt + tool descriptions in the snapshot. */
54
57
  state?: AgentState;
55
58
  /**
56
- * Redacts the snapshot before sealing: deep-walks every string (entries,
57
- * header, system prompt, tool descriptions) through the obfuscator, so
58
- * secrets that landed in persisted entries (tool outputs reading .env,
59
- * etc.) never leave the machine. Pass undefined to skip.
59
+ * Redacts the snapshot before sealing via a typed, per-field walk over the
60
+ * session (header title/cwd, system prompt, tool descriptions, entry summaries,
61
+ * labels, and message text including tool-result output and `@file` mentions),
62
+ * so secrets that landed in persisted entries (tool outputs reading .env, etc.)
63
+ * never leave the machine. Inline image bytes are preserved (size-trimmed
64
+ * separately); opaque provider-replay blobs (`providerPayload`,
65
+ * `redactedThinking`, `compaction.preserveData`) and untyped extension payloads
66
+ * (`details`/`data`/`outputSchema`) are dropped rather than walked. Pass
67
+ * undefined to skip redaction entirely.
60
68
  */
61
69
  obfuscator?: SecretObfuscator;
62
70
  }
@@ -75,7 +83,189 @@ export interface ShareSessionResult {
75
83
  /** Build the snapshot that gets sealed and uploaded, redacted when an obfuscator is provided. */
76
84
  export function buildShareSnapshot(sm: SessionManager, options?: ShareSessionOptions): SessionData {
77
85
  const data = buildSessionData(sm, options?.state);
78
- return options?.obfuscator?.hasSecrets() ? options.obfuscator.obfuscateObject(data) : data;
86
+ return options?.obfuscator?.hasSecrets() ? redactSessionDataForShare(options.obfuscator, data) : data;
87
+ }
88
+
89
+ /**
90
+ * Redact secrets from a share snapshot. A share blob leaves the machine, so
91
+ * every text-bearing field is rewritten through the obfuscator. The walk is
92
+ * typed end-to-end (no generic object traversal): inline image bytes are left
93
+ * intact (size-trimmed later by {@link stripImagePayloads}) and opaque,
94
+ * untyped payloads we cannot redact field-by-field (`compaction.preserveData`,
95
+ * extension `details`/`data`, `mode_change.data`, structured output schemas)
96
+ * are dropped so they cannot leak.
97
+ */
98
+ function redactShareHeader(o: SecretObfuscator, header: SessionHeader | null): SessionHeader | null {
99
+ if (!header) return header;
100
+ return {
101
+ ...header,
102
+ title: header.title === undefined ? undefined : o.obfuscate(header.title),
103
+ cwd: o.obfuscate(header.cwd),
104
+ };
105
+ }
106
+
107
+ function redactSessionDataForShare(o: SecretObfuscator, data: SessionData): SessionData {
108
+ return {
109
+ ...data,
110
+ header: redactShareHeader(o, data.header),
111
+ systemPrompt: data.systemPrompt === undefined ? undefined : o.obfuscate(data.systemPrompt),
112
+ tools: data.tools?.map(tool => ({ ...tool, description: o.obfuscate(tool.description) })),
113
+ entries: data.entries.map(entry => redactShareEntry(o, entry)),
114
+ subSessions: data.subSessions
115
+ ? Object.fromEntries(
116
+ Object.entries(data.subSessions).map(([key, sub]) => [key, redactShareSubSession(o, sub)]),
117
+ )
118
+ : data.subSessions,
119
+ };
120
+ }
121
+
122
+ function redactShareSubSession(o: SecretObfuscator, sub: SubSession): SubSession {
123
+ return {
124
+ ...sub,
125
+ header: redactShareHeader(o, sub.header),
126
+ entries: sub.entries.map(entry => redactShareEntry(o, entry)),
127
+ };
128
+ }
129
+
130
+ function redactShareEntry(o: SecretObfuscator, entry: SessionEntry): SessionEntry {
131
+ switch (entry.type) {
132
+ case "message":
133
+ return { ...entry, message: redactShareMessage(o, entry.message) };
134
+ case "compaction":
135
+ return {
136
+ ...entry,
137
+ summary: o.obfuscate(entry.summary),
138
+ shortSummary: entry.shortSummary === undefined ? undefined : o.obfuscate(entry.shortSummary),
139
+ details: undefined,
140
+ preserveData: undefined,
141
+ };
142
+ case "branch_summary":
143
+ return { ...entry, summary: o.obfuscate(entry.summary), details: undefined };
144
+ case "custom_message":
145
+ return { ...entry, content: redactShareContent(o, entry.content), details: undefined };
146
+ case "custom":
147
+ return { ...entry, data: undefined };
148
+ case "mode_change":
149
+ return { ...entry, data: undefined };
150
+ case "session_init":
151
+ return {
152
+ ...entry,
153
+ systemPrompt: o.obfuscate(entry.systemPrompt),
154
+ task: o.obfuscate(entry.task),
155
+ outputSchema: undefined,
156
+ };
157
+ case "label":
158
+ return { ...entry, label: entry.label === undefined ? undefined : o.obfuscate(entry.label) };
159
+ default:
160
+ return entry;
161
+ }
162
+ }
163
+
164
+ function redactShareContent(
165
+ o: SecretObfuscator,
166
+ content: string | (TextContent | ImageContent)[],
167
+ ): string | (TextContent | ImageContent)[] {
168
+ if (typeof content === "string") return o.obfuscate(content);
169
+ return content.map(block => (block.type === "text" ? { ...block, text: o.obfuscate(block.text) } : block));
170
+ }
171
+
172
+ /** Redact freeform strings in tool output metadata (source path/URL, diagnostics); numeric truncation info is preserved. */
173
+ function redactShareOutputMeta(o: SecretObfuscator, meta: OutputMeta | undefined): OutputMeta | undefined {
174
+ if (!meta) return meta;
175
+ return {
176
+ ...meta,
177
+ source: meta.source ? { ...meta.source, value: o.obfuscate(meta.source.value) } : meta.source,
178
+ diagnostics: meta.diagnostics
179
+ ? {
180
+ summary: o.obfuscate(meta.diagnostics.summary),
181
+ messages: meta.diagnostics.messages.map(message => o.obfuscate(message)),
182
+ }
183
+ : meta.diagnostics,
184
+ };
185
+ }
186
+
187
+ function redactShareMessage(o: SecretObfuscator, message: AgentMessage): AgentMessage {
188
+ switch (message.role) {
189
+ case "user":
190
+ case "developer":
191
+ return {
192
+ ...message,
193
+ providerPayload: undefined,
194
+ content: redactShareContent(o, message.content),
195
+ } as AgentMessage;
196
+ case "custom":
197
+ case "hookMessage":
198
+ return { ...message, details: undefined, content: redactShareContent(o, message.content) } as AgentMessage;
199
+ case "toolResult":
200
+ return {
201
+ ...message,
202
+ details: undefined,
203
+ content: redactShareContent(o, message.content) as (TextContent | ImageContent)[],
204
+ };
205
+ case "assistant":
206
+ // Drop opaque provider-replay state (encrypted reasoning / native history) the viewer
207
+ // never reads and we cannot redact field-by-field: `providerPayload` and any
208
+ // `redactedThinking` blocks.
209
+ return {
210
+ ...message,
211
+ providerPayload: undefined,
212
+ errorMessage: message.errorMessage === undefined ? undefined : o.obfuscate(message.errorMessage),
213
+ content: message.content.flatMap((block): AssistantMessage["content"] => {
214
+ if (block.type === "redactedThinking") return [];
215
+ if (block.type === "text") return [{ ...block, text: o.obfuscate(block.text) }];
216
+ if (block.type === "thinking") return [{ ...block, thinking: o.obfuscate(block.thinking) }];
217
+ if (block.type === "toolCall") {
218
+ return [
219
+ {
220
+ ...block,
221
+ arguments: obfuscateToolArguments(o, block.arguments),
222
+ intent: block.intent === undefined ? undefined : o.obfuscate(block.intent),
223
+ rawBlock: block.rawBlock === undefined ? undefined : o.obfuscate(block.rawBlock),
224
+ },
225
+ ];
226
+ }
227
+ return [block];
228
+ }),
229
+ };
230
+ case "bashExecution":
231
+ return {
232
+ ...message,
233
+ command: o.obfuscate(message.command),
234
+ output: o.obfuscate(message.output),
235
+ meta: redactShareOutputMeta(o, message.meta),
236
+ };
237
+ case "pythonExecution":
238
+ return {
239
+ ...message,
240
+ code: o.obfuscate(message.code),
241
+ output: o.obfuscate(message.output),
242
+ meta: redactShareOutputMeta(o, message.meta),
243
+ };
244
+ case "branchSummary":
245
+ return { ...message, summary: o.obfuscate(message.summary) };
246
+ case "compactionSummary":
247
+ return {
248
+ ...message,
249
+ providerPayload: undefined,
250
+ summary: o.obfuscate(message.summary),
251
+ shortSummary: message.shortSummary === undefined ? undefined : o.obfuscate(message.shortSummary),
252
+ blocks:
253
+ message.blocks === undefined
254
+ ? undefined
255
+ : (redactShareContent(o, message.blocks) as (TextContent | ImageContent)[]),
256
+ };
257
+ case "fileMention":
258
+ return {
259
+ ...message,
260
+ files: message.files.map(file => ({
261
+ ...file,
262
+ path: o.obfuscate(file.path),
263
+ content: o.obfuscate(file.content),
264
+ })),
265
+ };
266
+ default:
267
+ return message;
268
+ }
79
269
  }
80
270
 
81
271
  /** Share the session; tries a secret gist first, then the share server. */
package/src/main.ts CHANGED
@@ -149,8 +149,16 @@ const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
149
149
  "bash.autoBackground.thresholdMs",
150
150
  ];
151
151
 
152
+ // Protocol-mode hosts opt into a small set of paths whose host-default we
153
+ // re-apply at startup so embedders inherit OMP's neutral defaults instead of
154
+ // the local user's globally-persisted preferences for interactive use. The
155
+ // guard preserves any explicit configuration — caller `Settings.isolated`
156
+ // overrides, project `.claude/settings.yml`, `--config` overlays, or global
157
+ // `config.yml` — so the host default only kicks in when nothing is set. Without
158
+ // it the override clobbers every caller/host choice (#2598, #3207).
152
159
  function applyDefaultSettingOverrides(settingPaths: SettingPath[], targetSettings: Settings): void {
153
160
  for (const settingPath of settingPaths) {
161
+ if (targetSettings.isConfigured(settingPath)) continue;
154
162
  targetSettings.override(settingPath, getDefault(settingPath));
155
163
  }
156
164
  }
package/src/sdk.ts CHANGED
@@ -97,6 +97,7 @@ import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
97
97
  import {
98
98
  collectEnvSecrets,
99
99
  deobfuscateSessionContext,
100
+ deobfuscateToolArguments,
100
101
  loadSecrets,
101
102
  obfuscateMessages,
102
103
  obfuscateProviderContext,
@@ -2535,7 +2536,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2535
2536
  result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
2536
2537
  }
2537
2538
  if (obfuscator?.hasSecrets()) {
2538
- result = obfuscator.deobfuscateObject(result);
2539
+ result = deobfuscateToolArguments(obfuscator, result);
2539
2540
  }
2540
2541
  return result;
2541
2542
  },
@@ -6,9 +6,9 @@ import { compileSecretRegex } from "./regex";
6
6
 
7
7
  export {
8
8
  deobfuscateSessionContext,
9
+ deobfuscateToolArguments,
9
10
  obfuscateMessages,
10
11
  obfuscateProviderContext,
11
- obfuscateProviderTools,
12
12
  type SecretEntry,
13
13
  SecretObfuscator,
14
14
  } from "./obfuscator";