@wix/pathgrade 1.0.10 → 1.0.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/README.md +10 -11
- package/dist/agents/claude/sdk-message-projector.d.ts +5 -0
- package/dist/agents/claude/sdk-message-projector.js +32 -6
- package/dist/agents/claude/tool-results.d.ts +19 -0
- package/dist/agents/claude/tool-results.js +117 -0
- package/dist/agents/claude.d.ts +4 -0
- package/dist/agents/claude.js +17 -2
- package/dist/agents/opencode/contract.d.ts +42 -6
- package/dist/agents/opencode/contract.js +59 -16
- package/dist/agents/opencode/runtime-policy.d.ts +18 -0
- package/dist/agents/opencode/runtime-policy.js +156 -0
- package/dist/agents/opencode.js +40 -55
- package/dist/providers/credentials.d.ts +13 -1
- package/dist/providers/credentials.js +91 -8
- package/dist/providers/sandbox.d.ts +2 -0
- package/dist/providers/workspace.js +19 -2
- package/dist/sdk/agent.js +11 -16
- package/dist/sdk/chat.js +10 -12
- package/dist/sdk/judge-tools.js +23 -2
- package/dist/sdk/managed-session.js +7 -4
- package/dist/sdk/persona.d.ts +1 -0
- package/dist/sdk/persona.js +5 -1
- package/dist/sdk/tool-event-log.d.ts +3 -0
- package/dist/sdk/tool-event-log.js +7 -0
- package/dist/tool-events.d.ts +19 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/llm-providers/anthropic.js +15 -11
- package/dist/utils/llm-providers/openai.js +13 -4
- package/dist/utils/llm.d.ts +2 -2
- package/dist/utils/llm.js +9 -4
- package/dist/viewer.html +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
|
-
**Prerequisites**: Node.js 20.11+, Vitest 4+ or Jest 30+, and at least one configured agent runtime. Claude uses the bundled `@anthropic-ai/claude-agent-sdk` binary by default; Codex requires the `codex` CLI; Cursor requires the `cursor-agent` CLI. OpenCode requires the exact v1.18.
|
|
16
|
+
**Prerequisites**: Node.js 20.11+, Vitest 4+ or Jest 30+, and at least one configured agent runtime. Claude uses the bundled `@anthropic-ai/claude-agent-sdk` binary by default; Codex requires the `codex` CLI; Cursor requires the `cursor-agent` CLI. OpenCode requires the exact Wix-registry v1.18.14 macOS ARM64 or Linux ARM64 executable.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
yarn add -D @wix/pathgrade
|
|
@@ -41,8 +41,9 @@ By default, Pathgrade tries to reuse the agent CLI's native auth before falling
|
|
|
41
41
|
- macOS: reuses `cursor-agent login` OAuth tokens from the login Keychain
|
|
42
42
|
- surfaces a clear error when neither is available (run `cursor-agent login` or set `CURSOR_API_KEY`)
|
|
43
43
|
- **OpenCode**
|
|
44
|
-
-
|
|
45
|
-
-
|
|
44
|
+
- defaults to `anthropic/claude-sonnet-5` with an explicit or host `ANTHROPIC_API_KEY` and optional HTTPS `/v1` proxy
|
|
45
|
+
- supports `model: 'openai/gpt-5.4'` with an explicit or host `OPENAI_API_KEY`
|
|
46
|
+
- when no OpenAI key or proxy is set, stages the validated OpenAI access record with a disabled refresh sentinel; it never copies the host refresh token or writes credentials back
|
|
46
47
|
|
|
47
48
|
If you set `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, or `CURSOR_API_BASE_URL`, set the matching API key too.
|
|
48
49
|
|
|
@@ -243,15 +244,13 @@ OpenCode v1 is only for fixtures, prompts, skills, and generated MCP mocks contr
|
|
|
243
244
|
```typescript
|
|
244
245
|
const agent = await createAgent({
|
|
245
246
|
agent: 'opencode',
|
|
246
|
-
opencodeExecutable: '/absolute/path/to/opencode-v1.18.
|
|
247
|
-
env: {
|
|
248
|
-
ANTHROPIC_API_KEY: process.env.APP_ANTHROPIC_API_KEY!,
|
|
249
|
-
ANTHROPIC_BASE_URL: 'https://api.example.com/v1',
|
|
250
|
-
},
|
|
247
|
+
opencodeExecutable: '/absolute/path/to/opencode-v1.18.14',
|
|
251
248
|
});
|
|
252
249
|
```
|
|
253
250
|
|
|
254
|
-
The backend supports
|
|
251
|
+
The backend supports `anthropic/claude-sonnet-5` (default) and `openai/gpt-5.4`, generated stdio `mcpMock` servers, and clean hosts without system-managed OpenCode configuration. `PATHGRADE_AGENT=opencode` selects the backend but does not supply the required executable. Selecting GPT-5.4 opts into the ChatGPT Codex endpoint/account plane. A custom `OPENAI_BASE_URL` always requires a matching key and never receives the local OAuth bearer token. Explicit API keys use only an explicitly paired base URL; they never inherit an ambient host proxy.
|
|
252
|
+
|
|
253
|
+
Pathgrade's default persona, summarization, and plain-judge helpers follow the selected OpenCode provider. GPT-5.4 API-key runs use the OpenAI helper provider and never fall through to Claude CLI. Local OAuth authenticates only the isolated OpenCode process; OAuth-only runs must inject an LLM for helper calls or provide an explicit `OPENAI_API_KEY`. Tool-using judges still require the Anthropic HTTP provider.
|
|
255
254
|
|
|
256
255
|
### `agent.prompt()` - One shot
|
|
257
256
|
|
|
@@ -430,8 +429,8 @@ Notes:
|
|
|
430
429
|
|
|
431
430
|
| Variable | Purpose |
|
|
432
431
|
|----------|---------|
|
|
433
|
-
| `ANTHROPIC_API_KEY` | Claude auth;
|
|
434
|
-
| `OPENAI_API_KEY` | Codex auth
|
|
432
|
+
| `ANTHROPIC_API_KEY` | Claude auth; explicit or host API-key auth for OpenCode's default model |
|
|
433
|
+
| `OPENAI_API_KEY` | Codex auth; optional explicit or host API-key auth for OpenCode GPT-5.4, and required with `OPENAI_BASE_URL` |
|
|
435
434
|
| `CURSOR_API_KEY` | Cursor auth and the required key when using `CURSOR_API_BASE_URL` |
|
|
436
435
|
| `ANTHROPIC_BASE_URL` | Custom Anthropic-compatible endpoint |
|
|
437
436
|
| `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint |
|
|
@@ -19,9 +19,12 @@ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
|
19
19
|
import type { AgentTurnResult } from '../../types.js';
|
|
20
20
|
import type { AskUserAnswerStore } from './ask-user-answer-store.js';
|
|
21
21
|
import type { ClaudeDeniedMcpEventStore } from './denied-mcp-event-store.js';
|
|
22
|
+
import { type ClaudeSdkMessageTiming } from './tool-results.js';
|
|
22
23
|
export interface ProjectTurnInput {
|
|
23
24
|
/** Buffered typed-message stream from one `query()` call. */
|
|
24
25
|
messages: SDKMessage[];
|
|
26
|
+
/** Local receive timing captured while consuming each corresponding SDK message. */
|
|
27
|
+
messageTimings?: readonly (ClaudeSdkMessageTiming | undefined)[];
|
|
25
28
|
/** Forwarded onto `ToolEvent.turnNumber`; optional for projector unit use. */
|
|
26
29
|
turnNumber?: number;
|
|
27
30
|
/** First user message of the turn — used for slash-command skill detection. */
|
|
@@ -39,6 +42,8 @@ export interface ProjectTurnInput {
|
|
|
39
42
|
mcpServerNames?: readonly string[];
|
|
40
43
|
/** Policy-denied MCP events recorded by the per-turn permission bridge. */
|
|
41
44
|
deniedMcpEvents?: ClaudeDeniedMcpEventStore;
|
|
45
|
+
/** Exact runtime credential values to redact before tool results become trace data. */
|
|
46
|
+
sensitiveValues?: readonly string[];
|
|
42
47
|
}
|
|
43
48
|
export interface ProjectedTurn {
|
|
44
49
|
result: AgentTurnResult;
|
|
@@ -23,6 +23,7 @@ const SDK_ERROR_SUBTYPES = [
|
|
|
23
23
|
];
|
|
24
24
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
|
|
25
25
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
26
|
+
import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
|
|
26
27
|
export function projectSdkMessages(input) {
|
|
27
28
|
let sessionId;
|
|
28
29
|
let initSkills;
|
|
@@ -37,7 +38,9 @@ export function projectSdkMessages(input) {
|
|
|
37
38
|
let costUsd;
|
|
38
39
|
let errorSubtype;
|
|
39
40
|
const toolEvents = [];
|
|
40
|
-
|
|
41
|
+
const pendingTools = new Map();
|
|
42
|
+
for (const [messageIndex, msg] of input.messages.entries()) {
|
|
43
|
+
const timing = input.messageTimings?.[messageIndex];
|
|
41
44
|
switch (msg.type) {
|
|
42
45
|
case 'system': {
|
|
43
46
|
const sid = msg.session_id;
|
|
@@ -60,13 +63,30 @@ export function projectSdkMessages(input) {
|
|
|
60
63
|
continue;
|
|
61
64
|
}
|
|
62
65
|
if (block.type === 'tool_use') {
|
|
63
|
-
const event = buildToolEvent(block, input.turnNumber, input.answerStore, input.mcpServerNames ?? [], input.deniedMcpEvents);
|
|
64
|
-
if (event)
|
|
66
|
+
const event = buildToolEvent(block, input.turnNumber, input.answerStore, input.mcpServerNames ?? [], input.deniedMcpEvents, timing);
|
|
67
|
+
if (event) {
|
|
65
68
|
toolEvents.push(event);
|
|
69
|
+
if (event.toolUseId) {
|
|
70
|
+
pendingTools.set(event.toolUseId, {
|
|
71
|
+
event,
|
|
72
|
+
startedMonotonicMs: timing?.receivedMonotonicMs,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
break;
|
|
69
79
|
}
|
|
80
|
+
case 'user': {
|
|
81
|
+
for (const observed of extractObservedToolResults(msg, timing)) {
|
|
82
|
+
const pending = pendingTools.get(observed.toolUseId);
|
|
83
|
+
if (!pending)
|
|
84
|
+
continue;
|
|
85
|
+
applyObservedToolResult(pending.event, observed, pending.startedMonotonicMs, input.sensitiveValues);
|
|
86
|
+
pendingTools.delete(observed.toolUseId);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
70
90
|
case 'result': {
|
|
71
91
|
const r = msg;
|
|
72
92
|
if (r.session_id)
|
|
@@ -167,7 +187,7 @@ function prependSlashCommandSkillEvent(events, firstMessage, initSkills) {
|
|
|
167
187
|
...events,
|
|
168
188
|
];
|
|
169
189
|
}
|
|
170
|
-
function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMcpEvents) {
|
|
190
|
+
function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMcpEvents, timing) {
|
|
171
191
|
const providerToolName = String(block.name || 'unknown');
|
|
172
192
|
const rawInput = block.input ?? undefined;
|
|
173
193
|
const toolUseId = typeof block.id === 'string' ? block.id : undefined;
|
|
@@ -180,15 +200,18 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
180
200
|
...(rawInput ?? {}),
|
|
181
201
|
server: mcpTool.server,
|
|
182
202
|
tool: mcpTool.tool,
|
|
183
|
-
status: '
|
|
203
|
+
status: 'incomplete',
|
|
184
204
|
};
|
|
185
205
|
return {
|
|
186
206
|
action: 'mcp_tool_call',
|
|
187
207
|
provider: 'claude',
|
|
188
208
|
providerToolName: normalizedProviderToolName,
|
|
209
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
189
210
|
turnNumber,
|
|
190
211
|
arguments: args,
|
|
191
|
-
|
|
212
|
+
status: 'incomplete',
|
|
213
|
+
...(timing ? { startedAt: timing.receivedAt } : {}),
|
|
214
|
+
summary: `MCP tool ${normalizedProviderToolName}`,
|
|
192
215
|
confidence: 'high',
|
|
193
216
|
rawSnippet: JSON.stringify(block).slice(0, 200),
|
|
194
217
|
};
|
|
@@ -203,8 +226,11 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
203
226
|
action,
|
|
204
227
|
provider: 'claude',
|
|
205
228
|
providerToolName,
|
|
229
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
206
230
|
turnNumber,
|
|
207
231
|
arguments: args,
|
|
232
|
+
status: 'incomplete',
|
|
233
|
+
...(timing ? { startedAt: timing.receivedAt } : {}),
|
|
208
234
|
summary,
|
|
209
235
|
confidence: 'high',
|
|
210
236
|
rawSnippet,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { ToolEvent } from '../../tool-events.js';
|
|
3
|
+
export declare const TOOL_RESULT_MAX_CHARS: number;
|
|
4
|
+
export interface ClaudeSdkMessageTiming {
|
|
5
|
+
receivedAt: string;
|
|
6
|
+
receivedMonotonicMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ObservedToolResult {
|
|
9
|
+
toolUseId: string;
|
|
10
|
+
isError: boolean;
|
|
11
|
+
structuredResult?: Record<string, unknown>;
|
|
12
|
+
content?: string;
|
|
13
|
+
completedAt?: string;
|
|
14
|
+
completedMonotonicMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function hasToolLifecycleBoundary(message: SDKMessage): boolean;
|
|
17
|
+
export declare function extractObservedToolResults(message: SDKMessage, timing?: ClaudeSdkMessageTiming): ObservedToolResult[];
|
|
18
|
+
export declare function applyObservedToolResult(event: ToolEvent, observed: ObservedToolResult, startedMonotonicMs?: number, sensitiveValues?: readonly string[]): void;
|
|
19
|
+
export declare function collectSensitiveEnvValues(env: Readonly<Record<string, string>>): string[];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export const TOOL_RESULT_MAX_CHARS = 64 * 1024;
|
|
2
|
+
const SECRET_ENV_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)/i;
|
|
3
|
+
export function hasToolLifecycleBoundary(message) {
|
|
4
|
+
if (message.type !== 'assistant' && message.type !== 'user')
|
|
5
|
+
return false;
|
|
6
|
+
const content = message.message?.content;
|
|
7
|
+
return Array.isArray(content) && content.some((block) => isRecord(block) && (block.type === 'tool_use' || block.type === 'tool_result'));
|
|
8
|
+
}
|
|
9
|
+
export function extractObservedToolResults(message, timing) {
|
|
10
|
+
if (message.type !== 'user')
|
|
11
|
+
return [];
|
|
12
|
+
const user = message;
|
|
13
|
+
const content = user.message?.content;
|
|
14
|
+
if (!Array.isArray(content))
|
|
15
|
+
return [];
|
|
16
|
+
const blocks = content.filter(isToolResultBlock);
|
|
17
|
+
const structuredResult = blocks.length === 1 && isRecord(user.tool_use_result)
|
|
18
|
+
? user.tool_use_result
|
|
19
|
+
: undefined;
|
|
20
|
+
return blocks.flatMap((block) => {
|
|
21
|
+
if (typeof block.tool_use_id !== 'string')
|
|
22
|
+
return [];
|
|
23
|
+
const text = extractTextContent(block.content);
|
|
24
|
+
return [{
|
|
25
|
+
toolUseId: block.tool_use_id,
|
|
26
|
+
isError: block.is_error === true,
|
|
27
|
+
...(structuredResult ? { structuredResult } : {}),
|
|
28
|
+
...(text === undefined ? {} : { content: text }),
|
|
29
|
+
...(timing ? {
|
|
30
|
+
completedAt: timing.receivedAt,
|
|
31
|
+
completedMonotonicMs: timing.receivedMonotonicMs,
|
|
32
|
+
} : {}),
|
|
33
|
+
}];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function applyObservedToolResult(event, observed, startedMonotonicMs, sensitiveValues = []) {
|
|
37
|
+
const remainsBackgrounded = event.arguments?.run_in_background === true;
|
|
38
|
+
const result = buildBoundedToolResult(observed, sensitiveValues);
|
|
39
|
+
const failedExit = result.exitCode !== undefined && result.exitCode !== 0;
|
|
40
|
+
const failed = observed.isError || failedExit;
|
|
41
|
+
event.status = failed ? 'error' : (remainsBackgrounded ? 'incomplete' : 'completed');
|
|
42
|
+
if (!remainsBackgrounded || failed) {
|
|
43
|
+
if (observed.completedAt)
|
|
44
|
+
event.completedAt = observed.completedAt;
|
|
45
|
+
if (startedMonotonicMs !== undefined && observed.completedMonotonicMs !== undefined) {
|
|
46
|
+
event.durationMs = Math.max(0, observed.completedMonotonicMs - startedMonotonicMs);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(result).length > 0)
|
|
50
|
+
event.result = result;
|
|
51
|
+
if (event.action === 'mcp_tool_call' && event.arguments) {
|
|
52
|
+
event.arguments.status = event.status;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function collectSensitiveEnvValues(env) {
|
|
56
|
+
return [...new Set(Object.entries(env)
|
|
57
|
+
.filter(([key, value]) => SECRET_ENV_KEY_PATTERN.test(key) && value.length > 0)
|
|
58
|
+
.map(([, value]) => value))]
|
|
59
|
+
.sort((a, b) => b.length - a.length);
|
|
60
|
+
}
|
|
61
|
+
function buildBoundedToolResult(observed, sensitiveValues) {
|
|
62
|
+
const raw = observed.structuredResult;
|
|
63
|
+
const result = {};
|
|
64
|
+
let truncated = false;
|
|
65
|
+
const addBounded = (key, value) => {
|
|
66
|
+
if (typeof value !== 'string')
|
|
67
|
+
return;
|
|
68
|
+
const bounded = boundText(redactSensitiveValues(value, sensitiveValues));
|
|
69
|
+
result[key] = bounded.value;
|
|
70
|
+
truncated ||= bounded.truncated;
|
|
71
|
+
};
|
|
72
|
+
addBounded('stdout', raw?.stdout);
|
|
73
|
+
addBounded('stderr', raw?.stderr);
|
|
74
|
+
if (observed.content !== raw?.stdout)
|
|
75
|
+
addBounded('content', observed.content);
|
|
76
|
+
const exitCode = raw?.exitCode ?? raw?.exit_code;
|
|
77
|
+
if (typeof exitCode === 'number' && Number.isFinite(exitCode))
|
|
78
|
+
result.exitCode = exitCode;
|
|
79
|
+
if (truncated)
|
|
80
|
+
result.truncated = true;
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function redactSensitiveValues(value, sensitiveValues) {
|
|
84
|
+
let redacted = value;
|
|
85
|
+
for (const secret of sensitiveValues) {
|
|
86
|
+
if (secret.length > 0)
|
|
87
|
+
redacted = redacted.split(secret).join('<redacted>');
|
|
88
|
+
}
|
|
89
|
+
return redacted;
|
|
90
|
+
}
|
|
91
|
+
function boundText(value) {
|
|
92
|
+
if (value.length <= TOOL_RESULT_MAX_CHARS)
|
|
93
|
+
return { value, truncated: false };
|
|
94
|
+
const marker = '\n[truncated by PathGrade]';
|
|
95
|
+
return {
|
|
96
|
+
value: `${value.slice(0, TOOL_RESULT_MAX_CHARS - marker.length)}${marker}`,
|
|
97
|
+
truncated: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function extractTextContent(content) {
|
|
101
|
+
if (typeof content === 'string')
|
|
102
|
+
return content;
|
|
103
|
+
if (!Array.isArray(content))
|
|
104
|
+
return undefined;
|
|
105
|
+
const text = content
|
|
106
|
+
.filter(isRecord)
|
|
107
|
+
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
108
|
+
.map((block) => block.text)
|
|
109
|
+
.join('');
|
|
110
|
+
return text || undefined;
|
|
111
|
+
}
|
|
112
|
+
function isToolResultBlock(value) {
|
|
113
|
+
return isRecord(value) && value.type === 'tool_result';
|
|
114
|
+
}
|
|
115
|
+
function isRecord(value) {
|
|
116
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
package/dist/agents/claude.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface ClaudeAgentDeps {
|
|
|
39
39
|
envExecutable?: string;
|
|
40
40
|
/** Optional macOS sandbox-exec profile. None today; preserves the seam. */
|
|
41
41
|
sandboxProfile?: string;
|
|
42
|
+
/** Local wall-clock seam for deterministic tool lifecycle tests. */
|
|
43
|
+
now?: () => Date;
|
|
44
|
+
/** Monotonic clock seam for deterministic tool duration tests. */
|
|
45
|
+
monotonicNow?: () => number;
|
|
42
46
|
}
|
|
43
47
|
export interface ClaudeAgentOptions {
|
|
44
48
|
/**
|
package/dist/agents/claude.js
CHANGED
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
* parser wholesale.
|
|
23
23
|
*/
|
|
24
24
|
import { query as sdkQuery, } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import { performance } from 'node:perf_hooks';
|
|
25
26
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
26
27
|
import { createSandboxedClaudeSpawn } from '../providers/sandboxed-claude-spawn.js';
|
|
27
28
|
import { assertClaudeLiveMcpSafetyPreflight, assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
|
|
28
29
|
import { buildClaudeSdkOptions, resolveClaudeCodeExecutable, } from './claude/sdk-options.js';
|
|
29
30
|
import { projectSdkMessages } from './claude/sdk-message-projector.js';
|
|
31
|
+
import { collectSensitiveEnvValues, hasToolLifecycleBoundary, } from './claude/tool-results.js';
|
|
30
32
|
import { createAskUserAnswerStore } from './claude/ask-user-answer-store.js';
|
|
31
33
|
import { createClaudeToolPermissionBridge } from './claude/tool-permission-bridge.js';
|
|
32
34
|
import { createClaudeDeniedMcpEventStore } from './claude/denied-mcp-event-store.js';
|
|
@@ -63,6 +65,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
63
65
|
const platform = this.deps.platform ?? process.platform;
|
|
64
66
|
const hostEnv = this.deps.hostEnv ?? process.env;
|
|
65
67
|
const envExecutable = this.deps.envExecutable ?? process.env.PATHGRADE_CLAUDE_CODE_EXECUTABLE;
|
|
68
|
+
const runtimeEnv = getRuntimeEnv(runtime);
|
|
69
|
+
const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
|
|
66
70
|
const sandboxedSpawn = createSandboxedClaudeSpawn({
|
|
67
71
|
platform,
|
|
68
72
|
hostEnv,
|
|
@@ -75,7 +79,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
75
79
|
const mcpMountOptions = {
|
|
76
80
|
workspacePath,
|
|
77
81
|
mcpConfigPath: sessionOptions?.mcpConfigPath,
|
|
78
|
-
runtimeEnv
|
|
82
|
+
runtimeEnv,
|
|
79
83
|
};
|
|
80
84
|
await assertClaudeLiveMcpSafetyPreflight({
|
|
81
85
|
...mcpMountOptions,
|
|
@@ -117,7 +121,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
117
121
|
workspacePath,
|
|
118
122
|
spawnClaudeCodeProcess: sandboxedSpawn,
|
|
119
123
|
canUseTool: bridge,
|
|
120
|
-
runtimeEnv
|
|
124
|
+
runtimeEnv,
|
|
121
125
|
model: sessionOptions?.model,
|
|
122
126
|
claudeCodeExecutable,
|
|
123
127
|
resume: priorSessionId,
|
|
@@ -125,9 +129,18 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
125
129
|
abortController: createLinkedAbortController(getTurnAbortSignal(sessionOptions)),
|
|
126
130
|
});
|
|
127
131
|
const messages = [];
|
|
132
|
+
const messageTimings = [];
|
|
133
|
+
const now = this.deps.now ?? (() => new Date());
|
|
134
|
+
const monotonicNow = this.deps.monotonicNow ?? (() => performance.now());
|
|
128
135
|
const stream = queryFn({ prompt: message, options: sdkOptions });
|
|
129
136
|
for await (const msg of stream) {
|
|
130
137
|
messages.push(msg);
|
|
138
|
+
messageTimings.push(hasToolLifecycleBoundary(msg)
|
|
139
|
+
? {
|
|
140
|
+
receivedAt: now().toISOString(),
|
|
141
|
+
receivedMonotonicMs: monotonicNow(),
|
|
142
|
+
}
|
|
143
|
+
: undefined);
|
|
131
144
|
}
|
|
132
145
|
// The legacy NDJSON parser only synthesized the slash-command
|
|
133
146
|
// `use_skill` event from the *opening* user message. The Claude
|
|
@@ -140,11 +153,13 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
140
153
|
const projectorFirstMessage = turnNumber === 1 ? message : undefined;
|
|
141
154
|
const projected = projectSdkMessages({
|
|
142
155
|
messages,
|
|
156
|
+
messageTimings,
|
|
143
157
|
turnNumber,
|
|
144
158
|
firstMessage: projectorFirstMessage,
|
|
145
159
|
answerStore,
|
|
146
160
|
mcpServerNames: mcpServers ? Object.keys(mcpServers) : [],
|
|
147
161
|
deniedMcpEvents,
|
|
162
|
+
sensitiveValues,
|
|
148
163
|
});
|
|
149
164
|
// Capture the SDK-reported session id BEFORE checking for a bus
|
|
150
165
|
// rejection so the next turn's `Options.resume` points at this
|
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
import type { MockMcpServerDescriptor } from '../../core/mcp-mock.types.js';
|
|
2
2
|
import type { AgentName, AgentOptions } from '../../sdk/types.js';
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
3
|
+
export declare const OPENCODE_VERSION = "1.18.14";
|
|
4
|
+
export declare const DEFAULT_OPENCODE_MODEL = "anthropic/claude-sonnet-5";
|
|
5
|
+
export interface OpenCodeModelContract {
|
|
6
|
+
provider: 'anthropic' | 'openai';
|
|
7
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY';
|
|
8
|
+
baseUrlEnv: 'ANTHROPIC_BASE_URL' | 'OPENAI_BASE_URL';
|
|
9
|
+
allowsLocalOAuth: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const OPENCODE_MODELS: {
|
|
12
|
+
readonly 'anthropic/claude-sonnet-5': {
|
|
13
|
+
readonly provider: "anthropic";
|
|
14
|
+
readonly apiKeyEnv: "ANTHROPIC_API_KEY";
|
|
15
|
+
readonly baseUrlEnv: "ANTHROPIC_BASE_URL";
|
|
16
|
+
readonly allowsLocalOAuth: false;
|
|
17
|
+
};
|
|
18
|
+
readonly 'openai/gpt-5.4': {
|
|
19
|
+
readonly provider: "openai";
|
|
20
|
+
readonly apiKeyEnv: "OPENAI_API_KEY";
|
|
21
|
+
readonly baseUrlEnv: "OPENAI_BASE_URL";
|
|
22
|
+
readonly allowsLocalOAuth: true;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export type OpenCodeModel = keyof typeof OPENCODE_MODELS;
|
|
26
|
+
export declare const OPENCODE_DISABLED_REFRESH_TOKEN = "pathgrade-refresh-disabled";
|
|
27
|
+
export interface OpenCodeOAuthRecord {
|
|
28
|
+
type: 'oauth';
|
|
29
|
+
access: string;
|
|
30
|
+
refresh: string;
|
|
31
|
+
expires: number;
|
|
32
|
+
accountId?: string;
|
|
33
|
+
enterpriseUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function sanitizeOpenCodeOAuthRecord(value: unknown): OpenCodeOAuthRecord | undefined;
|
|
36
|
+
export declare function resolveOpenCodeModel(model?: string): {
|
|
37
|
+
model: OpenCodeModel;
|
|
38
|
+
contract: OpenCodeModelContract;
|
|
39
|
+
};
|
|
5
40
|
export interface OpenCodeRuntimeLockEntry {
|
|
6
41
|
version: typeof OPENCODE_VERSION;
|
|
7
42
|
executableSha256: string;
|
|
8
43
|
}
|
|
9
44
|
export declare const OPENCODE_RUNTIME_LOCK: {
|
|
10
45
|
readonly 'darwin-arm64': {
|
|
11
|
-
readonly version: "1.18.
|
|
12
|
-
readonly executableSha256: "
|
|
46
|
+
readonly version: "1.18.14";
|
|
47
|
+
readonly executableSha256: "8b7c4e116c1ac5163c02fa85eee5a13d4d00c8a08e677dac4100f55aa56532fa";
|
|
13
48
|
};
|
|
14
49
|
readonly 'linux-arm64': {
|
|
15
|
-
readonly version: "1.18.
|
|
16
|
-
readonly executableSha256: "
|
|
50
|
+
readonly version: "1.18.14";
|
|
51
|
+
readonly executableSha256: "79d42436517e485e9444cfc6e92582bf9224a9e36fa77fb3957b343486fec81d";
|
|
17
52
|
};
|
|
18
53
|
};
|
|
19
54
|
export type OpenCodePlatformKey = keyof typeof OPENCODE_RUNTIME_LOCK;
|
|
20
55
|
export declare function getOpenCodePlatformKey(platform: NodeJS.Platform, arch: string): OpenCodePlatformKey | undefined;
|
|
21
56
|
export declare function currentOpenCodePlatformKey(): OpenCodePlatformKey | undefined;
|
|
57
|
+
export declare function validateOpenCodeBaseUrl(value: string, variable?: string): void;
|
|
22
58
|
export declare function validateOpenCodeDeclaration(agent: AgentName, opts: AgentOptions): void;
|
|
23
59
|
export declare function sanitizeOpenCodeToolName(value: string): string;
|
|
24
60
|
export declare function collectOpenCodeMcpToolNames(declaration: MockMcpServerDescriptor | MockMcpServerDescriptor[] | undefined): string[];
|
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
2
|
+
export const OPENCODE_VERSION = '1.18.14';
|
|
3
|
+
export const DEFAULT_OPENCODE_MODEL = 'anthropic/claude-sonnet-5';
|
|
4
|
+
export const OPENCODE_MODELS = {
|
|
5
|
+
'anthropic/claude-sonnet-5': {
|
|
6
|
+
provider: 'anthropic', apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
7
|
+
baseUrlEnv: 'ANTHROPIC_BASE_URL', allowsLocalOAuth: false,
|
|
8
|
+
},
|
|
9
|
+
'openai/gpt-5.4': {
|
|
10
|
+
provider: 'openai', apiKeyEnv: 'OPENAI_API_KEY',
|
|
11
|
+
baseUrlEnv: 'OPENAI_BASE_URL', allowsLocalOAuth: true,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const OPENCODE_DISABLED_REFRESH_TOKEN = 'pathgrade-refresh-disabled';
|
|
15
|
+
export function sanitizeOpenCodeOAuthRecord(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
|
+
return undefined;
|
|
18
|
+
const record = value;
|
|
19
|
+
if (record.type !== 'oauth'
|
|
20
|
+
|| typeof record.access !== 'string' || !record.access.trim()
|
|
21
|
+
|| typeof record.refresh !== 'string' || !record.refresh.trim()
|
|
22
|
+
|| typeof record.expires !== 'number' || !Number.isSafeInteger(record.expires) || record.expires < 0
|
|
23
|
+
|| (record.accountId !== undefined && typeof record.accountId !== 'string')
|
|
24
|
+
|| (record.enterpriseUrl !== undefined && typeof record.enterpriseUrl !== 'string')) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
type: 'oauth',
|
|
29
|
+
access: record.access,
|
|
30
|
+
refresh: record.refresh,
|
|
31
|
+
expires: record.expires,
|
|
32
|
+
...(record.accountId !== undefined ? { accountId: record.accountId } : {}),
|
|
33
|
+
...(record.enterpriseUrl !== undefined ? { enterpriseUrl: record.enterpriseUrl } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function resolveOpenCodeModel(model) {
|
|
37
|
+
const selected = model ?? DEFAULT_OPENCODE_MODEL;
|
|
38
|
+
if (!hasOwn(OPENCODE_MODELS, selected)) {
|
|
39
|
+
throw new Error(`OpenCode supports only models ${Object.keys(OPENCODE_MODELS).join(' and ')}`);
|
|
40
|
+
}
|
|
41
|
+
const normalized = selected;
|
|
42
|
+
return { model: normalized, contract: OPENCODE_MODELS[normalized] };
|
|
43
|
+
}
|
|
4
44
|
export const OPENCODE_RUNTIME_LOCK = {
|
|
5
45
|
'darwin-arm64': {
|
|
6
46
|
version: OPENCODE_VERSION,
|
|
7
|
-
executableSha256: '
|
|
47
|
+
executableSha256: '8b7c4e116c1ac5163c02fa85eee5a13d4d00c8a08e677dac4100f55aa56532fa',
|
|
8
48
|
},
|
|
9
49
|
'linux-arm64': {
|
|
10
50
|
version: OPENCODE_VERSION,
|
|
11
|
-
executableSha256: '
|
|
51
|
+
executableSha256: '79d42436517e485e9444cfc6e92582bf9224a9e36fa77fb3957b343486fec81d',
|
|
12
52
|
},
|
|
13
53
|
};
|
|
14
54
|
export function getOpenCodePlatformKey(platform, arch) {
|
|
@@ -21,16 +61,17 @@ export function currentOpenCodePlatformKey() {
|
|
|
21
61
|
function hasOwn(record, key) {
|
|
22
62
|
return Object.prototype.hasOwnProperty.call(record, key);
|
|
23
63
|
}
|
|
24
|
-
function
|
|
64
|
+
export function validateOpenCodeBaseUrl(value, variable = 'ANTHROPIC_BASE_URL') {
|
|
65
|
+
const error = `OpenCode ${variable} must be an absolute HTTPS API root ending in /v1`;
|
|
25
66
|
let url;
|
|
26
67
|
try {
|
|
27
68
|
url = new URL(value);
|
|
28
69
|
}
|
|
29
70
|
catch {
|
|
30
|
-
throw new Error(
|
|
71
|
+
throw new Error(error);
|
|
31
72
|
}
|
|
32
73
|
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !url.pathname.endsWith('/v1')) {
|
|
33
|
-
throw new Error(
|
|
74
|
+
throw new Error(error);
|
|
34
75
|
}
|
|
35
76
|
}
|
|
36
77
|
export function validateOpenCodeDeclaration(agent, opts) {
|
|
@@ -46,18 +87,20 @@ export function validateOpenCodeDeclaration(agent, opts) {
|
|
|
46
87
|
if (!opts.opencodeExecutable || !path.isAbsolute(opts.opencodeExecutable)) {
|
|
47
88
|
throw new Error('OpenCode requires an absolute opencodeExecutable path');
|
|
48
89
|
}
|
|
49
|
-
|
|
50
|
-
throw new Error(`OpenCode v1 supports only model ${OPENCODE_MODEL}`);
|
|
51
|
-
}
|
|
90
|
+
const { contract } = resolveOpenCodeModel(opts.model);
|
|
52
91
|
const env = opts.env ?? {};
|
|
53
|
-
if (
|
|
54
|
-
throw new Error(
|
|
92
|
+
if (hasOwn(env, contract.apiKeyEnv) && !env[contract.apiKeyEnv]?.trim()) {
|
|
93
|
+
throw new Error(`OpenCode env.${contract.apiKeyEnv} must be nonempty when provided`);
|
|
55
94
|
}
|
|
56
|
-
if (hasOwn(env,
|
|
57
|
-
if (!env.
|
|
58
|
-
throw new Error(
|
|
95
|
+
if (hasOwn(env, contract.baseUrlEnv)) {
|
|
96
|
+
if (!env[contract.baseUrlEnv]) {
|
|
97
|
+
throw new Error(`OpenCode ${contract.baseUrlEnv} must be a nonempty absolute HTTPS API root ending in /v1`);
|
|
98
|
+
}
|
|
99
|
+
validateOpenCodeBaseUrl(env[contract.baseUrlEnv], contract.baseUrlEnv);
|
|
100
|
+
if (!hasOwn(env, contract.apiKeyEnv)) {
|
|
101
|
+
throw new Error(`OpenCode env.${contract.baseUrlEnv} requires an explicit env.${contract.apiKeyEnv}; ` +
|
|
102
|
+
'host credentials are never sent to caller-provided endpoints.');
|
|
59
103
|
}
|
|
60
|
-
assertOpenCodeBaseUrl(env.ANTHROPIC_BASE_URL);
|
|
61
104
|
}
|
|
62
105
|
if (opts.copyFromHome !== undefined)
|
|
63
106
|
throw new Error('OpenCode does not support copyFromHome');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type OpenCodeModel } from './contract.js';
|
|
2
|
+
export declare const OPENCODE_PERMISSION: string;
|
|
3
|
+
export declare class OpenCodeRuntimePolicy {
|
|
4
|
+
readonly model: OpenCodeModel;
|
|
5
|
+
readonly oauth: boolean;
|
|
6
|
+
private readonly authPath;
|
|
7
|
+
private readonly expectedAuthDigest;
|
|
8
|
+
private readonly expectedAuthFingerprint;
|
|
9
|
+
private authWatcher;
|
|
10
|
+
private authMutationObserved;
|
|
11
|
+
private constructor();
|
|
12
|
+
static create(runtimeEnv: Record<string, string>, requestedModel?: string): Promise<OpenCodeRuntimePolicy>;
|
|
13
|
+
environment(): Record<string, string>;
|
|
14
|
+
beforeTurn(remainingMs: number, now?: number): Promise<void>;
|
|
15
|
+
afterTurn(): Promise<void>;
|
|
16
|
+
private startAuthMonitor;
|
|
17
|
+
private readUnchanged;
|
|
18
|
+
}
|