@wix/pathgrade 1.0.17 → 1.0.19

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 (50) hide show
  1. package/README.md +7 -4
  2. package/dist/agents/claude/sdk-message-projector.js +12 -4
  3. package/dist/agents/claude/tool-results.d.ts +1 -2
  4. package/dist/agents/claude/tool-results.js +9 -40
  5. package/dist/agents/claude.js +6 -4
  6. package/dist/agents/codex-app-server/agent.d.ts +5 -0
  7. package/dist/agents/codex-app-server/agent.js +145 -179
  8. package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
  9. package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
  10. package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
  11. package/dist/agents/codex-app-server/item-projection.js +135 -0
  12. package/dist/agents/codex-app-server/managed-auth.d.ts +12 -0
  13. package/dist/agents/codex-app-server/managed-auth.js +53 -0
  14. package/dist/agents/codex-app-server/transport.d.ts +2 -0
  15. package/dist/agents/codex-app-server/transport.js +30 -3
  16. package/dist/agents/opencode/host-safety.d.ts +3 -0
  17. package/dist/agents/opencode/host-safety.js +30 -0
  18. package/dist/agents/opencode.d.ts +2 -4
  19. package/dist/agents/opencode.js +47 -38
  20. package/dist/openai-oauth/chatgpt-oauth-llm.d.ts +25 -0
  21. package/dist/openai-oauth/chatgpt-oauth-llm.js +398 -0
  22. package/dist/openai-oauth/codex-auth-broker.d.ts +32 -0
  23. package/dist/openai-oauth/codex-auth-broker.js +110 -0
  24. package/dist/openai-oauth/index.d.ts +2 -0
  25. package/dist/openai-oauth/index.js +1 -0
  26. package/dist/providers/credentials.d.ts +4 -1
  27. package/dist/providers/credentials.js +4 -3
  28. package/dist/providers/sandbox.d.ts +2 -0
  29. package/dist/providers/scripted-mcp-mock-host.js +6 -3
  30. package/dist/providers/workspace.d.ts +1 -0
  31. package/dist/providers/workspace.js +9 -1
  32. package/dist/sdk/agent-result-log.js +4 -2
  33. package/dist/sdk/agent.js +7 -0
  34. package/dist/sdk/judge-tools.js +14 -3
  35. package/dist/sdk/managed-session.d.ts +2 -0
  36. package/dist/sdk/managed-session.js +22 -5
  37. package/dist/sdk/mcp-safety.js +2 -18
  38. package/dist/sdk/snapshots.d.ts +1 -0
  39. package/dist/sdk/snapshots.js +3 -2
  40. package/dist/sdk/tool-event-log.js +5 -2
  41. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  42. package/dist/sdk/tool-event-secrets.js +14 -0
  43. package/dist/sdk/turn-result-secrets.d.ts +5 -0
  44. package/dist/sdk/turn-result-secrets.js +12 -0
  45. package/dist/tool-event-results.d.ts +10 -0
  46. package/dist/tool-event-results.js +171 -0
  47. package/dist/types.d.ts +2 -0
  48. package/dist/utils/llm.js +11 -0
  49. package/docs/OPENAI_OAUTH_JUDGE.md +91 -0
  50. package/package.json +13 -2
package/README.md CHANGED
@@ -35,7 +35,8 @@ By default, Pathgrade tries to reuse the agent CLI's native auth before falling
35
35
  - **Codex**
36
36
  - forwards `OPENAI_API_KEY` when present
37
37
  - or runs `codex login --with-api-key` inside the sandbox when an API key is present
38
- - `codex exec` can reuse cached `~/.codex/auth.json` when no key is available; `app-server` may reject cached ChatGPT-token refreshes, so prefer `OPENAI_API_KEY` for the default transport
38
+ - without API credentials, the default `app-server` transport reuses the active Codex login through short-lived auth-only stdio sessions; `codex exec` retains its cached `~/.codex/auth.json` behavior
39
+ - keyless Codex evals automatically use that managed login for plain and tool-using judges; an explicit `evaluate(..., { llm })` still wins
39
40
  - **Cursor**
40
41
  - forwards `CURSOR_API_KEY` when set
41
42
  - macOS: reuses `cursor-agent login` OAuth tokens from the login Keychain
@@ -47,7 +48,9 @@ By default, Pathgrade tries to reuse the agent CLI's native auth before falling
47
48
 
48
49
  If you set `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, or `CURSOR_API_BASE_URL`, set the matching API key too.
49
50
 
50
- Do not add `copyFromHome: ['.codex']` just to reuse Codex authentication. When no OpenAI API key is available, Pathgrade automatically stages only `~/.codex/auth.json`. A deliberate full `.codex` copy remains supported, but it also imports host config, MCP definitions, plugins, caches, sessions, and other potentially large or sensitive state; Pathgrade prints a warning when requested.
51
+ An [experimental ChatGPT judge that reuses Codex-managed authentication](docs/OPENAI_OAUTH_JUDGE.md) is also available. It requires no API key, auth-file option, or second login and is selected automatically for keyless Codex evals.
52
+
53
+ Do not add `copyFromHome: ['.codex']` just to reuse Codex authentication. Keyless app-server runs stage no credential file; the legacy exec transport stages only `~/.codex/auth.json`. A deliberate full `.codex` copy remains supported, but it also imports host config, MCP definitions, plugins, caches, sessions, and other potentially large or sensitive state; Pathgrade prints a warning when requested.
51
54
 
52
55
  Override credentials per test with `env`:
53
56
 
@@ -64,7 +67,7 @@ const agent = await createAgent({
64
67
 
65
68
  Codex supports two transports and Pathgrade defaults to `app-server`:
66
69
 
67
- - `app-server` (default) — uses `codex app-server` and keeps native thread state. Required for `AskUserReaction` handshakes (`request_user_input` reaches the model). Prefer `OPENAI_API_KEY`; cached ChatGPT auth can fail if the app-server asks Pathgrade to refresh tokens.
70
+ - `app-server` (default) — uses `codex app-server` and keeps native thread state. Required for `AskUserReaction` handshakes (`request_user_input` reaches the model). Without API credentials it acquires and refreshes access sessions through the active Codex login.
68
71
  - `exec` — uses `codex exec` and re-injects the transcript every turn. Kept for stateless CI matrices that don't need the handshake.
69
72
 
70
73
  Precedence: `createAgent({ transport })` > `PATHGRADE_CODEX_TRANSPORT` env > default (`app-server`). An invalid env value throws at `createAgent` time.
@@ -80,7 +83,7 @@ If `transport: 'exec'` is resolved and any `AskUserReaction` is present in `Conv
80
83
 
81
84
  Migrating from `exec` to `app-server`:
82
85
 
83
- - Export `OPENAI_API_KEY`, or set `transport: 'exec'` / `PATHGRADE_CODEX_TRANSPORT=exec` to stay on the old transport and its cached-auth behavior.
86
+ - Sign in with `codex login` for subscription-backed app-server and judge calls, export `OPENAI_API_KEY` for API billing, or select `transport: 'exec'` for the old cached-auth behavior.
84
87
  - The `noninteractive-user-question` runtime policy no longer attaches under `app-server`. Snapshots that captured model output influenced by that policy text may need re-recording.
85
88
  - `MAX_TURN_RETRIES` does not apply under `app-server` — a crashed turn ends the conversation with `completionReason: 'agent_crashed'`.
86
89
 
@@ -22,6 +22,9 @@ const SDK_ERROR_SUBTYPES = [
22
22
  'error_max_structured_output_retries',
23
23
  ];
24
24
  import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
25
+ import { sanitizePersistenceValue } from '../../tool-event-results.js';
26
+ import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
27
+ import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
25
28
  import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
26
29
  import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
27
30
  import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
@@ -132,13 +135,15 @@ export function projectSdkMessages(input) {
132
135
  const trimmedAssistant = assistantText.trim();
133
136
  const trimmedResult = resultText.trim();
134
137
  const visible = isError ? '' : (trimmedAssistant || trimmedResult);
135
- const rawOutput = resultText || assistantText;
138
+ const rawOutput = sanitizePersistenceValue(resultText || assistantText, input.sensitiveValues);
136
139
  const enriched = enrichSkillEvents([
137
140
  ...toolEvents,
138
141
  ...(input.deniedMcpEvents?.all() ?? []),
139
142
  ]);
140
- const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills);
141
- const traceOutput = input.messages.map((m) => JSON.stringify(m)).join('\n');
143
+ const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills).map((event) => attachToolEventSensitiveValues(event, input.sensitiveValues ?? []));
144
+ const traceOutput = sanitizePersistenceValue(input.messages, input.sensitiveValues)
145
+ .map((message) => JSON.stringify(message))
146
+ .join('\n');
142
147
  const result = {
143
148
  rawOutput,
144
149
  assistantMessage: visible,
@@ -155,7 +160,10 @@ export function projectSdkMessages(input) {
155
160
  ...(costUsd !== undefined ? { costUsd } : {}),
156
161
  ...(errorSubtype !== undefined ? { errorSubtype } : {}),
157
162
  };
158
- return { result, sessionId };
163
+ return {
164
+ result: attachTurnResultSensitiveValues(result, input.sensitiveValues ?? []),
165
+ sessionId,
166
+ };
159
167
  }
160
168
  /**
161
169
  * If the first user message of the turn is `/<name>` and `<name>` is one of
@@ -1,6 +1,6 @@
1
1
  import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { ToolEvent } from '../../tool-events.js';
3
- export declare const TOOL_RESULT_MAX_CHARS: number;
3
+ export { collectSensitiveEnvValues, TOOL_RESULT_MAX_CHARS } from '../../tool-event-results.js';
4
4
  export interface ClaudeSdkMessageTiming {
5
5
  receivedAt: string;
6
6
  receivedMonotonicMs: number;
@@ -16,4 +16,3 @@ export interface ObservedToolResult {
16
16
  export declare function hasToolLifecycleBoundary(message: SDKMessage): boolean;
17
17
  export declare function extractObservedToolResults(message: SDKMessage, timing?: ClaudeSdkMessageTiming): ObservedToolResult[];
18
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[];
@@ -1,5 +1,5 @@
1
- export const TOOL_RESULT_MAX_CHARS = 64 * 1024;
2
- const SECRET_ENV_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)/i;
1
+ import { sanitizeToolEventResult, } from '../../tool-event-results.js';
2
+ export { collectSensitiveEnvValues, TOOL_RESULT_MAX_CHARS } from '../../tool-event-results.js';
3
3
  export function hasToolLifecycleBoundary(message) {
4
4
  if (message.type !== 'assistant' && message.type !== 'user')
5
5
  return false;
@@ -52,50 +52,19 @@ export function applyObservedToolResult(event, observed, startedMonotonicMs, sen
52
52
  event.arguments.status = event.status;
53
53
  }
54
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
55
  function buildBoundedToolResult(observed, sensitiveValues) {
62
56
  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;
57
+ const result = {
58
+ ...(typeof raw?.stdout === 'string' ? { stdout: raw.stdout } : {}),
59
+ ...(typeof raw?.stderr === 'string' ? { stderr: raw.stderr } : {}),
60
+ ...(typeof observed.content === 'string' && observed.content !== raw?.stdout
61
+ ? { content: observed.content }
62
+ : {}),
71
63
  };
72
- addBounded('stdout', raw?.stdout);
73
- addBounded('stderr', raw?.stderr);
74
- if (observed.content !== raw?.stdout)
75
- addBounded('content', observed.content);
76
64
  const exitCode = raw?.exitCode ?? raw?.exit_code;
77
65
  if (typeof exitCode === 'number' && Number.isFinite(exitCode))
78
66
  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
- };
67
+ return sanitizeToolEventResult(result, sensitiveValues);
99
68
  }
100
69
  function extractTextContent(content) {
101
70
  if (typeof content === 'string')
@@ -28,6 +28,8 @@ import { createSandboxedClaudeSpawn } from '../providers/sandboxed-claude-spawn.
28
28
  import { assertClaudeLiveMcpSafetyPreflight, assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
29
29
  import { buildClaudeSdkOptions, resolveClaudeCodeExecutable, } from './claude/sdk-options.js';
30
30
  import { projectSdkMessages } from './claude/sdk-message-projector.js';
31
+ import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
32
+ import { cloneTurnResultWithSensitiveValues } from '../sdk/turn-result-secrets.js';
31
33
  import { collectSensitiveEnvValues, hasToolLifecycleBoundary, } from './claude/tool-results.js';
32
34
  import { createAskUserAnswerStore } from './claude/ask-user-answer-store.js';
33
35
  import { createClaudeToolPermissionBridge } from './claude/tool-permission-bridge.js';
@@ -195,7 +197,8 @@ export class ClaudeAgent extends BaseAgent {
195
197
  deniedMcpEvents,
196
198
  sensitiveValues,
197
199
  });
198
- projected.result.toolEvents = [...scriptedEvents, ...projected.result.toolEvents];
200
+ projected.result.toolEvents = [...scriptedEvents, ...projected.result.toolEvents]
201
+ .map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
199
202
  // Capture the SDK-reported session id BEFORE checking for a bus
200
203
  // rejection so the next turn's `Options.resume` points at this
201
204
  // turn's session even when the turn ended in an ask-bus error.
@@ -214,12 +217,11 @@ export class ClaudeAgent extends BaseAgent {
214
217
  const errorMessage = bridgeError instanceof Error
215
218
  ? bridgeError.message
216
219
  : String(bridgeError);
217
- return {
218
- ...projected.result,
220
+ return cloneTurnResultWithSensitiveValues(projected.result, {
219
221
  exitCode: 1,
220
222
  errorSubtype: 'bus_rejection',
221
223
  rawOutput: errorMessage,
222
- };
224
+ });
223
225
  }
224
226
  return projected.result;
225
227
  };
@@ -1,5 +1,6 @@
1
1
  import { AgentCommandRunner, AgentSession, AgentSessionOptions, BaseAgent, EnvironmentHandle } from '../../types.js';
2
2
  import { type AppServerSessionHandle } from './transport.js';
3
+ import { type LifecycleClock } from './item-lifecycle.js';
3
4
  type SandboxMode = 'workspace-write' | 'danger-full-access';
4
5
  export interface PermissionGrantLogEntry {
5
6
  type: 'permissions_granted';
@@ -25,6 +26,10 @@ export interface CodexAppServerAgentDeps {
25
26
  sandboxMode?: SandboxMode;
26
27
  /** Observer for per-grant audit entries (§7 of design decisions). */
27
28
  onPermissionGrant?: (entry: PermissionGrantLogEntry) => void;
29
+ /** Injectable clocks keep wall timestamps and monotonic durations independently testable. */
30
+ clock?: LifecycleClock;
31
+ /** Inject Codex-managed ChatGPT access sessions for deterministic tests. */
32
+ authBroker?: import('../../openai-oauth/codex-auth-broker.js').CodexAuthBroker;
28
33
  }
29
34
  export declare class CodexAppServerAgent extends BaseAgent {
30
35
  private deps;