@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
@@ -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.
package/dist/sdk/agent.js CHANGED
@@ -17,6 +17,7 @@ import fs from 'fs-extra';
17
17
  import * as path from 'path';
18
18
  import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
19
19
  import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
20
+ import { collectSensitiveEnvValues } from '../tool-event-results.js';
20
21
  import { compileMcpMockApprovalSession, } from './mcp-mock-approvals.js';
21
22
  /**
22
23
  * Test-only injection point: override the sink used by the next emitter
@@ -49,6 +50,7 @@ class AgentImpl {
49
50
  opencodeMcpToolNames;
50
51
  activeChatSession;
51
52
  scriptedMcp;
53
+ sensitiveValues;
52
54
  constructor(opts) {
53
55
  this.ws = opts.workspace;
54
56
  this.agentName = opts.agentName;
@@ -65,6 +67,7 @@ class AgentImpl {
65
67
  this.opencodeExecutable = opts.opencodeExecutable;
66
68
  this.opencodeMcpToolNames = opts.opencodeMcpToolNames;
67
69
  this.scriptedMcp = opts.scriptedMcp;
70
+ this.sensitiveValues = opts.sensitiveValues;
68
71
  }
69
72
  get messages() {
70
73
  return this._messages;
@@ -110,6 +113,7 @@ class AgentImpl {
110
113
  ...(this.opencodeExecutable !== undefined ? { opencodeExecutable: this.opencodeExecutable } : {}),
111
114
  ...(this.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: this.opencodeMcpToolNames } : {}),
112
115
  ...(this.scriptedMcp !== undefined ? { scriptedMcp: this.scriptedMcp } : {}),
116
+ sensitiveValues: this.sensitiveValues,
113
117
  });
114
118
  }
115
119
  resolveTimeoutSec(mode, maxTurns) {
@@ -349,6 +353,7 @@ class AgentImpl {
349
353
  log: this._log,
350
354
  conversationResult: this.lastConversationResult,
351
355
  workspace: dest,
356
+ sensitiveValues: this.sensitiveValues,
352
357
  });
353
358
  await fs.writeJSON(path.join(dest, 'run-snapshot.json'), snapshot, { spaces: 2 });
354
359
  }
@@ -425,6 +430,7 @@ export async function createAgent(opts) {
425
430
  const workspace = await prepareWorkspace({
426
431
  ...rest,
427
432
  agent: agentName,
433
+ transport,
428
434
  mcp: scriptedMcp
429
435
  ? undefined
430
436
  : mcpConfigFile ? { configFile: mcpConfigFile } : mcpMock ? { mock: mcpMock } : undefined,
@@ -457,6 +463,7 @@ export async function createAgent(opts) {
457
463
  opencodeExecutable,
458
464
  opencodeMcpToolNames: agentName === 'opencode' ? collectOpenCodeMcpToolNames(mcpMock) : undefined,
459
465
  scriptedMcp,
466
+ sensitiveValues: workspace.sensitiveValues ?? collectSensitiveEnvValues(workspace.env),
460
467
  });
461
468
  lifecycleCore.registerAgent(agent);
462
469
  return agent;
@@ -69,10 +69,21 @@ async function checkFinalContainment(workspace, absolutePath) {
69
69
  return false;
70
70
  }
71
71
  }
72
+ async function resolveExistingInWorkspace(workspace, relPath) {
73
+ const resolved = await resolveInWorkspace(workspace, relPath);
74
+ const [workspaceReal, targetReal] = await Promise.all([
75
+ fs.realpath(workspace),
76
+ fs.realpath(resolved),
77
+ ]);
78
+ if (!isInside(workspaceReal, targetReal)) {
79
+ throw new Error(`Path "${relPath}" is outside workspace`);
80
+ }
81
+ return targetReal;
82
+ }
72
83
  export async function readFile(ctx, relPath) {
73
- const resolved = await resolveInWorkspace(ctx.workspace, relPath);
74
84
  let content;
75
85
  try {
86
+ const resolved = await resolveExistingInWorkspace(ctx.workspace, relPath);
76
87
  content = await fs.readFile(resolved, 'utf8');
77
88
  }
78
89
  catch (err) {
@@ -88,9 +99,9 @@ export async function readFile(ctx, relPath) {
88
99
  return content;
89
100
  }
90
101
  export async function listDir(ctx, relPath) {
91
- const resolved = await resolveInWorkspace(ctx.workspace, relPath);
92
102
  let entries;
93
103
  try {
104
+ const resolved = await resolveExistingInWorkspace(ctx.workspace, relPath);
94
105
  entries = await fs.readdir(resolved, { withFileTypes: true });
95
106
  }
96
107
  catch (err) {
@@ -107,7 +118,7 @@ export async function listDir(ctx, relPath) {
107
118
  }
108
119
  export async function grep(ctx, pattern, relPath) {
109
120
  const rootRel = relPath ?? '.';
110
- const root = await resolveInWorkspace(ctx.workspace, rootRel);
121
+ const root = await resolveExistingInWorkspace(ctx.workspace, rootRel);
111
122
  const workspaceReal = await fs.realpath(ctx.workspace);
112
123
  const regex = new RegExp(pattern);
113
124
  const matches = [];
@@ -36,6 +36,8 @@ export interface ManagedSessionDeps {
36
36
  opencodeMcpToolNames?: string[];
37
37
  /** Trusted pre-workspace compiled generated-MCP declaration. */
38
38
  scriptedMcp?: CompiledMcpMockSession;
39
+ /** Runtime-only values that must not enter session logs or persisted results. */
40
+ sensitiveValues?: readonly string[];
39
41
  }
40
42
  export interface ManagedSession {
41
43
  /** Full lifecycle: log start/result, push messages, check exit code. */
@@ -6,6 +6,9 @@ import { buildToolEventLogEntry } from './tool-event-log.js';
6
6
  import { planRuntimePolicies } from './runtime-policy.js';
7
7
  import { getVisibleAssistantMessage } from './visible-turn.js';
8
8
  import { createAskBus } from './ask-bus/bus.js';
9
+ import { attachTurnResultSensitiveValues, cloneTurnResultWithSensitiveValues, getTurnResultSensitiveValues, } from './turn-result-secrets.js';
10
+ import { attachToolEventSensitiveValues, getToolEventSensitiveValues } from './tool-event-secrets.js';
11
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
9
12
  import { startScriptedMcpMockHost, } from '../providers/scripted-mcp-mock-host.js';
10
13
  export function createManagedSession(deps) {
11
14
  const { ws, agentName, timeoutSec, messages, log, model, conversationWindow, llm } = deps;
@@ -28,6 +31,7 @@ export function createManagedSession(deps) {
28
31
  ...(deps.mcpSafety !== undefined ? { mcpSafety: deps.mcpSafety } : {}),
29
32
  ...(deps.opencodeExecutable !== undefined ? { opencodeExecutable: deps.opencodeExecutable } : {}),
30
33
  ...(deps.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: deps.opencodeMcpToolNames } : {}),
34
+ ...(deps.sensitiveValues !== undefined ? { sensitiveValues: deps.sensitiveValues } : {}),
31
35
  getAbortSignal: () => currentSignal,
32
36
  getRemainingMs: () => Math.max(0, deadlineMs - Date.now()),
33
37
  };
@@ -40,14 +44,14 @@ export function createManagedSession(deps) {
40
44
  let disposePromise;
41
45
  const runCommand = async (cmd) => {
42
46
  const result = await ws.exec(cmd, { signal: currentSignal });
43
- log.push({
47
+ log.push(sanitizePersistenceValue({
44
48
  type: 'command',
45
49
  timestamp: new Date().toISOString(),
46
50
  command: cmd,
47
51
  stdout: result.stdout,
48
52
  stderr: result.stderr,
49
53
  exitCode: result.exitCode,
50
- });
54
+ }, deps.sensitiveValues));
51
55
  return result;
52
56
  };
53
57
  const executeTurn = async (message) => {
@@ -94,17 +98,30 @@ export function createManagedSession(deps) {
94
98
  }
95
99
  const turnNumber = ++nextTurnNumber;
96
100
  scriptedHost?.beginTurn(turnNumber);
97
- const result = turnNumber === 1
101
+ let result = turnNumber === 1
98
102
  ? await session.start({ message })
99
103
  : await session.reply({ message });
100
104
  if (scriptedHost) {
101
105
  const settled = scriptedHost.settleEvents(result.toolEvents);
102
106
  result.toolEvents = settled.events;
103
107
  if (settled.error) {
104
- return { ...result, exitCode: 1, rawOutput: settled.error.message };
108
+ result = cloneTurnResultWithSensitiveValues(result, {
109
+ exitCode: 1,
110
+ rawOutput: settled.error.message,
111
+ });
105
112
  }
106
113
  }
107
- return result;
114
+ const sensitiveValues = [...new Set([
115
+ ...(deps.sensitiveValues ?? []),
116
+ ...getTurnResultSensitiveValues(result),
117
+ ])];
118
+ for (const event of result.toolEvents) {
119
+ attachToolEventSensitiveValues(event, [...new Set([
120
+ ...sensitiveValues,
121
+ ...getToolEventSensitiveValues(event),
122
+ ])]);
123
+ }
124
+ return attachTurnResultSensitiveValues(result, sensitiveValues);
108
125
  }, remaining, label);
109
126
  }
110
127
  finally {
@@ -1,4 +1,4 @@
1
- const SECRET_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)|authorization/i;
1
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
2
2
  export function decideMcpToolCall(options, request) {
3
3
  const runMode = options?.runMode ?? 'mock';
4
4
  if (runMode === 'mock')
@@ -31,7 +31,7 @@ export function decideMcpToolCall(options, request) {
31
31
  return { action: 'allow' };
32
32
  }
33
33
  export function redactMcpSecrets(value) {
34
- return redactValue(value, '');
34
+ return sanitizePersistenceValue(value);
35
35
  }
36
36
  function ruleMatches(rule, request) {
37
37
  if (rule.serverName !== undefined && rule.serverName !== request.serverName)
@@ -43,19 +43,3 @@ function ruleMatches(rule, request) {
43
43
  function deny(reason, message) {
44
44
  return { action: 'deny', reason, message };
45
45
  }
46
- function redactValue(value, key) {
47
- if (SECRET_KEY_PATTERN.test(key))
48
- return '<redacted>';
49
- if (Array.isArray(value))
50
- return value.map((entry) => redactValue(entry, key));
51
- if (isRecord(value)) {
52
- return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
53
- entryKey,
54
- redactValue(entryValue, entryKey),
55
- ]));
56
- }
57
- return value;
58
- }
59
- function isRecord(value) {
60
- return !!value && typeof value === 'object' && !Array.isArray(value);
61
- }
@@ -25,6 +25,7 @@ export declare function buildRunSnapshot(params: {
25
25
  conversationResult: ConversationResult;
26
26
  workspace: string | null;
27
27
  timestamp?: string;
28
+ sensitiveValues?: readonly string[];
28
29
  }): RunSnapshot;
29
30
  export declare class SnapshotParseError extends Error {
30
31
  constructor(message: string, options?: {
@@ -1,3 +1,4 @@
1
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
1
2
  import fs from 'fs-extra';
2
3
  export const RUN_SNAPSHOT_VERSION = 2;
3
4
  export function buildRunSnapshot(params) {
@@ -5,7 +6,7 @@ export function buildRunSnapshot(params) {
5
6
  const toolEvents = log
6
7
  .filter((entry) => entry.type === 'tool_event' && entry.tool_event)
7
8
  .map((entry) => entry.tool_event);
8
- return {
9
+ return sanitizePersistenceValue({
9
10
  version: agent === 'opencode' ? 2 : 1,
10
11
  timestamp: timestamp ?? new Date().toISOString(),
11
12
  agent,
@@ -20,7 +21,7 @@ export function buildRunSnapshot(params) {
20
21
  turnTimings: [...conversationResult.turnTimings],
21
22
  },
22
23
  workspace,
23
- };
24
+ }, params.sensitiveValues);
24
25
  }
25
26
  export class SnapshotParseError extends Error {
26
27
  constructor(message, options) {
@@ -1,7 +1,10 @@
1
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
2
+ import { getToolEventSensitiveValues } from './tool-event-secrets.js';
1
3
  export function buildToolEventLogEntry(toolEvent, fallbackTimestamp) {
4
+ const persistedEvent = sanitizePersistenceValue(toolEvent, getToolEventSensitiveValues(toolEvent));
2
5
  return {
3
6
  type: 'tool_event',
4
- timestamp: toolEvent.startedAt ?? fallbackTimestamp,
5
- tool_event: toolEvent,
7
+ timestamp: persistedEvent.startedAt ?? fallbackTimestamp,
8
+ tool_event: persistedEvent,
6
9
  };
7
10
  }
@@ -0,0 +1,4 @@
1
+ import type { ToolEvent } from '../tool-events.js';
2
+ export declare function attachToolEventSensitiveValues(event: ToolEvent, sensitiveValues: readonly string[]): ToolEvent;
3
+ export declare function getToolEventSensitiveValues(event: ToolEvent): readonly string[];
4
+ export declare function cloneToolEventWithRuntimeMetadata(source: ToolEvent, overrides: Partial<ToolEvent>): ToolEvent;
@@ -0,0 +1,14 @@
1
+ import { attachOriginalMcpInput, getOriginalMcpInput } from './mcp-event-input.js';
2
+ const sensitiveValuesByEvent = new WeakMap();
3
+ export function attachToolEventSensitiveValues(event, sensitiveValues) {
4
+ sensitiveValuesByEvent.set(event, [...sensitiveValues]);
5
+ return event;
6
+ }
7
+ export function getToolEventSensitiveValues(event) {
8
+ return sensitiveValuesByEvent.get(event) ?? [];
9
+ }
10
+ export function cloneToolEventWithRuntimeMetadata(source, overrides) {
11
+ const clone = attachToolEventSensitiveValues({ ...source, ...overrides }, getToolEventSensitiveValues(source));
12
+ const originalInput = getOriginalMcpInput(source);
13
+ return originalInput ? attachOriginalMcpInput(clone, originalInput) : clone;
14
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgentTurnResult } from '../types.js';
2
+ /** Attach runtime-only redaction context without changing the serialized result contract. */
3
+ export declare function attachTurnResultSensitiveValues(result: AgentTurnResult, sensitiveValues: readonly string[]): AgentTurnResult;
4
+ export declare function getTurnResultSensitiveValues(result: AgentTurnResult): readonly string[];
5
+ export declare function cloneTurnResultWithSensitiveValues(source: AgentTurnResult, overrides: Partial<AgentTurnResult>): AgentTurnResult;
@@ -0,0 +1,12 @@
1
+ const sensitiveValuesByResult = new WeakMap();
2
+ /** Attach runtime-only redaction context without changing the serialized result contract. */
3
+ export function attachTurnResultSensitiveValues(result, sensitiveValues) {
4
+ sensitiveValuesByResult.set(result, [...sensitiveValues]);
5
+ return result;
6
+ }
7
+ export function getTurnResultSensitiveValues(result) {
8
+ return sensitiveValuesByResult.get(result) ?? [];
9
+ }
10
+ export function cloneTurnResultWithSensitiveValues(source, overrides) {
11
+ return attachTurnResultSensitiveValues({ ...source, ...overrides }, getTurnResultSensitiveValues(source));
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { ToolEventResult } from './tool-events.js';
2
+ export declare const TOOL_RESULT_MAX_CHARS: number;
3
+ export declare function collectSensitiveEnvValues(env?: Readonly<Record<string, string>>): string[];
4
+ /**
5
+ * Clone a persistence payload while removing secrets from both structured
6
+ * containers and any other strings that repeat their values. The second pass
7
+ * is important for summaries, snippets, traces, and provider error text.
8
+ */
9
+ export declare function sanitizePersistenceValue<T>(source: T, explicitSensitiveValues?: readonly string[]): T;
10
+ export declare function sanitizeToolEventResult(source: Readonly<ToolEventResult>, sensitiveValues: readonly string[]): ToolEventResult;
@@ -0,0 +1,171 @@
1
+ export const TOOL_RESULT_MAX_CHARS = 64 * 1024;
2
+ const SECRET_KEY_NAMES = [
3
+ 'apikey',
4
+ 'accesskey',
5
+ 'accesskeyid',
6
+ 'accesstoken',
7
+ 'refreshtoken',
8
+ 'idtoken',
9
+ 'sessiontoken',
10
+ 'token',
11
+ 'secret',
12
+ 'clientsecret',
13
+ 'password',
14
+ 'passwd',
15
+ 'authorization',
16
+ 'auth',
17
+ 'bearer',
18
+ 'cookie',
19
+ 'setcookie',
20
+ 'privatekey',
21
+ 'signingkey',
22
+ 'serviceaccount',
23
+ 'databaseurl',
24
+ 'connectionstring',
25
+ 'credentials',
26
+ 'credential',
27
+ ];
28
+ export function collectSensitiveEnvValues(env) {
29
+ return [...new Set(Object.entries(env ?? {})
30
+ .filter(([key, value]) => isSecretKey(key) && value.length > 0)
31
+ .map(([, value]) => value))]
32
+ .sort((a, b) => b.length - a.length);
33
+ }
34
+ /**
35
+ * Clone a persistence payload while removing secrets from both structured
36
+ * containers and any other strings that repeat their values. The second pass
37
+ * is important for summaries, snippets, traces, and provider error text.
38
+ */
39
+ export function sanitizePersistenceValue(source, explicitSensitiveValues = []) {
40
+ const sensitiveValues = [...new Set([
41
+ ...explicitSensitiveValues.filter((value) => value.length > 0),
42
+ ...collectStructuredSensitiveValues(source),
43
+ ])].sort((a, b) => b.length - a.length);
44
+ return sanitizeValue(source, '', sensitiveValues);
45
+ }
46
+ export function sanitizeToolEventResult(source, sensitiveValues) {
47
+ const result = {};
48
+ let truncated = source.truncated === true;
49
+ const addBounded = (key, value) => {
50
+ if (typeof value !== 'string')
51
+ return;
52
+ const bounded = boundText(sanitizePersistenceValue(value, sensitiveValues));
53
+ result[key] = bounded.value;
54
+ truncated ||= bounded.truncated;
55
+ };
56
+ addBounded('content', source.content);
57
+ addBounded('stdout', source.stdout);
58
+ addBounded('stderr', source.stderr);
59
+ if (typeof source.exitCode === 'number' && Number.isFinite(source.exitCode)) {
60
+ result.exitCode = source.exitCode;
61
+ }
62
+ if (truncated)
63
+ result.truncated = true;
64
+ return result;
65
+ }
66
+ function redactSensitiveValues(value, sensitiveValues) {
67
+ let redacted = value;
68
+ for (const secret of sensitiveValues) {
69
+ if (secret.length > 0)
70
+ redacted = redacted.split(secret).join('<redacted>');
71
+ }
72
+ return redacted;
73
+ }
74
+ function collectStructuredSensitiveValues(source) {
75
+ const values = new Set();
76
+ const visit = (value, key) => {
77
+ if (typeof value === 'string' && isSecretKey(key) && value.length > 0) {
78
+ values.add(value);
79
+ return;
80
+ }
81
+ if (Array.isArray(value)) {
82
+ for (const entry of value)
83
+ visit(entry, key);
84
+ return;
85
+ }
86
+ if (isRecord(value)) {
87
+ for (const [entryKey, entryValue] of Object.entries(value))
88
+ visit(entryValue, entryKey);
89
+ }
90
+ };
91
+ visit(source, '');
92
+ return [...values];
93
+ }
94
+ function sanitizeValue(value, key, sensitiveValues) {
95
+ if (isSecretKey(key))
96
+ return '<redacted>';
97
+ if (typeof value === 'string')
98
+ return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues));
99
+ if (Array.isArray(value))
100
+ return value.map((entry) => sanitizeValue(entry, key, sensitiveValues));
101
+ if (isRecord(value)) {
102
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
103
+ entryKey,
104
+ sanitizeValue(entryValue, entryKey, sensitiveValues),
105
+ ]));
106
+ }
107
+ return value;
108
+ }
109
+ function redactCredentialShapes(value) {
110
+ let redacted = value;
111
+ const structured = parseStructuredJson(redacted);
112
+ if (structured !== undefined) {
113
+ redacted = JSON.stringify(sanitizePersistenceValue(structured));
114
+ }
115
+ if (redacted.includes('PRIVATE KEY-----')) {
116
+ redacted = redacted.replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\n]*PRIVATE KEY-----/g, '<redacted>');
117
+ }
118
+ if (/\b(?:bearer|basic)\s/i.test(redacted)) {
119
+ redacted = redacted.replace(/(\b(?:bearer|basic)\s+)[^\s,;"']+/gi, '$1<redacted>');
120
+ }
121
+ if (redacted.includes('://')) {
122
+ redacted = redacted.replace(/([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s/@]+(@)/gi, '$1<redacted>$2');
123
+ }
124
+ if (redacted.includes('=') || redacted.includes(':')) {
125
+ redacted = redacted.replace(/(\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|session(?:id)?|cookie)\s*[=:]\s*)[^\s,;"'}]+/gi, '$1<redacted>');
126
+ }
127
+ return redacted;
128
+ }
129
+ function isSecretKey(key) {
130
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
131
+ if (SECRET_KEY_NAMES.some((name) => normalized === name))
132
+ return true;
133
+ const compoundNames = SECRET_KEY_NAMES.filter((name) => name !== 'auth');
134
+ if (compoundNames.some((name) => normalized.startsWith(name) || normalized.endsWith(name)))
135
+ return true;
136
+ const segments = key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
137
+ for (let start = 0; start < segments.length; start++) {
138
+ let candidate = '';
139
+ for (let end = start; end < segments.length; end++) {
140
+ candidate += segments[end];
141
+ if (SECRET_KEY_NAMES.some((name) => candidate === name))
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+ function parseStructuredJson(value) {
148
+ const trimmed = value.trim();
149
+ if (!(trimmed.startsWith('{') && trimmed.endsWith('}'))
150
+ && !(trimmed.startsWith('[') && trimmed.endsWith(']')))
151
+ return undefined;
152
+ try {
153
+ const parsed = JSON.parse(trimmed);
154
+ return isRecord(parsed) || Array.isArray(parsed) ? parsed : undefined;
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ }
160
+ function isRecord(value) {
161
+ return !!value && typeof value === 'object' && !Array.isArray(value);
162
+ }
163
+ function boundText(value) {
164
+ if (value.length <= TOOL_RESULT_MAX_CHARS)
165
+ return { value, truncated: false };
166
+ const marker = '\n[truncated by PathGrade]';
167
+ return {
168
+ value: `${value.slice(0, TOOL_RESULT_MAX_CHARS - marker.length)}${marker}`,
169
+ truncated: true,
170
+ };
171
+ }
package/dist/types.d.ts CHANGED
@@ -387,6 +387,8 @@ export interface AgentSessionOptions {
387
387
  opencodeMcpToolNames?: string[];
388
388
  /** Managed-session-owned runtime for scripted generated MCP. */
389
389
  scriptedMcpHost?: import('./providers/scripted-mcp-mock-host.js').ScriptedMcpMockHost;
390
+ /** Runtime-only values inherited by drivers and persistence sinks for redaction. */
391
+ sensitiveValues?: readonly string[];
390
392
  }
391
393
  export declare abstract class BaseAgent {
392
394
  createSession(runtime: EnvironmentHandle, runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
package/dist/utils/llm.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolveCodexModel } from '../agents/codex-model.js';
2
+ import { createChatGptOAuthJudgeLLM } from '../openai-oauth/chatgpt-oauth-llm.js';
1
3
  import { cliProvider } from './llm-providers/cli.js';
2
4
  import { anthropicProvider } from './llm-providers/anthropic.js';
3
5
  import { openaiProvider } from './llm-providers/openai.js';
@@ -191,6 +193,15 @@ export async function callLLM(prompt, opts = {}) {
191
193
  * the agent's env propagates to persona/judge/summarization calls.
192
194
  */
193
195
  export function createAgentLLM(agentName, agentEnv, agentModel) {
196
+ const effectiveAgentEnv = agentEnv ?? process.env;
197
+ if (agentName === 'codex'
198
+ && !effectiveAgentEnv.OPENAI_API_KEY
199
+ && !effectiveAgentEnv.OPENAI_BASE_URL) {
200
+ return createChatGptOAuthJudgeLLM({
201
+ model: resolveCodexModel(agentModel),
202
+ reasoningEffort: 'high',
203
+ });
204
+ }
194
205
  // Tool-using judges need a provider that implements callWithTools.
195
206
  // For claude, anthropicProvider is added as a tool-use-capable fallback
196
207
  // alongside the CLI. The CLI still wins for plain call() when available.
@@ -0,0 +1,91 @@
1
+ # Experimental Codex-authenticated ChatGPT judge
2
+
3
+ Status: **experimental**. W0b passed for the direct
4
+ `@openai-oauth/core@2.0.0` Responses mapping, and W0c passed for acquiring a
5
+ managed ChatGPT access session from Codex App Server.
6
+
7
+ ## Install and authenticate
8
+
9
+ Install the exact optional peer and sign in once with Codex:
10
+
11
+ ```bash
12
+ yarn add -D @wix/pathgrade @openai-oauth/core@2.0.0
13
+ codex login
14
+ ```
15
+
16
+ Pathgrade starts a short-lived `codex app-server` child over stdio, calls only
17
+ the auth-status RPC, then closes it before making the direct judge request. It
18
+ does not read, copy, or write `~/.codex/auth.json`, receive a refresh token,
19
+ open a local port, or start a Codex model thread. After a real upstream 401 it
20
+ asks Codex to refresh, retries once, and otherwise fails closed.
21
+
22
+ ## Inject explicitly
23
+
24
+ Codex agents select this judge automatically when neither `OPENAI_API_KEY` nor
25
+ `OPENAI_BASE_URL` is present. The judge uses the resolved Codex model (Luna by
26
+ default) with high reasoning effort. Passing `evaluate(agent, scorers, { llm })`
27
+ remains the highest-precedence override. Explicit construction is available for
28
+ other agents and custom flows:
29
+
30
+ ```typescript
31
+ import { createChatGptOAuthJudgeLLM } from '@wix/pathgrade/openai-oauth';
32
+ import { evaluate, judge } from '@wix/pathgrade';
33
+
34
+ const llm = createChatGptOAuthJudgeLLM({
35
+ model: 'gpt-5.6-sol',
36
+ reasoningEffort: 'high',
37
+ requestTimeoutMs: 60_000,
38
+ });
39
+
40
+ const result = await evaluate(agent, [
41
+ judge('quality', { rubric: 'Apply the frozen rubric.', model: 'gpt-5.6-sol' }),
42
+ ], { llm });
43
+ ```
44
+
45
+ The scorer model must exactly equal the factory model. There is no auth-file,
46
+ API-key, base-URL, login callback, provider fallback, model rewrite, or default
47
+ provider registration. Plain calls expose zero tools. Tool judges expose only
48
+ their declared Pathgrade schemas through the existing bounded tool registry.
49
+
50
+ ## Stable public failures
51
+
52
+ - `OAUTH_CONFIG_INVALID`
53
+ - `OAUTH_DEPENDENCY_UNAVAILABLE`
54
+ - `OAUTH_CODEX_UNAVAILABLE`
55
+ - `OAUTH_CODEX_LOGIN_REQUIRED`
56
+ - `OAUTH_AUTH_TIMEOUT`
57
+ - `OAUTH_MODEL_MISMATCH`
58
+ - `OAUTH_MODEL_UNAVAILABLE`
59
+ - `OAUTH_REQUEST_TIMEOUT`
60
+ - `OAUTH_UPSTREAM_RATE_LIMITED`
61
+ - `OAUTH_UPSTREAM_FAILED`
62
+ - `OAUTH_PROTOCOL_RESPONSE_INVALID`
63
+
64
+ Tokens, Codex paths, auth JSON, prompts, App Server stderr/RPC text, upstream
65
+ bodies, and tool results are never included in public errors. Failures never
66
+ fall back to another provider.
67
+
68
+ ## Repository-only verification
69
+
70
+ These maintainer probes run from a source checkout and are not included in the
71
+ published npm package. Run them from the repository root.
72
+
73
+ The no-cost installed-binary probe reports only version, auth-mode booleans,
74
+ and its ADAPT/STOP verdict:
75
+
76
+ ```bash
77
+ yarn smoke:codex-auth
78
+ ```
79
+
80
+ The explicit paid live suite uses only the active Codex login. It exercises
81
+ direct plain and bounded `readFile` judges, then a real keyless Codex app-server
82
+ agent followed by automatically selected plain and `readFile` judges:
83
+
84
+ ```bash
85
+ yarn test:evals:judge-oauth:live
86
+ ```
87
+
88
+ It writes redacted reports to `.pathgrade/judge-oauth-live-smoke.{json,md}` and
89
+ `.pathgrade/codex-oauth-agent-judge-e2e.json`.
90
+ Any `@openai-oauth/core` or Codex protocol upgrade must rerun W0b, W0c, and
91
+ the live smoke.