@wix/pathgrade 1.0.16 → 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.
- package/dist/agents/claude/sdk-message-projector.js +12 -4
- package/dist/agents/claude/tool-results.d.ts +1 -2
- package/dist/agents/claude/tool-results.js +9 -40
- package/dist/agents/claude.js +6 -4
- package/dist/agents/codex-app-server/agent.d.ts +3 -0
- package/dist/agents/codex-app-server/agent.js +109 -162
- package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
- package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
- package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
- package/dist/agents/codex-app-server/item-projection.js +135 -0
- package/dist/agents/opencode/host-safety.d.ts +3 -0
- package/dist/agents/opencode/host-safety.js +30 -0
- package/dist/agents/opencode.d.ts +2 -4
- package/dist/agents/opencode.js +55 -38
- package/dist/providers/credentials.d.ts +2 -0
- package/dist/providers/credentials.js +1 -0
- package/dist/providers/scripted-mcp-mock-host.js +6 -3
- package/dist/providers/workspace.d.ts +1 -0
- package/dist/providers/workspace.js +5 -0
- package/dist/sdk/agent-result-log.js +4 -2
- package/dist/sdk/agent.js +6 -0
- package/dist/sdk/managed-session.d.ts +2 -0
- package/dist/sdk/managed-session.js +22 -5
- package/dist/sdk/mcp-safety.js +2 -18
- package/dist/sdk/snapshots.d.ts +1 -0
- package/dist/sdk/snapshots.js +3 -2
- package/dist/sdk/tool-event-log.js +5 -2
- package/dist/sdk/tool-event-secrets.d.ts +4 -0
- package/dist/sdk/tool-event-secrets.js +14 -0
- package/dist/sdk/turn-result-secrets.d.ts +5 -0
- package/dist/sdk/turn-result-secrets.js +12 -0
- package/dist/tool-event-results.d.ts +10 -0
- package/dist/tool-event-results.js +171 -0
- package/dist/types.d.ts +2 -0
- 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
|
|
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 {};
|
package/dist/agents/opencode.js
CHANGED
|
@@ -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,20 +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');
|
|
224
|
+
const time = state.time === undefined ? undefined : record(state.time, 'tool_use state.time');
|
|
225
|
+
const startedAtMs = time === undefined ? undefined : finiteNumber(time.start, 'tool_use state.time.start');
|
|
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;
|
|
220
237
|
const action = mcpToolNames.has(tool)
|
|
221
238
|
? 'mcp_tool_call'
|
|
222
239
|
: NATIVE_TOOL_ACTIONS[tool] ?? 'unknown';
|
|
223
|
-
|
|
240
|
+
const toolEvent = {
|
|
224
241
|
action,
|
|
225
242
|
provider: 'opencode',
|
|
226
243
|
providerToolName: tool,
|
|
244
|
+
status,
|
|
245
|
+
...(callId ? { toolUseId: callId } : {}),
|
|
227
246
|
...(input ? { arguments: input } : {}),
|
|
247
|
+
...(startedAtMs !== undefined ? { startedAt: new Date(startedAtMs).toISOString() } : {}),
|
|
248
|
+
...(completedAtMs !== undefined ? { completedAt: new Date(completedAtMs).toISOString() } : {}),
|
|
249
|
+
...(startedAtMs !== undefined && completedAtMs !== undefined
|
|
250
|
+
? { durationMs: Math.max(0, completedAtMs - startedAtMs) }
|
|
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
|
+
: {}),
|
|
228
260
|
summary: buildSummary(action, tool, input),
|
|
229
261
|
confidence: action === 'unknown' ? 'low' : 'high',
|
|
230
262
|
rawSnippet: JSON.stringify({ tool, status: state.status, input }).slice(0, 2_000),
|
|
231
|
-
}
|
|
232
|
-
|
|
263
|
+
};
|
|
264
|
+
toolEvents.push(action === 'mcp_tool_call' && input
|
|
265
|
+
? attachOriginalMcpInput(toolEvent, input)
|
|
266
|
+
: toolEvent);
|
|
267
|
+
sanitizedTrace.push({ type, tool, status, input });
|
|
233
268
|
continue;
|
|
234
269
|
}
|
|
235
270
|
if (type === 'step_finish') {
|
|
@@ -266,34 +301,28 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames) {
|
|
|
266
301
|
if (stepFinishCount === 0)
|
|
267
302
|
throw new Error('OpenCode protocol error: missing step_finish');
|
|
268
303
|
const assistantMessage = textParts.join('');
|
|
269
|
-
const traceOutput = sanitizedTrace
|
|
304
|
+
const traceOutput = sanitizePersistenceValue(sanitizedTrace, sensitiveValues)
|
|
305
|
+
.map((event) => JSON.stringify(event))
|
|
306
|
+
.join('\n');
|
|
270
307
|
return {
|
|
271
308
|
sessionId,
|
|
272
|
-
result: {
|
|
309
|
+
result: attachTurnResultSensitiveValues({
|
|
273
310
|
rawOutput: traceOutput,
|
|
274
311
|
traceOutput,
|
|
275
312
|
assistantMessage,
|
|
276
313
|
visibleAssistantMessage: assistantMessage,
|
|
277
314
|
visibleAssistantMessageSource: 'assistant_message',
|
|
278
315
|
exitCode: 0,
|
|
279
|
-
toolEvents: enrichSkillEvents(toolEvents)
|
|
316
|
+
toolEvents: enrichSkillEvents(toolEvents)
|
|
317
|
+
.map((event) => attachToolEventSensitiveValues(event, sensitiveValues)),
|
|
280
318
|
inputTokens,
|
|
281
319
|
outputTokens,
|
|
282
320
|
cacheCreationInputTokens,
|
|
283
321
|
cacheReadInputTokens,
|
|
284
322
|
costUsd,
|
|
285
|
-
},
|
|
323
|
+
}, sensitiveValues),
|
|
286
324
|
};
|
|
287
325
|
}
|
|
288
|
-
function sha256File(filename) {
|
|
289
|
-
return new Promise((resolve, reject) => {
|
|
290
|
-
const hash = createHash('sha256');
|
|
291
|
-
const stream = createReadStream(filename);
|
|
292
|
-
stream.on('error', reject);
|
|
293
|
-
stream.on('data', (chunk) => hash.update(chunk));
|
|
294
|
-
stream.on('end', () => resolve(hash.digest('hex')));
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
326
|
async function assertNoProjectConfig(workspacePath) {
|
|
298
327
|
for (const name of ['opencode.json', 'opencode.jsonc', '.opencode']) {
|
|
299
328
|
if (await fs.pathExists(path.join(workspacePath, name))) {
|
|
@@ -301,23 +330,6 @@ async function assertNoProjectConfig(workspacePath) {
|
|
|
301
330
|
}
|
|
302
331
|
}
|
|
303
332
|
}
|
|
304
|
-
export function managedOpenCodeConfigPaths(platform = process.platform, username = os.userInfo().username) {
|
|
305
|
-
return platform === 'linux'
|
|
306
|
-
? ['/etc/opencode/opencode.json', '/etc/opencode/opencode.jsonc']
|
|
307
|
-
: [
|
|
308
|
-
'/Library/Application Support/opencode/opencode.json',
|
|
309
|
-
'/Library/Application Support/opencode/opencode.jsonc',
|
|
310
|
-
`/Library/Managed Preferences/${username}/ai.opencode.managed.plist`,
|
|
311
|
-
'/Library/Managed Preferences/ai.opencode.managed.plist',
|
|
312
|
-
];
|
|
313
|
-
}
|
|
314
|
-
export async function assertCleanManagedOpenCodeHost(candidates = managedOpenCodeConfigPaths()) {
|
|
315
|
-
for (const candidate of candidates) {
|
|
316
|
-
if (await fs.pathExists(candidate)) {
|
|
317
|
-
throw new Error(`OpenCode managed host configuration is not supported: ${candidate}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
333
|
async function projectMcpConfig(workspacePath, mcpConfigPath) {
|
|
322
334
|
if (!mcpConfigPath)
|
|
323
335
|
return {};
|
|
@@ -347,6 +359,7 @@ class OpenCodeSession {
|
|
|
347
359
|
getAbortSignal;
|
|
348
360
|
getRemainingMs;
|
|
349
361
|
requestedModel;
|
|
362
|
+
sensitiveValues;
|
|
350
363
|
xdgDirs;
|
|
351
364
|
runtimePolicy;
|
|
352
365
|
resolvedExecutable;
|
|
@@ -366,6 +379,10 @@ class OpenCodeSession {
|
|
|
366
379
|
this.getAbortSignal = options.getAbortSignal ?? (() => options.abortSignal);
|
|
367
380
|
this.getRemainingMs = options.getRemainingMs ?? (() => 0);
|
|
368
381
|
this.requestedModel = options.model;
|
|
382
|
+
this.sensitiveValues = [...new Set([
|
|
383
|
+
...collectSensitiveEnvValues(this.runtimeEnv),
|
|
384
|
+
...(options.sensitiveValues ?? []),
|
|
385
|
+
])];
|
|
369
386
|
const home = this.runtimeEnv.HOME;
|
|
370
387
|
if (!home)
|
|
371
388
|
throw new Error('OpenCode requires a managed HOME');
|
|
@@ -428,7 +445,7 @@ class OpenCodeSession {
|
|
|
428
445
|
finally {
|
|
429
446
|
await this.runtimePolicy.afterTurn();
|
|
430
447
|
}
|
|
431
|
-
const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames);
|
|
448
|
+
const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames, this.sensitiveValues);
|
|
432
449
|
if (this.sessionId && parsed.sessionId !== this.sessionId) {
|
|
433
450
|
throw new Error('OpenCode protocol error: resumed session ID changed');
|
|
434
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
|
|
336
|
-
|
|
337
|
-
|
|
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.
|