praxis-agent 0.64.1 → 0.66.0

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.
@@ -0,0 +1,348 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { posix, win32 } from 'node:path';
3
+ import { redactSensitiveValue, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
4
+ import { validatePraxisBuildIdentity, } from '../platform/praxis-build-identity.js';
5
+ export const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION = '1.1';
6
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
7
+ const MAX_DEPTH = 32;
8
+ const MAX_NODES = 500_000;
9
+ const MAX_STRING = 64 * 1024;
10
+ function fail(message) {
11
+ throw new Error(`Invalid Project Eval identity: ${message}`);
12
+ }
13
+ function codeUnitCompare(left, right) {
14
+ return left < right ? -1 : left > right ? 1 : 0;
15
+ }
16
+ function assertSerializable(value, path = '$', depth = 0, state = { nodes: 0 }, ancestry = new WeakSet()) {
17
+ state.nodes += 1;
18
+ if (state.nodes > MAX_NODES)
19
+ fail(`${path} exceeds object node limit`);
20
+ if (depth > MAX_DEPTH)
21
+ fail(`${path} exceeds object depth limit`);
22
+ if (value === null ||
23
+ typeof value === 'boolean' ||
24
+ typeof value === 'number') {
25
+ if (typeof value === 'number' && !Number.isFinite(value))
26
+ fail(`${path} contains a non-finite number`);
27
+ return;
28
+ }
29
+ if (typeof value === 'string') {
30
+ if (value.length > MAX_STRING)
31
+ fail(`${path} contains an oversized string`);
32
+ return;
33
+ }
34
+ if (typeof value === 'bigint' ||
35
+ typeof value === 'function' ||
36
+ typeof value === 'symbol' ||
37
+ value === undefined)
38
+ fail(`${path} contains an unsupported value`);
39
+ if (Array.isArray(value)) {
40
+ if (ancestry.has(value))
41
+ fail(`${path} contains a circular reference`);
42
+ ancestry.add(value);
43
+ try {
44
+ for (let index = 0; index < value.length; index += 1)
45
+ assertSerializable(value[index], `${path}[${index}]`, depth + 1, state, ancestry);
46
+ }
47
+ finally {
48
+ ancestry.delete(value);
49
+ }
50
+ return;
51
+ }
52
+ if (typeof value === 'object') {
53
+ const prototype = Object.getPrototypeOf(value);
54
+ if (prototype !== Object.prototype && prototype !== null)
55
+ fail(`${path} contains an unsupported object`);
56
+ if (ancestry.has(value))
57
+ fail(`${path} contains a circular reference`);
58
+ ancestry.add(value);
59
+ try {
60
+ for (const [key, child] of Object.entries(value)) {
61
+ if (key.length > 512)
62
+ fail(`${path} contains an oversized key`);
63
+ assertSerializable(child, `${path}.${key}`, depth + 1, state, ancestry);
64
+ }
65
+ }
66
+ finally {
67
+ ancestry.delete(value);
68
+ }
69
+ return;
70
+ }
71
+ fail(`${path} contains an unsupported value`);
72
+ }
73
+ function canonical(value) {
74
+ assertSerializable(value);
75
+ const ancestry = new WeakSet();
76
+ const serialize = (item) => {
77
+ if (item === null || typeof item !== 'object')
78
+ return JSON.stringify(item);
79
+ if (ancestry.has(item))
80
+ fail('contains a circular reference');
81
+ ancestry.add(item);
82
+ try {
83
+ if (Array.isArray(item))
84
+ return `[${item.map((child) => serialize(child)).join(',')}]`;
85
+ return `{${Object.entries(item)
86
+ .sort(([left], [right]) => codeUnitCompare(left, right))
87
+ .map(([key, child]) => `${JSON.stringify(key)}:${serialize(child)}`)
88
+ .join(',')}}`;
89
+ }
90
+ finally {
91
+ ancestry.delete(item);
92
+ }
93
+ };
94
+ return serialize(value);
95
+ }
96
+ function digest(value) {
97
+ return `sha256:${createHash('sha256').update(canonical(value), 'utf8').digest('hex')}`;
98
+ }
99
+ function string(value, path) {
100
+ if (typeof value !== 'string' ||
101
+ value.length === 0 ||
102
+ value.length > MAX_STRING)
103
+ fail(`${path} must be a bounded non-empty string`);
104
+ return value;
105
+ }
106
+ function digestField(value, path) {
107
+ if (typeof value !== 'string' || !DIGEST.test(value))
108
+ fail(`${path} must be a sha256 digest`);
109
+ return value;
110
+ }
111
+ function positiveInteger(value, path) {
112
+ if (!Number.isSafeInteger(value) || value < 1)
113
+ fail(`${path} must be a positive integer`);
114
+ return value;
115
+ }
116
+ function configurationSource(input) {
117
+ const c = input.case;
118
+ const sensitiveValues = sensitiveEnvironmentValues(c.execution.env);
119
+ const graders = c.graders.map((grader) => grader.type === 'tool_used' && !Number.isFinite(grader.max)
120
+ ? { ...grader, max: 'unbounded' }
121
+ : grader);
122
+ const configuration = {
123
+ case: {
124
+ name: c.name,
125
+ schema_version: c.schemaVersion,
126
+ risk: c.risk,
127
+ },
128
+ execution: {
129
+ max_turns: c.execution.maxTurns,
130
+ timeout_seconds: c.execution.timeoutSeconds,
131
+ env: c.execution.env,
132
+ },
133
+ verification: {
134
+ enabled: input.runVerification,
135
+ definitions: c.verification,
136
+ },
137
+ graders,
138
+ expect: {
139
+ allowed_changed_paths: c.expect.allowedChangedPaths,
140
+ expected_changed_paths: c.expect.expectedChangedPaths,
141
+ forbidden_changed_paths: c.expect.forbiddenChangedPaths,
142
+ },
143
+ };
144
+ return normalizeConfiguration(redactSensitiveValue(configuration, sensitiveValues));
145
+ }
146
+ function normalizeConfiguration(value) {
147
+ if (typeof value === 'string')
148
+ return posix.isAbsolute(value) || win32.isAbsolute(value)
149
+ ? '[ABSOLUTE_PATH]'
150
+ : value;
151
+ if (Array.isArray(value))
152
+ return value.map(normalizeConfiguration);
153
+ if (value && typeof value === 'object')
154
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
155
+ key,
156
+ normalizeConfiguration(child),
157
+ ]));
158
+ return value;
159
+ }
160
+ function corpusSource(sourceBefore) {
161
+ return {
162
+ files: Object.fromEntries(Object.entries(sourceBefore.files).map(([path, file]) => [
163
+ path,
164
+ {
165
+ hash: file.hash,
166
+ size: file.size,
167
+ mode: file.mode,
168
+ },
169
+ ])),
170
+ total_bytes: sourceBefore.totalBytes,
171
+ };
172
+ }
173
+ function freeze(value) {
174
+ if (value && typeof value === 'object') {
175
+ Object.freeze(value);
176
+ for (const child of Object.values(value))
177
+ freeze(child);
178
+ }
179
+ return value;
180
+ }
181
+ export function createProjectEvalIdentity(input) {
182
+ const provider = input.provider;
183
+ const buildIdentity = validatePraxisBuildIdentity(input.buildIdentity);
184
+ const runtimeBase = {
185
+ engine: 'praxis',
186
+ praxis_version: string(input.praxisVersion, 'praxis_version'),
187
+ node_version: string(input.nodeVersion ?? process.version, 'node_version'),
188
+ platform: string(input.platform ?? process.platform, 'platform'),
189
+ architecture: string(input.architecture ?? process.arch, 'architecture'),
190
+ build: buildIdentity,
191
+ };
192
+ const identityWithoutDigests = {
193
+ schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
194
+ provider_id: string(provider.providerId, 'provider_id'),
195
+ profile_id: string(provider.profileId, 'profile_id'),
196
+ protocol: string(provider.protocol, 'protocol'),
197
+ endpoint_sha256: digest(string(provider.endpoint, 'endpoint')),
198
+ model_id: string(provider.modelId, 'model_id'),
199
+ configuration_sha256: digest(configurationSource(input)),
200
+ tools_sha256: digest(input.effectiveTools),
201
+ prompt_sha256: digest({
202
+ prompt: input.case.execution.prompt,
203
+ append_system_prompt: input.case.execution.appendSystemPrompt ?? null,
204
+ }),
205
+ corpus_sha256: digest(corpusSource(input.sourceBefore)),
206
+ runtime: {
207
+ ...runtimeBase,
208
+ runtime_sha256: digest(runtimeBase),
209
+ },
210
+ };
211
+ return freeze({
212
+ ...identityWithoutDigests,
213
+ identity_sha256: digest(identityWithoutDigests),
214
+ });
215
+ }
216
+ function validateRuntime(value, path) {
217
+ const runtime = value;
218
+ if (!runtime || typeof runtime !== 'object' || Array.isArray(runtime))
219
+ fail(`${path} must be an object`);
220
+ const runtimeKeys = [
221
+ 'engine',
222
+ 'praxis_version',
223
+ 'node_version',
224
+ 'platform',
225
+ 'architecture',
226
+ 'build',
227
+ 'runtime_sha256',
228
+ ];
229
+ const runtimeUnknown = Object.keys(runtime).find((key) => !runtimeKeys.includes(key));
230
+ if (runtimeUnknown)
231
+ fail(`${path}.${runtimeUnknown} is not supported`);
232
+ if (runtime.engine !== 'praxis')
233
+ fail(`${path}.engine must be "praxis"`);
234
+ const validated = {
235
+ engine: 'praxis',
236
+ praxis_version: string(runtime.praxis_version, `${path}.praxis_version`),
237
+ node_version: string(runtime.node_version, `${path}.node_version`),
238
+ platform: string(runtime.platform, `${path}.platform`),
239
+ architecture: string(runtime.architecture, `${path}.architecture`),
240
+ build: validatePraxisBuildIdentity(runtime.build),
241
+ runtime_sha256: digestField(runtime.runtime_sha256, `${path}.runtime_sha256`),
242
+ };
243
+ if (validated.runtime_sha256 !==
244
+ digest({
245
+ engine: validated.engine,
246
+ praxis_version: validated.praxis_version,
247
+ node_version: validated.node_version,
248
+ platform: validated.platform,
249
+ architecture: validated.architecture,
250
+ build: validated.build,
251
+ }))
252
+ fail(`${path}.runtime_sha256 does not match runtime fields`);
253
+ return validated;
254
+ }
255
+ export function validateProjectEvalIdentity(value) {
256
+ if (!value || typeof value !== 'object' || Array.isArray(value))
257
+ fail('must be an object');
258
+ const source = value;
259
+ const keys = [
260
+ 'schema_version',
261
+ 'provider_id',
262
+ 'profile_id',
263
+ 'protocol',
264
+ 'endpoint_sha256',
265
+ 'model_id',
266
+ 'configuration_sha256',
267
+ 'tools_sha256',
268
+ 'prompt_sha256',
269
+ 'corpus_sha256',
270
+ 'runtime',
271
+ 'identity_sha256',
272
+ ];
273
+ const unknown = Object.keys(source).find((key) => !keys.includes(key));
274
+ if (unknown)
275
+ fail(`${unknown} is not supported`);
276
+ if (source.schema_version !== PROJECT_EVAL_IDENTITY_SCHEMA_VERSION)
277
+ fail('schema_version must be "1.1"');
278
+ const validated = {
279
+ schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
280
+ provider_id: string(source.provider_id, 'provider_id'),
281
+ profile_id: string(source.profile_id, 'profile_id'),
282
+ protocol: string(source.protocol, 'protocol'),
283
+ endpoint_sha256: digestField(source.endpoint_sha256, 'endpoint_sha256'),
284
+ model_id: string(source.model_id, 'model_id'),
285
+ configuration_sha256: digestField(source.configuration_sha256, 'configuration_sha256'),
286
+ tools_sha256: digestField(source.tools_sha256, 'tools_sha256'),
287
+ prompt_sha256: digestField(source.prompt_sha256, 'prompt_sha256'),
288
+ corpus_sha256: digestField(source.corpus_sha256, 'corpus_sha256'),
289
+ runtime: validateRuntime(source.runtime, 'runtime'),
290
+ identity_sha256: digestField(source.identity_sha256, 'identity_sha256'),
291
+ };
292
+ const withoutIdentity = { ...validated };
293
+ delete withoutIdentity.identity_sha256;
294
+ if (validated.identity_sha256 !== digest(withoutIdentity))
295
+ fail('identity_sha256 does not match identity fields');
296
+ return freeze(validated);
297
+ }
298
+ export function computeProjectEvalAggregateIdentity(runs) {
299
+ const entries = runs
300
+ .map((run) => ({
301
+ case: string(run.case, 'runs.case'),
302
+ run: positiveInteger(run.run, 'runs.run'),
303
+ identity_sha256: digestField(run.identity_sha256, 'runs.identity_sha256'),
304
+ }))
305
+ .sort((left, right) => {
306
+ const caseOrder = codeUnitCompare(left.case, right.case);
307
+ return caseOrder === 0 ? left.run - right.run : caseOrder;
308
+ });
309
+ return digest(entries);
310
+ }
311
+ export function validateProjectEvalAggregateIdentity(value, runs) {
312
+ const declared = digestField(value, 'identity_sha256');
313
+ const expected = computeProjectEvalAggregateIdentity(runs);
314
+ if (declared !== expected)
315
+ fail('aggregate identity_sha256 does not match runs');
316
+ return declared;
317
+ }
318
+ const comparableIdentityDimensions = [
319
+ ['provider_id', 'provider_id'],
320
+ ['profile_id', 'profile_id'],
321
+ ['protocol', 'protocol'],
322
+ ['endpoint_sha256', 'endpoint_sha256'],
323
+ ['model_id', 'model_id'],
324
+ ['configuration_sha256', 'configuration_sha256'],
325
+ ['tools_sha256', 'tools_sha256'],
326
+ ['prompt_sha256', 'prompt_sha256'],
327
+ ['corpus_sha256', 'corpus_sha256'],
328
+ ['runtime.engine', 'runtime.engine'],
329
+ ['runtime.node_version', 'runtime.node_version'],
330
+ ['runtime.platform', 'platform'],
331
+ ['runtime.architecture', 'architecture'],
332
+ ];
333
+ export function firstProjectEvalIdentityMismatch(left, right) {
334
+ for (const [field, label] of comparableIdentityDimensions) {
335
+ const read = (identity) => field.startsWith('runtime.')
336
+ ? identity.runtime[field.slice('runtime.'.length)]
337
+ : identity[field];
338
+ if (read(left) !== read(right))
339
+ return label;
340
+ }
341
+ return null;
342
+ }
343
+ export function assertProjectEvalIdentitiesComparable(left, right, context) {
344
+ const mismatch = firstProjectEvalIdentityMismatch(left, right);
345
+ if (mismatch)
346
+ throw new Error(`${context ? `${context}: ` : ''}non-comparable identity: ${mismatch} differs`);
347
+ }
348
+ //# sourceMappingURL=project-eval-identity.js.map
@@ -1,10 +1,15 @@
1
1
  import type { ModelUsage } from '../core/runtime.js';
2
- import { type EvalGraderResult, type EvalRuntimeFactory } from './eval-contract.js';
2
+ import { type EvalGraderResult, type IdentifiedEvalRuntimeFactory } from './eval-contract.js';
3
+ import { type ProjectEvalIdentity } from './project-eval-identity.js';
3
4
  import type { ProjectEvalCase } from './project-eval-schema.js';
5
+ import type { PraxisBuildIdentity } from '../platform/praxis-build-identity.js';
4
6
  export type ProjectEvalTermination = 'timeout' | 'interrupted' | null;
5
7
  export interface ProjectEvalVerificationResult {
6
- schema_version: '1.0';
8
+ schema_version: '1.1';
7
9
  name: string;
10
+ required: true;
11
+ expect: 'pass';
12
+ status: 'passed' | 'failed' | 'not_run';
8
13
  command: string;
9
14
  argv: readonly string[];
10
15
  exit_code: number | null;
@@ -17,11 +22,13 @@ export interface ProjectEvalVerificationResult {
17
22
  error: string | null;
18
23
  }
19
24
  export interface ProjectEvalRunResult {
20
- schema_version: '1.0';
25
+ schema_version: '1.2';
21
26
  case: string;
27
+ risk: ProjectEvalCase['risk'];
22
28
  run: number;
23
29
  version: string;
24
- model: string | null;
30
+ model: string;
31
+ identity: ProjectEvalIdentity;
25
32
  passed: boolean;
26
33
  score: 0 | 1;
27
34
  turns: number;
@@ -40,19 +47,36 @@ export interface ProjectEvalRunResult {
40
47
  retries: number;
41
48
  graders: readonly EvalGraderResult[];
42
49
  checks: readonly EvalGraderResult[];
50
+ verification: ProjectEvalVerificationEvidence;
43
51
  artifacts: {
44
52
  trace: 'trace.jsonl';
45
53
  workspace_diff: 'workspace-diff.json';
46
54
  verification: 'verification.json';
55
+ identity: 'identity.json';
47
56
  result: 'result.json';
48
57
  };
49
58
  error: string | null;
50
59
  cleanup_errors: readonly string[];
51
60
  temp_root: string | null;
52
61
  }
62
+ export interface ProjectEvalCheckSummary {
63
+ name: string;
64
+ passed: boolean;
65
+ }
66
+ export interface ProjectEvalVerifierOutcome {
67
+ name: string;
68
+ required: true;
69
+ expect: 'pass';
70
+ status: 'passed' | 'failed' | 'not_run';
71
+ }
72
+ export interface ProjectEvalVerificationEvidence {
73
+ outcomes: readonly ProjectEvalVerifierOutcome[];
74
+ satisfied: boolean;
75
+ }
76
+ export declare const PROJECT_EVAL_SAFETY_CHECKS: readonly ["trace-bounds", "runtime-close", "workspace-manifest", "source-unchanged", "allowed-paths", "forbidden-paths", "artifact-write", "temp-cleanup"];
53
77
  interface ProjectEvalRunOptions {
54
78
  case: ProjectEvalCase;
55
- factory: EvalRuntimeFactory;
79
+ factory: IdentifiedEvalRuntimeFactory;
56
80
  run: number;
57
81
  allowTools?: readonly string[];
58
82
  model?: string;
@@ -60,6 +84,7 @@ interface ProjectEvalRunOptions {
60
84
  runVerification?: boolean;
61
85
  outputDir: string;
62
86
  version: string;
87
+ buildIdentity: PraxisBuildIdentity;
63
88
  signal?: AbortSignal;
64
89
  }
65
90
  export declare function runProjectEvalCase(options: ProjectEvalRunOptions): Promise<ProjectEvalRunResult>;