mtech-ai-local-runner 0.1.43 → 0.1.45

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/README.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## 0.1.45: pin official Codex alpha.11 outside the app
2
+
3
+ Manual and daemon Codex registration now require the reviewed official standalone
4
+ `codex-cli 0.163.0-alpha.11` (SHA256
5
+ `80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466`) at
6
+ `<employee-home>/.local/share/mtec-ai-local-runner/codex/0.163.0-alpha.11/<sha256>/codex`.
7
+ There is no ChatGPT app, PATH or environment fallback. A missing, changed,
8
+ symlinked, foreign-owned or group/world-writable installation blocks Codex.
9
+ Existing login/settings, the Claude path, policy observer and execution limits
10
+ are unchanged. This package does not install the standalone automatically;
11
+ provisioning, exact Gateway runtime pins and daemon transition need their own
12
+ reviewed delivery plan. A package update alone is not a successful transition.
13
+
14
+ Executable integrity checks still hash every byte on every call, with a bounded
15
+ 1 MiB read buffer to reduce repeated filesystem dispatch. Tests separate
16
+ independent scenarios and distinguish completion output from actual target exit.
17
+ No timeout was expanded. The historical MCP startup timeout is not declared fixed.
18
+
19
+ ### Interactive LaunchAgent
20
+
21
+ `setup` writes the runner LaunchAgent with `ProcessType` `Interactive`. The `Background` class kept the daemon, Codex and
22
+ every verification command at priority 4 on the efficiency cores, so P1 verification took 12.5 minutes on a busy Mac;
23
+ `Standard` was barely faster. An installed agent changes class only when its plist is rewritten and reloaded;
24
+ `launchctl print gui/$(id -u)/com.mtec.ai-local-runner` should then show `spawn type = interactive (4)`.
25
+
26
+ ## 0.1.44: stop repeating whole-checkout checks in P1 verification
27
+
28
+ The P1 verifier rereads the whole model checkout for every source check and every candidate check. On the AI
29
+ Gateway repository (3,781 files) the live pilot spent about 12.5 minutes in `p1_verify`, and part of that was the same
30
+ check run twice with nothing in between. Each command is now bracketed by one fresh source check and one fresh
31
+ candidate check: before the first command, between commands and after the last. A source re-check compares the Git
32
+ listings and every file's mode and sha256 with the values recorded at the seal instead of rereading every base blob,
33
+ and an object already present in the private checkout is verified without being compressed again. Every file is
34
+ still read and hashed at each check, so the same changes are rejected as before.
35
+
1
36
  ## 0.1.43: give whole-checkout snapshots whole-checkout bounds
2
37
 
3
38
  The P1 verifier snapshots the whole model checkout before it runs the reviewed commands. It passed the candidate limits
package/dist/src/cli.js CHANGED
File without changes
@@ -26,6 +26,9 @@ export class LaunchdService {
26
26
  this.deps = deps;
27
27
  this.plistPath = join(deps.home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
28
28
  }
29
+ // Interactive, because the employee waits on every job and its verification. Background kept
30
+ // the daemon on the efficiency cores at priority 4, and Standard (priority 20) was barely faster
31
+ // on a busy Mac: P1 verification took 553-698 s there against 60-165 s at priority 31.
29
32
  render(executablePath, stateRoot) {
30
33
  return `<?xml version="1.0" encoding="UTF-8"?>
31
34
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -34,7 +37,7 @@ export class LaunchdService {
34
37
  <key>ProgramArguments</key><array><string>${xml(process.execPath)}</string><string>${xml(executablePath)}</string><string>run</string><string>--state-root=${xml(stateRoot)}</string></array>
35
38
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(this.deps.path ?? process.env.PATH ?? "/usr/bin:/bin:/usr/sbin:/sbin")}</string></dict>
36
39
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
37
- <key>ProcessType</key><string>Background</string>
40
+ <key>ProcessType</key><string>Interactive</string>
38
41
  </dict></plist>
39
42
  `;
40
43
  }
@@ -3,11 +3,10 @@ import { promisify } from 'node:util';
3
3
  import { constants } from 'node:fs';
4
4
  import { lstat, mkdir, mkdtemp, open, realpath, readdir, readFile, rm, writeFile } from 'node:fs/promises';
5
5
  import { createHash } from 'node:crypto';
6
- import { deflateSync } from 'node:zlib';
7
6
  import path from 'node:path';
8
7
  import { measureCommandFile } from '../safety/registered-command-integrity.js';
9
8
  import { protocolHash } from './candidate.js';
10
- import { RawGitObjects } from './raw-git-objects.js';
9
+ import { deflateObject, RawGitObjects } from './raw-git-objects.js';
11
10
  const execute = promisify(execFile), denied = () => Error('SNAPSHOT_REJECTED');
12
11
  const digest = (data) => createHash('sha256').update(data).digest('hex');
13
12
  const safe = (p) => p.length > 0 && !path.isAbsolute(p) && !p.includes('\\') && !p.includes('\0') && p.split('/').every(s => s !== '' && s !== '.' && s !== '..' && !/^(?:\.git|\.ssh|\.aws|\.codex|\.claude|\.env|\.netrc|\.npmrc|credentials(?:\..*)?)$/i.test(s));
@@ -21,6 +20,7 @@ export const isP1WorkingBranch = (branch) => typeof branch === 'string' && branc
21
20
  export class GitSnapshotHost {
22
21
  options;
23
22
  seals = new WeakSet();
23
+ listings = new WeakMap();
24
24
  ownedRoots = new Set();
25
25
  constructor(options) { this.options = structuredClone(options); }
26
26
  async git(args) {
@@ -135,15 +135,21 @@ export class GitSnapshotHost {
135
135
  await fd.close();
136
136
  }
137
137
  }
138
- async capture() {
138
+ /** Checks the options every read relies on, then that HEAD is the base commit on the working branch. */
139
+ async head() {
139
140
  const o = this.options, branchAllowed = o.p1WorkingBranch === undefined ? /^ai-work\/local\/[A-Za-z0-9-]+$/.test(o.workingBranch) : o.p1WorkingBranch === o.workingBranch && isP1WorkingBranch(o.workingBranch);
140
141
  if (!/^[a-f0-9]{40}$/.test(o.baseCommitSha) || !branchAllowed || await realpath(o.root) !== o.root || !Number.isSafeInteger(o.maxFileBytes) || o.maxFileBytes < 1 || !Number.isSafeInteger(o.maxTotalBytes) || o.maxTotalBytes < o.maxFileBytes)
141
142
  throw denied();
142
143
  const head = (await this.git(['rev-parse', '--verify', 'HEAD'])).toString().trim(), branch = (await this.git(['symbolic-ref', '--short', 'HEAD'])).toString().trim();
143
144
  if (head !== o.baseCommitSha || branch !== o.workingBranch)
144
145
  throw denied();
145
- const base = new Map(), gitlinks = new Map();
146
- for (const line of (await this.git(['ls-tree', '-r', '-z', '--full-tree', o.baseCommitSha])).toString().split('\0').filter(Boolean)) {
146
+ }
147
+ async capture() {
148
+ const o = this.options;
149
+ await this.head();
150
+ const head = o.baseCommitSha, branch = o.workingBranch;
151
+ const base = new Map(), gitlinks = new Map(), baseListing = await this.git(['ls-tree', '-r', '-z', '--full-tree', o.baseCommitSha]);
152
+ for (const line of baseListing.toString().split('\0').filter(Boolean)) {
147
153
  const link = /^160000 commit ([a-f0-9]{40})\t(.+)$/s.exec(line);
148
154
  if (link) {
149
155
  if (!safe(link[2]))
@@ -158,8 +164,8 @@ export class GitSnapshotHost {
158
164
  }
159
165
  // Gitlinks remain exact base-tree references. Never traverse or copy their source.
160
166
  // Staged replacement/deletion is refused rather than publishing hidden changes.
161
- const staged = new Map();
162
- for (const line of (await this.git(['ls-files', '--stage', '-z'])).toString().split('\0').filter(Boolean)) {
167
+ const staged = new Map(), stagedListing = await this.git(['ls-files', '--stage', '-z']);
168
+ for (const line of stagedListing.toString().split('\0').filter(Boolean)) {
163
169
  const entry = /^(100644|100755|160000) ([a-f0-9]{40}) 0\t(.+)$/s.exec(line);
164
170
  if (!entry || !safe(entry[3]))
165
171
  throw denied();
@@ -170,11 +176,12 @@ export class GitSnapshotHost {
170
176
  expected.set(name, '160000 ' + oid);
171
177
  if (protocolHash([...expected].sort()) !== protocolHash([...staged].sort()))
172
178
  throw denied();
173
- const names = new Set([...base.keys(), ...(await this.git(['ls-files', '--others', '--exclude-standard', '-z', '--'])).toString().split('\0').filter(Boolean)]);
179
+ const untrackedListing = await this.git(['ls-files', '--others', '--exclude-standard', '-z', '--']);
180
+ const names = [...new Set([...base.keys(), ...untrackedListing.toString().split('\0').filter(Boolean)])].sort();
174
181
  const blobs = await this.batchObjects([...base.values()].map(v => v.blob));
175
182
  const files = [], changes = [];
176
183
  let total = 0;
177
- for (const name of [...names].sort()) {
184
+ for (const name of names) {
178
185
  if ([...gitlinks.keys()].some(p => name === p || name.startsWith(p + '/') || p.startsWith(name + '/')))
179
186
  throw denied();
180
187
  const current = await this.currentFile(name), prior = base.get(name);
@@ -196,7 +203,30 @@ export class GitSnapshotHost {
196
203
  changes.push({ path: name, status: !prior ? 'added' : !current ? 'deleted' : 'modified', before: before?.toString('base64') ?? null, after: current?.bytes.toString('base64') ?? null, before_mode: prior?.mode ?? null, after_mode: current?.mode ?? null });
197
204
  }
198
205
  const observation = { base_commit_sha: o.baseCommitSha, head_commit_sha: head, working_branch: branch, diff: JSON.stringify({ format: 'local-candidate-binary-diff-v1', changes }), changed_files: changes.map(({ path, status }) => ({ path, status })) };
199
- return { observation, files, fingerprint: protocolHash({ observation, files: files.map(f => ({ path: f.path, mode: f.mode, sha256: digest(f.bytes) })) }) };
206
+ const listing = { base: digest(baseListing), staged: digest(stagedListing), untracked: digest(untrackedListing), names };
207
+ return { observation, files, listing, fingerprint: protocolHash({ observation, files: files.map(f => ({ path: f.path, mode: f.mode, sha256: digest(f.bytes) })) }) };
208
+ }
209
+ /** The checkout still equals a capture when HEAD, the branch, the three Git listings and every
210
+ * file's mode and sha256 are unchanged. The capture's observation follows from those alone: the
211
+ * base blobs that an unchanged listing names are immutable objects, so they are not read again. */
212
+ async matches(listing, treeFingerprint) {
213
+ const o = this.options;
214
+ await this.head();
215
+ if (digest(await this.git(['ls-tree', '-r', '-z', '--full-tree', o.baseCommitSha])) !== listing.base || digest(await this.git(['ls-files', '--stage', '-z'])) !== listing.staged || digest(await this.git(['ls-files', '--others', '--exclude-standard', '-z', '--'])) !== listing.untracked)
216
+ throw denied();
217
+ const files = [];
218
+ let total = 0;
219
+ for (const name of listing.names) {
220
+ const current = await this.currentFile(name);
221
+ if (!current)
222
+ continue;
223
+ total += current.bytes.length;
224
+ if (total > o.maxTotalBytes)
225
+ throw denied();
226
+ files.push({ path: current.path, mode: current.mode, sha256: digest(current.bytes) });
227
+ }
228
+ if (protocolHash(files) !== treeFingerprint)
229
+ throw denied();
200
230
  }
201
231
  /** Creates a separate local-only Git checkout with exact base parent objects.
202
232
  * Raw object copying and raw file writes invoke no filters, templates or hooks;
@@ -241,7 +271,7 @@ export class GitSnapshotHost {
241
271
  throw denied();
242
272
  const dir = path.join(gitdir, 'objects', oid.slice(0, 2));
243
273
  await mkdir(dir, { recursive: true, mode: 0o700 });
244
- await writeFile(path.join(dir, oid.slice(2)), deflateSync(raw), { flag: 'wx', mode: 0o400 });
274
+ await writeFile(path.join(dir, oid.slice(2)), deflateObject(raw), { flag: 'wx', mode: 0o400 });
245
275
  }
246
276
  const copy = async (from, to) => { for (const name of await readdir(from)) {
247
277
  const source = path.join(from, name), target = path.join(to, name), stat = await lstat(source);
@@ -272,11 +302,10 @@ export class GitSnapshotHost {
272
302
  async publicationCheckout(sealed) { return this.privateCheckout(sealed); }
273
303
  async assertSourceUnchanged(sealed) {
274
304
  try {
275
- if (!this.seals.has(sealed))
276
- throw denied();
277
- const capture = await this.capture();
278
- if (protocolHash(capture.observation) !== protocolHash(sealed.observation) || protocolHash(capture.files.map(f => ({ path: f.path, mode: f.mode, sha256: digest(f.bytes) }))) !== sealed.treeFingerprint)
305
+ const listing = this.listings.get(sealed);
306
+ if (!this.seals.has(sealed) || !listing)
279
307
  throw denied();
308
+ await this.matches(listing, sealed.treeFingerprint);
280
309
  }
281
310
  catch {
282
311
  throw denied();
@@ -371,9 +400,9 @@ export class GitSnapshotHost {
371
400
  await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
372
401
  await writeFile(file, f.bytes, { flag: 'wx', mode: (f.mode & 0o111) ? 0o500 : 0o400 });
373
402
  }
374
- if ((await this.capture()).fingerprint !== capture.fingerprint)
375
- throw denied();
403
+ // The source must not have changed while it was copied.
376
404
  const expected = protocolHash(capture.files.map(f => ({ path: f.path, mode: f.mode, sha256: digest(f.bytes) })));
405
+ await this.matches(capture.listing, expected);
377
406
  const assertUnchanged = async () => {
378
407
  const entries = [];
379
408
  const walk = async (dir, prefix = '') => { for (const item of (await readdir(dir)).sort()) {
@@ -396,6 +425,7 @@ export class GitSnapshotHost {
396
425
  await assertUnchanged();
397
426
  const seal = { root, observation: structuredClone(capture.observation), treeFingerprint: expected, assertUnchanged };
398
427
  this.seals.add(seal);
428
+ this.listings.set(seal, capture.listing);
399
429
  return Object.freeze(seal);
400
430
  }
401
431
  catch {
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
2
2
  import { deflateSync, inflateSync } from 'node:zlib';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ /** Git reads any zlib level, and these objects live only as long as one private checkout. */
6
+ export const deflateObject = (raw) => deflateSync(raw, { level: 1 });
5
7
  /** Local raw objects only: no filters, hooks, Git identity/config or transport. */
6
8
  export class RawGitObjects {
7
9
  checkout;
@@ -13,13 +15,21 @@ export class RawGitObjects {
13
15
  if (write) {
14
16
  const dir = path.join(this.checkout, '.git', 'objects', oid.slice(0, 2)), file = path.join(dir, oid.slice(2));
15
17
  await mkdir(dir, { recursive: true, mode: 0o700 });
16
- try {
17
- await writeFile(file, deflateSync(raw), { flag: 'wx', mode: 0o400 });
18
- }
19
- catch (e) {
20
- if (e.code !== 'EEXIST' || !inflateSync(await readFile(file)).equals(raw))
21
- throw Error('GIT_OBJECT_REJECTED');
22
- }
18
+ // An object already present is verified, not compressed again only to be discarded.
19
+ let existing = await readFile(file).catch((e) => { if (e.code !== 'ENOENT')
20
+ throw Error('GIT_OBJECT_REJECTED'); return undefined; });
21
+ if (existing === undefined)
22
+ try {
23
+ await writeFile(file, deflateObject(raw), { flag: 'wx', mode: 0o400 });
24
+ return oid;
25
+ }
26
+ catch (e) {
27
+ if (e.code !== 'EEXIST')
28
+ throw Error('GIT_OBJECT_REJECTED');
29
+ existing = await readFile(file);
30
+ }
31
+ if (!inflateSync(existing).equals(raw))
32
+ throw Error('GIT_OBJECT_REJECTED');
23
33
  }
24
34
  return oid;
25
35
  }
@@ -1,5 +1,5 @@
1
1
  import { CODEX_DAEMON_EXEC_RELEASES } from './codex-daemon-releases.js';
2
- import { registeredPrelaunchDependencies, CODEX_MANUAL_BINARY } from './codex-manual-registered.js';
2
+ import { registeredPrelaunchDependencies } from './codex-manual-registered.js';
3
3
  import { createPrivilegedDaemonController } from './codex-manual-controller.js';
4
4
  import { createPolicyInventoryClient, verifyPolicyObserver } from './policy-inventory-client.js';
5
5
  import { PRIVILEGED_MDM_MODE } from '../adapters/codex/prelaunch-policy.js';
@@ -52,7 +52,7 @@ export function createCodexDaemonHost(input, deps) {
52
52
  throw Error('CODEX_DAEMON_PREPARATION_REUSED');
53
53
  const pin = releases[0].policyObserver;
54
54
  const client = createPolicyInventoryClient(pin);
55
- const controller = createPrivilegedDaemonController({ runnerId: input.runnerId, binary: native.binary ?? CODEX_MANUAL_BINARY, home: input.home, codexHome: input.codexHome, claim }, { releases, observer: pin, readLocal: native.readLocal, readNativeAuth: native.readNativeAuth, collectInventory: native.collectInventory ?? client.collectOnce });
55
+ const controller = createPrivilegedDaemonController({ runnerId: input.runnerId, binary: native.binary, home: input.home, codexHome: input.codexHome, claim }, { releases, observer: pin, readLocal: native.readLocal, readNativeAuth: native.readNativeAuth, collectInventory: native.collectInventory ?? client.collectOnce });
56
56
  controllers.set(jobId, controller);
57
57
  const host = await controller.prepare();
58
58
  if (closed) {
@@ -8,11 +8,11 @@ export const CODEX_DAEMON_EXEC_RELEASES = Object.freeze([{
8
8
  "policyScope": "unmanaged_personal_only",
9
9
  "managedEnforcement": "not_run",
10
10
  "executableVersion": "codex-cli 0.163.0-alpha.11",
11
- "binarySha256": "8a6dc3938a2f5d30cc19c50b9ccad83a5aa2ca4a2e3411665430bb78a917220a",
11
+ "binarySha256": "80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466",
12
12
  "modelIds": [
13
13
  "gpt-5.6-sol"
14
14
  ],
15
- "decisionId": "16d7057c-b613-45c0-92e0-05262af96d89",
15
+ "decisionId": "21d710f0-d8f2-4a5f-b8c5-f7412e2a2100",
16
16
  "acceptedResidualRisks": [
17
17
  "vendor_native_policy_enforcement",
18
18
  "managed_policy_not_directly_observed_per_job",
@@ -26,14 +26,14 @@ export const CODEX_DAEMON_EXEC_RELEASES = Object.freeze([{
26
26
  "credentialBackend": "auto",
27
27
  "mdmVerification": "privileged_prelaunch",
28
28
  "fixtureEvidence": {
29
- "binary_release": "e392e2e1a577a5fb91f8cbb508ec3ebf287073a3c92389586531d81d4751b14f",
30
- "official_permissions": "3c12b20dda72cc638a4109b2f25f5066f64e302a4b465e941a3188f8f51d21c1",
29
+ "binary_release": "00bfa3a8438d10787c6b18f6d56fde9dc37b1f4ae64c13ac723a5e472e54498a",
30
+ "official_permissions": "ae0906f5302344f41ae06aee053764ba98511fbd5072af81b5ec857a2d6c42c0",
31
31
  "managed_scope_exclusion": "a559199e229e153759f7a7cbd682aae423c0e3d33ef2a692fcccb5eed7c57ed3",
32
- "required_mcp_startup": "94f51ddb832948eaeeb51040b4d5eace56f245328c356d3b9042e09080b4173c",
33
- "source_exclusion": "36f4e02fee13fe4c1535f26b539cb2f15218e257ebc857c79c6b9e5676218c2b",
32
+ "required_mcp_startup": "d2f482b5dd5e74684d7c77652b803314b1555354970d67b795dbefcdddcf7bc2",
33
+ "source_exclusion": "c44cf5ca899e42349c17ea37524984eb8aa6ffe505494ff6a253b159a0cada64",
34
34
  "auth_backend_compatibility": "7c42b882a52698fef3e14b4b956ede91ed1e3b29511fbff3176387063b8d74f0"
35
35
  },
36
- "fixturesFingerprint": "cc9e1586bbed7c4b0548dad6ffb91453d3cadd084504379178e5820687a2c892",
36
+ "fixturesFingerprint": "51b0e58037712a01d3f9499bcd6ee65ab727334119d3aaf8d9ed56a5c592e366",
37
37
  "commands": [],
38
38
  "policyObserver": {
39
39
  "protocol": "policy-inventory-v1",
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import { lstat, realpath } from 'node:fs/promises';
8
8
  import { parse } from 'smol-toml';
9
9
  import { CODEX_REPORT_SCHEMA } from '../adapters/codex/prompt-compiler.js';
10
+ import { codexStandaloneBinaryPath, verifyCodexStandaloneBinary, assertCodexStandaloneBinaryLocation } from './codex-standalone-binary.js';
10
11
  import { lstatSync } from 'node:fs';
11
12
  import { readStableText, regularFileStamp as stamp } from './stable-file-read.js';
12
13
  import { observePinnedGitHead } from './pinned-git-head.js';
@@ -43,6 +44,8 @@ export function createCodexExecProductionHost(original) {
43
44
  throw reject();
44
45
  const paths = path.isAbsolute(input.binary) ? [input.binary] : input.binary === 'codex' && input.searchPath?.split(path.delimiter).every(p => path.isAbsolute(p)) ? input.searchPath.split(path.delimiter).map(p => path.join(p, 'codex')) : [];
45
46
  for (const binary of paths) {
47
+ if (binary === codexStandaloneBinaryPath(input.home))
48
+ await verifyCodexStandaloneBinary(input.home);
46
49
  let stat;
47
50
  try {
48
51
  stat = await lstat(binary);
@@ -109,7 +112,7 @@ export function createCodexExecProductionHost(original) {
109
112
  const accountDirStamp = (file) => { const s = lstatSync(file, { bigint: true }); if (!s.isDirectory() || s.isSymbolicLink())
110
113
  throw reject(); return [s.dev, s.ino, s.mode, s.uid].join(':'); };
111
114
  const current = () => { try {
112
- return !revoked && !!selected && !!ctx && headObservation?.isCurrent() === true && (selected.release.securityMode !== UNMANAGED_EXEC_MODE || selected.policyObservation?.isCurrent() === true) && (lease.get(id)?.expires ?? 0) > Date.now() && [...baseline].every(([file, value]) => stamp(file) === value) && [...directories].every(([file, value]) => dirStamp(file) === value) && [...accountDirectories].every(([file, value]) => accountDirStamp(file) === value);
115
+ return !revoked && !!selected && !!ctx && (selected.binary !== codexStandaloneBinaryPath(input.home) || assertCodexStandaloneBinaryLocation(input.home) === selected.binary) && headObservation?.isCurrent() === true && (selected.release.securityMode !== UNMANAGED_EXEC_MODE || selected.policyObservation?.isCurrent() === true) && (lease.get(id)?.expires ?? 0) > Date.now() && [...baseline].every(([file, value]) => stamp(file) === value) && [...directories].every(([file, value]) => dirStamp(file) === value) && [...accountDirectories].every(([file, value]) => accountDirStamp(file) === value);
113
116
  }
114
117
  catch {
115
118
  return false;
@@ -9,18 +9,18 @@ import { measureCommandFile } from './registered-command-integrity.js';
9
9
  import { readStableText } from './stable-file-read.js';
10
10
  import { eligiblePersonalAccount } from '../adapters/codex/unmanaged-scope.js';
11
11
  import { CLAUDE_CAPABILITIES } from '../adapters/discovery.js';
12
- /** Compiled reviewed identity; never selected by task, environment or receipts. */
13
- export const CODEX_MANUAL_BINARY = '/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
12
+ import { codexStandaloneBinaryPath, verifyCodexStandaloneBinary } from './codex-standalone-binary.js';
14
13
  export const CODEX_MANUAL_NATIVE_READER = Object.freeze({ "binary": "/Library/PrivilegedHelperTools/com.mtec.ai-gateway.codex-observe.681fb12e0b83ff8fbb612dc71f9a5f5c9be5334bcfead29314ca5b979b1dd986", "sha256": "681fb12e0b83ff8fbb612dc71f9a5f5c9be5334bcfead29314ca5b979b1dd986" });
15
14
  export function registeredManualDependencies(home, codexHome) { return registeredPrelaunchDependencies(home, codexHome, 'manual_prelaunch', CODEX_EXEC_RELEASES); }
16
15
  /** Trusted compiled registration shared by manual and daemon host composition. */
17
16
  export function registeredPrelaunchDependencies(home, codexHome, mode, releases) {
18
- const account = path.resolve(codexHome ?? path.join(home, '.codex')), pin = CODEX_MANUAL_NATIVE_READER;
17
+ const account = path.resolve(codexHome ?? path.join(home, '.codex')), pin = CODEX_MANUAL_NATIVE_READER, binary = codexStandaloneBinaryPath(home);
19
18
  if (!path.isAbsolute(home) || !pin)
20
19
  throw Error('CODEX_MANUAL_REGISTRATION_UNAVAILABLE');
21
20
  const reader = createNativeObservationReader({ accountRoot: account, backend: 'auto', keyringKind: 'direct', process: nativeMetadataProcess(pin.binary, pin.sha256), systemSource: systemPolicySource, mdmInventory: () => { throw Error('MANUAL_INVENTORY_ONLY'); } });
22
- return Object.freeze({ binary: CODEX_MANUAL_BINARY, readLocal: reader.readLocal, readNativeAuth: () => reader.readAuth(), async probe() {
23
- const digest = await measureCommandFile(CODEX_MANUAL_BINARY), matches = releases.filter(r => reviewedCodexExecRelease(r) && r.binarySha256 === digest && r.mdmVerification === mode && r.credentialBackend === 'auto');
21
+ return Object.freeze({ binary, readLocal: reader.readLocal, readNativeAuth: () => reader.readAuth(), async probe() {
22
+ await verifyCodexStandaloneBinary(home);
23
+ const digest = await measureCommandFile(binary), matches = releases.filter(r => reviewedCodexExecRelease(r) && r.binarySha256 === digest && r.mdmVerification === mode && r.credentialBackend === 'auto');
24
24
  const config = parse((await readStableText(path.join(account, 'config.toml'))).text);
25
25
  if (matches.length !== 1 || (config.cli_auth_credentials_store ?? 'auto') !== 'auto' || config.features?.secret_auth_storage === true)
26
26
  throw Error('CODEX_MANUAL_REGISTRATION_UNAVAILABLE');
@@ -7,11 +7,11 @@ export const REVIEWED_MANUAL_CODEX_RELEASE = {
7
7
  "policyScope": "unmanaged_personal_only",
8
8
  "managedEnforcement": "not_run",
9
9
  "executableVersion": "codex-cli 0.163.0-alpha.11",
10
- "binarySha256": "8a6dc3938a2f5d30cc19c50b9ccad83a5aa2ca4a2e3411665430bb78a917220a",
10
+ "binarySha256": "80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466",
11
11
  "modelIds": [
12
12
  "gpt-5.6-sol"
13
13
  ],
14
- "decisionId": "16d7057c-b613-45c0-92e0-05262af96d89",
14
+ "decisionId": "21d710f0-d8f2-4a5f-b8c5-f7412e2a2100",
15
15
  "acceptedResidualRisks": [
16
16
  "vendor_native_policy_enforcement",
17
17
  "managed_policy_not_directly_observed_per_job",
@@ -25,13 +25,13 @@ export const REVIEWED_MANUAL_CODEX_RELEASE = {
25
25
  "credentialBackend": "auto",
26
26
  "mdmVerification": "manual_prelaunch",
27
27
  "fixtureEvidence": {
28
- "binary_release": "e392e2e1a577a5fb91f8cbb508ec3ebf287073a3c92389586531d81d4751b14f",
29
- "official_permissions": "3c12b20dda72cc638a4109b2f25f5066f64e302a4b465e941a3188f8f51d21c1",
28
+ "binary_release": "00bfa3a8438d10787c6b18f6d56fde9dc37b1f4ae64c13ac723a5e472e54498a",
29
+ "official_permissions": "ae0906f5302344f41ae06aee053764ba98511fbd5072af81b5ec857a2d6c42c0",
30
30
  "managed_scope_exclusion": "3bb8b2ad50a44cdea5a2a3f388f8009b51d8c60d46230fb3d0b7fed0b9f7452d",
31
- "required_mcp_startup": "94f51ddb832948eaeeb51040b4d5eace56f245328c356d3b9042e09080b4173c",
32
- "source_exclusion": "36f4e02fee13fe4c1535f26b539cb2f15218e257ebc857c79c6b9e5676218c2b",
31
+ "required_mcp_startup": "d2f482b5dd5e74684d7c77652b803314b1555354970d67b795dbefcdddcf7bc2",
32
+ "source_exclusion": "c44cf5ca899e42349c17ea37524984eb8aa6ffe505494ff6a253b159a0cada64",
33
33
  "auth_backend_compatibility": "7c42b882a52698fef3e14b4b956ede91ed1e3b29511fbff3176387063b8d74f0"
34
34
  },
35
- "fixturesFingerprint": "b0f0778c69ffade85ad0694ae39018312dfd93a7c2c1ebfd948d2d15e6d57e2d",
35
+ "fixturesFingerprint": "4b226b26432f92720f4b5e67156bcffab324cdd5c765d44bccef568db5e14bcc",
36
36
  "commands": []
37
37
  };
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import { lstatSync, realpathSync } from 'node:fs';
4
+ import { measureCommandFile } from './registered-command-integrity.js';
5
+ /** Trusted compiled artifact, not selected by config, PATH, task or receipts. */
6
+ export const CODEX_STANDALONE_ARTIFACT = Object.freeze({
7
+ version: '0.163.0-alpha.11',
8
+ binarySha256: '80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466',
9
+ archiveSha256: '4ceb37d6b7b5929925e4c36aeae912f1d040a43203696129fc6b3ffe09cb452a',
10
+ });
11
+ const reject = () => new Error('CODEX_STANDALONE_BINARY_REJECTED');
12
+ /** The optional identity is for trusted in-process fixture composition only.
13
+ * Production registration always uses the compiled default. No authority is
14
+ * created by resolving or measuring a path. */
15
+ export function codexStandaloneBinaryPath(home, sha = CODEX_STANDALONE_ARTIFACT.binarySha256) {
16
+ if (!path.isAbsolute(home) || path.resolve(home) !== home || home === '/' || home.length > 4096 || /[\r\n\0]/.test(home) || !(/^[a-f0-9]{64}$/).test(sha))
17
+ throw reject();
18
+ return path.join(home, '.local', 'share', 'mtec-ai-local-runner', 'codex', CODEX_STANDALONE_ARTIFACT.version, sha, 'codex');
19
+ }
20
+ /** Rechecked by the retained per-execution current-input fence before spawn. */
21
+ export function assertCodexStandaloneBinaryLocation(home, sha = CODEX_STANDALONE_ARTIFACT.binarySha256) {
22
+ try {
23
+ const binary = codexStandaloneBinaryPath(home, sha), uid = process.getuid?.();
24
+ if (uid === undefined || realpathSync(home) !== home || realpathSync(binary) !== binary)
25
+ throw reject();
26
+ let current = home;
27
+ const parts = ['', ...path.relative(home, binary).split(path.sep)];
28
+ for (let i = 0; i < parts.length; i++) {
29
+ current = path.join(current, parts[i]);
30
+ const s = lstatSync(current), last = i === parts.length - 1;
31
+ if (s.uid !== uid || (s.mode & 0o022) || s.isSymbolicLink() || (!last && !s.isDirectory()) || (last && (!s.isFile() || s.nlink !== 1 || !(s.mode & 0o111))))
32
+ throw reject();
33
+ }
34
+ return binary;
35
+ }
36
+ catch {
37
+ throw reject();
38
+ }
39
+ }
40
+ /** Owner-only installation chain plus existing stable no-follow file measure.
41
+ * This is not atomic exec against the employee owner; per-execution digest,
42
+ * stamp and OS confinement checks remain mandatory in the existing host. */
43
+ export async function verifyCodexStandaloneBinary(home, sha = CODEX_STANDALONE_ARTIFACT.binarySha256) {
44
+ try {
45
+ const binary = codexStandaloneBinaryPath(home, sha), uid = process.getuid?.();
46
+ if (uid === undefined || await realpath(home) !== home)
47
+ throw reject();
48
+ const paths = [home];
49
+ let current = home;
50
+ for (const part of path.relative(home, binary).split(path.sep)) {
51
+ current = path.join(current, part);
52
+ paths.push(current);
53
+ }
54
+ const inspect = async () => Promise.all(paths.map(async (file, index) => {
55
+ const s = await lstat(file, { bigint: true }), last = index === paths.length - 1;
56
+ if (s.uid !== BigInt(uid) || (s.mode & 18n) !== 0n || s.isSymbolicLink() || (!last && !s.isDirectory()) || (last && (!s.isFile() || s.nlink !== 1n || (s.mode & 73n) === 0n)))
57
+ throw reject();
58
+ return { dev: s.dev, ino: s.ino, uid: s.uid, mode: s.mode };
59
+ }));
60
+ const before = await inspect();
61
+ if (await realpath(binary) !== binary || await measureCommandFile(binary) !== sha)
62
+ throw reject();
63
+ const after = await inspect();
64
+ if (after.some((s, i) => s.dev !== before[i].dev || s.ino !== before[i].ino || s.uid !== before[i].uid || s.mode !== before[i].mode) || await realpath(binary) !== binary)
65
+ throw reject();
66
+ return binary;
67
+ }
68
+ catch {
69
+ throw reject();
70
+ }
71
+ }
File without changes
@@ -12,7 +12,9 @@ export async function measureCommandFile(file) {
12
12
  throw denied();
13
13
  const hash = createHash('sha256');
14
14
  let size = 0n;
15
- for await (const chunk of fd.createReadStream({ autoClose: false })) {
15
+ // Rehash every byte on every call, but avoid one filesystem dispatch per
16
+ // 64 KiB when snapshot fences repeatedly inspect multi-megabyte executables.
17
+ for await (const chunk of fd.createReadStream({ autoClose: false, highWaterMark: 1024 * 1024 })) {
16
18
  size += BigInt(chunk.length);
17
19
  if (size > 512n * 1024n * 1024n)
18
20
  throw denied();
@@ -108,9 +108,7 @@ export class RunnerObservedVerifier {
108
108
  if (observation.base_commit_sha !== f.base_commit_sha || observation.head_commit_sha !== f.base_commit_sha || observation.working_branch !== f.working_branch || !observation.changed_files.length || observation.changed_files.length > o.candidateLimits.max_changed_files || Buffer.byteLength(observation.diff) > o.candidateLimits.max_diff_bytes || observation.changed_files.some(e => !o.allowedPaths.includes(e.path) || !exactPath(e.path) || isBlocked(e.path, o.blockedPaths)) || changedFileBytes(observation.diff).some(n => n > o.candidateLimits.max_file_bytes))
109
109
  throw denied();
110
110
  await this.active();
111
- await sealed.assertUnchanged();
112
111
  const candidate = await host.verificationCandidate(sealed);
113
- await candidate.assertUnchanged();
114
112
  if (!hex(candidate.commitSha, 40))
115
113
  throw denied();
116
114
  const changed_paths = buildP1ChangedPaths({ candidate_commit_sha: candidate.commitSha, base_commit_sha: f.base_commit_sha, entries: observation.changed_files }, v.bounds);
@@ -118,15 +116,15 @@ export class RunnerObservedVerifier {
118
116
  const registered = await buildReviewedProjectRegistration({ root: candidate.root, manifest: o.registration.manifest, review: o.registration.review, allowedPaths: o.allowedPaths, blockedPaths: o.blockedPaths });
119
117
  const policy = await createIsolatedCommandPolicy({ root: candidate.root, commands: registered.commands, allowedPaths: o.allowedPaths, blockedPaths: o.blockedPaths }, true);
120
118
  const receipts = [];
119
+ // One fresh source and candidate check before the first command, between commands and after the
120
+ // last. Each check rereads the whole checkout, and repeating one with nothing run in between proves nothing.
121
+ const snapshots = host, check = async () => { await this.active(); await snapshots.assertSourceUnchanged(sealed); await candidate.assertUnchanged(); };
121
122
  try {
122
- for (const command of registered.commands) {
123
- await this.active();
124
- await host.assertSourceUnchanged(sealed);
125
- await candidate.assertUnchanged();
123
+ await check();
124
+ for (const [index, command] of registered.commands.entries()) {
126
125
  const measurement = await policy.measureTest(command.id, { timeoutMs: v.bounds.max_command_runtime_ms, maxOutputBytes: v.bounds.max_output_bytes }, o.signal);
127
- await this.active();
128
- await host.assertSourceUnchanged(sealed);
129
- await candidate.assertUnchanged();
126
+ if (index < registered.commands.length - 1)
127
+ await check();
130
128
  const identity = { binding_id: f.binding_id, job_id: f.job_id, generation: f.generation, contract_fingerprint: f.contract_fingerprint, profile_fingerprint: v.profile_fingerprint, frozen_criteria_fingerprint: v.frozen_criteria_fingerprint, candidate_commit_sha: candidate.commitSha, snapshot_fingerprint, command_id: command.id, command_fingerprint: protocolHash(command) };
131
129
  receipts.push({ schema_version: 'runner-command-receipt-v1', receipt_id: protocolHash({ digest_domain: 'p1-command-receipt-id-v1', ...identity }), ...identity, observed_at: Math.floor(Date.now() / 1000), ...measurement });
132
130
  }
@@ -134,9 +132,7 @@ export class RunnerObservedVerifier {
134
132
  finally {
135
133
  policy.revoke();
136
134
  }
137
- await this.active();
138
- await host.assertSourceUnchanged(sealed);
139
- await candidate.assertUnchanged();
135
+ await check();
140
136
  return { candidate_commit_sha: candidate.commitSha, snapshot_fingerprint, changed_paths, receipts };
141
137
  }
142
138
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {