mtech-ai-local-runner 0.1.33 → 0.1.35

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.
@@ -6,25 +6,29 @@ import { protocolHash } from './candidate.js';
6
6
  /** Reuses the exact registered-command sandbox: no ambient credentials, network,
7
7
  * subprocess permissions, repository writes, or additional dependency grants.
8
8
  * Unsupported PHP/Node project commands remain denied by existing confinement. */
9
- function buildIsolatedVerifier(input, allowCandidateReads = false) {
9
+ export async function createIsolatedCommandPolicy(input, allowCandidateReads = false) {
10
10
  const options = { ...input, commands: structuredClone(input.commands), allowedPaths: [...input.allowedPaths], blockedPaths: [...input.blockedPaths] };
11
- return async (snapshot) => {
12
- const executionCommands = [];
13
- for (const command of options.commands) {
14
- const derived = structuredClone(command), names = command.candidateReadPaths ?? [];
15
- if (!Array.isArray(names) || new Set(names).size !== names.length || (!allowCandidateReads && names.length))
11
+ const executionCommands = [];
12
+ for (const command of options.commands) {
13
+ const derived = structuredClone(command), names = command.candidateReadPaths ?? [];
14
+ if (!Array.isArray(names) || new Set(names).size !== names.length || (!allowCandidateReads && names.length))
15
+ throw Error('ISOLATED_VERIFICATION_REJECTED');
16
+ for (const name of names) {
17
+ if (typeof name !== 'string' || !name || path.isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..' || /^(?:\.git|\.env(?:\..*)?|\.ssh|\.aws|\.codex|\.claude|\.npmrc|\.netrc|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)|AGENTS\.md|CLAUDE\.md)$/i.test(p)) || !options.allowedPaths.includes(name) || Object.hasOwn(command.integrity.readFiles, name) || options.blockedPaths.some(p => name === p || name.startsWith(p.replace(/\/\*\*$/, '') + '/')))
16
18
  throw Error('ISOLATED_VERIFICATION_REJECTED');
17
- for (const name of names) {
18
- if (typeof name !== 'string' || !name || path.isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..' || /^(?:\.git|\.env(?:\..*)?|\.ssh|\.aws|\.codex|\.claude|\.npmrc|\.netrc|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)|AGENTS\.md|CLAUDE\.md)$/i.test(p)) || !options.allowedPaths.includes(name) || Object.hasOwn(command.integrity.readFiles, name) || options.blockedPaths.some(p => name === p || name.startsWith(p.replace(/\/\*\*$/, '') + '/')))
19
- throw Error('ISOLATED_VERIFICATION_REJECTED');
20
- const file = path.join(options.root, name);
21
- if (await realpath(file) !== file)
22
- throw Error('ISOLATED_VERIFICATION_REJECTED');
23
- derived.integrity.readFiles[name] = await measureCommandFile(file);
24
- }
25
- executionCommands.push(derived);
19
+ const file = path.join(options.root, name);
20
+ if (await realpath(file) !== file)
21
+ throw Error('ISOLATED_VERIFICATION_REJECTED');
22
+ derived.integrity.readFiles[name] = await measureCommandFile(file);
26
23
  }
27
- const policy = new LocalToolPolicy({ worktreeRoot: options.root, access: 'read', allowedPaths: options.allowedPaths, blockedPaths: options.blockedPaths, requiredCommands: executionCommands.map(c => ({ ...c, readPaths: Object.keys(c.integrity.readFiles) })) });
24
+ executionCommands.push(derived);
25
+ }
26
+ return new LocalToolPolicy({ worktreeRoot: options.root, access: 'read', allowedPaths: options.allowedPaths, blockedPaths: options.blockedPaths, requiredCommands: executionCommands.map(c => ({ ...c, readPaths: Object.keys(c.integrity.readFiles) })) });
27
+ }
28
+ function buildIsolatedVerifier(input, allowCandidateReads = false) {
29
+ const options = { ...input, commands: structuredClone(input.commands), allowedPaths: [...input.allowedPaths], blockedPaths: [...input.blockedPaths] };
30
+ return async (snapshot) => {
31
+ const policy = await createIsolatedCommandPolicy(options, allowCandidateReads);
28
32
  try {
29
33
  if (snapshot.commands.length !== options.commands.length)
30
34
  throw Error('ISOLATED_VERIFICATION_REJECTED');
@@ -5,7 +5,7 @@ import { evaluateCommand } from '../safety/command-policy.js';
5
5
  import { protocolHash } from './candidate.js';
6
6
  const denied = () => new Error('PROJECT_REGISTRATION_REJECTED');
7
7
  const privateSegment = /^(?:\.git|\.mcp(?:\..*)?|\.env(?:\..*)?|\.ssh|\.aws|\.config|\.codex|\.claude|\.runner|\.local-runner|\.npmrc|\.netrc|id_rsa|id_ed25519|credentials(?:\..*)?|auth\.(?:json|toml|ya?ml)(?:\..*)?|mcp\.(?:json|toml|ya?ml)(?:\..*)?|AGENTS\.md|CLAUDE\.md|vendor|node_modules)$/i;
8
- function exactName(name) { return typeof name === 'string' && !!name && !path.isAbsolute(name) && !/[\\\0*?\[\]{}]/.test(name) && name.split('/').every(s => !!s && s !== '.' && s !== '..' && !privateSegment.test(s)); }
8
+ function exactName(name) { return typeof name === 'string' && !!name && !name.startsWith('-') && !path.isAbsolute(name) && !/[\\\0*?\[\]{}]/.test(name) && name.split('/').every(s => !!s && s !== '.' && s !== '..' && !privateSegment.test(s)); }
9
9
  function blocked(name, scopes) { return scopes.some(scope => scope === '.env.*' ? name.startsWith('.env.') : name === scope || name.startsWith(scope.replace(/\/\*\*$/, '') + '/')); }
10
10
  /** Validates reviewed exact registrations against the current host and scope.
11
11
  * Receipt authenticity/ownership belongs to host release attestation; this API
@@ -0,0 +1,42 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { deflateSync, inflateSync } from 'node:zlib';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ /** Local raw objects only: no filters, hooks, Git identity/config or transport. */
6
+ export class RawGitObjects {
7
+ checkout;
8
+ constructor(checkout) {
9
+ this.checkout = checkout;
10
+ }
11
+ async object(type, bytes, write) {
12
+ const raw = Buffer.concat([Buffer.from(`${type} ${bytes.length}\0`), bytes]), oid = createHash('sha1').update(raw).digest('hex');
13
+ if (write) {
14
+ const dir = path.join(this.checkout, '.git', 'objects', oid.slice(0, 2)), file = path.join(dir, oid.slice(2));
15
+ 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
+ }
23
+ }
24
+ return oid;
25
+ }
26
+ async tree(entries, write, prefix = '') {
27
+ const names = new Map(), children = new Set();
28
+ for (const entry of entries) {
29
+ if (!entry.path.startsWith(prefix))
30
+ continue;
31
+ const rest = entry.path.slice(prefix.length), slash = rest.indexOf('/');
32
+ if (slash >= 0)
33
+ children.add(rest.slice(0, slash));
34
+ else
35
+ names.set(rest, { mode: entry.mode.toString(8), oid: entry.gitlink ?? await this.object('blob', entry.bytes, write) });
36
+ }
37
+ for (const child of children)
38
+ names.set(child, { mode: '40000', oid: await this.tree(entries, write, prefix + child + '/') });
39
+ const chunks = [...names].sort(([a, av], [b, bv]) => Buffer.compare(Buffer.from(a + (av.mode === '40000' ? '/' : '')), Buffer.from(b + (bv.mode === '40000' ? '/' : '')))).map(([name, v]) => Buffer.concat([Buffer.from(`${v.mode} ${name}\0`), Buffer.from(v.oid, 'hex')]));
40
+ return this.object('tree', Buffer.concat(chunks), write);
41
+ }
42
+ }
@@ -9,6 +9,8 @@ import { parse } from 'smol-toml';
9
9
  import { CODEX_REPORT_SCHEMA } from '../adapters/codex/prompt-compiler.js';
10
10
  import { lstatSync } from 'node:fs';
11
11
  import { readStableText, regularFileStamp as stamp } from './stable-file-read.js';
12
+ import { observePinnedGitHead } from './pinned-git-head.js';
13
+ import { executionError } from './execution-failure.js';
12
14
  import { readCodexCredentialStatus, registeredCommandValid } from './codex-production-host.js';
13
15
  import { securityRequirementsFingerprint as hash, requireSecurityRequirements } from './security-requirements.js';
14
16
  import { fingerprint } from '../daemon/execution-coordinator.js';
@@ -101,13 +103,13 @@ export function createCodexExecProductionHost(original) {
101
103
  const close = () => { revoked = true; return closing ??= (Promise.resolve().then(() => cleanup?.())); };
102
104
  let revoked = false, prepared = false, selected, ctx;
103
105
  const baseline = new Map(), directories = new Map(), accountDirectories = new Map();
104
- let boundPlan;
106
+ let boundPlan, headObservation;
105
107
  const dirStamp = (file) => { const s = lstatSync(file, { bigint: true }); if (!s.isDirectory() || (s.mode & 63n) !== 0n || process.getuid && s.uid !== BigInt(process.getuid()))
106
108
  throw reject(); return [s.dev, s.ino, s.mode, s.uid].join(':'); };
107
109
  const accountDirStamp = (file) => { const s = lstatSync(file, { bigint: true }); if (!s.isDirectory() || s.isSymbolicLink())
108
110
  throw reject(); return [s.dev, s.ino, s.mode, s.uid].join(':'); };
109
111
  const current = () => { try {
110
- return !revoked && !!selected && !!ctx && (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);
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);
111
113
  }
112
114
  catch {
113
115
  return false;
@@ -118,34 +120,19 @@ export function createCodexExecProductionHost(original) {
118
120
  throw reject();
119
121
  prepared = true;
120
122
  ctx = structuredClone(value);
123
+ let stage = 'codex_prepare_release';
121
124
  try {
122
125
  selected = await inspect();
126
+ stage = 'codex_prepare_binding';
123
127
  const r = requireSecurityRequirements(p.security_requirements, { adapterId: 'codex_cli', adapterVersion: '1' }), release = selected.release;
124
128
  if (r.mdm_verification !== release.mdmVerification || !isDeepStrictEqual(r.policy_observer, release.policyObserver) || r.security_mode !== release.securityMode || value.generation !== j.generation || value.resolvedModelId !== p.resolved_model_id || !release.modelIds.includes(value.resolvedModelId) || fingerprint(value.approvedTaskPackage) !== c.task_package_fingerprint || value.securityBinding?.contractFingerprint !== c.contract_fingerprint || value.securityBinding?.taskPackageFingerprint !== c.task_package_fingerprint || value.securityBinding?.launchFingerprint !== j.launch_fingerprint || r.release_manifest_sha256 !== execReleaseFingerprint(release) || r.binary_sha256 !== release.binarySha256 || r.fixtures_sha256 !== release.fixturesFingerprint || r.commands_sha256 !== commandRegistryFingerprint(release.commands) || r.tools_sha256 !== fingerprint({ tools: localToolInventory() }) || JSON.stringify(r.model_ids) !== JSON.stringify(release.modelIds))
125
129
  throw reject();
130
+ stage = 'codex_prepare_source';
126
131
  const source = await realpath(sourceRoot), home = await realpath(account);
127
132
  if (inside(source, home) || inside(home, source))
128
133
  throw reject();
129
- const dot = path.join(source, '.git');
130
- const ds = await lstat(dot);
131
- let head;
132
- if (ds.isFile()) {
133
- const gitData = await readStableText(dot, 4096), git = gitData.text;
134
- if (!git.startsWith('gitdir: '))
135
- throw reject();
136
- const dir = path.resolve(source, git.slice(8).trim());
137
- if (inside(home, dir))
138
- throw reject();
139
- head = path.join(dir, 'HEAD');
140
- baseline.set(dot, gitData.stamp);
141
- }
142
- else if (ds.isDirectory() && !ds.isSymbolicLink())
143
- head = path.join(dot, 'HEAD');
144
- else
145
- throw reject();
146
- const headData = await readStableText(head, 4096);
147
- if (headData.text.trim() !== p.repository?.base_commit_sha)
148
- throw reject();
134
+ headObservation = await observePinnedGitHead(source, p.repository, home);
135
+ stage = 'codex_prepare_instructions';
149
136
  const wrapper = CODEX_GLOBAL_INSTRUCTION_WRAPPER, ws = await lstat(wrapper);
150
137
  if (!ws.isFile() || ws.isSymbolicLink() || ws.uid !== 0 || (ws.mode & 0o022) || await realpath(wrapper) !== wrapper)
151
138
  throw reject();
@@ -156,9 +143,9 @@ export function createCodexExecProductionHost(original) {
156
143
  throw reject();
157
144
  accountDirectories.set(ancestor, accountDirStamp(ancestor));
158
145
  }
159
- baseline.set(head, headData.stamp);
160
146
  baseline.set(selected.binary, selected.binaryStamp);
161
147
  baseline.set(selected.config, selected.configStamp);
148
+ stage = 'codex_prepare_commands';
162
149
  for (const command of release.commands) {
163
150
  const paths = command.readPaths ?? [], files = paths.map(v => path.join(source, v));
164
151
  const measured = [command.executable, ...files].map(f => [f, stamp(f)]);
@@ -169,13 +156,14 @@ export function createCodexExecProductionHost(original) {
169
156
  baseline.set(f, s);
170
157
  }
171
158
  }
159
+ stage = 'codex_prepare_current';
172
160
  if (!current())
173
161
  throw reject();
174
162
  return { binary: selected.binary, release: structuredClone(release), actorId: c.security_context.actor_id, runnerId: input.runnerId };
175
163
  }
176
- catch {
164
+ catch (error) {
177
165
  revoked = true;
178
- throw reject();
166
+ throw executionError(error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED');
179
167
  }
180
168
  },
181
169
  async inspect(plan) {
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { protocolHash } from '../publication/candidate.js';
2
3
  function object(value) {
3
4
  if (!value || typeof value !== 'object' || Array.isArray(value))
4
5
  throw new Error('Invalid Job policy');
@@ -51,6 +52,14 @@ export function jobToolPolicyOptions(worktreeRoot, pkg, approved, registry, runt
51
52
  const approvedCommands = strings(approved.required_commands);
52
53
  if (commands.length !== approvedCommands.length || JSON.stringify([...commands].sort()) !== JSON.stringify([...approvedCommands].sort()))
53
54
  throw new Error('Job required commands differ from approval');
55
+ if (approved.verification !== undefined) {
56
+ const verification = object(approved.verification);
57
+ if (runtime.runtimeAdapter !== 'codex_cli' || runtime.runtimeAdapterVersion !== '1' || approved.runtime_transport !== 'official_exec_v1' || verification.runtime_adapter !== runtime.runtimeAdapter || verification.runtime_transport !== approved.runtime_transport || verification.policy_key !== 'local-runner-verified-v1' || protocolHash(repo.blocked_paths) !== protocolHash(source.blocked_paths))
58
+ throw new Error('P1 Job runtime or scope differs from approval');
59
+ if (approved.local_fix_scope !== undefined || verification.mode !== 'runner_observed_v1' || verification.schema_version !== 'local-runner-verification-v1' || pkg.verification !== undefined && protocolHash(verification) !== protocolHash(pkg.verification) || protocolHash(verification) !== protocolHash(object(pkg.contract).verification) || protocolHash(repo.allowed_paths) !== protocolHash(approved.allowed_paths) || protocolHash(verification.allowed_paths) !== protocolHash(approved.allowed_paths))
60
+ throw new Error('P1 Job verification differs from approval');
61
+ return { worktreeRoot, access: repo.access === 'write' && source.access === 'write' ? 'write' : 'read', allowedPaths: strings(approved.allowed_paths), blockedPaths: strings(repo.blocked_paths), requiredCommands: [] };
62
+ }
54
63
  // Writable local Fix checks run only after the owned model/tool endpoint is
55
64
  // closed, in the reviewed host verifier. They are not model command grants.
56
65
  const localFix = approved.local_fix_scope;
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { lstat, realpath } from 'node:fs/promises';
4
4
  import { evaluateCommand } from './command-policy.js';
5
- import { runSandboxed } from './subprocess-sandbox.js';
5
+ import { runSandboxed, runSandboxedMeasured } from './subprocess-sandbox.js';
6
6
  import { LOCAL_TOOL_WORKER } from './local-tool-worker.js';
7
7
  import { verifyCommandIntegrity } from './registered-command-integrity.js';
8
8
  export const LOCAL_TOOL_NAMES = ['local_read', 'local_search', 'local_edit', 'local_test'];
@@ -86,7 +86,9 @@ export class LocalToolPolicy {
86
86
  throw new Error('DENIED_SIZE');
87
87
  return file;
88
88
  }
89
- async test(input) {
89
+ async prepareTest(input) {
90
+ if (this.abort.signal.aborted)
91
+ throw new Error('DENIED_REVOKED');
90
92
  const command = this.options.requiredCommands.find(command => command.id === input.command);
91
93
  if (!command || !path.isAbsolute(command.executable) || command.argv.some(arg => typeof arg !== 'string'))
92
94
  throw new Error('DENIED_COMMAND');
@@ -99,7 +101,19 @@ export class LocalToolPolicy {
99
101
  const argv = executable === await realpath(process.execPath) ? ['--no-addons', '--no-global-search-paths', ...command.argv] : command.argv;
100
102
  const readFiles = await Promise.all((command.readPaths ?? []).map(file => this.authorizedFile(file)));
101
103
  await verifyCommandIntegrity(command.integrity, executable, command.readPaths ?? [], readFiles);
102
- const output = await runSandboxed({ executable, argv, readFiles, cwd: this.options.worktreeRoot, signal: this.abort.signal });
104
+ return { command, executable, readFiles, request: { executable, argv, readFiles, cwd: this.options.worktreeRoot, signal: this.abort.signal } };
105
+ }
106
+ /** Same scope/integrity preparation and sandbox as legacy local_test, with
107
+ * no raw output retained. Only host verification calls this API. */
108
+ async measureTest(commandId, bounds, signal) {
109
+ const { command, executable, readFiles, request } = await this.prepareTest({ command: commandId });
110
+ const measurement = await runSandboxedMeasured({ ...request, ...bounds, signal: signal ? AbortSignal.any([this.abort.signal, signal]) : this.abort.signal });
111
+ await verifyCommandIntegrity(command.integrity, executable, command.readPaths ?? [], readFiles);
112
+ return measurement;
113
+ }
114
+ async test(input) {
115
+ const { command, executable, readFiles, request } = await this.prepareTest(input);
116
+ const output = await runSandboxed(request);
103
117
  await verifyCommandIntegrity(command.integrity, executable, command.readPaths ?? [], readFiles);
104
118
  if (this.abort.signal.aborted)
105
119
  throw new Error('DENIED_REVOKED');
@@ -0,0 +1,113 @@
1
+ import path from 'node:path';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import { lstatSync, realpathSync } from 'node:fs';
4
+ import { readStableText, regularFileStamp } from './stable-file-read.js';
5
+ const denied = () => Error('PINNED_GIT_HEAD_REJECTED');
6
+ const inside = (root, file) => file === root || file.startsWith(root + path.sep);
7
+ const branchValid = (value) => typeof value === 'string' && value.length <= 255
8
+ && /^ai-work\/[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)
9
+ && value.split('/').every(p => !p.includes('..') && !p.endsWith('.') && !p.endsWith('.lock'));
10
+ // Packed refs may have no loose-ref parent yet. Check the nearest existing
11
+ // ancestor too, so a symlink to an empty directory cannot evade absence checks.
12
+ function canonicalParent(file) {
13
+ let parent = path.dirname(file);
14
+ for (;;) {
15
+ try {
16
+ return lstatSync(parent).isDirectory() && realpathSync(parent) === parent;
17
+ }
18
+ catch (e) {
19
+ if (e.code !== 'ENOENT' || parent === path.dirname(parent))
20
+ return false;
21
+ parent = path.dirname(parent);
22
+ }
23
+ }
24
+ }
25
+ /** Observe Git metadata only: no Git process, filters, hooks, account or config reads.
26
+ * A symbolic HEAD is permitted only for the exact approved writable branch. */
27
+ export async function observePinnedGitHead(source, repository, excludedRoot) {
28
+ const files = new Map(), absent = new Set();
29
+ const directory = async (file) => { if (inside(excludedRoot, file) || inside(file, excludedRoot) || await realpath(file) !== file || !(await lstat(file)).isDirectory())
30
+ throw denied(); return file; };
31
+ const read = async (file, max = 4096) => { if (await realpath(file) !== file)
32
+ throw denied(); const value = await readStableText(file, max); files.set(file, value.stamp); return value.text; };
33
+ const optional = async (file, max = 4096) => { try {
34
+ return await read(file, max);
35
+ }
36
+ catch (e) {
37
+ if (e.code !== 'ENOENT')
38
+ throw e;
39
+ if (!canonicalParent(file))
40
+ throw denied();
41
+ absent.add(file);
42
+ return undefined;
43
+ } };
44
+ const expected = repository.base_commit_sha;
45
+ if (typeof expected !== 'string' || !/^[a-f0-9]{40}$/.test(expected))
46
+ throw denied();
47
+ const dot = path.join(source, '.git'), entry = await lstat(dot);
48
+ let gitdir;
49
+ if (entry.isFile()) {
50
+ const value = await read(dot), match = /^gitdir: ([^\r\n\0]+)\n?$/.exec(value);
51
+ if (!match)
52
+ throw denied();
53
+ gitdir = await directory(path.resolve(source, match[1]));
54
+ }
55
+ else if (entry.isDirectory() && !entry.isSymbolicLink())
56
+ gitdir = await directory(dot);
57
+ else
58
+ throw denied();
59
+ const head = (await read(path.join(gitdir, 'HEAD'))).trim();
60
+ if (/^[a-f0-9]{40}$/.test(head)) {
61
+ if (head !== expected)
62
+ throw denied();
63
+ }
64
+ else {
65
+ const branch = repository.working_branch;
66
+ if (repository.access !== 'write' || !branchValid(branch) || head !== `ref: refs/heads/${branch}`)
67
+ throw denied();
68
+ const commonText = await optional(path.join(gitdir, 'commondir'));
69
+ if (commonText !== undefined && !/^[^\r\n\0]+\n?$/.test(commonText))
70
+ throw denied();
71
+ const common = commonText === undefined ? gitdir : await directory(path.resolve(gitdir, commonText.trim()));
72
+ const ref = `refs/heads/${branch}`, loose = await optional(path.join(common, ref));
73
+ let commit;
74
+ if (loose !== undefined) {
75
+ if (!/^[a-f0-9]{40}\n?$/.test(loose))
76
+ throw denied();
77
+ commit = loose.trim();
78
+ }
79
+ else {
80
+ const packed = await read(path.join(common, 'packed-refs'), 1_048_576);
81
+ for (const line of packed.split('\n')) {
82
+ if (!line || line.startsWith('#') || /^\^[a-f0-9]{40}$/.test(line))
83
+ continue;
84
+ const match = /^([a-f0-9]{40}) (refs\/[^\s\0]+)$/.exec(line);
85
+ if (!match)
86
+ throw denied();
87
+ if (match[2] === ref) {
88
+ if (commit !== undefined)
89
+ throw denied();
90
+ commit = match[1];
91
+ }
92
+ }
93
+ }
94
+ if (commit !== expected)
95
+ throw denied();
96
+ }
97
+ const isCurrent = () => { try {
98
+ return [...files].every(([file, stamp]) => realpathSync(file) === file && regularFileStamp(file) === stamp) && [...absent].every(file => { if (!canonicalParent(file))
99
+ return false; try {
100
+ lstatSync(file);
101
+ return false;
102
+ }
103
+ catch (e) {
104
+ return e.code === 'ENOENT';
105
+ } });
106
+ }
107
+ catch {
108
+ return false;
109
+ } };
110
+ if (!isCurrent())
111
+ throw denied();
112
+ return { isCurrent };
113
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
4
5
  const quote = (value) => JSON.stringify(value);
5
6
  /** Deny-default Seatbelt profile. No inherited profile, environment or shell. */
6
7
  export function sandboxProfile(request) {
@@ -17,46 +18,79 @@ export function sandboxProfile(request) {
17
18
  const write = (request.writeFiles ?? []).map(file => `(literal ${quote(file)})`).join(' ');
18
19
  return `(version 1)(deny default)(allow process-exec (literal ${quote(executable)}))(allow sysctl-read)(allow file-read-metadata)(allow file-map-executable (subpath "/System/Library") (subpath "/usr/lib") (literal ${quote(executable)}))(allow file-read-data (subpath "/System/Library") (subpath "/usr/lib") (subpath "/usr/share") ${read})${write ? `(allow file-write* ${write})` : ''}`;
19
20
  }
20
- export async function runSandboxed(request) {
21
+ /** One restricted execution primitive. P1 retains no output; only legacy callers
22
+ * opt into bounded stdout retention. Completion waits for close (stream drain),
23
+ * never exit or a timeout callback. No grants differ from sandboxProfile. */
24
+ async function executeSandboxed(request, retainStdout) {
25
+ const start = performance.now();
26
+ const empty = (status, reason) => ({ measurement: { status, reason, exit_code: null, duration_ms: Math.max(0, Math.floor(performance.now() - start)), stdout_sha256: null, stderr_sha256: null, stdout_bytes: 0, stderr_bytes: 0 }, output: '' });
27
+ if (request.signal?.aborted)
28
+ return empty('cancelled', 'aborted');
21
29
  if (process.platform !== 'darwin')
22
- throw new Error('LOCAL_SANDBOX_UNAVAILABLE: macOS Seatbelt required');
23
- const profile = sandboxProfile(request);
24
- return new Promise((resolve, reject) => {
25
- const chunks = [];
26
- let size = 0, settled = false;
27
- const child = spawn('/usr/bin/sandbox-exec', ['-p', profile, realpathSync(request.executable), ...request.argv], {
28
- cwd: request.cwd ?? '/', env: { PATH: '/usr/bin:/bin', LANG: 'C', HOME: '/nonexistent', TMPDIR: '/nonexistent' }, stdio: ['ignore', 'pipe', 'pipe'], detached: true, signal: request.signal,
29
- });
30
- const finish = (error) => {
31
- if (settled)
32
- return;
33
- settled = true;
30
+ return empty('blocked', 'sandbox_unavailable');
31
+ const limit = request.maxOutputBytes ?? 1024 * 1024;
32
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1024 * 1024 || (request.timeoutMs !== undefined && (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs < 1)))
33
+ return empty('blocked', 'invalid_bounds');
34
+ let profile, executable;
35
+ try {
36
+ profile = sandboxProfile(request);
37
+ executable = realpathSync(request.executable);
38
+ }
39
+ catch {
40
+ return empty('blocked', 'spawn_failed');
41
+ }
42
+ return new Promise(resolve => {
43
+ const stdout = createHash('sha256'), stderr = createHash('sha256'), chunks = [];
44
+ let outBytes = 0, errBytes = 0, reason, timer;
45
+ let child;
46
+ try {
47
+ child = spawn('/usr/bin/sandbox-exec', ['-p', profile, executable, ...request.argv], {
48
+ cwd: request.cwd ?? '/', env: { PATH: '/usr/bin:/bin', LANG: 'C', HOME: '/nonexistent', TMPDIR: '/nonexistent' }, stdio: ['ignore', 'pipe', 'pipe'], detached: true,
49
+ });
50
+ }
51
+ catch {
52
+ resolve(empty('blocked', 'spawn_failed'));
53
+ return;
54
+ }
55
+ const kill = () => { try {
56
+ if (child.pid)
57
+ process.kill(-child.pid, 'SIGKILL');
58
+ }
59
+ catch { /* own group already exited */ } };
60
+ const stop = (why) => { reason ??= why; kill(); };
61
+ const abort = () => stop('aborted');
62
+ request.signal?.addEventListener('abort', abort, { once: true });
63
+ if (request.signal?.aborted)
64
+ abort();
65
+ if (request.timeoutMs !== undefined)
66
+ timer = setTimeout(() => stop('timeout'), request.timeoutMs);
67
+ child.stdout.on('data', (chunk) => { outBytes += chunk.length; stdout.update(chunk); if (outBytes + (retainStdout ? 0 : errBytes) > limit)
68
+ stop('output_limit');
69
+ else if (retainStdout)
70
+ chunks.push(chunk); });
71
+ // Legacy callers bounded stdout only and drained arbitrary stderr.
72
+ // P1 bounds both streams together, without retaining either stream.
73
+ child.stderr.on('data', (chunk) => { errBytes += chunk.length; stderr.update(chunk); if (!retainStdout && outBytes + errBytes > limit)
74
+ stop('output_limit'); });
75
+ child.stdout.on('error', () => stop('spawn_failed'));
76
+ child.stderr.on('error', () => stop('spawn_failed'));
77
+ child.on('error', () => stop('spawn_failed'));
78
+ child.on('close', (code, signal) => {
34
79
  clearTimeout(timer);
35
- if (error)
36
- reject(error);
37
- else
38
- resolve(Buffer.concat(chunks).toString('utf8'));
39
- };
40
- const kill = () => {
41
- try {
42
- if (child.pid)
43
- process.kill(-child.pid, 'SIGKILL');
44
- }
45
- catch { /* already exited */ }
46
- };
47
- const timer = request.timeoutMs === undefined ? undefined : setTimeout(() => { kill(); finish(new Error('LOCAL_SANDBOX_UNAVAILABLE: command timed out')); }, request.timeoutMs);
48
- child.stdout.on('data', (chunk) => {
49
- size += chunk.length;
50
- if (size > 1024 * 1024) {
51
- kill();
52
- finish(new Error('LOCAL_SANDBOX_UNAVAILABLE: output limit'));
53
- }
54
- else
55
- chunks.push(chunk);
80
+ request.signal?.removeEventListener('abort', abort);
81
+ kill();
82
+ if (request.signal?.aborted)
83
+ reason = 'aborted';
84
+ reason ??= signal ? 'signal' : code === 0 ? undefined : 'nonzero_exit';
85
+ const status = reason === 'aborted' ? 'cancelled' : reason === 'timeout' ? 'timed_out' : reason === 'output_limit' || reason === 'spawn_failed' ? 'blocked' : reason ? 'failed' : 'passed';
86
+ resolve({ measurement: { status, exit_code: code, duration_ms: Math.max(0, Math.floor(performance.now() - start)), stdout_sha256: reason === 'spawn_failed' ? null : stdout.digest('hex'), stderr_sha256: reason === 'spawn_failed' ? null : stderr.digest('hex'), stdout_bytes: outBytes, stderr_bytes: errBytes, ...(reason ? { reason } : {}) }, output: retainStdout && status === 'passed' ? Buffer.concat(chunks).toString('utf8') : '' });
56
87
  });
57
- // Do not disclose arbitrary subprocess stderr or paths to the model.
58
- child.stderr.resume();
59
- child.on('error', () => { kill(); finish(new Error('LOCAL_SANDBOX_UNAVAILABLE: could not start restricted process')); });
60
- child.on('close', (code) => { kill(); finish(code === 0 ? undefined : new Error('LOCAL_SANDBOX_UNAVAILABLE: restricted process failed')); });
61
88
  });
62
89
  }
90
+ export async function runSandboxedMeasured(request) { return (await executeSandboxed(request, false)).measurement; }
91
+ export async function runSandboxed(request) {
92
+ const result = await executeSandboxed(request, true);
93
+ if (result.measurement.status !== 'passed')
94
+ throw new Error('LOCAL_SANDBOX_UNAVAILABLE: restricted process failed');
95
+ return result.output;
96
+ }
@@ -0,0 +1,78 @@
1
+ import { RunnerObservedVerifier } from './runner-observed-verifier.js';
2
+ import { isP1WorkingBranch } from '../publication/git-snapshot.js';
3
+ import { protocolHash } from '../publication/candidate.js';
4
+ export class RunnerObservedExecutionHost {
5
+ privateRoot;
6
+ registration;
7
+ constructor(privateRoot, registration) {
8
+ this.privateRoot = privateRoot;
9
+ this.registration = structuredClone(registration);
10
+ }
11
+ options(input) {
12
+ const p = input.job.taskPackage, r = p.repository;
13
+ return { root: input.checkoutPath, privateRoot: this.privateRoot, registration: this.registration, verification: p.verification,
14
+ fence: { binding_id: input.bindingId, job_id: input.job.id, generation: input.job.generation, contract_fingerprint: p.contract_fingerprint, base_commit_sha: r.base_commit_sha, working_branch: r.working_branch },
15
+ allowedPaths: p.allowed_paths, blockedPaths: r.blocked_paths, candidateLimits: p.candidate_limits,
16
+ current: input.current, freezeOwnedModel: input.freezeOwnedModel, assertModelFrozen: input.assertModelFrozen, signal: input.signal };
17
+ }
18
+ assertEligible(job) {
19
+ const r = this.registration;
20
+ if (r.manifest.schema !== 'local-project-verification-v1' || !r.review.receiptId?.trim() || !r.review.reviewer?.trim() || r.review.manifestFingerprint !== protocolHash(r.manifest))
21
+ throw Error('P1_REGISTRATION_REJECTED');
22
+ new RunnerObservedVerifier(this.options({ job, checkoutPath: '', bindingId: 'preflight', signal: new AbortController().signal, current: async () => false, freezeOwnedModel: async () => { throw Error('P1_NOT_FROZEN'); }, assertModelFrozen: async () => { throw Error('P1_NOT_FROZEN'); } })).assertContract();
23
+ }
24
+ verify(input) { return new RunnerObservedVerifier(this.options(input)).verify(); }
25
+ }
26
+ const record = (v) => { if (!v || typeof v !== 'object' || Array.isArray(v))
27
+ throw Error('P1_CONTRACT_REJECTED'); return v; };
28
+ const equal = (a, b) => a !== undefined && b !== undefined && protocolHash(a) === protocolHash(b);
29
+ export const hasP1 = (p) => {
30
+ const v = p.verification, policy = p.policy;
31
+ return policy?.policy_key === 'local-runner-verified-v1' || !!v && (v.schema_version !== undefined || v.mode !== undefined);
32
+ };
33
+ /** Compare host launch claims to authoritative WEC/bundle fields before start. */
34
+ export function assertP1Bindings(p, canonical, bundle = false) {
35
+ const v = record(p.verification), r = record(p.repository), limits = record(p.candidate_limits);
36
+ if (p.local_fix_scope !== undefined || p.verification_mode === 'gateway_ci_v1' || p.runtime_adapter !== 'codex_cli' || p.runtime_adapter_version !== '1' || p.runtime_transport !== 'official_exec_v1' || v.schema_version !== 'local-runner-verification-v1' || v.policy_key !== 'local-runner-verified-v1' || v.mode !== 'runner_observed_v1' || v.runtime_adapter !== p.runtime_adapter || v.runtime_transport !== p.runtime_transport || r.access !== 'write' || !isP1WorkingBranch(r.working_branch) || !equal(p.allowed_paths, v.allowed_paths) || !equal(r.allowed_paths, v.allowed_paths) || !Array.isArray(r.blocked_paths))
37
+ throw Error('P1_CONTRACT_REJECTED');
38
+ if (Object.keys(limits).sort().join(',') !== 'max_changed_files,max_diff_bytes,max_file_bytes' || Object.values(limits).some(n => !Number.isSafeInteger(n) || Number(n) < 1))
39
+ throw Error('P1_CONTRACT_REJECTED');
40
+ if (!canonical)
41
+ return;
42
+ const execution = canonical.agent_execution;
43
+ if (canonical.local_fix_scope !== undefined || execution?.local_fix_scope !== undefined || execution?.publication_mode === 'gateway_ci_v1')
44
+ throw Error('P1_CONTRACT_REJECTED');
45
+ if (!bundle) {
46
+ const policy = record(canonical.policy);
47
+ if (policy.policy_key !== 'local-runner-verified-v1' || policy.loop_type !== 'agent_runtime_implementation')
48
+ throw Error('P1_CONTRACT_REJECTED');
49
+ }
50
+ const repos = canonical.repositories;
51
+ if (!Array.isArray(repos) || repos.length !== 1)
52
+ throw Error('P1_CONTRACT_REJECTED');
53
+ const repo = record(repos[0]);
54
+ const bound = bundle ? record(canonical.contract) : canonical;
55
+ if (!equal(v, bound.verification) || !equal(limits, record(bound.policy).candidate_limits) || repo.repository_key !== r.key || repo.access !== r.access || repo.base_commit_sha !== r.base_commit_sha || repo.working_branch !== r.working_branch || !equal(repo.allowed_paths, r.allowed_paths) || !equal(repo.blocked_paths, r.blocked_paths))
56
+ throw Error('P1_CONTRACT_REJECTED');
57
+ if (bundle && (bound.content_fingerprint !== p.contract_fingerprint || canonical.verification !== undefined && !equal(v, canonical.verification) || canonical.candidate_limits !== undefined && !equal(limits, canonical.candidate_limits)))
58
+ throw Error('P1_CONTRACT_REJECTED');
59
+ }
60
+ /** P1 model events carry narrative/progress only. Proof is injected afterwards. */
61
+ export function p1ModelEvent(event) {
62
+ if (event.eventType === 'test_started' || event.eventType === 'test_result')
63
+ return { eventType: 'progress', summary: 'model tool activity', payload: { kind: 'tool_activity' } };
64
+ const payload = {};
65
+ for (const key of ['reason', 'investigation_summary', 'question']) {
66
+ const value = event.payload?.[key];
67
+ if (typeof value === 'string')
68
+ payload[key] = value.slice(0, 6000);
69
+ }
70
+ // Only parsers can attach these provider observations; never completion text.
71
+ const usage = event.payload?.usage;
72
+ if (usage && typeof usage === 'object' && !Array.isArray(usage)) {
73
+ const u = usage, provider = ['openai', 'anthropic'].includes(String(u.provider)) ? u.provider : null;
74
+ const source = ['codex_cli.turn.completed', 'codex_app_server.thread/tokenUsage/updated', 'claude_code_cli.result'].includes(String(u.source)) ? u.source : null;
75
+ payload.usage = { provider, source, ...Object.fromEntries(['input_tokens', 'cached_input_tokens', 'output_tokens', 'duration_ms'].map(k => [k, typeof u[k] === 'number' && Number.isSafeInteger(u[k]) && Number(u[k]) >= 0 && Number(u[k]) <= 2147483647 ? u[k] : null])) };
76
+ }
77
+ return { ...event, payload };
78
+ }