@wix/pathgrade 1.0.17 → 1.0.18

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.
Files changed (35) hide show
  1. package/dist/agents/claude/sdk-message-projector.js +12 -4
  2. package/dist/agents/claude/tool-results.d.ts +1 -2
  3. package/dist/agents/claude/tool-results.js +9 -40
  4. package/dist/agents/claude.js +6 -4
  5. package/dist/agents/codex-app-server/agent.d.ts +3 -0
  6. package/dist/agents/codex-app-server/agent.js +109 -162
  7. package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
  8. package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
  9. package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
  10. package/dist/agents/codex-app-server/item-projection.js +135 -0
  11. package/dist/agents/opencode/host-safety.d.ts +3 -0
  12. package/dist/agents/opencode/host-safety.js +30 -0
  13. package/dist/agents/opencode.d.ts +2 -4
  14. package/dist/agents/opencode.js +47 -38
  15. package/dist/providers/credentials.d.ts +2 -0
  16. package/dist/providers/credentials.js +1 -0
  17. package/dist/providers/scripted-mcp-mock-host.js +6 -3
  18. package/dist/providers/workspace.d.ts +1 -0
  19. package/dist/providers/workspace.js +5 -0
  20. package/dist/sdk/agent-result-log.js +4 -2
  21. package/dist/sdk/agent.js +6 -0
  22. package/dist/sdk/managed-session.d.ts +2 -0
  23. package/dist/sdk/managed-session.js +22 -5
  24. package/dist/sdk/mcp-safety.js +2 -18
  25. package/dist/sdk/snapshots.d.ts +1 -0
  26. package/dist/sdk/snapshots.js +3 -2
  27. package/dist/sdk/tool-event-log.js +5 -2
  28. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  29. package/dist/sdk/tool-event-secrets.js +14 -0
  30. package/dist/sdk/turn-result-secrets.d.ts +5 -0
  31. package/dist/sdk/turn-result-secrets.js +12 -0
  32. package/dist/tool-event-results.d.ts +10 -0
  33. package/dist/tool-event-results.js +171 -0
  34. package/dist/types.d.ts +2 -0
  35. package/package.json +2 -2
@@ -0,0 +1,95 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { canonicalizeJson } from '../../core/canonical-json.js';
3
+ export class CodexItemLifecycle {
4
+ options;
5
+ turnId;
6
+ pending = [];
7
+ started = new Map();
8
+ completed = new Map();
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ receiveStarted(params) {
13
+ this.receive('started', params);
14
+ }
15
+ receiveCompleted(params) {
16
+ this.receive('completed', params);
17
+ }
18
+ setAuthoritativeTurn(turnId) {
19
+ if (!turnId) {
20
+ this.options.onFailure('turn/start did not return an authoritative turn id');
21
+ return false;
22
+ }
23
+ if (this.turnId && this.turnId !== turnId) {
24
+ this.options.onFailure('turn/start returned conflicting authoritative turn ids');
25
+ return false;
26
+ }
27
+ this.turnId = turnId;
28
+ for (const entry of this.pending.splice(0))
29
+ this.process(entry.phase, entry.receipt);
30
+ return true;
31
+ }
32
+ receive(phase, params) {
33
+ const clock = this.options.clock ?? DEFAULT_CLOCK;
34
+ const receipt = { params, wallMs: clock.wallNow(), monotonicMs: clock.monotonicNow() };
35
+ if (!this.validateEnvelope(params))
36
+ return;
37
+ if (!this.turnId) {
38
+ this.pending.push({ phase, receipt });
39
+ return;
40
+ }
41
+ this.process(phase, receipt);
42
+ }
43
+ validateEnvelope(params) {
44
+ if (!params.item || typeof params.item.id !== 'string' || !params.item.id
45
+ || typeof params.threadId !== 'string' || !params.threadId
46
+ || typeof params.turnId !== 'string' || !params.turnId) {
47
+ this.options.onFailure('Codex item lifecycle notification is missing threadId, turnId, or item.id');
48
+ return false;
49
+ }
50
+ // Notifications for an earlier thread are stale, not failures of this turn.
51
+ return params.threadId === this.options.threadId;
52
+ }
53
+ process(phase, receipt) {
54
+ const { params } = receipt;
55
+ if (params.turnId !== this.turnId || !params.item || typeof params.item.id !== 'string')
56
+ return;
57
+ const itemId = params.item.id;
58
+ const fingerprint = canonicalizeJson(params.item);
59
+ if (phase === 'started') {
60
+ const prior = this.started.get(itemId);
61
+ if (prior) {
62
+ if (prior.fingerprint !== fingerprint) {
63
+ this.options.onFailure(`Conflicting item/started notification for item ${itemId}`);
64
+ }
65
+ return;
66
+ }
67
+ if (this.completed.has(itemId)) {
68
+ this.options.onFailure(`item/started arrived after item/completed for item ${itemId}`);
69
+ return;
70
+ }
71
+ this.started.set(itemId, { ...receipt, fingerprint });
72
+ this.options.onStarted?.(params);
73
+ return;
74
+ }
75
+ const priorTerminal = this.completed.get(itemId);
76
+ if (priorTerminal !== undefined) {
77
+ if (priorTerminal !== fingerprint) {
78
+ this.options.onFailure(`Conflicting item/completed notification for item ${itemId}`);
79
+ }
80
+ return;
81
+ }
82
+ this.completed.set(itemId, fingerprint);
83
+ const start = this.started.get(itemId);
84
+ this.started.delete(itemId);
85
+ this.options.onCompleted(params.item, {
86
+ ...(start ? { startedAtWallMs: start.wallMs } : {}),
87
+ completedAtWallMs: receipt.wallMs,
88
+ ...(start ? { observedDurationMs: Math.max(0, receipt.monotonicMs - start.monotonicMs) } : {}),
89
+ }, params);
90
+ }
91
+ }
92
+ const DEFAULT_CLOCK = {
93
+ wallNow: () => Date.now(),
94
+ monotonicNow: () => performance.now(),
95
+ };
@@ -0,0 +1,62 @@
1
+ import { type ToolEvent } from '../../tool-events.js';
2
+ export interface CodexTurnProjection {
3
+ turnNumber: number;
4
+ nonAskToolEvents: ToolEvent[];
5
+ assistantMessageParts: string[];
6
+ }
7
+ interface CodexCommandExecutionAction {
8
+ type?: string;
9
+ name?: string;
10
+ path?: string;
11
+ command?: string;
12
+ }
13
+ interface CodexCommandExecutionItem {
14
+ type: 'commandExecution';
15
+ id: string;
16
+ command: string;
17
+ status?: 'inProgress' | 'completed' | 'failed' | 'declined';
18
+ cwd?: string;
19
+ commandActions?: CodexCommandExecutionAction[];
20
+ aggregatedOutput?: string | null;
21
+ exitCode?: number | null;
22
+ durationMs?: number | null;
23
+ }
24
+ interface CodexMcpToolCallItem {
25
+ type: 'mcpToolCall';
26
+ id: string;
27
+ server: string;
28
+ tool: string;
29
+ status?: string;
30
+ arguments?: unknown;
31
+ result?: unknown;
32
+ error?: {
33
+ message?: string;
34
+ } | null;
35
+ durationMs?: number | null;
36
+ }
37
+ export type CodexItem = {
38
+ type: 'agentMessage';
39
+ id: string;
40
+ text: string;
41
+ phase?: string;
42
+ } | CodexCommandExecutionItem | {
43
+ type: 'fileChange';
44
+ id: string;
45
+ changes: Array<{
46
+ path: string;
47
+ kind?: unknown;
48
+ diff?: string;
49
+ }>;
50
+ } | CodexMcpToolCallItem | {
51
+ type: string;
52
+ id?: string;
53
+ };
54
+ export interface ItemTiming {
55
+ startedAtWallMs?: number;
56
+ completedAtWallMs?: number;
57
+ observedDurationMs?: number;
58
+ }
59
+ export declare function projectItemIntoTurn(item: CodexItem, turn: CodexTurnProjection, sensitiveValues: readonly string[], timing?: ItemTiming): void;
60
+ /** Provider duration is authoritative; observed monotonic duration is the fallback. */
61
+ export declare function projectToolTiming(timing: ItemTiming, providerDurationMs: number | undefined): Pick<ToolEvent, 'startedAt' | 'completedAt' | 'durationMs'>;
62
+ export {};
@@ -0,0 +1,135 @@
1
+ import { buildSummary, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
2
+ import { sanitizeToolEventResult } from '../../tool-event-results.js';
3
+ import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
4
+ export function projectItemIntoTurn(item, turn, sensitiveValues, timing = {}) {
5
+ if (item.type === 'agentMessage') {
6
+ const message = item;
7
+ if (message.text)
8
+ turn.assistantMessageParts.push(message.text);
9
+ return;
10
+ }
11
+ if (item.type === 'commandExecution') {
12
+ projectCommand(item, turn, sensitiveValues, timing);
13
+ return;
14
+ }
15
+ if (item.type === 'fileChange') {
16
+ const changes = item.changes;
17
+ for (const change of changes ?? []) {
18
+ turn.nonAskToolEvents.push({
19
+ action: 'edit_file', provider: 'codex', providerToolName: 'fileChange',
20
+ turnNumber: turn.turnNumber, arguments: { file_path: change.path },
21
+ summary: `edit_file: ${change.path}`, confidence: 'high',
22
+ rawSnippet: JSON.stringify(change),
23
+ });
24
+ }
25
+ return;
26
+ }
27
+ if (item.type === 'mcpToolCall') {
28
+ projectMcpCall(item, turn, sensitiveValues, timing);
29
+ }
30
+ }
31
+ function projectCommand(cmd, turn, sensitiveValues, timing) {
32
+ const action = inferCodexExecAction(cmd.command);
33
+ const skillPath = extractSkillPathFromText(cmd.command);
34
+ const args = { command: cmd.command, ...(skillPath ? { path: skillPath } : {}) };
35
+ const exitCode = finiteNumber(cmd.exitCode);
36
+ const failed = cmd.status === 'failed' || cmd.status === 'declined'
37
+ || (exitCode !== undefined && exitCode !== 0);
38
+ const status = failed ? 'error' : cmd.status === 'completed' ? 'completed' : 'incomplete';
39
+ const timingFields = projectToolTiming(timing, finiteNumber(cmd.durationMs));
40
+ turn.nonAskToolEvents.push({
41
+ action, provider: 'codex', providerToolName: 'commandExecution', toolUseId: cmd.id,
42
+ turnNumber: turn.turnNumber, arguments: args, status, ...timingFields,
43
+ ...(typeof cmd.aggregatedOutput === 'string' || exitCode !== undefined ? {
44
+ result: sanitizeToolEventResult({
45
+ ...(typeof cmd.aggregatedOutput === 'string' ? { content: cmd.aggregatedOutput } : {}),
46
+ ...(exitCode !== undefined ? { exitCode } : {}),
47
+ }, sensitiveValues),
48
+ } : {}),
49
+ summary: buildSummary(action, 'commandExecution', args), confidence: 'high',
50
+ rawSnippet: JSON.stringify({
51
+ type: cmd.type, id: cmd.id, command: cmd.command, status: cmd.status,
52
+ cwd: cmd.cwd, commandActions: cmd.commandActions,
53
+ }),
54
+ });
55
+ const recordedSkills = new Set();
56
+ for (const commandAction of cmd.commandActions ?? []) {
57
+ const skillName = extractCommandActionSkillName(commandAction) ?? extractSkillNameFromText(cmd.command);
58
+ if (!skillName || recordedSkills.has(skillName))
59
+ continue;
60
+ recordedSkills.add(skillName);
61
+ turn.nonAskToolEvents.push({
62
+ action: 'use_skill', provider: 'codex',
63
+ providerToolName: `commandExecution.commandActions.${commandAction.type ?? 'unknown'}`,
64
+ turnNumber: turn.turnNumber, arguments: { path: commandAction.path, name: commandAction.name },
65
+ summary: `use_skill ${skillName}`, confidence: 'high', rawSnippet: JSON.stringify(commandAction), skillName,
66
+ });
67
+ }
68
+ }
69
+ function projectMcpCall(call, turn, sensitiveValues, timing) {
70
+ if (turn.nonAskToolEvents.some((event) => event.action === 'mcp_tool_call'
71
+ && event.toolUseId === call.id && event.mcp?.invocation === 'not_invoked'))
72
+ return;
73
+ const args = recordFromUnknown(call.arguments);
74
+ const providerToolName = `${call.server}.${call.tool}`;
75
+ const error = typeof call.error?.message === 'string' ? call.error.message : undefined;
76
+ const status = call.status === 'completed' && error === undefined
77
+ ? 'completed' : call.status === 'failed' || error !== undefined ? 'error' : 'incomplete';
78
+ const resultContent = call.result === undefined ? undefined : JSON.stringify(call.result);
79
+ turn.nonAskToolEvents.push(attachOriginalMcpInput({
80
+ action: 'mcp_tool_call', provider: 'codex', providerToolName, toolUseId: call.id,
81
+ turnNumber: turn.turnNumber, status, ...projectToolTiming(timing, finiteNumber(call.durationMs)),
82
+ arguments: { ...args, server: call.server, tool: call.tool, status: call.status ?? 'unknown' },
83
+ summary: `MCP tool ${providerToolName} ${call.status ?? 'unknown'}`, confidence: 'high',
84
+ rawSnippet: JSON.stringify({
85
+ type: call.type, id: call.id, server: call.server, tool: call.tool,
86
+ status: call.status, arguments: call.arguments, durationMs: call.durationMs,
87
+ }),
88
+ ...(resultContent !== undefined || error !== undefined ? {
89
+ result: sanitizeToolEventResult({
90
+ ...(resultContent !== undefined ? { content: resultContent } : {}),
91
+ ...(error !== undefined ? { content: error } : {}),
92
+ }, sensitiveValues),
93
+ } : {}),
94
+ }, args));
95
+ }
96
+ /** Provider duration is authoritative; observed monotonic duration is the fallback. */
97
+ export function projectToolTiming(timing, providerDurationMs) {
98
+ const startedAtMs = finiteTimestamp(timing.startedAtWallMs);
99
+ const completedAtMs = finiteTimestamp(timing.completedAtWallMs);
100
+ const observedDurationMs = finiteTimestamp(timing.observedDurationMs);
101
+ const validProviderDurationMs = finiteTimestamp(providerDurationMs);
102
+ return {
103
+ ...(startedAtMs !== undefined ? { startedAt: new Date(startedAtMs).toISOString() } : {}),
104
+ ...(completedAtMs !== undefined ? { completedAt: new Date(completedAtMs).toISOString() } : {}),
105
+ ...(validProviderDurationMs !== undefined
106
+ ? { durationMs: validProviderDurationMs }
107
+ : observedDurationMs !== undefined ? { durationMs: observedDurationMs } : {}),
108
+ };
109
+ }
110
+ function extractCommandActionSkillName(action) {
111
+ if (typeof action.path === 'string') {
112
+ const direct = extractSkillNameFromPath(action.path);
113
+ if (direct)
114
+ return direct;
115
+ const embedded = extractSkillNameFromText(action.path);
116
+ if (embedded)
117
+ return embedded;
118
+ }
119
+ return typeof action.command === 'string' ? extractSkillNameFromText(action.command) : undefined;
120
+ }
121
+ function extractSkillNameFromText(value) {
122
+ return value?.match(/(?:^|[/\s"'])\.(?:agents|claude)\/skills\/([^/\s"']+)\/SKILL\.md(?:$|[\s"'])/)?.[1];
123
+ }
124
+ function extractSkillPathFromText(value) {
125
+ return value?.match(/(?:^|[\s"'])(?<path>(?:\/|\.{1,2}\/)?[^\s"']*(?:\.agents|\.claude)\/skills\/[^/\s"']+\/SKILL\.md)(?:$|[\s"'])/)?.groups?.path;
126
+ }
127
+ function recordFromUnknown(value) {
128
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
129
+ }
130
+ function finiteTimestamp(value) {
131
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
132
+ }
133
+ function finiteNumber(value) {
134
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
135
+ }
@@ -0,0 +1,3 @@
1
+ export declare function sha256File(filename: string): Promise<string>;
2
+ export declare function managedOpenCodeConfigPaths(platform?: NodeJS.Platform, username?: string): string[];
3
+ export declare function assertCleanManagedOpenCodeHost(candidates?: string[]): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import * as os from 'node:os';
2
+ import fs from 'fs-extra';
3
+ import { createHash } from 'node:crypto';
4
+ import { createReadStream } from 'node:fs';
5
+ export function sha256File(filename) {
6
+ return new Promise((resolve, reject) => {
7
+ const hash = createHash('sha256');
8
+ const stream = createReadStream(filename);
9
+ stream.on('error', reject);
10
+ stream.on('data', (chunk) => hash.update(chunk));
11
+ stream.on('end', () => resolve(hash.digest('hex')));
12
+ });
13
+ }
14
+ export function managedOpenCodeConfigPaths(platform = process.platform, username = os.userInfo().username) {
15
+ return platform === 'linux'
16
+ ? ['/etc/opencode/opencode.json', '/etc/opencode/opencode.jsonc']
17
+ : [
18
+ '/Library/Application Support/opencode/opencode.json',
19
+ '/Library/Application Support/opencode/opencode.jsonc',
20
+ `/Library/Managed Preferences/${username}/ai.opencode.managed.plist`,
21
+ '/Library/Managed Preferences/ai.opencode.managed.plist',
22
+ ];
23
+ }
24
+ export async function assertCleanManagedOpenCodeHost(candidates = managedOpenCodeConfigPaths()) {
25
+ for (const candidate of candidates) {
26
+ if (await fs.pathExists(candidate)) {
27
+ throw new Error(`OpenCode managed host configuration is not supported: ${candidate}`);
28
+ }
29
+ }
30
+ }
@@ -1,4 +1,5 @@
1
1
  import { BaseAgent, type AgentCommandRunner, type AgentSession, type AgentSessionOptions, type AgentTurnResult, type EnvironmentHandle } from '../types.js';
2
+ export { assertCleanManagedOpenCodeHost, managedOpenCodeConfigPaths } from './opencode/host-safety.js';
2
3
  interface SpawnResult {
3
4
  stdout: string;
4
5
  exitCode: number | null;
@@ -18,10 +19,7 @@ interface ParsedOpenCodeTurn {
18
19
  result: AgentTurnResult;
19
20
  sessionId: string;
20
21
  }
21
- export declare function parseOpenCodeOutput(stdout: string, processResult: Pick<SpawnResult, 'exitCode' | 'overflow' | 'aborted'>, mcpToolNames: ReadonlySet<string>): ParsedOpenCodeTurn;
22
- export declare function managedOpenCodeConfigPaths(platform?: NodeJS.Platform, username?: string): string[];
23
- export declare function assertCleanManagedOpenCodeHost(candidates?: string[]): Promise<void>;
22
+ export declare function parseOpenCodeOutput(stdout: string, processResult: Pick<SpawnResult, 'exitCode' | 'overflow' | 'aborted'>, mcpToolNames: ReadonlySet<string>, sensitiveValues?: readonly string[]): ParsedOpenCodeTurn;
24
23
  export declare class OpenCodeAgent extends BaseAgent {
25
24
  createSession(runtime: EnvironmentHandle, _runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
26
25
  }
27
- export {};
@@ -1,16 +1,19 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import { createReadStream } from 'node:fs';
4
- import * as os from 'node:os';
5
2
  import * as path from 'node:path';
6
3
  import fs from 'fs-extra';
7
4
  import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
8
5
  import { buildSummary, enrichSkillEvents } from '../tool-events.js';
6
+ import { collectSensitiveEnvValues, sanitizePersistenceValue, sanitizeToolEventResult, } from '../tool-event-results.js';
9
7
  import { readStagedMcpServers } from '../providers/mcp-config.js';
10
8
  import { removeSandboxRoot } from '../providers/sandbox-lifecycle.js';
9
+ import { attachTurnResultSensitiveValues } from '../sdk/turn-result-secrets.js';
10
+ import { attachOriginalMcpInput } from '../sdk/mcp-event-input.js';
11
+ import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
11
12
  import { currentOpenCodePlatformKey, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
12
13
  import { OpenCodeRuntimePolicy, OPENCODE_PERMISSION } from './opencode/runtime-policy.js';
13
14
  import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
15
+ import { assertCleanManagedOpenCodeHost, sha256File } from './opencode/host-safety.js';
16
+ export { assertCleanManagedOpenCodeHost, managedOpenCodeConfigPaths } from './opencode/host-safety.js';
14
17
  const OUTPUT_CAP_BYTES = 16 * 1024 * 1024;
15
18
  const NATIVE_TOOL_ACTIONS = {
16
19
  bash: 'run_shell',
@@ -147,7 +150,7 @@ function sanitizedProviderError(event) {
147
150
  const retryable = typeof data.isRetryable === 'boolean' ? ` retryable=${data.isRetryable}` : '';
148
151
  return new Error(`OpenCode provider error${status}${retryable}`);
149
152
  }
150
- export function parseOpenCodeOutput(stdout, processResult, mcpToolNames) {
153
+ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensitiveValues = []) {
151
154
  if (processResult.overflow)
152
155
  throw new Error('OpenCode output exceeded the 16 MiB limit');
153
156
  if (processResult.aborted)
@@ -216,28 +219,52 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames) {
216
219
  if (state.status !== 'completed' && state.status !== 'error') {
217
220
  throw new Error(`OpenCode protocol error: incomplete tool ${tool}`);
218
221
  }
222
+ const projectedStatus = state.status;
219
223
  const input = state.input === undefined ? undefined : record(state.input, 'tool_use state.input');
220
224
  const time = state.time === undefined ? undefined : record(state.time, 'tool_use state.time');
221
225
  const startedAtMs = time === undefined ? undefined : finiteNumber(time.start, 'tool_use state.time.start');
222
226
  const completedAtMs = time === undefined ? undefined : finiteNumber(time.end, 'tool_use state.time.end');
227
+ const callId = typeof part.callID === 'string' ? part.callID : undefined;
228
+ const metadata = state.metadata === undefined ? undefined : record(state.metadata, 'tool_use state.metadata');
229
+ const output = state.status === 'completed' && typeof state.output === 'string' ? state.output : undefined;
230
+ const error = state.status === 'error' && typeof state.error === 'string' ? state.error : undefined;
231
+ const exitCode = metadata && typeof metadata.exit === 'number' && Number.isFinite(metadata.exit)
232
+ ? metadata.exit
233
+ : undefined;
234
+ const status = projectedStatus === 'completed' && exitCode !== undefined && exitCode !== 0
235
+ ? 'error'
236
+ : projectedStatus;
223
237
  const action = mcpToolNames.has(tool)
224
238
  ? 'mcp_tool_call'
225
239
  : NATIVE_TOOL_ACTIONS[tool] ?? 'unknown';
226
- toolEvents.push({
240
+ const toolEvent = {
227
241
  action,
228
242
  provider: 'opencode',
229
243
  providerToolName: tool,
244
+ status,
245
+ ...(callId ? { toolUseId: callId } : {}),
230
246
  ...(input ? { arguments: input } : {}),
231
247
  ...(startedAtMs !== undefined ? { startedAt: new Date(startedAtMs).toISOString() } : {}),
232
248
  ...(completedAtMs !== undefined ? { completedAt: new Date(completedAtMs).toISOString() } : {}),
233
249
  ...(startedAtMs !== undefined && completedAtMs !== undefined
234
250
  ? { durationMs: Math.max(0, completedAtMs - startedAtMs) }
235
251
  : {}),
252
+ ...(output !== undefined || error !== undefined || exitCode !== undefined || metadata?.truncated === true
253
+ ? { result: sanitizeToolEventResult({
254
+ ...(output !== undefined ? { content: output } : {}),
255
+ ...(error !== undefined ? { content: error } : {}),
256
+ ...(exitCode !== undefined ? { exitCode } : {}),
257
+ ...(metadata?.truncated === true ? { truncated: true } : {}),
258
+ }, sensitiveValues) }
259
+ : {}),
236
260
  summary: buildSummary(action, tool, input),
237
261
  confidence: action === 'unknown' ? 'low' : 'high',
238
262
  rawSnippet: JSON.stringify({ tool, status: state.status, input }).slice(0, 2_000),
239
- });
240
- sanitizedTrace.push({ type, tool, status: state.status, input });
263
+ };
264
+ toolEvents.push(action === 'mcp_tool_call' && input
265
+ ? attachOriginalMcpInput(toolEvent, input)
266
+ : toolEvent);
267
+ sanitizedTrace.push({ type, tool, status, input });
241
268
  continue;
242
269
  }
243
270
  if (type === 'step_finish') {
@@ -274,34 +301,28 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames) {
274
301
  if (stepFinishCount === 0)
275
302
  throw new Error('OpenCode protocol error: missing step_finish');
276
303
  const assistantMessage = textParts.join('');
277
- const traceOutput = sanitizedTrace.map((event) => JSON.stringify(event)).join('\n');
304
+ const traceOutput = sanitizePersistenceValue(sanitizedTrace, sensitiveValues)
305
+ .map((event) => JSON.stringify(event))
306
+ .join('\n');
278
307
  return {
279
308
  sessionId,
280
- result: {
309
+ result: attachTurnResultSensitiveValues({
281
310
  rawOutput: traceOutput,
282
311
  traceOutput,
283
312
  assistantMessage,
284
313
  visibleAssistantMessage: assistantMessage,
285
314
  visibleAssistantMessageSource: 'assistant_message',
286
315
  exitCode: 0,
287
- toolEvents: enrichSkillEvents(toolEvents),
316
+ toolEvents: enrichSkillEvents(toolEvents)
317
+ .map((event) => attachToolEventSensitiveValues(event, sensitiveValues)),
288
318
  inputTokens,
289
319
  outputTokens,
290
320
  cacheCreationInputTokens,
291
321
  cacheReadInputTokens,
292
322
  costUsd,
293
- },
323
+ }, sensitiveValues),
294
324
  };
295
325
  }
296
- function sha256File(filename) {
297
- return new Promise((resolve, reject) => {
298
- const hash = createHash('sha256');
299
- const stream = createReadStream(filename);
300
- stream.on('error', reject);
301
- stream.on('data', (chunk) => hash.update(chunk));
302
- stream.on('end', () => resolve(hash.digest('hex')));
303
- });
304
- }
305
326
  async function assertNoProjectConfig(workspacePath) {
306
327
  for (const name of ['opencode.json', 'opencode.jsonc', '.opencode']) {
307
328
  if (await fs.pathExists(path.join(workspacePath, name))) {
@@ -309,23 +330,6 @@ async function assertNoProjectConfig(workspacePath) {
309
330
  }
310
331
  }
311
332
  }
312
- export function managedOpenCodeConfigPaths(platform = process.platform, username = os.userInfo().username) {
313
- return platform === 'linux'
314
- ? ['/etc/opencode/opencode.json', '/etc/opencode/opencode.jsonc']
315
- : [
316
- '/Library/Application Support/opencode/opencode.json',
317
- '/Library/Application Support/opencode/opencode.jsonc',
318
- `/Library/Managed Preferences/${username}/ai.opencode.managed.plist`,
319
- '/Library/Managed Preferences/ai.opencode.managed.plist',
320
- ];
321
- }
322
- export async function assertCleanManagedOpenCodeHost(candidates = managedOpenCodeConfigPaths()) {
323
- for (const candidate of candidates) {
324
- if (await fs.pathExists(candidate)) {
325
- throw new Error(`OpenCode managed host configuration is not supported: ${candidate}`);
326
- }
327
- }
328
- }
329
333
  async function projectMcpConfig(workspacePath, mcpConfigPath) {
330
334
  if (!mcpConfigPath)
331
335
  return {};
@@ -355,6 +359,7 @@ class OpenCodeSession {
355
359
  getAbortSignal;
356
360
  getRemainingMs;
357
361
  requestedModel;
362
+ sensitiveValues;
358
363
  xdgDirs;
359
364
  runtimePolicy;
360
365
  resolvedExecutable;
@@ -374,6 +379,10 @@ class OpenCodeSession {
374
379
  this.getAbortSignal = options.getAbortSignal ?? (() => options.abortSignal);
375
380
  this.getRemainingMs = options.getRemainingMs ?? (() => 0);
376
381
  this.requestedModel = options.model;
382
+ this.sensitiveValues = [...new Set([
383
+ ...collectSensitiveEnvValues(this.runtimeEnv),
384
+ ...(options.sensitiveValues ?? []),
385
+ ])];
377
386
  const home = this.runtimeEnv.HOME;
378
387
  if (!home)
379
388
  throw new Error('OpenCode requires a managed HOME');
@@ -436,7 +445,7 @@ class OpenCodeSession {
436
445
  finally {
437
446
  await this.runtimePolicy.afterTurn();
438
447
  }
439
- const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames);
448
+ const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames, this.sensitiveValues);
440
449
  if (this.sessionId && parsed.sessionId !== this.sessionId) {
441
450
  throw new Error('OpenCode protocol error: resumed session ID changed');
442
451
  }
@@ -39,6 +39,8 @@ export interface CredentialResult {
39
39
  linkFromHome?: string[];
40
40
  /** Filtered sensitive files to create inside the isolated HOME. */
41
41
  sensitiveHomeFiles?: SensitiveHomeFile[];
42
+ /** Runtime-only values that persistence sinks must redact. */
43
+ sensitiveValues?: string[];
42
44
  }
43
45
  /** Default ports using real process.env, Keychain, and filesystem. */
44
46
  export declare function defaultPorts(): CredentialPorts;
@@ -152,6 +152,7 @@ function filterOpenCodeOAuthRecord(raw) {
152
152
  const sanitized = { ...record, refresh: OPENCODE_DISABLED_REFRESH_TOKEN };
153
153
  return {
154
154
  env: {}, setupCommands: [], copyFromHome: [],
155
+ sensitiveValues: [record.access],
155
156
  sensitiveHomeFiles: [{
156
157
  relativePath: path.join('.local', 'share', 'opencode', 'auth.json'),
157
158
  content: JSON.stringify({ openai: sanitized }),
@@ -7,6 +7,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
7
7
  import { compileScriptedMcpSchemaForProvider } from '../core/mcp-schema-profile.js';
8
8
  import { MCP_ANNOTATION_PROTOCOL_FLOOR } from '../core/generated-mcp-protocol.js';
9
9
  import { getOriginalMcpInput } from '../sdk/mcp-event-input.js';
10
+ import { cloneToolEventWithRuntimeMetadata } from '../sdk/tool-event-secrets.js';
10
11
  import { createMcpArgumentDigest, createMcpReceiptKey, matchesMcpApprovalArguments, } from '../sdk/mcp-mock-approvals.js';
11
12
  let testObserver = null;
12
13
  /** Non-public acceptance seam. It observes only authenticated manifest-member calls. */
@@ -332,9 +333,11 @@ function eventInput(event) {
332
333
  }
333
334
  function withInvocation(event, identity, invocation) {
334
335
  const status = String(event.arguments?.status ?? 'unknown').toLowerCase();
335
- return invocation === 'confirmed'
336
- ? { ...event, mcp: { ...identity, invocation, outcome: status.includes('fail') || status.includes('error') ? 'tool_error' : 'completed' } }
337
- : { ...event, mcp: { ...identity, invocation, outcome: 'unknown' } };
336
+ return cloneToolEventWithRuntimeMetadata(event, {
337
+ mcp: invocation === 'confirmed'
338
+ ? { ...identity, invocation, outcome: status.includes('fail') || status.includes('error') ? 'tool_error' : 'completed' }
339
+ : { ...identity, invocation, outcome: 'unknown' },
340
+ });
338
341
  }
339
342
  async function readJsonBody(req) {
340
343
  const chunks = [];
@@ -6,6 +6,7 @@ export interface Workspace {
6
6
  readonly mcpConfigPath: string | undefined;
7
7
  readonly env: Record<string, string>;
8
8
  readonly setupCommands: string[];
9
+ readonly sensitiveValues?: readonly string[];
9
10
  exec(command: string, opts?: {
10
11
  signal?: AbortSignal;
11
12
  }): Promise<CommandResult>;
@@ -7,6 +7,7 @@ import { stageMcpConfig } from './mcp-config.js';
7
7
  import { sandboxExec } from './sandbox-exec.js';
8
8
  import { resolveCredentials } from './credentials.js';
9
9
  import { isPortableCopyEntry } from './copy-filter.js';
10
+ import { collectSensitiveEnvValues } from '../tool-event-results.js';
10
11
  async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
11
12
  const realHome = os.homedir();
12
13
  for (const relPath of pathsToCopy) {
@@ -59,6 +60,10 @@ export async function prepareWorkspace(spec) {
59
60
  mcpConfigPath,
60
61
  env: sandboxEnv,
61
62
  setupCommands: creds.setupCommands,
63
+ sensitiveValues: [...new Set([
64
+ ...collectSensitiveEnvValues(sandboxEnv),
65
+ ...(creds.sensitiveValues ?? []),
66
+ ])],
62
67
  exec: (command, opts) => sandboxExec(command, { cwd: workspacePath, env: sandboxEnv }, opts),
63
68
  async dispose() {
64
69
  if (disposed)
@@ -1,4 +1,6 @@
1
1
  import { getTurnResultLogMetadata, getVisibleAssistantMessage } from './visible-turn.js';
2
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
3
+ import { getTurnResultSensitiveValues } from './turn-result-secrets.js';
2
4
  function getOutputMetrics(message) {
3
5
  return {
4
6
  output_lines: message.split('\n').length,
@@ -7,7 +9,7 @@ function getOutputMetrics(message) {
7
9
  }
8
10
  export function buildModelAgentResultLogEntry(params) {
9
11
  const assistantMessage = params.assistantMessage ?? getVisibleAssistantMessage(params.turnResult);
10
- return {
12
+ return sanitizePersistenceValue({
11
13
  type: 'agent_result',
12
14
  timestamp: params.timestamp,
13
15
  ...(params.turnNumber === undefined ? {} : { turn_number: params.turnNumber }),
@@ -30,7 +32,7 @@ export function buildModelAgentResultLogEntry(params) {
30
32
  ? { error_subtype: params.turnResult.errorSubtype }
31
33
  : {}),
32
34
  ...getOutputMetrics(assistantMessage),
33
- };
35
+ }, getTurnResultSensitiveValues(params.turnResult));
34
36
  }
35
37
  /**
36
38
  * Build one `ask_batch` LogEntry per AskBus batch emitted in the given turn.