@wix/pathgrade 1.0.37 → 1.0.39

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 (53) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter-kit/index.d.ts +2 -2
  3. package/dist/adapter-kit/index.js +1 -1
  4. package/dist/adapters/jest/lifecycle.js +4 -2
  5. package/dist/adapters/node-test/index.js +4 -2
  6. package/dist/agents/claude/sdk-message-projector.js +4 -5
  7. package/dist/agents/claude/tool-permission-bridge.js +2 -1
  8. package/dist/agents/codex-app-server/item-projection.js +2 -2
  9. package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
  10. package/dist/agents/opencode.js +4 -5
  11. package/dist/commands/report.js +5 -25
  12. package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
  13. package/dist/reporters/cli.js +20 -1
  14. package/dist/reporters/github-comment.js +18 -7
  15. package/dist/reporters/loader.d.ts +4 -0
  16. package/dist/reporters/loader.js +26 -9
  17. package/dist/reporting/comparison-contract.js +20 -6
  18. package/dist/reporting/core.js +18 -8
  19. package/dist/reporting/report-parser.js +57 -2
  20. package/dist/reporting/types.d.ts +2 -1
  21. package/dist/runners/adapter.d.ts +4 -2
  22. package/dist/runners/lifecycle-hooks.js +6 -1
  23. package/dist/runners/orchestrator.js +3 -4
  24. package/dist/runners/repeated-invocation.js +2 -5
  25. package/dist/runners/report-projection.js +1 -0
  26. package/dist/runners/vitest-lifecycle.d.ts +1 -0
  27. package/dist/runners/vitest-lifecycle.js +21 -4
  28. package/dist/sdk/agent-flow.d.ts +51 -0
  29. package/dist/sdk/agent-flow.js +23 -0
  30. package/dist/sdk/agent.js +10 -1
  31. package/dist/sdk/evaluate.d.ts +2 -1
  32. package/dist/sdk/evaluate.js +114 -4
  33. package/dist/sdk/index.d.ts +4 -2
  34. package/dist/sdk/index.js +2 -2
  35. package/dist/sdk/judge-prompt-builder.js +11 -7
  36. package/dist/sdk/lifecycle.d.ts +3 -1
  37. package/dist/sdk/lifecycle.js +68 -5
  38. package/dist/sdk/mcp-event-input.d.ts +1 -0
  39. package/dist/sdk/mcp-event-input.js +3 -0
  40. package/dist/sdk/mcp-evidence.js +16 -3
  41. package/dist/sdk/mcp-safety.js +2 -2
  42. package/dist/sdk/result-capture.d.ts +14 -1
  43. package/dist/sdk/result-capture.js +49 -0
  44. package/dist/sdk/scripted-mcp-events.js +3 -2
  45. package/dist/sdk/tool-event-log.js +18 -3
  46. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  47. package/dist/sdk/tool-event-secrets.js +33 -2
  48. package/dist/sdk/types.d.ts +6 -0
  49. package/dist/tool-event-results.d.ts +3 -0
  50. package/dist/tool-event-results.js +153 -23
  51. package/dist/types.d.ts +13 -7
  52. package/docs/agent-flow-evaluation.md +31 -0
  53. package/package.json +3 -2
@@ -24,6 +24,10 @@ const SECRET_KEY_NAMES = [
24
24
  'connectionstring',
25
25
  'credentials',
26
26
  'credential',
27
+ 'grant',
28
+ 'capability',
29
+ 'capabilityurl',
30
+ 'cursor',
27
31
  'svsession',
28
32
  'smsession',
29
33
  'wixsession',
@@ -32,6 +36,10 @@ const SECRET_KEY_NAMES = [
32
36
  const EXACT_ONLY_SECRET_KEY_NAMES = new Set([
33
37
  'auth',
34
38
  'token',
39
+ 'grant',
40
+ 'capability',
41
+ 'capabilityurl',
42
+ 'cursor',
35
43
  'svsession',
36
44
  'smsession',
37
45
  'wixsession',
@@ -39,6 +47,9 @@ const EXACT_ONLY_SECRET_KEY_NAMES = new Set([
39
47
  ]);
40
48
  const NON_SECRET_ENVIRONMENT_KEY_NAMES = new Set(['tokencount', 'tokenizersparallelism', 'tokenusage']);
41
49
  const BOUNDARY_ONLY_SENSITIVE_VALUE_MAX_LENGTH = 1;
50
+ const MAX_SANITIZE_DEPTH = 64;
51
+ const MAX_SENSITIVE_SCAN_NODES = 100_000;
52
+ const MAX_SENSITIVE_JSON_CHARS = 1024 * 1024;
42
53
  export function collectSensitiveEnvValues(env) {
43
54
  return [...new Set(Object.entries(env ?? {})
44
55
  .filter((entry) => typeof entry[1] === 'string')
@@ -60,15 +71,36 @@ export function sanitizePersistenceValue(source, explicitSensitiveValues = []) {
60
71
  ])].sort((a, b) => b.length - a.length);
61
72
  return sanitizeValue(source, '', sensitiveValues, new Set(explicitValues));
62
73
  }
74
+ export function sanitizeUntrustedPersistenceValue(source, explicitSensitiveValues = []) {
75
+ try {
76
+ return sanitizePersistenceValue(source, explicitSensitiveValues);
77
+ }
78
+ catch (error) {
79
+ if (!isSensitiveValueScanLimitError(error))
80
+ throw error;
81
+ if (Array.isArray(source))
82
+ return [];
83
+ if (isRecord(source))
84
+ return { redacted: true };
85
+ return '<redacted>';
86
+ }
87
+ }
88
+ export function isSensitiveValueScanLimitError(error) {
89
+ return error instanceof Error && error.message.startsWith('Sensitive-value scan exceeded');
90
+ }
63
91
  export function sanitizeToolEventResult(source, sensitiveValues) {
64
92
  const result = {};
65
93
  let truncated = source.truncated === true;
66
94
  const addBounded = (key, value) => {
67
95
  if (typeof value !== 'string')
68
96
  return;
69
- const bounded = boundText(sanitizePersistenceValue(value, sensitiveValues));
70
- result[key] = bounded.value;
71
- truncated ||= bounded.truncated;
97
+ const explicitValues = [...new Set(sensitiveValues.filter((entry) => entry.length > 0))];
98
+ const overlap = explicitValues.reduce((max, entry) => Math.max(max, entry.length), 0);
99
+ const prefix = value.slice(0, TOOL_RESULT_MAX_CHARS + Math.max(overlap, 1));
100
+ const bounded = boundText(redactSensitiveValues(prefix, explicitValues, new Set(explicitValues)));
101
+ const sanitized = boundText(sanitizePersistenceValue(bounded.value, sensitiveValues));
102
+ result[key] = sanitized.value;
103
+ truncated ||= value.length > prefix.length || bounded.truncated || sanitized.truncated;
72
104
  };
73
105
  addBounded('content', source.content);
74
106
  addBounded('stdout', source.stdout);
@@ -96,56 +128,93 @@ function redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues)
96
128
  }
97
129
  return redacted;
98
130
  }
99
- function collectStructuredSensitiveValues(source) {
131
+ export function collectStructuredSensitiveValues(source) {
100
132
  const values = new Set();
101
- const visit = (value, key) => {
102
- if (typeof value === 'string' && !isNumericTokenTelemetry(key, value)
103
- && isSecretKey(key) && value.length > 0) {
104
- values.add(value);
105
- return;
133
+ let scannedNodes = 0;
134
+ let parsedJsonChars = 0;
135
+ const pending = [
136
+ { value: source, key: '' },
137
+ ];
138
+ while (pending.length > 0) {
139
+ if (++scannedNodes > MAX_SENSITIVE_SCAN_NODES) {
140
+ throw new Error('Sensitive-value scan exceeded the input node limit');
141
+ }
142
+ const { value, key } = pending.pop();
143
+ if (typeof value === 'string') {
144
+ if (value.length > 0 && !isNumericTokenTelemetry(key, value) && isSecretKey(key)) {
145
+ values.add(value);
146
+ }
147
+ for (const match of value.matchAll(/[?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|auth|password|passwd|session(?:id)?|cookie)=([^&#\s]+)/gi)) {
148
+ if (match[1])
149
+ values.add(match[1]);
150
+ }
151
+ for (const sensitiveValue of collectCredentialShapeSensitiveValues(value)) {
152
+ values.add(sensitiveValue);
153
+ }
154
+ const trimmed = value.trim();
155
+ const looksStructured = (trimmed.startsWith('{') && trimmed.endsWith('}'))
156
+ || (trimmed.startsWith('[') && trimmed.endsWith(']'));
157
+ if (looksStructured && (parsedJsonChars += value.length) > MAX_SENSITIVE_JSON_CHARS) {
158
+ throw new Error('Sensitive-value scan exceeded the nested JSON limit');
159
+ }
160
+ const structured = looksStructured ? parseStructuredJson(value) : undefined;
161
+ if (structured !== undefined)
162
+ pending.push({ value: structured, key });
163
+ continue;
164
+ }
165
+ if ((typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
166
+ && !isNumericTokenTelemetry(key, value) && isSecretKey(key)) {
167
+ values.add(String(value));
168
+ continue;
106
169
  }
107
170
  if (Array.isArray(value)) {
108
171
  for (const entry of value)
109
- visit(entry, key);
110
- return;
172
+ pending.push({ value: entry, key });
173
+ continue;
111
174
  }
112
175
  if (isRecord(value)) {
113
- for (const [entryKey, entryValue] of Object.entries(value))
114
- visit(entryValue, entryKey);
176
+ for (const [entryKey, entryValue] of Object.entries(value)) {
177
+ pending.push({ value: entryValue, key: entryKey });
178
+ }
115
179
  }
116
- };
117
- visit(source, '');
180
+ }
118
181
  return [...values];
119
182
  }
120
- function sanitizeValue(value, key, sensitiveValues, explicitSensitiveValues) {
183
+ function sanitizeValue(value, key, sensitiveValues, explicitSensitiveValues, depth = 0) {
121
184
  if (!isNumericTokenTelemetry(key, value) && isSecretKey(key))
122
185
  return '<redacted>';
123
186
  if (typeof value === 'string') {
124
- return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues), sensitiveValues, explicitSensitiveValues);
187
+ return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues), sensitiveValues, explicitSensitiveValues, depth);
125
188
  }
189
+ if (depth >= MAX_SANITIZE_DEPTH && (Array.isArray(value) || isRecord(value)))
190
+ return '<redacted>';
126
191
  if (Array.isArray(value)) {
127
- return value.map((entry) => sanitizeValue(entry, key, sensitiveValues, explicitSensitiveValues));
192
+ return value.map((entry) => sanitizeValue(entry, key, sensitiveValues, explicitSensitiveValues, depth + 1));
128
193
  }
129
194
  if (isRecord(value)) {
130
195
  return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
131
196
  entryKey,
132
- sanitizeValue(entryValue, entryKey, sensitiveValues, explicitSensitiveValues),
197
+ sanitizeValue(entryValue, entryKey, sensitiveValues, explicitSensitiveValues, depth + 1),
133
198
  ]));
134
199
  }
135
200
  return value;
136
201
  }
137
- function redactCredentialShapes(value, sensitiveValues, explicitSensitiveValues) {
202
+ function redactCredentialShapes(value, sensitiveValues, explicitSensitiveValues, depth) {
138
203
  let redacted = value;
204
+ const credentialShapeValues = collectCredentialShapeSensitiveValues(redacted);
205
+ redacted = redactSensitiveValues(redacted, credentialShapeValues, new Set(credentialShapeValues));
139
206
  const structured = parseStructuredJson(redacted);
140
207
  if (structured !== undefined) {
141
208
  const nestedSensitiveValues = [...new Set([
142
209
  ...sensitiveValues,
143
210
  ...collectStructuredSensitiveValues(structured),
144
211
  ])].sort((a, b) => b.length - a.length);
145
- redacted = JSON.stringify(sanitizeValue(structured, '', nestedSensitiveValues, explicitSensitiveValues));
212
+ redacted = JSON.stringify(sanitizeValue(structured, '', nestedSensitiveValues, explicitSensitiveValues, depth + 1));
146
213
  }
147
214
  if (redacted.includes('PRIVATE KEY-----')) {
148
- redacted = redacted.replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\n]*PRIVATE KEY-----/g, '<redacted>');
215
+ for (const block of findPrivateKeyBlocks(redacted).reverse()) {
216
+ redacted = `${redacted.slice(0, block.start)}<redacted>${redacted.slice(block.end)}`;
217
+ }
149
218
  }
150
219
  if (/\b(?:bearer|basic)\s/i.test(redacted)) {
151
220
  redacted = redacted.replace(/(\b(?:bearer|basic)\s+)[^\s,;"']+/gi, '$1<redacted>');
@@ -154,10 +223,71 @@ function redactCredentialShapes(value, sensitiveValues, explicitSensitiveValues)
154
223
  redacted = redacted.replace(/([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s/@]+(@)/gi, '$1<redacted>$2');
155
224
  }
156
225
  if (redacted.includes('=') || redacted.includes(':')) {
157
- redacted = redacted.replace(/(\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|auth|password|passwd|session(?:id)?|cookie)\s*[=:]\s*)[^\s,;"'}]+/gi, '$1<redacted>');
226
+ redacted = redacted.replace(/((?:^|[\s{[(,;])["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|auth|password|passwd|session(?:id)?|cookie)["']?\s*[=:]\s*["']?)[^\s,;"'}]+/gi, '$1<redacted>');
227
+ redacted = redacted.replace(/((?:^|[\s{[(,;])["'](?:grant|capability|capabilityurl|cursor)["']\s*:\s*["']?)[^\s,;"'}]+/gi, '$1<redacted>');
158
228
  }
159
229
  return redacted;
160
230
  }
231
+ function collectCredentialShapeSensitiveValues(value) {
232
+ const values = new Set();
233
+ for (const match of value.matchAll(/https?:\/\/[^\s,;"']+\/(?:cap|capability)\/[^\s,;"']+/gi)) {
234
+ values.add(match[0]);
235
+ }
236
+ if (value.includes('PRIVATE KEY-----')) {
237
+ for (const block of findPrivateKeyBlocks(value)) {
238
+ values.add(value.slice(block.start, block.end));
239
+ if (block.body)
240
+ values.add(block.body);
241
+ }
242
+ }
243
+ if (/\b(?:bearer|basic)\s/i.test(value)) {
244
+ for (const match of value.matchAll(/\b(?:bearer|basic)\s+([^\s,;"']+)/gi)) {
245
+ if (match[1])
246
+ values.add(match[1]);
247
+ }
248
+ }
249
+ if (value.includes('://')) {
250
+ for (const match of value.matchAll(/[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:([^\s/@]+)@/gi)) {
251
+ if (match[1])
252
+ values.add(match[1]);
253
+ }
254
+ }
255
+ if (value.includes('=') || value.includes(':')) {
256
+ for (const match of value.matchAll(/(?:^|[\s{[(,;])["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|auth|password|passwd|session(?:id)?|cookie)["']?\s*[=:]\s*["']?([^\s,;"'}]+)/gi)) {
257
+ if (match[1])
258
+ values.add(match[1]);
259
+ }
260
+ for (const match of value.matchAll(/(?:^|[\s{[(,;])["'](?:grant|capability|capabilityurl|cursor)["']\s*:\s*["']?([^\s,;"'}]+)/gi)) {
261
+ if (match[1])
262
+ values.add(match[1]);
263
+ }
264
+ }
265
+ return [...values].sort((a, b) => b.length - a.length);
266
+ }
267
+ function findPrivateKeyBlocks(value) {
268
+ const blocks = [];
269
+ let open;
270
+ let cursor = 0;
271
+ while (cursor < value.length) {
272
+ const start = value.indexOf('-----', cursor);
273
+ if (start < 0)
274
+ break;
275
+ const end = value.indexOf('-----', start + 5);
276
+ if (end < 0)
277
+ break;
278
+ const markerEnd = end + 5;
279
+ const marker = value.slice(start, markerEnd);
280
+ if (!open && marker.startsWith('-----BEGIN ') && marker.endsWith('PRIVATE KEY-----')) {
281
+ open = { start, bodyStart: markerEnd };
282
+ }
283
+ else if (open && marker.startsWith('-----END ') && marker.endsWith('PRIVATE KEY-----')) {
284
+ blocks.push({ start: open.start, end: markerEnd, body: value.slice(open.bodyStart, start).trim() });
285
+ open = undefined;
286
+ }
287
+ cursor = markerEnd;
288
+ }
289
+ return blocks;
290
+ }
161
291
  function isSecretKey(key) {
162
292
  const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
163
293
  if (normalized === 'tokens')
package/dist/types.d.ts CHANGED
@@ -172,6 +172,8 @@ export interface TrialResult {
172
172
  scoring_duration_ms?: number;
173
173
  recorded_at?: string;
174
174
  agent?: import('./sdk/types.js').AgentExecutionMetadata;
175
+ flow_summary?: import('./sdk/types.js').AgentFlowSummary;
176
+ flow_trace?: import('./sdk/types.js').AgentFlowTrace;
175
177
  conversation?: {
176
178
  turns: ConversationTurn[];
177
179
  total_turns: number;
@@ -183,6 +185,10 @@ export interface EvalReport {
183
185
  task: string;
184
186
  /** Canonical status for this report or consolidated group when available. */
185
187
  status?: 'pass' | 'fail';
188
+ /** Whether every reportable runner attempt passed. */
189
+ runner_status?: 'pass' | 'fail';
190
+ /** Whether mean reward meets the configured threshold. */
191
+ threshold_status?: 'pass' | 'fail' | 'not_configured';
186
192
  /** Schema-v2 canonical average score. */
187
193
  mean_reward?: number;
188
194
  /** Binary success frequency, absent when attempts are ineligible. */
@@ -211,12 +217,12 @@ export interface ComparisonContract {
211
217
  unavailable_reasons?: ComparisonUnavailableReason[];
212
218
  }
213
219
  /**
214
- * TrialResult with `session_log` and `conversation` stripped. These fields
220
+ * TrialResult with large trace fields stripped. These fields
215
221
  * live only in the per-group trace files; the consolidated results.json keeps
216
222
  * the rest so consumers (preview, `pathgrade report`) can compute summaries
217
223
  * without loading trace data.
218
224
  */
219
- export type StrippedTrialResult = Omit<TrialResult, 'session_log' | 'conversation'>;
225
+ export type StrippedTrialResult = Omit<TrialResult, 'session_log' | 'conversation' | 'flow_trace'>;
220
226
  /**
221
227
  * Per-group entry in the consolidated `.pathgrade/results.json` report.
222
228
  */
@@ -264,11 +270,11 @@ export interface PathgradeReport {
264
270
  overall_mean_reward?: number;
265
271
  attempts_requested?: number;
266
272
  attempts_completed?: number;
267
- /**
268
- * Threshold check result. When `threshold` is set: `'pass'` iff
269
- * `overall_pass_rate >= threshold`. Otherwise: `'pass'` iff every trial
270
- * in every group passed its vitest test.
271
- */
273
+ /** Whether every reportable runner attempt passed. */
274
+ runner_status?: 'pass' | 'fail';
275
+ /** Whether overall mean reward meets the configured threshold. */
276
+ threshold_status?: 'pass' | 'fail' | 'not_configured';
277
+ /** Conjunction of runner assertions and the optional aggregate threshold. */
272
278
  status: 'pass' | 'fail';
273
279
  /** Agent Evals compatibility manifest when changed selection finds no runnable evals. */
274
280
  run_kind?: 'evaluation' | 'no-affected';
@@ -0,0 +1,31 @@
1
+ # Evaluating agent flows
2
+
3
+ Use `evaluateFlow()` when the subject is an already-observed interaction graph rather than a Pathgrade-managed coding agent. The input is protocol-neutral, so an adapter can project A2A or another orchestration protocol into the same evidence contract.
4
+
5
+ ```typescript
6
+ import { check, evaluateFlow, type AgentFlowTrace } from '@wix/pathgrade';
7
+
8
+ const flow: AgentFlowTrace = {
9
+ version: 1,
10
+ protocol: 'a2a',
11
+ rootParticipantId: 'planner',
12
+ participants: [{ id: 'planner' }, { id: 'flights' }],
13
+ interactions: [{
14
+ id: 'delegate-1', sequence: 1,
15
+ sourceParticipantId: 'planner', targetParticipantId: 'flights',
16
+ operation: 'message/send', state: 'completed',
17
+ }],
18
+ outcome: { state: 'completed' },
19
+ completeness: {
20
+ topology: 'complete', outcomes: 'partial', timing: 'unavailable',
21
+ usage: 'unavailable', runtimeIdentity: 'partial',
22
+ },
23
+ };
24
+
25
+ await evaluateFlow(flow, [
26
+ check('delegated flight search', ({ flow }) =>
27
+ flow?.interactions.some(item => item.targetParticipantId === 'flights') === true),
28
+ ]);
29
+ ```
30
+
31
+ `check()`, `score()`, and `judge()` work with flow evidence through `ScorerContext.flow`. `toolUsage()` is agent-only and is rejected explicitly for flows. Consolidated reports retain a compact `flow_summary`; the full `flow_trace` stays in the trace artifact. Completeness fields describe evidence availability and must not be read as inferred downstream success or timing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -64,6 +64,7 @@
64
64
  "!dist/**/*.d.ts.map",
65
65
  "bin/",
66
66
  "docs/OPENAI_OAUTH_JUDGE.md",
67
+ "docs/agent-flow-evaluation.md",
67
68
  "templates/",
68
69
  "README.md"
69
70
  ],
@@ -141,5 +142,5 @@
141
142
  "typescript": "^5.9.3",
142
143
  "zod": "4.3.6"
143
144
  },
144
- "falconPackageHash": "f09fc542f60a4327a1e66eed12cb32899949f10edc82ae397902743f"
145
+ "falconPackageHash": "df08d3b446a37c3224730fca89a408f6238eef420358705733692eee"
145
146
  }