praxis-agent 0.65.0 → 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,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.66.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",