contextecf-codeledger 0.10.81 → 0.10.82

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.
package/bin/codeledger.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * CodeLedger npm launcher — thin shim.
4
4
  *
5
- * This file is the ONLY JavaScript that ships to npm under @codeledger/cli.
5
+ * This launcher and its installation helpers ship to npm under @codeledger/cli.
6
6
  * It contains no engine code, no scoring logic, no formulas. All CodeLedger
7
7
  * functionality lives inside the hardened binary that is downloaded during
8
8
  * `postinstall` from the GitHub Release for this exact version, verified
@@ -18,11 +18,22 @@
18
18
  'use strict';
19
19
 
20
20
  const { spawnSync } = require('node:child_process');
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const crypto = require('node:crypto');
24
+ const { resolveRuntimePin, compareRuntimeVersions, isRuntimePinTrusted } = require('../scripts/runtime-pin.js');
21
25
  const { existsSync, realpathSync, statSync } = require('node:fs');
22
- const { dirname, join, resolve } = require('node:path');
26
+ const { basename, dirname, join, resolve } = require('node:path');
23
27
  const { homedir, platform } = require('node:os');
24
28
 
25
- const { version } = require('../package.json');
29
+ const { name: packageName, version } = require('../package.json');
30
+
31
+ // `codeledger-mcp` bin alias (same file, no extra JS): for MCP registries
32
+ // that cannot pass arguments, it always runs `mcp start` + the user's args.
33
+ // Every other invocation forwards argv unchanged, including a bare launch,
34
+ // so the CLI's own bare-launch MCP detection applies.
35
+ const MCP_ALIAS = /^codeledger-mcp(\.c?js)?$/i.test(basename(process.argv[1] || ''));
36
+ const ARGS = MCP_ALIAS ? ['mcp', 'start', ...process.argv.slice(2)] : process.argv.slice(2);
26
37
 
27
38
  const CACHE_DIR =
28
39
  process.env.CODELEDGER_CACHE_DIR ||
@@ -40,6 +51,34 @@ function die(msg, code = 1) {
40
51
  process.exit(code);
41
52
  }
42
53
 
54
+ // Repair commands use the installed version, so an old pin cannot trap an upgrade.
55
+ function repairing() {
56
+ const commands = [];
57
+ for (let i = 0; i < ARGS.length && ARGS[i] !== '--'; i++) {
58
+ if (ARGS[i] === '--cwd') { i++; continue; }
59
+ if (!ARGS[i].startsWith('-')) commands.push(ARGS[i]);
60
+ }
61
+ return ['vendor', 'upgrade', 'update', 'init'].includes(commands[0]);
62
+ }
63
+
64
+ function findPinnedRuntime(startDir) {
65
+ if (process.env.CODELEDGER_GLOBAL_VERSION_PROBE === '1' || repairing()) return null;
66
+ let root = resolve(startDir);
67
+ while (true) {
68
+ const pin = resolveRuntimePin(join(root, '.codeledger'), { fs, path, crypto }, compareRuntimeVersions);
69
+ if (pin) {
70
+ const trustDir = process.env.CODELEDGER_RUNTIME_TRUST_DIR || join(homedir(), '.cache', 'codeledger', 'runtime-trust');
71
+ if (isRuntimePinTrusted(root, pin.manifest, { fs, path, crypto }, trustDir)) return { kind: 'standalone', path: pin.path, repoRoot: root };
72
+ process.stderr.write('codeledger: repository runtime has no installed trust receipt; using the global installation. Run the installed codeledger vendor command to install and trust its verified runtime.\n');
73
+ return null;
74
+ }
75
+ if (existsSync(join(root, '.git'))) return null;
76
+ const parent = dirname(root);
77
+ if (parent === root) return null;
78
+ root = parent;
79
+ }
80
+ }
81
+
43
82
  // Repo-local fallback: an already-`codeledger init`'d repo vendors its own
44
83
  // runtime under .codeledger/bin/. Checked before attempting a network
45
84
  // download since it's instant and needs no connectivity — the global cache
@@ -49,10 +88,14 @@ function findRepoLocalRuntime(startDir) {
49
88
  if (process.env.CODELEDGER_GLOBAL_VERSION_PROBE === '1') return null;
50
89
  let current = resolve(startDir);
51
90
  while (true) {
91
+ if (existsSync(join(current, '.codeledger', 'runtime.json'))) return null;
92
+ // Legacy scripts require an explicit environment-level repository trust decision.
93
+ if (process.env.CODELEDGER_TRUST_LEGACY_RUNTIME !== '1') return null;
52
94
  const wrapper = join(current, '.codeledger', 'bin', 'codeledger');
53
95
  const standalone = join(current, '.codeledger', 'bin', 'codeledger-standalone.cjs');
54
96
  if (existsSync(wrapper)) return { kind: 'wrapper', path: wrapper, repoRoot: current };
55
97
  if (existsSync(standalone)) return { kind: 'standalone', path: standalone, repoRoot: current };
98
+ if (existsSync(join(current, '.git'))) return null;
56
99
  const parent = dirname(current);
57
100
  if (parent === current) return null;
58
101
  current = parent;
@@ -62,18 +105,18 @@ function findRepoLocalRuntime(startDir) {
62
105
  function runRepoLocal(runtime) {
63
106
  process.stderr.write(
64
107
  [
65
- `codeledger: ContextECF CodeLedger hardened binary for v${version} is not installed in the global cache.`,
108
+ `codeledger: using the repository runtime instead of the global v${version} installation.`,
66
109
  `codeledger: using repo-local runtime at ${runtime.path}`,
67
110
  '',
68
111
  ].join('\n'),
69
112
  );
70
113
  const command = runtime.kind === 'standalone' ? process.execPath : runtime.path;
71
114
  const args = runtime.kind === 'standalone'
72
- ? [runtime.path, ...process.argv.slice(2)]
73
- : process.argv.slice(2);
115
+ ? [runtime.path, ...ARGS]
116
+ : ARGS;
74
117
  const result = spawnSync(command, args, {
75
118
  stdio: 'inherit',
76
- cwd: runtime.repoRoot,
119
+ cwd: process.cwd(),
77
120
  env: { ...process.env, CODELEDGER_DELEGATED: '1' },
78
121
  });
79
122
  if (result.error) {
@@ -89,7 +132,7 @@ function runRepoLocal(runtime) {
89
132
  const ON_DEMAND_DEADLINE_MS = 30_000;
90
133
 
91
134
  function launchDirectory() {
92
- const args = process.argv.slice(2);
135
+ const args = ARGS;
93
136
  let target;
94
137
  for (let i = 0; i < args.length && args[i] !== '--'; i++) {
95
138
  if (args[i].startsWith('--cwd=')) target = args[i].slice(6);
@@ -98,7 +141,7 @@ function launchDirectory() {
98
141
  if (!target || target.startsWith('--')) die('--cwd requires a repository directory.', 2);
99
142
  }
100
143
  }
101
- if (target === undefined && args[0] === 'mcp') target = process.env.CODELEDGER_REPO_ROOT;
144
+ if (target === undefined && (args[0] === 'mcp' || (args.length === 0 && !process.stdin.isTTY))) target = process.env.CODELEDGER_REPO_ROOT;
102
145
  if (target !== undefined && !target.trim()) die('Repository target cannot be empty.', 2);
103
146
  try {
104
147
  const root = realpathSync(resolve(target ?? process.cwd()));
@@ -108,8 +151,17 @@ function launchDirectory() {
108
151
  }
109
152
 
110
153
  async function main() {
154
+ const pinned = findPinnedRuntime(launchDirectory());
155
+ if (pinned) runRepoLocal(pinned);
156
+ if (existsSync(BINARY_PATH)) {
157
+ const problem = require('../scripts/postinstall.js').verifyCachedBinary(BINARY_PATH);
158
+ if (problem) {
159
+ process.stderr.write(`codeledger: cached binary at ${BINARY_PATH} failed verification (${problem}); discarding it and downloading a verified copy.\n`);
160
+ try { fs.unlinkSync(BINARY_PATH); } catch (error) { die(`could not remove the unverified cached binary: ${error.message}. Delete ${BINARY_PATH} and retry.`); }
161
+ }
162
+ }
111
163
  if (!existsSync(BINARY_PATH)) {
112
- const repoLocal = findRepoLocalRuntime(launchDirectory());
164
+ const repoLocal = repairing() ? null : findRepoLocalRuntime(launchDirectory());
113
165
  if (repoLocal) {
114
166
  runRepoLocal(repoLocal);
115
167
  }
@@ -118,7 +170,7 @@ async function main() {
118
170
  const platformKey = `${platform()}-${arch()}`;
119
171
  const installerPath = join(__dirname, '..', 'scripts', 'postinstall.js');
120
172
 
121
- const { PLATFORM_MAP, downloadAndVerifyBinary, unsupportedPlatformMessage } = require('../scripts/postinstall.js');
173
+ const { PLATFORM_MAP, downloadAndVerifyBinary, unsupportedPlatformMessage, readInstallFailure, unpublishedAssetsMessage } = require('../scripts/postinstall.js');
122
174
  if (!PLATFORM_MAP[platformKey]) {
123
175
  // Don't waste time on a download attempt that can only ever fail —
124
176
  // getBinaryPaths() would reject this platformKey immediately anyway.
@@ -127,35 +179,43 @@ async function main() {
127
179
  die(unsupportedPlatformMessage(platformKey));
128
180
  }
129
181
 
182
+ const priorFailure = readInstallFailure();
183
+ if (priorFailure) {
184
+ process.stderr.write(`codeledger: the install step could not fetch the hardened binary (${priorFailure.at}): ${priorFailure.message}\n`);
185
+ }
130
186
  process.stderr.write(
131
187
  `codeledger: hardened binary for v${version} not cached locally — downloading now (one-time)...\n`,
132
188
  );
133
189
  try {
134
190
  await downloadAndVerifyBinary({ deadlineMs: ON_DEMAND_DEADLINE_MS });
135
191
  } catch (err) {
192
+ const unpublished = unpublishedAssetsMessage(err);
136
193
  die(
137
194
  [
138
195
  `hardened binary for v${version} not found at ${BINARY_PATH}, and the on-demand download just failed: ${err.message}`,
139
- '',
140
- 'The postinstall step may have been skipped (e.g. --ignore-scripts).',
196
+ ...(unpublished ? ['', unpublished,
197
+ 'Retrying or reinstalling this version will not help until they are published.',
198
+ 'To use a published version instead, list them with: npm view @codeledger/cli versions'] : [
199
+ '',
200
+ 'The postinstall step may have been skipped (e.g. --ignore-scripts).']),
141
201
  'Confirm the official scoped package is installed: @codeledger/cli.',
142
202
  'Avoid the unrelated unscoped npm package: codeledger.',
143
203
  '',
144
204
  'Recover with one of:',
145
205
  ' 1. Retry: codeledger --version',
146
- ' 2. Reinstall: npm install -g @codeledger/cli@latest',
206
+ ` 2. Reinstall this version: npm install -g ${packageName}@${version}`,
147
207
  ` 3. node "${installerPath}"`,
148
208
  ' 4. Re-run the install without --ignore-scripts',
149
209
  ' 5. In an initialized repo, use: ./.codeledger/bin/codeledger ready',
150
210
  '',
151
- 'Or download the binary manually from:',
211
+ `Release page for v${version} (it exists only once that release is published):`,
152
212
  ` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
153
213
  ].join('\n'),
154
214
  );
155
215
  }
156
216
  }
157
217
 
158
- const result = spawnSync(BINARY_PATH, process.argv.slice(2), {
218
+ const result = spawnSync(BINARY_PATH, ARGS, {
159
219
  stdio: 'inherit',
160
220
  env: process.env,
161
221
  });
@@ -167,4 +227,4 @@ async function main() {
167
227
  process.exit(result.status ?? 1);
168
228
  }
169
229
 
170
- main();
230
+ main().catch((error) => die(error.message));
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "contextecf-codeledger",
3
- "version": "0.10.81",
3
+ "version": "0.10.82",
4
4
  "mcpName": "io.github.codeledgerECF/codeledger",
5
5
  "description": "ContextECF CodeLedger — deterministic context selection for AI coding agents. Safe unscoped alias for @codeledger/cli; avoid the unrelated codeledger package.",
6
6
  "type": "commonjs",
7
7
  "bin": {
8
- "codeledger": "./bin/codeledger.js"
8
+ "codeledger": "./bin/codeledger.js",
9
+ "codeledger-mcp": "./bin/codeledger.js"
9
10
  },
10
11
  "scripts": {
11
12
  "postinstall": "node scripts/postinstall.js"
@@ -42,6 +42,8 @@ const {
42
42
  chmodSync,
43
43
  existsSync,
44
44
  unlinkSync,
45
+ readFileSync,
46
+ statSync,
45
47
  } = require('node:fs');
46
48
  const { join } = require('node:path');
47
49
  const { homedir, platform, arch } = require('node:os');
@@ -101,9 +103,79 @@ function getBinaryPaths(platformKey) {
101
103
  };
102
104
  }
103
105
 
106
+ // The verified SHA-256 is recorded beside the cached binary so every launch can
107
+ // reject a truncated, replaced, or tampered cache before executing it.
108
+ function receiptPath(binaryPath) {
109
+ return `${binaryPath}.sha256.json`;
110
+ }
111
+
112
+ function writeVerifiedReceipt(binaryPath, sha256) {
113
+ const stat = statSync(binaryPath);
114
+ writeFileSync(receiptPath(binaryPath), JSON.stringify({ schema: 1, version, sha256, size: stat.size }) + '\n');
115
+ }
116
+
117
+ /** Returns null when the cached binary matches its install receipt, else the reason it does not. */
118
+ function verifyCachedBinary(binaryPath) {
119
+ let receipt;
120
+ try {
121
+ receipt = JSON.parse(readFileSync(receiptPath(binaryPath), 'utf8'));
122
+ } catch {
123
+ return 'no install receipt';
124
+ }
125
+ if (!receipt || receipt.schema !== 1 || receipt.version !== version || !/^[a-f0-9]{64}$/.test(String(receipt.sha256))) return 'invalid install receipt';
126
+ let stat;
127
+ try {
128
+ stat = statSync(binaryPath);
129
+ } catch {
130
+ return 'binary missing';
131
+ }
132
+ if (!stat.isFile() || stat.size === 0) return 'binary is empty or not a file';
133
+ if (stat.size !== receipt.size) return 'binary size changed since install';
134
+ // Hashed on every launch: size or timestamps can be preserved by a same-size replacement.
135
+ const actual = createHash('sha256').update(readFileSync(binaryPath)).digest('hex');
136
+ return actual === receipt.sha256 ? null : 'SHA256 mismatch with install receipt';
137
+ }
138
+
139
+ // npm hides postinstall output by default, so a soft failure is recorded where
140
+ // the first `codeledger` run can report it.
141
+ function installFailurePath() {
142
+ return join(getCacheDir(), `install-failure-v${version}.json`);
143
+ }
144
+
145
+ function recordInstallFailure(err) {
146
+ try {
147
+ mkdirSync(getCacheDir(), { recursive: true });
148
+ writeFileSync(installFailurePath(), JSON.stringify({
149
+ schema: 1, version, at: new Date().toISOString(),
150
+ status: typeof err.statusCode === 'number' ? err.statusCode : null,
151
+ message: String(err.message).split('\n')[0].slice(0, 300),
152
+ }) + '\n');
153
+ } catch { /* Reporting is best effort; the install stays a soft failure. */ }
154
+ }
155
+
156
+ function readInstallFailure() {
157
+ try {
158
+ const record = JSON.parse(readFileSync(installFailurePath(), 'utf8'));
159
+ return record && record.schema === 1 && record.version === version ? record : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ function clearInstallFailure() {
166
+ try { unlinkSync(installFailurePath()); } catch { /* Nothing recorded. */ }
167
+ }
168
+
169
+ /** A 404 after the propagation window means this version's release assets are not published. */
170
+ function unpublishedAssetsMessage(err) {
171
+ return err && err.statusCode === 404
172
+ ? `The v${version} release assets were not found (HTTP 404): this version's hardened binaries are not published yet, or were withdrawn.`
173
+ : null;
174
+ }
175
+
104
176
  function isBinaryCached() {
105
177
  const paths = getBinaryPaths(getPlatformKey());
106
- return Boolean(paths && existsSync(paths.binaryLocalPath));
178
+ return Boolean(paths && existsSync(paths.binaryLocalPath) && verifyCachedBinary(paths.binaryLocalPath) === null);
107
179
  }
108
180
 
109
181
  // ─── http ────────────────────────────────────────────────────────────────────
@@ -268,6 +340,8 @@ async function downloadAndVerifyBinary({ deadlineMs = DEFAULT_DEADLINE_MS } = {}
268
340
  chmodSync(tmpPath, 0o755);
269
341
  if (existsSync(paths.binaryLocalPath)) unlinkSync(paths.binaryLocalPath);
270
342
  require('node:fs').renameSync(tmpPath, paths.binaryLocalPath);
343
+ writeVerifiedReceipt(paths.binaryLocalPath, actualSha);
344
+ clearInstallFailure();
271
345
 
272
346
  console.error(`codeledger: installed ${paths.binaryLocalPath}`);
273
347
  console.error(` sha256: ${actualSha}`);
@@ -356,7 +430,10 @@ async function runPostinstall() {
356
430
  // existing working install (the exact incident this is designed
357
431
  // around). bin/codeledger.js retries this same download on-demand,
358
432
  // synchronously, the first time it's actually needed.
433
+ recordInstallFailure(err);
359
434
  console.error(`codeledger: could not pre-fetch the hardened binary — ${err.message}`);
435
+ const unpublished = unpublishedAssetsMessage(err);
436
+ if (unpublished) console.error(`codeledger: ${unpublished}`);
360
437
  console.error('codeledger: not fatal — it will download automatically on first "codeledger" invocation.');
361
438
  console.error('codeledger: or, in an initialized repo, use: ./.codeledger/bin/codeledger ready');
362
439
  }
@@ -378,4 +455,9 @@ module.exports = {
378
455
  getBinaryPaths,
379
456
  isBinaryCached,
380
457
  unsupportedPlatformMessage,
458
+ verifyCachedBinary,
459
+ writeVerifiedReceipt,
460
+ readInstallFailure,
461
+ recordInstallFailure,
462
+ unpublishedAssetsMessage,
381
463
  };
@@ -0,0 +1,125 @@
1
+ /* Generated by scripts/generate-runtime-resolver.mjs. Public installation contract only. */
2
+ 'use strict';
3
+ module.exports = (() => {
4
+ /** Strict SemVer 2.0, shared by pin validation and explicit installation. */
5
+ function parseRuntimeVersion(value) {
6
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
7
+ if (!match || match[0] !== value) throw new Error(`Invalid runtime version: ${value}. Use an exact semantic version.`);
8
+ const core = match.slice(1, 4).map(Number);
9
+ const prerelease = match[4]?.split('.') ?? [];
10
+ if (core.some((n) => !Number.isSafeInteger(n)) || prerelease.some((p) => /^\d+$/.test(p) && (p.length > 1 && p.startsWith('0')))) {
11
+ throw new Error(`Invalid runtime version: ${value}`);
12
+ }
13
+ return { core, prerelease };
14
+ }
15
+
16
+ function compareRuntimeVersions(left, right) {
17
+ const a = parseRuntimeVersion(left);
18
+ const b = parseRuntimeVersion(right);
19
+ for (let i = 0; i < 3; i++) {
20
+ if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
21
+ }
22
+ if (!a.prerelease.length || !b.prerelease.length) {
23
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
24
+ }
25
+ for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) {
26
+ const x = a.prerelease[i];
27
+ const y = b.prerelease[i];
28
+ if (x === y) continue;
29
+ if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
30
+ const xn = /^\d+$/.test(x);
31
+ const yn = /^\d+$/.test(y);
32
+ if (xn && yn) return x.length !== y.length ? (x.length < y.length ? -1 : 1) : x < y ? -1 : 1;
33
+ if (xn !== yn) return xn ? -1 : 1;
34
+ return x < y ? -1 : 1;
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ function runtimeGenerationSha256(bytes, notices, crypto) {
40
+ return crypto.createHash('sha256').update(String(bytes.length) + ':').update(bytes).update(notices === null ? '-1:' : String(notices.length) + ':').update(notices || Buffer.alloc(0)).digest('hex');
41
+ }
42
+
43
+ function resolveRuntimePin(
44
+ codeledgerDir,
45
+ io,
46
+ compare,
47
+ ) {
48
+ const manifestPath = io.path.join(codeledgerDir, 'runtime.json');
49
+ if (!io.fs.existsSync(manifestPath)) return null;
50
+ if (io.fs.lstatSync(codeledgerDir).isSymbolicLink() || io.fs.lstatSync(manifestPath).isSymbolicLink()) throw new Error('Runtime state must not be symlinked. Run codeledger vendor from the owning repository.');
51
+ if (io.fs.statSync(manifestPath).size > 65536) throw new Error('runtime.json exceeds its 64 KiB contract bound. Repair with codeledger vendor.');
52
+ let raw;
53
+ try { raw = JSON.parse(io.fs.readFileSync(manifestPath, 'utf8')); } catch (error) { throw new Error('.codeledger/runtime.json is not valid JSON (' + error.message + '). Restore it from version control, or repair with codeledger vendor.'); }
54
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid runtime.json: expected an object. Repair with codeledger vendor.');
55
+ const m = raw;
56
+ if (m.schema_version !== 'codeledger/runtime/v1' || typeof m.installation_id !== 'string' || typeof m.version !== 'string' || typeof m.min_runtime !== 'string'
57
+ || typeof m.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(m.sha256)
58
+ || (m.distribution !== 'full' && m.distribution !== 'open') || !['fail-open-v1', 'legacy'].includes(m.hook_contract)
59
+ || !m.source || typeof m.source.kind !== 'string' || typeof m.source.location !== 'string'
60
+ || !m.build || m.build.content_sha256 !== m.sha256 || (m.build.revision !== null && typeof m.build.revision !== 'string')
61
+ || (m.build.source_dirty !== undefined && m.build.source_dirty !== null && typeof m.build.source_dirty !== 'boolean')
62
+ || (m.build.generation_sha256 === undefined && m.build.legal_sha256 !== undefined)
63
+ || (m.build.generation_sha256 !== undefined && (typeof m.build.generation_sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(m.build.generation_sha256) || (m.build.legal_sha256 !== null && !/^[a-f0-9]{64}$/.test(m.build.legal_sha256))))
64
+ || typeof m.entrypoint !== 'string' || m.entrypoint !== `bin/runtime/${m.version}-${m.build.generation_sha256 || m.sha256}/codeledger-standalone.cjs`) {
65
+ throw new Error('Invalid runtime.json contract. Repair with codeledger vendor.');
66
+ }
67
+ for (const reference of [m.previous, m.migration_fallback]) {
68
+ if (reference === undefined) continue;
69
+ if (!reference || typeof reference !== 'object' || !/^[a-f0-9]{64}$/.test(reference.sha256) || typeof reference.entrypoint !== 'string') throw new Error('Invalid previous runtime reference. Repair with codeledger vendor.');
70
+ if (reference.version !== null) compare(reference.version, reference.version);
71
+ const generation = reference.generation_sha256 || reference.sha256;
72
+ if (!/^[a-f0-9]{64}$/.test(generation) || (reference.entrypoint !== `bin/runtime/legacy-${reference.sha256}/codeledger-standalone.cjs` && (reference.version === null || reference.entrypoint !== `bin/runtime/${reference.version}-${generation}/codeledger-standalone.cjs`))) throw new Error('Invalid previous runtime path. Repair with codeledger vendor.');
73
+ }
74
+ if (compare(m.version, m.min_runtime) < 0) throw new Error('Pinned runtime is older than its configuration contract. Run codeledger upgrade.');
75
+ if (compare(m.version, '0.10.82-0') >= 0 && m.build.generation_sha256 === undefined) throw new Error('Runtime generation identity is missing. Repair with codeledger vendor.');
76
+ const root = io.fs.realpathSync(codeledgerDir);
77
+ if (!io.fs.existsSync(io.path.resolve(root, m.entrypoint))) throw new Error('Pinned runtime is missing: .codeledger/' + m.entrypoint + '. Restore it from version control (git checkout -- .codeledger/bin), or repair with codeledger vendor.');
78
+ const runtimePath = io.fs.realpathSync(io.path.resolve(root, m.entrypoint));
79
+ const rel = io.path.relative(root, runtimePath);
80
+ if (rel.startsWith('..') || io.path.isAbsolute(rel) || runtimePath !== io.path.resolve(root, m.entrypoint) || !io.fs.statSync(runtimePath).isFile()) {
81
+ throw new Error('Pinned runtime escapes the repository. Repair with codeledger vendor.');
82
+ }
83
+ const bytes = io.fs.readFileSync(runtimePath);
84
+ if (io.crypto.createHash('sha256').update(bytes).digest('hex') !== m.sha256) {
85
+ throw new Error('Pinned runtime checksum mismatch. Repair with codeledger vendor; do not execute the modified artifact.');
86
+ }
87
+ if (m.build.generation_sha256 !== undefined) {
88
+ let notices = null;
89
+ if (m.build.legal_sha256 !== null) {
90
+ const legal = runtimePath + '.LEGAL.txt';
91
+ if (!io.fs.existsSync(legal)) throw new Error('Pinned notices are missing: ' + legal + '. Restore them from version control, or repair with codeledger vendor.');
92
+ if (io.fs.lstatSync(legal).isSymbolicLink()) throw new Error('Pinned notices must not be symlinked. Repair with codeledger vendor.');
93
+ notices = io.fs.readFileSync(legal);
94
+ if (io.crypto.createHash('sha256').update(notices).digest('hex') !== m.build.legal_sha256) throw new Error('Pinned notices checksum mismatch. Repair with codeledger vendor.');
95
+ }
96
+ if (runtimeGenerationSha256(bytes, notices, io.crypto) !== m.build.generation_sha256) throw new Error('Pinned generation checksum mismatch. Repair with codeledger vendor.');
97
+ }
98
+ const header = bytes.subarray(0, 65536).toString('utf8');
99
+ const version = /^\/\* CODELEDGER_STANDALONE_VERSION:([^\s*]+) \*\//m.exec(header)?.[1];
100
+ const flavor = header.includes('/* CodeLedger Open CLI') ? 'open' : header.includes('/* CodeLedger Standalone CLI') ? 'full' : null;
101
+ if (version !== m.version || flavor !== m.distribution) throw new Error('Pinned runtime version or distribution does not match runtime.json.');
102
+ return { manifest: m, path: runtimePath };
103
+ }
104
+
105
+
106
+ function runtimeTrustKey(root, manifest, io) {
107
+ return io.crypto.createHash('sha256').update(JSON.stringify([io.fs.realpathSync(root), manifest])).digest('hex');
108
+ }
109
+ function isRuntimePinTrusted(root, manifest, io, trustDir) {
110
+ if (!trustDir || trustDir === 'disabled') return false;
111
+ try {
112
+ const directory = io.fs.realpathSync(trustDir);
113
+ const repo = io.fs.realpathSync(root);
114
+ const relative = io.path.relative(repo, directory);
115
+ if (!relative || (!relative.startsWith('..' + io.path.sep) && relative !== '..' && !io.path.isAbsolute(relative))) return false;
116
+ const key = runtimeTrustKey(root, manifest, io);
117
+ const file = io.path.join(directory, key + '.json');
118
+ const stat = io.fs.lstatSync(file);
119
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) return false;
120
+ const receipt = JSON.parse(io.fs.readFileSync(file, 'utf8'));
121
+ return receipt.schema === 1 && receipt.key === key;
122
+ } catch { return false; }
123
+ }
124
+ return {parseRuntimeVersion, compareRuntimeVersions, resolveRuntimePin, runtimeGenerationSha256, runtimeTrustKey, isRuntimePinTrusted};
125
+ })();