praxis-agent 0.65.0 → 0.67.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.
Files changed (30) hide show
  1. package/README.md +12 -7
  2. package/dist/application/compaction-accounting.d.ts +49 -0
  3. package/dist/application/compaction-accounting.js +349 -0
  4. package/dist/application/compaction-errors.d.ts +21 -0
  5. package/dist/application/compaction-errors.js +24 -0
  6. package/dist/application/context-engine.js +10 -1
  7. package/dist/application/native-session-transcript.d.ts +3 -0
  8. package/dist/application/native-session-transcript.js +36 -4
  9. package/dist/application/session-service.d.ts +2 -0
  10. package/dist/application/session-service.js +526 -197
  11. package/dist/application/turn-accounting.js +0 -4
  12. package/dist/build-identity.json +1 -0
  13. package/dist/cli-runtime.js +2 -0
  14. package/dist/evals/project-eval-comparison.d.ts +4 -1
  15. package/dist/evals/project-eval-comparison.js +234 -78
  16. package/dist/evals/project-eval-identity.d.ts +4 -1
  17. package/dist/evals/project-eval-identity.js +9 -2
  18. package/dist/evals/project-eval-runner.d.ts +24 -2
  19. package/dist/evals/project-eval-runner.js +118 -59
  20. package/dist/evals/project-eval-schema.d.ts +8 -2
  21. package/dist/evals/project-eval-schema.js +27 -11
  22. package/dist/evals/project-eval.d.ts +22 -2
  23. package/dist/evals/project-eval.js +34 -1
  24. package/dist/native/ownership.d.ts +6 -1
  25. package/dist/native/ownership.js +6 -0
  26. package/dist/persistence/native-compaction-receipt-store.d.ts +40 -0
  27. package/dist/persistence/native-compaction-receipt-store.js +380 -0
  28. package/dist/platform/praxis-build-identity.d.ts +14 -0
  29. package/dist/platform/praxis-build-identity.js +204 -0
  30. package/package.json +2 -2
@@ -0,0 +1,380 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, mkdir, open, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { writeFileAtomically } from '../platform/atomic-write.js';
5
+ import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
6
+ export const NATIVE_COMPACTION_RECEIPT_VERSION = 1;
7
+ const MAX_RECEIPTS = 4096;
8
+ const MAX_BYTES = 512 * 1024;
9
+ function recordKeys(value, keys) {
10
+ return Object.keys(value).every((key) => keys.includes(key));
11
+ }
12
+ function exactKeys(value, keys) {
13
+ return Object.keys(value).length === keys.length && recordKeys(value, keys);
14
+ }
15
+ function nonblank(value, field) {
16
+ if (typeof value !== 'string' || value.trim() === '')
17
+ throw new TypeError(`${field} must not be blank`);
18
+ }
19
+ function usage(value) {
20
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
21
+ return false;
22
+ const row = value;
23
+ if (!recordKeys(row, [
24
+ 'inputTokens',
25
+ 'outputTokens',
26
+ 'cacheReadInputTokens',
27
+ 'cacheCreationInputTokens',
28
+ 'cacheCreationInputTokens1h',
29
+ 'webSearchRequests',
30
+ 'contextWindow',
31
+ 'maxOutputTokens',
32
+ ]))
33
+ return false;
34
+ for (const field of [
35
+ 'inputTokens',
36
+ 'outputTokens',
37
+ 'cacheReadInputTokens',
38
+ 'cacheCreationInputTokens',
39
+ 'cacheCreationInputTokens1h',
40
+ 'webSearchRequests',
41
+ ]) {
42
+ const n = row[field];
43
+ if (n !== undefined && (!Number.isSafeInteger(n) || Number(n) < 0))
44
+ return false;
45
+ }
46
+ for (const field of ['contextWindow', 'maxOutputTokens']) {
47
+ const n = row[field];
48
+ if (n !== undefined && (!Number.isSafeInteger(n) || Number(n) < 0))
49
+ return false;
50
+ }
51
+ if (Number(row.cacheCreationInputTokens1h ?? 0) >
52
+ Number(row.cacheCreationInputTokens ?? 0))
53
+ return false;
54
+ return (Number.isSafeInteger(row.inputTokens) &&
55
+ Number(row.inputTokens) >= 0 &&
56
+ Number.isSafeInteger(row.outputTokens) &&
57
+ Number(row.outputTokens) >= 0);
58
+ }
59
+ function metric(value) {
60
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
61
+ return false;
62
+ const row = value;
63
+ if (!recordKeys(row, [
64
+ 'usage',
65
+ 'model',
66
+ 'durationApiMs',
67
+ 'durationApiWithoutRetriesMs',
68
+ ]) ||
69
+ !usage(row.usage))
70
+ return false;
71
+ if (row.model !== undefined &&
72
+ (typeof row.model !== 'string' || row.model.trim() === ''))
73
+ return false;
74
+ if ([
75
+ row.usage.inputTokens,
76
+ row.usage.outputTokens,
77
+ row.usage.cacheReadInputTokens,
78
+ row.usage.cacheCreationInputTokens,
79
+ row.usage.cacheCreationInputTokens1h,
80
+ row.usage.webSearchRequests,
81
+ ].some((value) => typeof value === 'number' && value > 0) &&
82
+ row.model === undefined)
83
+ return false;
84
+ return [row.durationApiMs, row.durationApiWithoutRetriesMs].every((n) => typeof n === 'number' && Number.isFinite(n) && Number(n) >= 0);
85
+ }
86
+ function parseReceipt(value) {
87
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
88
+ throw new TypeError('Invalid compaction receipt');
89
+ const row = value;
90
+ const keys = [
91
+ 'version',
92
+ 'receiptId',
93
+ 'sessionId',
94
+ 'boundaryId',
95
+ 'summaryId',
96
+ 'trigger',
97
+ 'metric',
98
+ 'costUsd',
99
+ 'before',
100
+ 'after',
101
+ ];
102
+ if (!exactKeys(row, keys) || row.version !== 1 || !metric(row.metric))
103
+ throw new TypeError('Invalid compaction receipt');
104
+ for (const key of [
105
+ 'receiptId',
106
+ 'sessionId',
107
+ 'boundaryId',
108
+ 'summaryId',
109
+ 'before',
110
+ 'after',
111
+ ])
112
+ nonblank(row[key], key);
113
+ safeId(row.receiptId);
114
+ safeId(row.sessionId);
115
+ safeId(row.boundaryId);
116
+ safeId(row.summaryId);
117
+ if (!/^[a-f0-9]{64}$/u.test(row.before) ||
118
+ !/^[a-f0-9]{64}$/u.test(row.after))
119
+ throw new TypeError('Invalid compaction accounting fingerprint');
120
+ if (row.receiptId === row.boundaryId ||
121
+ row.receiptId === row.summaryId ||
122
+ row.boundaryId === row.summaryId)
123
+ throw new TypeError('Compaction receipt IDs must be distinct');
124
+ if (row.trigger !== 'auto' && row.trigger !== 'manual')
125
+ throw new TypeError('Invalid compaction receipt trigger');
126
+ if (row.costUsd !== null &&
127
+ (typeof row.costUsd !== 'number' ||
128
+ !Number.isFinite(row.costUsd) ||
129
+ row.costUsd < 0))
130
+ throw new TypeError('Invalid compaction receipt cost');
131
+ const parsedMetric = row.metric;
132
+ if (parsedMetric.durationApiWithoutRetriesMs > parsedMetric.durationApiMs ||
133
+ (![
134
+ parsedMetric.usage.inputTokens,
135
+ parsedMetric.usage.outputTokens,
136
+ parsedMetric.usage.cacheReadInputTokens,
137
+ parsedMetric.usage.cacheCreationInputTokens,
138
+ parsedMetric.usage.cacheCreationInputTokens1h,
139
+ parsedMetric.usage.webSearchRequests,
140
+ ].some((entry) => typeof entry === 'number' && entry > 0) &&
141
+ row.costUsd !== null))
142
+ throw new TypeError('Invalid compaction receipt accounting outcome');
143
+ return structuredClone(row);
144
+ }
145
+ function safeId(value) {
146
+ if (!/^[A-Za-z0-9_-]{1,128}$/u.test(value))
147
+ throw new TypeError('Invalid compaction receipt ID');
148
+ }
149
+ export class NativeCompactionReceiptStore {
150
+ /** Compatibility alias for callers that name the file-backed adapter. */
151
+ directory;
152
+ lease;
153
+ constructor(options) {
154
+ nonblank(options.sidecarRoot, 'sidecarRoot');
155
+ this.directory = join(options.sidecarRoot, 'compaction-receipts');
156
+ this.lease = new ExclusiveFileLease(options.lockFile ??
157
+ join(options.sidecarRoot, 'locks', 'compaction-receipts.lock'));
158
+ }
159
+ async prepare(receipt) {
160
+ const checked = parseReceipt(receipt);
161
+ safeId(checked.receiptId);
162
+ const serialized = `${JSON.stringify(checked)}\n`;
163
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_BYTES)
164
+ throw new Error('Invalid compaction receipt file');
165
+ const handle = await this.acquire();
166
+ try {
167
+ const directory = join(this.directory, checked.sessionId);
168
+ try {
169
+ const stat = await lstat(directory);
170
+ if (!stat.isDirectory())
171
+ throw new Error('Compaction receipt session path is not a directory');
172
+ }
173
+ catch (error) {
174
+ if (error.code !== 'ENOENT')
175
+ throw error;
176
+ await mkdir(directory, { recursive: true, mode: 0o700 });
177
+ }
178
+ const requestedIds = new Set([
179
+ checked.receiptId,
180
+ checked.boundaryId,
181
+ checked.summaryId,
182
+ ]);
183
+ for (const { receipt: existingReceipt } of await this.list(checked.sessionId)) {
184
+ if (existingReceipt.receiptId === checked.receiptId)
185
+ continue;
186
+ if ([
187
+ existingReceipt.receiptId,
188
+ existingReceipt.boundaryId,
189
+ existingReceipt.summaryId,
190
+ ].some((id) => requestedIds.has(id)))
191
+ throw new Error('Duplicate compaction transaction identity');
192
+ }
193
+ const path = join(directory, `${checked.receiptId}.json`);
194
+ const ackPath = join(directory, `${checked.receiptId}.ack`);
195
+ try {
196
+ await lstat(ackPath);
197
+ throw new Error('Compaction receipt acknowledgement already exists for this receipt');
198
+ }
199
+ catch (error) {
200
+ if (error.code !== 'ENOENT')
201
+ throw error;
202
+ }
203
+ let existing = null;
204
+ try {
205
+ const reader = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
206
+ try {
207
+ const stat = await reader.stat();
208
+ if (!stat.isFile() ||
209
+ (stat.mode & 0o077) !== 0 ||
210
+ stat.size > MAX_BYTES)
211
+ throw new Error('Invalid compaction receipt file');
212
+ existing = await reader.readFile('utf8');
213
+ }
214
+ finally {
215
+ await reader.close();
216
+ }
217
+ }
218
+ catch (error) {
219
+ if (error.code !== 'ENOENT')
220
+ throw error;
221
+ }
222
+ if (existing !== null) {
223
+ const prior = parseReceipt(JSON.parse(existing));
224
+ if (JSON.stringify(prior) !== JSON.stringify(checked))
225
+ throw new Error('Compaction receipt ID already contains a different receipt');
226
+ return;
227
+ }
228
+ await writeFileAtomically(path, serialized, { mode: 0o600 });
229
+ }
230
+ finally {
231
+ await handle.release();
232
+ }
233
+ }
234
+ async acknowledge(sessionId, receiptId) {
235
+ safeId(sessionId);
236
+ safeId(receiptId);
237
+ const handle = await this.acquire();
238
+ try {
239
+ const directory = join(this.directory, sessionId);
240
+ const directoryStat = await lstat(directory);
241
+ if (!directoryStat.isDirectory())
242
+ throw new Error('Compaction receipt session path is not a directory');
243
+ const source = join(directory, `${receiptId}.json`);
244
+ const sourceHandle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
245
+ try {
246
+ const stat = await sourceHandle.stat();
247
+ if (!stat.isFile() || (stat.mode & 0o077) !== 0)
248
+ throw new Error('Compaction receipt is not a regular file');
249
+ if (stat.size > MAX_BYTES)
250
+ throw new Error('Invalid compaction receipt file');
251
+ const parsed = parseReceipt(JSON.parse(await sourceHandle.readFile('utf8')));
252
+ if (parsed.sessionId !== sessionId || parsed.receiptId !== receiptId)
253
+ throw new Error('Compaction receipt path mismatch');
254
+ }
255
+ finally {
256
+ await sourceHandle.close();
257
+ }
258
+ const ack = join(directory, `${receiptId}.ack`);
259
+ try {
260
+ const marker = await open(ack, constants.O_RDONLY | constants.O_NOFOLLOW);
261
+ try {
262
+ const stat = await marker.stat();
263
+ if (!stat.isFile() ||
264
+ (stat.mode & 0o077) !== 0 ||
265
+ stat.size > 256 ||
266
+ (await marker.readFile('utf8')) !== `${receiptId}\n`)
267
+ throw new Error('Invalid compaction receipt acknowledgement');
268
+ return;
269
+ }
270
+ finally {
271
+ await marker.close();
272
+ }
273
+ }
274
+ catch (error) {
275
+ if (error.code !== 'ENOENT')
276
+ throw error;
277
+ }
278
+ await writeFileAtomically(ack, `${receiptId}\n`, { mode: 0o600 });
279
+ }
280
+ finally {
281
+ await handle.release();
282
+ }
283
+ }
284
+ async list(sessionId) {
285
+ safeId(sessionId);
286
+ const sessionDirectory = join(this.directory, sessionId);
287
+ let files;
288
+ try {
289
+ const directoryStat = await lstat(sessionDirectory);
290
+ if (!directoryStat.isDirectory())
291
+ throw new Error('Compaction receipt session path is not a directory');
292
+ files = await readdir(sessionDirectory);
293
+ }
294
+ catch (error) {
295
+ if (error.code === 'ENOENT')
296
+ return [];
297
+ throw error;
298
+ }
299
+ if (files.length > MAX_RECEIPTS)
300
+ throw new Error('Too many compaction receipt artifacts');
301
+ const result = [];
302
+ const receiptIds = new Set();
303
+ const transactionIds = new Set();
304
+ const ackIds = new Set();
305
+ for (const name of files.sort()) {
306
+ if (!name.endsWith('.json') && !name.endsWith('.ack'))
307
+ throw new Error('Unknown compaction receipt artifact');
308
+ if (name.endsWith('.ack')) {
309
+ const ackId = name.slice(0, -4);
310
+ safeId(ackId);
311
+ ackIds.add(ackId);
312
+ continue;
313
+ }
314
+ const receiptId = name.slice(0, -5);
315
+ safeId(receiptId);
316
+ const handle = await open(join(sessionDirectory, name), constants.O_RDONLY | constants.O_NOFOLLOW);
317
+ try {
318
+ const stat = await handle.stat();
319
+ if (!stat.isFile() ||
320
+ (stat.mode & 0o077) !== 0 ||
321
+ stat.size > MAX_BYTES)
322
+ throw new Error('Invalid compaction receipt file');
323
+ const parsed = parseReceipt(JSON.parse(await handle.readFile('utf8')));
324
+ if (parsed.sessionId !== sessionId || parsed.receiptId !== receiptId)
325
+ throw new Error('Compaction receipt path mismatch');
326
+ if (receiptIds.has(receiptId))
327
+ throw new Error('Duplicate compaction receipt ID');
328
+ receiptIds.add(receiptId);
329
+ for (const id of [receiptId, parsed.boundaryId, parsed.summaryId]) {
330
+ if (transactionIds.has(id))
331
+ throw new Error('Duplicate compaction transaction identity');
332
+ transactionIds.add(id);
333
+ }
334
+ let acknowledged = false;
335
+ try {
336
+ const marker = await open(join(sessionDirectory, `${receiptId}.ack`), constants.O_RDONLY | constants.O_NOFOLLOW);
337
+ try {
338
+ const markerStat = await marker.stat();
339
+ if (!markerStat.isFile() ||
340
+ (markerStat.mode & 0o077) !== 0 ||
341
+ markerStat.size > 256 ||
342
+ (await marker.readFile('utf8')) !== `${receiptId}\n`)
343
+ throw new Error('Invalid compaction receipt acknowledgement');
344
+ }
345
+ finally {
346
+ await marker.close();
347
+ }
348
+ acknowledged = true;
349
+ }
350
+ catch (error) {
351
+ if (error.code !== 'ENOENT')
352
+ throw error;
353
+ }
354
+ result.push({ receipt: parsed, acknowledged });
355
+ }
356
+ finally {
357
+ await handle.close();
358
+ }
359
+ }
360
+ for (const ackId of ackIds)
361
+ if (!receiptIds.has(ackId))
362
+ throw new Error('Acknowledgement has no receipt');
363
+ return result.sort((a, b) => a.receipt.receiptId < b.receipt.receiptId
364
+ ? -1
365
+ : a.receipt.receiptId > b.receipt.receiptId
366
+ ? 1
367
+ : 0);
368
+ }
369
+ async acquire() {
370
+ for (let i = 0; i < 400; i += 1) {
371
+ const handle = await this.lease.tryAcquire();
372
+ if (handle)
373
+ return handle;
374
+ await new Promise((resolve) => setTimeout(resolve, 5));
375
+ }
376
+ throw new Error('Timed out acquiring compaction receipt lock');
377
+ }
378
+ }
379
+ export { NativeCompactionReceiptStore as NativeCompactionReceiptFileStore };
380
+ //# sourceMappingURL=native-compaction-receipt-store.js.map
@@ -0,0 +1,14 @@
1
+ export declare const PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION: "1.0";
2
+ export interface PraxisBuildIdentity {
3
+ schema_version: typeof PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION;
4
+ source_revision: `git:${string}` | 'unavailable';
5
+ source_dirty: boolean | null;
6
+ artifact_sha256: `sha256:${string}`;
7
+ }
8
+ export declare function writePraxisBuildIdentity({ sourceRoot, outputRoot, }: {
9
+ sourceRoot: string;
10
+ outputRoot: string;
11
+ }): Promise<PraxisBuildIdentity>;
12
+ export declare function validatePraxisBuildIdentity(value: unknown): PraxisBuildIdentity;
13
+ export declare function loadPraxisBuildIdentity(outputRoot?: string): Promise<PraxisBuildIdentity>;
14
+ //# sourceMappingURL=praxis-build-identity.d.ts.map
@@ -0,0 +1,204 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFile } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join, relative, resolve, sep } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ export const PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION = '1.0';
8
+ const execFileAsync = promisify(execFile);
9
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
10
+ const REVISION = /^git:[0-9a-f]{40}$/u;
11
+ const MAX_BYTES = 512 * 1024 * 1024;
12
+ const MAX_FILES = 10_000;
13
+ const MAX_DEPTH = 32;
14
+ const MAX_METADATA_BYTES = 64 * 1024;
15
+ function invalid(message) {
16
+ throw new Error(`Invalid Praxis build identity: ${message}`);
17
+ }
18
+ function codeUnitCompare(left, right) {
19
+ return left < right ? -1 : left > right ? 1 : 0;
20
+ }
21
+ async function artifactEntries(outputRoot) {
22
+ const entries = [];
23
+ let totalBytes = 0;
24
+ async function hashFile(path) {
25
+ const handle = await fs.open(path, 'r');
26
+ const hash = createHash('sha256');
27
+ const buffer = Buffer.alloc(64 * 1024);
28
+ try {
29
+ const stat = await handle.stat();
30
+ if (!stat.isFile())
31
+ invalid('emitted runtime tree contains a non-regular JavaScript file');
32
+ if (stat.size > MAX_BYTES - totalBytes)
33
+ invalid('emitted runtime tree exceeds byte limit');
34
+ let fileBytes = 0;
35
+ while (true) {
36
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
37
+ if (bytesRead === 0)
38
+ break;
39
+ fileBytes += bytesRead;
40
+ if (fileBytes > MAX_BYTES - totalBytes)
41
+ invalid('emitted runtime tree exceeds byte limit');
42
+ hash.update(buffer.subarray(0, bytesRead));
43
+ }
44
+ totalBytes += fileBytes;
45
+ return `sha256:${hash.digest('hex')}`;
46
+ }
47
+ finally {
48
+ await handle.close();
49
+ }
50
+ }
51
+ async function visit(directory, depth) {
52
+ if (depth > MAX_DEPTH)
53
+ invalid('emitted runtime tree exceeds depth limit');
54
+ const children = await fs.readdir(directory, { withFileTypes: true });
55
+ for (const child of children) {
56
+ const fullPath = join(directory, child.name);
57
+ if (child.isSymbolicLink())
58
+ invalid(`emitted runtime tree contains symlink ${child.name}`);
59
+ if (child.isDirectory()) {
60
+ await visit(fullPath, depth + 1);
61
+ continue;
62
+ }
63
+ if (!child.isFile())
64
+ invalid(`emitted runtime tree contains unsupported entry ${child.name}`);
65
+ if (!child.name.endsWith('.js'))
66
+ continue;
67
+ entries.push({
68
+ path: relative(outputRoot, fullPath).split(sep).join('/'),
69
+ sha256: await hashFile(fullPath),
70
+ });
71
+ if (entries.length > MAX_FILES)
72
+ invalid('emitted runtime tree exceeds file limit');
73
+ }
74
+ }
75
+ await visit(outputRoot, 0);
76
+ if (entries.length === 0)
77
+ invalid('emitted runtime tree contains no JavaScript files');
78
+ entries.sort((left, right) => codeUnitCompare(left.path, right.path));
79
+ return entries;
80
+ }
81
+ async function artifactDigest(outputRoot) {
82
+ const entries = await artifactEntries(outputRoot);
83
+ return `sha256:${createHash('sha256').update(JSON.stringify(entries), 'utf8').digest('hex')}`;
84
+ }
85
+ async function sourceState(sourceRoot) {
86
+ try {
87
+ const { stdout } = await execFileAsync('git', ['-C', sourceRoot, 'rev-parse', '--verify', 'HEAD'], { encoding: 'utf8' });
88
+ const revision = stdout.trim();
89
+ if (!/^[0-9a-f]{40}$/u.test(revision))
90
+ return { revision: 'unavailable', dirty: null };
91
+ let status;
92
+ try {
93
+ const result = await execFileAsync('git', ['-C', sourceRoot, 'status', '--porcelain=v1', '--untracked-files=all'], { encoding: 'utf8' });
94
+ status = result.stdout;
95
+ }
96
+ catch (error) {
97
+ throw new Error(`unable to read Git status: ${error instanceof Error ? error.message : String(error)}`);
98
+ }
99
+ return { revision: `git:${revision}`, dirty: status.length > 0 };
100
+ }
101
+ catch (error) {
102
+ if (error instanceof Error &&
103
+ error.message.startsWith('unable to read Git status:'))
104
+ throw error;
105
+ return { revision: 'unavailable', dirty: null };
106
+ }
107
+ }
108
+ export async function writePraxisBuildIdentity({ sourceRoot, outputRoot, }) {
109
+ const state = await sourceState(resolve(sourceRoot));
110
+ const identity = {
111
+ schema_version: PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION,
112
+ source_revision: state.revision,
113
+ source_dirty: state.dirty,
114
+ artifact_sha256: await artifactDigest(resolve(outputRoot)),
115
+ };
116
+ const target = join(resolve(outputRoot), 'build-identity.json');
117
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
118
+ await fs.writeFile(temporary, `${JSON.stringify(identity)}\n`, {
119
+ encoding: 'utf8',
120
+ mode: 0o644,
121
+ });
122
+ await fs.rename(temporary, target);
123
+ return identity;
124
+ }
125
+ export function validatePraxisBuildIdentity(value) {
126
+ if (!value || typeof value !== 'object' || Array.isArray(value))
127
+ invalid('metadata must be an object');
128
+ const source = value;
129
+ const keys = [
130
+ 'schema_version',
131
+ 'source_revision',
132
+ 'source_dirty',
133
+ 'artifact_sha256',
134
+ ];
135
+ const unknown = Object.keys(source).find((key) => !keys.includes(key));
136
+ if (unknown)
137
+ invalid(`${unknown} is not supported`);
138
+ if (source.schema_version !== PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION)
139
+ invalid('schema_version must be "1.0"');
140
+ if (typeof source.source_revision !== 'string' ||
141
+ (source.source_revision !== 'unavailable' &&
142
+ !REVISION.test(source.source_revision)))
143
+ invalid('source_revision is invalid');
144
+ if (source.source_revision === 'unavailable') {
145
+ if (source.source_dirty !== null)
146
+ invalid('source_dirty must be null when source_revision is unavailable');
147
+ }
148
+ else if (typeof source.source_dirty !== 'boolean')
149
+ invalid('source_dirty must be boolean for an available source revision');
150
+ if (typeof source.artifact_sha256 !== 'string' ||
151
+ !DIGEST.test(source.artifact_sha256))
152
+ invalid('artifact_sha256 must be a sha256 digest');
153
+ return Object.freeze({
154
+ schema_version: PRAXIS_BUILD_IDENTITY_SCHEMA_VERSION,
155
+ source_revision: source.source_revision,
156
+ source_dirty: source.source_dirty,
157
+ artifact_sha256: source.artifact_sha256,
158
+ });
159
+ }
160
+ export async function loadPraxisBuildIdentity(outputRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../dist')) {
161
+ const target = join(resolve(outputRoot), 'build-identity.json');
162
+ let raw;
163
+ try {
164
+ const handle = await fs.open(target, 'r');
165
+ try {
166
+ const stat = await handle.stat();
167
+ if (!stat.isFile())
168
+ invalid('metadata is not a regular file');
169
+ const buffer = Buffer.alloc(MAX_METADATA_BYTES + 1);
170
+ let bytesRead = 0;
171
+ while (bytesRead < buffer.length) {
172
+ const result = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
173
+ if (result.bytesRead === 0)
174
+ break;
175
+ bytesRead += result.bytesRead;
176
+ }
177
+ if (bytesRead > MAX_METADATA_BYTES || stat.size > MAX_METADATA_BYTES)
178
+ invalid('metadata exceeds 64 KiB');
179
+ raw = buffer.subarray(0, bytesRead).toString('utf8');
180
+ }
181
+ finally {
182
+ await handle.close();
183
+ }
184
+ }
185
+ catch (error) {
186
+ if (error instanceof Error &&
187
+ error.message.startsWith('Invalid Praxis build identity:'))
188
+ throw error;
189
+ invalid('metadata is missing or unreadable');
190
+ }
191
+ let parsed;
192
+ try {
193
+ parsed = JSON.parse(raw);
194
+ }
195
+ catch {
196
+ invalid('metadata is not valid JSON');
197
+ }
198
+ const identity = validatePraxisBuildIdentity(parsed);
199
+ const actual = await artifactDigest(resolve(outputRoot));
200
+ if (actual !== identity.artifact_sha256)
201
+ invalid('artifact_sha256 does not match emitted JavaScript');
202
+ return identity;
203
+ }
204
+ //# sourceMappingURL=praxis-build-identity.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -28,7 +28,7 @@
28
28
  "praxis": "./dist/cli.js"
29
29
  },
30
30
  "scripts": {
31
- "build": "tsc -p tsconfig.build.json",
31
+ "build": "tsc -p tsconfig.build.json && node scripts/write-build-identity.mjs",
32
32
  "check": "npm run format:check && npm run lint && npm run test:docs && npm run verify:release-automation && npm run verify:ci-coverage && npm run verify:fixture-contracts && npm run check:boundaries && npm run typecheck && npm run build:native && npm run build && npm test",
33
33
  "check:boundaries": "node scripts/check-boundaries.mjs",
34
34
  "dev": "tsx src/cli.ts",