mtech-ai-local-runner 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +23 -0
  2. package/dist/src/adapters/claude-code/adapter.js +10 -27
  3. package/dist/src/adapters/claude-code/runtime-options.js +1 -1
  4. package/dist/src/adapters/codex/account-sources.js +113 -0
  5. package/dist/src/adapters/codex/adapter.js +60 -4
  6. package/dist/src/adapters/codex/config-builder.js +3 -0
  7. package/dist/src/adapters/codex/exec-invocation.js +62 -0
  8. package/dist/src/adapters/codex/exec-launch-host.js +215 -0
  9. package/dist/src/adapters/codex/exec-observer.js +83 -0
  10. package/dist/src/adapters/codex/exec-preflight.js +41 -0
  11. package/dist/src/adapters/codex/exec-release-policy.js +44 -0
  12. package/dist/src/adapters/codex/exec-runtime-options.js +122 -0
  13. package/dist/src/adapters/codex/exec-tool-gate.js +81 -0
  14. package/dist/src/adapters/codex/global-instructions.js +32 -0
  15. package/dist/src/adapters/codex/manual-policy-scope.js +67 -0
  16. package/dist/src/adapters/codex/native-policy.js +20 -4
  17. package/dist/src/adapters/codex/native-session.js +4 -23
  18. package/dist/src/adapters/codex/unmanaged-scope.js +85 -0
  19. package/dist/src/auth/token-provider.js +2 -0
  20. package/dist/src/cli.js +54 -6
  21. package/dist/src/config.js +9 -2
  22. package/dist/src/daemon/execution-coordinator.js +25 -4
  23. package/dist/src/daemon/manual-codex.js +83 -0
  24. package/dist/src/daemon/runtime.js +17 -9
  25. package/dist/src/gateway-client.js +3 -2
  26. package/dist/src/safety/codex-exec-production-host.js +249 -0
  27. package/dist/src/safety/codex-exec-releases.js +3 -0
  28. package/dist/src/safety/codex-manual-controller.js +84 -0
  29. package/dist/src/safety/codex-manual-profile-query.js +30 -0
  30. package/dist/src/safety/codex-manual-registered.js +30 -0
  31. package/dist/src/safety/codex-native-observation.js +68 -0
  32. package/dist/src/safety/codex-native-process.js +59 -0
  33. package/dist/src/safety/codex-production-host.js +1 -1
  34. package/dist/src/safety/codex-reviewed-manual-release.js +37 -0
  35. package/dist/src/safety/codex-runtime-host.js +36 -0
  36. package/dist/src/safety/execution-failure.js +32 -0
  37. package/dist/src/safety/hook-cli.js +18 -2
  38. package/dist/src/safety/hook-command.js +156 -0
  39. package/dist/src/safety/hook-policy-builder.js +2 -0
  40. package/dist/src/safety/local-tool-server.js +41 -4
  41. package/dist/src/safety/security-requirements.js +11 -0
  42. package/dist/src/safety/stable-file-read.js +33 -0
  43. package/native/bin/codex-observe-darwin-arm64 +0 -0
  44. package/package.json +5 -3
@@ -0,0 +1,83 @@
1
+ /** Official exec JSONL exposes no native same-execution effective/managed policy
2
+ * inspection. stdin is consumed before startup; waiting for child discovery
3
+ * before submitting stdin cannot be used as an inference authorization barrier.
4
+ * Fixed limits, not user/task-provided release claims. */
5
+ export function officialExecObservationLimits() {
6
+ return [
7
+ 'EXEC_EFFECTIVE_POLICY_OBSERVATION_UNAVAILABLE',
8
+ 'EXEC_MANAGED_POLICY_OBSERVATION_UNAVAILABLE',
9
+ 'EXEC_REQUIRED_MCP_INFERENCE_BARRIER_UNVERIFIED',
10
+ 'EXEC_STDIN_REQUIRED_BEFORE_STARTUP',
11
+ ];
12
+ }
13
+ /** Raw wire diagnostics. Attach discovery only to this launch's authenticated
14
+ * server; do not call from task JSON/stdout. Possession of the endpoint token is
15
+ * not OS process identity or proof of when native inference begins.
16
+ * No permit, LiveExecInspection or ExecHandshake is produced by this observer. */
17
+ export class ExecWireObserver {
18
+ stage = 'unobserved';
19
+ thread = false;
20
+ reasons = new Set();
21
+ discovery = (event) => {
22
+ if (this.stage === 'revoked')
23
+ return;
24
+ if (event === 'revoked') {
25
+ this.stage = 'revoked';
26
+ return;
27
+ }
28
+ if (event === 'initialize_response' && this.stage === 'unobserved') {
29
+ this.stage = 'initialized';
30
+ return;
31
+ }
32
+ if (event === 'initialized_notification' && this.stage === 'initialized') {
33
+ this.stage = 'notified';
34
+ return;
35
+ }
36
+ if (event === 'inventory_response' && (this.stage === 'notified' || this.stage === 'observed')) {
37
+ this.stage = 'observed';
38
+ return;
39
+ }
40
+ this.stage = 'invalid';
41
+ this.reasons.add('EXEC_MCP_DISCOVERY_SEQUENCE_INVALID');
42
+ };
43
+ line(line) {
44
+ // Deliberately keep no raw payload, thread ID, path, stderr or provider usage.
45
+ try {
46
+ if (typeof line !== 'string' || Buffer.byteLength(line) > 256_000)
47
+ throw Error();
48
+ const e = JSON.parse(line);
49
+ if (!e || typeof e !== 'object' || Array.isArray(e) || typeof e.type !== 'string')
50
+ throw Error();
51
+ const event = e;
52
+ if (event.type === 'thread.started') {
53
+ if (Object.keys(event).length !== 2 || typeof event.thread_id !== 'string' || !event.thread_id || event.thread_id.length > 256)
54
+ throw Error();
55
+ this.thread = true;
56
+ return;
57
+ }
58
+ if (event.type === 'error' || event.type === 'turn.failed') {
59
+ this.reasons.add('EXEC_CHILD_FAILURE');
60
+ return;
61
+ }
62
+ if (['turn.started', 'turn.completed', 'item.started', 'item.updated', 'item.completed'].includes(event.type)) {
63
+ // These events occur after prompt submission; never elevate them to proof.
64
+ this.reasons.add('EXEC_POST_PROMPT_EVENT_OBSERVED');
65
+ return;
66
+ }
67
+ this.reasons.add('EXEC_UNSUPPORTED_WIRE_EVENT');
68
+ }
69
+ catch {
70
+ this.reasons.add('EXEC_INVALID_WIRE_EVENT');
71
+ }
72
+ }
73
+ report() {
74
+ return {
75
+ executionReady: false,
76
+ effectivePolicy: 'unverified', managedPolicy: 'unverified',
77
+ requiredMcpInferenceBarrier: 'unverified',
78
+ mcpDiscovery: this.stage === 'observed' ? 'observed' : this.stage === 'revoked' ? 'revoked' : 'unobserved',
79
+ threadObserved: this.thread,
80
+ reasonCodes: [...officialExecObservationLimits(), ...this.reasons],
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,41 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { SUPPORTED_CLI_GATES } from '../../safety/cli-security-modes.js';
3
+ import { localToolInventory } from '../../safety/local-tool-server.js';
4
+ import { EXEC_TRANSPORT, requireOfficialExecPlan } from './exec-invocation.js';
5
+ export const EXEC_GATES = Object.freeze([...SUPPORTED_CLI_GATES, 'effective_policy_observation', 'managed_policy_observation', 'required_mcp_inference_barrier']);
6
+ const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
7
+ const exact = (v, keys) => isDeepStrictEqual(Reflect.ownKeys(v).sort(), keys.sort());
8
+ const sha = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
9
+ /** Diagnostics only. Even a ready assessment is not a policy or spawn permit. */
10
+ export function assessExecPreflight(value, plan) {
11
+ const reasonCodes = [];
12
+ const need = (ok, code) => { if (!ok)
13
+ reasonCodes.push(code); };
14
+ try {
15
+ requireOfficialExecPlan(plan);
16
+ if (!object(value))
17
+ return { ready: false, reasonCodes: ['EXEC_OBSERVATION_UNAVAILABLE', ...EXEC_GATES.map(g => 'EXEC_' + g.toUpperCase() + '_UNVERIFIED'), 'EXEC_EFFECTIVE_POLICY_UNVERIFIED', 'EXEC_MANAGED_POLICY_UNVERIFIED', 'EXEC_MCP_BARRIER_UNVERIFIED'] };
18
+ need(exact(value, ['version', 'transport', 'executionNonce', 'identity', 'invocationFingerprint', 'checks', 'effectivePolicy', 'managedPolicy', 'mcpPolicy', 'warnings']), 'EXEC_EVIDENCE_SHAPE');
19
+ need(value.version === 'cli-exec-security-evidence-v1' && value.transport === EXEC_TRANSPORT, 'EXEC_EVIDENCE_VERSION');
20
+ need(value.executionNonce === plan.executionNonce && value.invocationFingerprint === plan.invocationFingerprint && isDeepStrictEqual(value.identity, plan.identity), 'EXEC_IDENTITY_MISMATCH');
21
+ need(Array.isArray(value.warnings) && value.warnings.length === 0, 'EXEC_WARNING');
22
+ const checks = Array.isArray(value.checks) ? value.checks : [];
23
+ need(checks.length === EXEC_GATES.length && new Set(checks.map(c => object(c) ? c.gate : null)).size === EXEC_GATES.length && checks.every(c => object(c) && exact(c, ['gate', 'status', 'basis']) && EXEC_GATES.includes(c.gate)), 'EXEC_CHECK_SET');
24
+ for (const gate of EXEC_GATES)
25
+ need(checks.filter(c => object(c) && c.gate === gate && c.status === 'verified' && c.basis === 'current_execution').length === 1, 'EXEC_' + gate.toUpperCase() + '_UNVERIFIED');
26
+ const p = object(value.effectivePolicy) ? value.effectivePolicy : {};
27
+ need(exact(p, ['source', 'executionNonce', 'modelId', 'provider', 'cwd', 'approvalPolicy', 'permissionProfile', 'nativeNetwork', 'fingerprint']) && p.source === 'owned_exec_pre_model_observation' && p.executionNonce === plan.executionNonce && p.modelId === plan.modelId && p.provider === 'openai' && p.cwd === plan.cwd && p.approvalPolicy === 'never' && p.permissionProfile === 'local_job' && p.nativeNetwork === false && sha(p.fingerprint), 'EXEC_EFFECTIVE_POLICY_UNVERIFIED');
28
+ const m = object(value.managedPolicy) ? value.managedPolicy : {};
29
+ need(exact(m, ['source', 'executionNonce', 'decision', 'fingerprint', 'sourcesFingerprint']) && m.source === 'owned_exec_pre_model_observation' && m.executionNonce === plan.executionNonce && m.decision === 'compatible' && sha(m.fingerprint) && sha(m.sourcesFingerprint), 'EXEC_MANAGED_POLICY_UNVERIFIED');
30
+ const b = object(value.mcpPolicy) ? value.mcpPolicy : {};
31
+ need(exact(b, ['source', 'executionNonce', 'endpoint', 'toolSchemaFingerprint', 'required', 'startupFailurePreventsInference', 'barrierFingerprint']) && b.source === 'owned_exec_native_barrier' && b.executionNonce === plan.executionNonce && b.endpoint === plan.endpoint && b.toolSchemaFingerprint === plan.identity.toolSchemaFingerprint && b.required === true && b.startupFailurePreventsInference === true && b.barrierFingerprint === plan.identity.barrierFingerprint, 'EXEC_MCP_BARRIER_UNVERIFIED');
32
+ }
33
+ catch {
34
+ reasonCodes.push('EXEC_OBSERVATION_UNAVAILABLE');
35
+ }
36
+ return { ready: reasonCodes.length === 0, reasonCodes };
37
+ }
38
+ export function requireExecHandshake(h, process, plan) {
39
+ if (!object(h) || h.process !== process || h.executionNonce !== plan.executionNonce || h.endpoint !== plan.endpoint || h.authenticated !== true || h.ready !== true || h.toolSchemaFingerprint !== plan.identity.toolSchemaFingerprint || !isDeepStrictEqual(h.tools, localToolInventory().map(t => t.name)) || typeof h.isCurrent !== 'function' || h.isCurrent() !== true)
40
+ throw Error('CODEX_EXEC_MCP_REJECTED');
41
+ }
@@ -0,0 +1,44 @@
1
+ import { MANUAL_MDM_RISK } from './manual-policy-scope.js';
2
+ import { UNMANAGED_EXEC_MODE, UNMANAGED_SCOPE, requirePolicyScope } from './unmanaged-scope.js';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { isReleaseExecMode, EXEC_TRANSPORT, requireOfficialExecPlan } from './exec-invocation.js';
5
+ export const EXEC_RELEASE_GATES = Object.freeze(['binary_release', 'official_permissions', 'managed_policy_enforcement', 'required_mcp_startup', 'source_exclusion', 'auth_backend_compatibility']);
6
+ export const EXEC_INPUT_GATES = Object.freeze(['exact_inputs', 'launch_binding', 'repository_scope', 'private_paths_environment', 'owned_lifecycle']);
7
+ export const EXEC_RELEASE_RISKS = Object.freeze(['vendor_native_policy_enforcement', 'managed_policy_not_directly_observed_per_job', 'first_inference_order_not_observed_per_job', 'job_mcp_token_possession_not_os_identity', 'account_startup_may_refresh_shared_credentials']);
8
+ export const releaseGates = (mode) => mode === UNMANAGED_EXEC_MODE ? EXEC_RELEASE_GATES.map(g => g === 'managed_policy_enforcement' ? 'managed_scope_exclusion' : g) : EXEC_RELEASE_GATES;
9
+ export const releaseRisks = (mode, mdmVerification) => mode === UNMANAGED_EXEC_MODE ? [...EXEC_RELEASE_RISKS, 'concurrent_policy_change_not_atomic', 'local_account_metadata_not_remote_entitlement', ...(mdmVerification === 'manual_prelaunch' ? [MANUAL_MDM_RISK] : [])] : EXEC_RELEASE_RISKS;
10
+ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
11
+ const exact = (v, keys) => isDeepStrictEqual(Object.keys(v).sort(), keys.sort());
12
+ const checks = (v, gates, basis) => Array.isArray(v) && v.length === gates.length && gates.every(g => v.filter(c => record(c) && exact(c, ['gate', 'status', 'basis']) && c.gate === g && c.status === 'verified' && c.basis === basis).length === 1);
13
+ export function requireReleaseDescriptor(r) {
14
+ const v3 = r.security_mode === UNMANAGED_EXEC_MODE;
15
+ if (!['release_manifest_sha256', 'binary_sha256', 'fixtures_sha256', 'commands_sha256', 'tools_sha256', 'fingerprint'].every(k => typeof r[k] === 'string' && /^[a-f0-9]{64}$/.test(r[k])) || !Array.isArray(r.model_ids) || !r.model_ids.length || new Set(r.model_ids).size !== r.model_ids.length || r.model_ids.some(m => typeof m !== 'string' || !m || m.length > 255 || /[\r\n\0]/.test(m)) || !exact(r, ['version', 'security_mode', 'transport', 'status', 'adapter_id', 'adapter_version', 'mandatory_gates', 'accepted_residual_risks', 'evidence_class', 'release_manifest_sha256', 'binary_sha256', 'fixtures_sha256', 'commands_sha256', 'tools_sha256', 'model_ids', 'fingerprint', ...(v3 ? ['policy_scope', 'managed_enforcement', ...(r.mdm_verification === undefined ? [] : ['mdm_verification'])] : [])]) || r.version !== (v3 ? 'cli-exec-security-requirements-v3' : 'cli-exec-security-requirements-v2') || !isReleaseExecMode(r.security_mode) || v3 && (r.policy_scope !== UNMANAGED_SCOPE || r.managed_enforcement !== 'not_run' || r.mdm_verification !== undefined && r.mdm_verification !== 'manual_prelaunch') || r.evidence_class !== 'reviewed_release_and_current_inputs' || !isDeepStrictEqual(r.mandatory_gates, [...releaseGates(r.security_mode), ...EXEC_INPUT_GATES, 'authenticated_mcp_discovery']) || !isDeepStrictEqual(r.accepted_residual_risks, releaseRisks(r.security_mode, r.mdm_verification)))
16
+ throw Error('EXEC_RELEASE_DESCRIPTOR_REJECTED');
17
+ }
18
+ /** Diagnostic validator only; live currentness and trusted dependency origin
19
+ * are checked by the host. Release checks never become direct native proof. */
20
+ export function assessReleaseExec(value, plan) {
21
+ const reasonCodes = [];
22
+ const need = (ok, code) => { if (!ok)
23
+ reasonCodes.push(code); };
24
+ try {
25
+ requireOfficialExecPlan(plan);
26
+ const v3 = plan.mode === UNMANAGED_EXEC_MODE;
27
+ if (!record(value))
28
+ return { ready: false, reasonCodes: ['EXEC_RELEASE_EVIDENCE_UNAVAILABLE', ...EXEC_RELEASE_GATES.map(g => 'EXEC_RELEASE_' + g.toUpperCase() + '_UNVERIFIED'), ...EXEC_INPUT_GATES.map(g => 'EXEC_INPUT_' + g.toUpperCase() + '_UNVERIFIED')] };
29
+ need(isReleaseExecMode(plan.mode) && exact(value, ['version', 'securityMode', 'transport', 'executionNonce', 'identity', 'invocationFingerprint', 'acceptedResidualRisks', 'release', 'inputs', 'warnings', ...(v3 ? ['policyScope'] : [])]) && value.version === (v3 ? 'cli-exec-release-evidence-v3' : 'cli-exec-release-evidence-v2') && value.securityMode === plan.mode && value.transport === EXEC_TRANSPORT, 'EXEC_RELEASE_EVIDENCE_SHAPE');
30
+ need(value.executionNonce === plan.executionNonce && value.invocationFingerprint === plan.invocationFingerprint && isDeepStrictEqual(value.identity, plan.identity), 'EXEC_RELEASE_IDENTITY_MISMATCH');
31
+ need(isDeepStrictEqual(value.acceptedResidualRisks, releaseRisks(plan.mode, record(value.policyScope) && record(value.policyScope.manual_verification) ? value.policyScope.manual_verification.mode : undefined)), 'EXEC_RELEASE_RISK_MISMATCH');
32
+ need(Array.isArray(value.warnings) && value.warnings.length === 0, 'EXEC_RELEASE_WARNING');
33
+ const r = record(value.release) ? value.release : {};
34
+ need(exact(r, ['source', 'binarySha256', 'manifestFingerprint', 'fixturesFingerprint', 'checks']) && r.source === 'reviewed_compiled_release' && r.binarySha256 === plan.identity.binarySha256 && r.manifestFingerprint === plan.identity.releaseManifestFingerprint && r.fixturesFingerprint === plan.identity.barrierFingerprint, 'EXEC_RELEASE_PIN_MISMATCH');
35
+ need(checks(r.checks, releaseGates(plan.mode), 'pinned_release_fixture'), 'EXEC_RELEASE_CHECKS_UNVERIFIED');
36
+ if (v3)
37
+ requirePolicyScope(value.policyScope, plan);
38
+ need(checks(value.inputs, EXEC_INPUT_GATES, 'current_execution'), 'EXEC_INPUT_CHECKS_UNVERIFIED');
39
+ }
40
+ catch {
41
+ reasonCodes.push('EXEC_RELEASE_EVIDENCE_UNAVAILABLE');
42
+ }
43
+ return { ready: reasonCodes.length === 0, reasonCodes };
44
+ }
@@ -0,0 +1,122 @@
1
+ import { executionError } from '../../safety/execution-failure.js';
2
+ import path from 'node:path';
3
+ import { mkdtemp, mkdir, realpath, writeFile, rm, lstat } from 'node:fs/promises';
4
+ import { NodeProcessGroupHost } from '../../process/node-process-group-host.js';
5
+ import { LocalToolPolicy } from '../../safety/local-tool-policy.js';
6
+ import { jobToolPolicyOptions } from '../../safety/job-tool-policy.js';
7
+ import { LocalToolServer, localToolInventory } from '../../safety/local-tool-server.js';
8
+ import { commandRegistryFingerprint } from '../../safety/job-security-session.js';
9
+ import { fingerprint } from '../../daemon/execution-coordinator.js';
10
+ import { buildOfficialExecPlan } from './exec-invocation.js';
11
+ import { ExecToolGate } from './exec-tool-gate.js';
12
+ import { CodexExecLaunchHost } from './exec-launch-host.js';
13
+ import { CODEX_REPORT_SCHEMA } from './prompt-compiler.js';
14
+ const inside = (a, b) => a === b || b.startsWith(a + path.sep);
15
+ const reject = () => Error('CODEX_EXEC_COMPOSITION_REJECTED');
16
+ /** The caller supplies a trusted claimed job, not a task-selected host factory. */
17
+ export function buildCodexExecJobOptions(input) {
18
+ let job, server, gate, policy, control, closed = false, used = false, closing;
19
+ let launchHost;
20
+ const dispose = () => closing ??= Promise.resolve().then(async () => { closed = true; gate?.revoke(); job?.revoke(); try {
21
+ server?.revoke();
22
+ }
23
+ finally {
24
+ policy?.revoke();
25
+ try {
26
+ await server?.close();
27
+ }
28
+ finally {
29
+ if (control)
30
+ await rm(control, { recursive: true, force: true });
31
+ }
32
+ } });
33
+ const close = async () => { try {
34
+ await launchHost?.cancel();
35
+ }
36
+ finally {
37
+ await dispose();
38
+ } };
39
+ const processHost = input.processHost ?? new NodeProcessGroupHost();
40
+ const env = Object.fromEntries(['HOME', 'CODEX_HOME', 'PATH', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'TMPDIR'].filter(k => input.env[k] !== undefined).map(k => [k, input.env[k]]));
41
+ return { worktreeRoot: input.worktreePath, jobId: input.jobId, env: input.env, processHost,
42
+ closeOfficialExec: close, revokeSecurity: () => { closed = true; gate?.revoke(); job?.revoke(); server?.revoke(); policy?.revoke(); },
43
+ createTools: () => { throw reject(); }, writeSchema: async () => { throw reject(); },
44
+ createOfficialExec: async (ctx, signal) => {
45
+ if (used || closed || signal.aborted)
46
+ throw reject();
47
+ used = true;
48
+ const abort = () => { void close().catch(() => { }); };
49
+ signal.addEventListener('abort', abort, { once: true });
50
+ let stage = 'codex_prepare';
51
+ try {
52
+ job = input.productionJob();
53
+ job.attachCleanup(close);
54
+ const admitted = await job.prepare(ctx);
55
+ if (closed || signal.aborted)
56
+ throw reject();
57
+ stage = 'codex_storage';
58
+ if (!input.env.HOME || !path.isAbsolute(input.stateRoot))
59
+ throw reject();
60
+ const account = path.resolve(input.env.CODEX_HOME ?? path.join(input.env.HOME, '.codex'));
61
+ await mkdir(input.stateRoot, { recursive: true, mode: 0o700 });
62
+ const state = await realpath(input.stateRoot), source = await realpath(input.worktreePath), home = await realpath(account);
63
+ if (inside(source, state) || inside(state, source) || inside(home, state) || inside(state, home))
64
+ throw reject();
65
+ control = await mkdtemp(path.join(state, 'codex-exec-'));
66
+ const cwd = path.join(control, 'cwd'), scratch = { tmpDir: path.join(control, 'tmp'), logDir: path.join(control, 'log'), sqliteHome: path.join(control, 'sqlite') };
67
+ await Promise.all([cwd, ...Object.values(scratch)].map(p => mkdir(p, { mode: 0o700 })));
68
+ if (closed || signal.aborted)
69
+ throw reject();
70
+ stage = 'codex_config_ancestry';
71
+ for (let dir = cwd;; dir = path.dirname(dir)) {
72
+ for (const base of ['.codex/config.toml']) {
73
+ const f = path.join(dir, base);
74
+ if (f !== path.join(account, 'config.toml')) {
75
+ try {
76
+ await lstat(f);
77
+ throw reject();
78
+ }
79
+ catch (e) {
80
+ if (e.code !== 'ENOENT')
81
+ throw e;
82
+ }
83
+ }
84
+ }
85
+ if (dir === path.dirname(dir))
86
+ break;
87
+ }
88
+ stage = 'codex_storage';
89
+ const schema = path.join(control, 'report-schema.json');
90
+ await writeFile(schema, JSON.stringify(CODEX_REPORT_SCHEMA), { mode: 0o600, flag: 'wx' });
91
+ if (closed || signal.aborted)
92
+ throw reject();
93
+ const approved = ctx.approvedTaskPackage, requirements = approved.security_requirements;
94
+ stage = 'codex_tool_policy';
95
+ policy = new LocalToolPolicy(jobToolPolicyOptions(source, ctx.taskPackage, approved, admitted.release.commands, { runtimeAdapter: 'codex_cli', runtimeAdapterVersion: '1' }));
96
+ stage = 'codex_tool_server';
97
+ server = new LocalToolServer({ policy, authorizeCall: () => gate?.allowsCall() === true, onDiscovery: (event, scope) => gate?.discovery(event, scope) });
98
+ const endpoint = await server.start();
99
+ if (closed || signal.aborted)
100
+ throw reject();
101
+ stage = 'codex_invocation';
102
+ const plan = buildOfficialExecPlan({ policyMode: admitted.release.securityMode, binary: admitted.binary, credentialBackend: admitted.release.credentialBackend, identity: { jobId: input.jobId, generation: ctx.generation, actorId: admitted.actorId, runnerId: admitted.runnerId, sourceSha: String(approved.repository.base_commit_sha), binarySha256: admitted.release.binarySha256, releaseManifestFingerprint: String(requirements.release_manifest_sha256), requirementsFingerprint: String(requirements.fingerprint), contractFingerprint: ctx.securityBinding.contractFingerprint, taskPackageFingerprint: ctx.securityBinding.taskPackageFingerprint, launchFingerprint: ctx.securityBinding.launchFingerprint, commandsFingerprint: commandRegistryFingerprint(admitted.release.commands), toolSchemaFingerprint: fingerprint({ tools: localToolInventory() }), barrierFingerprint: admitted.release.fixturesFingerprint }, invocation: { modelId: ctx.resolvedModelId, worktreeRoot: cwd, sourceRoot: source, schemaPath: schema, endpoint, env, scratch } });
103
+ gate = new ExecToolGate(plan);
104
+ const host = new CodexExecLaunchHost(plan, { processHost, inspectRelease: p => job.inspect(p), execTools: gate, revokeTools: () => { gate?.revoke(); server?.revoke(); policy?.revoke(); job?.revoke(); }, closeTools: dispose });
105
+ launchHost = host;
106
+ if (closed || signal.aborted)
107
+ throw reject();
108
+ return host;
109
+ }
110
+ catch (error) {
111
+ const safe = executionError(error, stage, 'CODEX_EXEC_COMPOSITION_REJECTED');
112
+ await close().catch(() => { });
113
+ if (control)
114
+ await rm(control, { recursive: true, force: true }).catch(() => { });
115
+ throw safe;
116
+ }
117
+ finally {
118
+ signal.removeEventListener('abort', abort);
119
+ }
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,81 @@
1
+ import { isReleaseExecMode, requireOfficialExecPlan } from './exec-invocation.js';
2
+ import { localDiscoveryScopeMatches, localToolInventory } from '../../safety/local-tool-server.js';
3
+ import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
4
+ const denied = () => Error('EXEC_TOOL_GATE_REJECTED');
5
+ /** Host-owned capability, never populated from stdout/task data. Authenticated
6
+ * discovery is server-side evidence; it does not attest OS client identity. */
7
+ export class ExecToolGate {
8
+ plan;
9
+ process;
10
+ current;
11
+ submitted = false;
12
+ revoked = false;
13
+ authorized = false;
14
+ stage = 0;
15
+ settle;
16
+ reject;
17
+ constructor(plan) {
18
+ this.plan = plan;
19
+ requireOfficialExecPlan(plan);
20
+ if (!isReleaseExecMode(plan.mode) || plan.identity.toolSchemaFingerprint !== hash({ tools: localToolInventory() }))
21
+ throw denied();
22
+ }
23
+ matches(plan) { return this.plan === plan && !this.process && !this.revoked; }
24
+ bind(process, current) { if (this.process || this.revoked)
25
+ throw denied(); this.process = process; this.current = current; }
26
+ active() { try {
27
+ return !this.revoked && !!this.process && this.current?.() === true;
28
+ }
29
+ catch {
30
+ return false;
31
+ } }
32
+ promptSubmitted(process) { if (this.process !== process || this.submitted || !this.active())
33
+ throw denied(); this.submitted = true; }
34
+ discovery = (event, scope) => {
35
+ const token = requireOfficialExecPlan(this.plan).env.AI_GATEWAY_JOB_MCP_TOKEN;
36
+ if (!localDiscoveryScopeMatches(scope, this.plan.endpoint, token)) {
37
+ this.revoke();
38
+ return;
39
+ }
40
+ if (event === 'revoked') {
41
+ this.revoke();
42
+ return;
43
+ }
44
+ if (this.revoked)
45
+ return;
46
+ if (!this.active() || !this.submitted) {
47
+ this.revoke();
48
+ return;
49
+ }
50
+ const expected = ['initialize_response', 'initialized_notification', 'inventory_response'];
51
+ if (this.stage === 3 && event === 'inventory_response')
52
+ return;
53
+ if (event !== expected[this.stage]) {
54
+ this.revoke();
55
+ return;
56
+ }
57
+ this.stage++;
58
+ if (this.stage === 3)
59
+ this.settle?.();
60
+ };
61
+ async wait(process, signal) {
62
+ if (this.process !== process || !this.submitted || !this.active() || signal.aborted)
63
+ throw denied();
64
+ if (this.stage !== 3)
65
+ await new Promise((resolve, reject) => {
66
+ const abort = () => { clear(); reject(denied()); };
67
+ const clear = () => { signal.removeEventListener('abort', abort); this.settle = undefined; this.reject = undefined; };
68
+ this.reject = abort;
69
+ this.settle = () => { clear(); resolve(); };
70
+ signal.addEventListener('abort', abort, { once: true });
71
+ });
72
+ if (!this.active() || signal.aborted || this.stage !== 3)
73
+ throw denied();
74
+ return { process, executionNonce: this.plan.executionNonce, endpoint: this.plan.endpoint, authenticated: true, ready: true, toolSchemaFingerprint: this.plan.identity.toolSchemaFingerprint, tools: localToolInventory().map(t => t.name), isCurrent: () => this.active() && this.stage === 3 };
75
+ }
76
+ authorize(process) { if (process !== this.process || !this.active() || this.stage !== 3)
77
+ throw denied(); this.authorized = true; }
78
+ allowsCall() { return this.authorized && this.submitted && this.stage === 3 && this.active(); }
79
+ revoke() { if (this.revoked)
80
+ return; this.revoked = true; this.authorized = false; this.reject?.(); }
81
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ export const CODEX_GLOBAL_INSTRUCTION_WRAPPER = '/usr/bin/sandbox-exec';
3
+ const reject = () => new Error('CODEX_GLOBAL_INSTRUCTION_BOUNDARY_REJECTED');
4
+ export function accountAncestorPaths(account) {
5
+ if (!path.isAbsolute(account) || path.resolve(account) !== account || account === '/' || account.length > 4096 || /[\r\n\0]/.test(account))
6
+ throw reject();
7
+ const parents = [];
8
+ for (let dir = account;; dir = path.dirname(dir)) {
9
+ parents.push(dir);
10
+ if (parents.length > 64)
11
+ throw reject();
12
+ if (dir === path.dirname(dir))
13
+ break;
14
+ }
15
+ return Object.freeze(parents);
16
+ }
17
+ /** Additional per-process denial, not a grant of authority or global policy edit.
18
+ * file-read* is intentional: data-only denial permits a final symlink alias.
19
+ * Ancestor symlink denials prevent a later account-directory redirect. */
20
+ export function globalInstructionProfile(account) {
21
+ const ancestors = accountAncestorPaths(account), literal = (p) => `(literal ${JSON.stringify(p)})`;
22
+ const profile = '(version 1)(allow default)'
23
+ + '(deny file-read* ' + ['AGENTS.md', 'AGENTS.override.md'].map(name => literal(path.join(account, name))).join(' ') + ')'
24
+ + '(deny file-read* (require-all (vnode-type SYMLINK) (require-any ' + ancestors.map(literal).join(' ') + ')))';
25
+ if (profile.length > 64000)
26
+ throw reject();
27
+ return profile;
28
+ }
29
+ export function requireGlobalInstructionInvocation(nativeBinary, account, invocation) {
30
+ if (invocation.binary !== CODEX_GLOBAL_INSTRUCTION_WRAPPER || invocation.args[0] !== '-p' || invocation.args[1] !== globalInstructionProfile(account) || invocation.args[2] !== nativeBinary)
31
+ throw reject();
32
+ }
@@ -0,0 +1,67 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { assessMdmInventory } from '../../safety/codex-native-observation.js';
3
+ import { securityRequirementsFingerprint as hash } from '../../safety/security-requirements.js';
4
+ import { classifyUnmanagedEnvironment } from './unmanaged-scope.js';
5
+ const denied = () => Error('CODEX_MANUAL_POLICY_REJECTED');
6
+ export const MANUAL_MDM_RISK = 'manual_prelaunch_mdm_snapshot_not_continuously_reobserved';
7
+ /** Trusted in-process controller only, never a task/env/receipt deserializer.
8
+ * Administrator provenance belongs to that controller. Arbitrary JSON is not
9
+ * OS attestation. This module cannot query profiles or create elevated code. */
10
+ export function createManualPolicyScopeVerifier(input) {
11
+ const binding = structuredClone(input.binding), inventory = structuredClone(input.inventory), lease = input.leaseDigest, read = input.readLocal, now = input.now ?? Date.now;
12
+ const identityKeys = ['jobId', 'generation', 'actorId', 'runnerId', 'sourceSha', 'binarySha256', 'releaseManifestFingerprint', 'requirementsFingerprint', 'contractFingerprint', 'taskPackageFingerprint', 'launchFingerprint', 'commandsFingerprint', 'toolSchemaFingerprint', 'barrierFingerprint'];
13
+ if (!isDeepStrictEqual(Object.keys(binding).sort(), identityKeys.sort()) || !Number.isSafeInteger(binding.generation) || binding.generation < 1 || !['jobId', 'actorId', 'runnerId'].every(k => typeof binding[k] === 'string' && binding[k].length > 0 && binding[k].length <= 256 && !/[\r\n\0]/.test(binding[k])) || !/^[a-f0-9]{40}$/.test(binding.sourceSha) || !['binarySha256', 'releaseManifestFingerprint', 'requirementsFingerprint', 'contractFingerprint', 'taskPackageFingerprint', 'launchFingerprint', 'commandsFingerprint', 'toolSchemaFingerprint', 'barrierFingerprint'].every(k => /^[a-f0-9]{64}$/.test(binding[k])) || !/^[a-f0-9]{64}$/.test(lease))
14
+ throw denied();
15
+ let revoked = false, spent = false;
16
+ return Object.freeze({ revoke() { revoked = true; }, observe() {
17
+ if (revoked || spent)
18
+ throw denied();
19
+ const evaluate = (started) => {
20
+ const before = now(), local = read(), time = now();
21
+ if (!Number.isSafeInteger(before) || time < before)
22
+ throw denied();
23
+ if (!Number.isSafeInteger(time) || time < inventory.observedAt)
24
+ throw denied();
25
+ // A started manual run retains its original inventory timestamp. Only the
26
+ // current forced preferences/nonprivileged sources/account are re-read.
27
+ const mdm = assessMdmInventory(inventory, local.forced, started ? inventory.observedAt : time);
28
+ const classified = classifyUnmanagedEnvironment({ version: 'codex-policy-observation-v1', observedAt: time, validUntil: time + 60_000, sources: { ...local.sources, mdm_config: mdm, mdm_requirements: mdm }, account: local.account }, time);
29
+ if (!classified.eligible)
30
+ throw denied();
31
+ return classified;
32
+ };
33
+ let first;
34
+ try {
35
+ first = evaluate(false);
36
+ }
37
+ catch {
38
+ throw denied();
39
+ }
40
+ let dead = false, started = false;
41
+ const current = () => { if (dead || revoked)
42
+ return false; try {
43
+ const c = evaluate(started);
44
+ if (c.sourcesFingerprint !== first.sourcesFingerprint || c.accountFingerprint !== first.accountFingerprint)
45
+ dead = true;
46
+ }
47
+ catch {
48
+ dead = true;
49
+ } return !dead; };
50
+ const verification = Object.freeze({ mode: 'manual_prelaunch', observedAt: inventory.observedAt, inventoryFingerprint: inventory.inventoryFingerprint, continuousMdmMonitoring: false, bindingFingerprint: hash({ ...binding, leaseDigest: lease }) });
51
+ return Object.freeze({ sourcesFingerprint: first.sourcesFingerprint, accountFingerprint: first.accountFingerprint, accountRootFingerprint: first.accountRootFingerprint, observedAt: inventory.observedAt, manualVerification: verification, isCurrent: current,
52
+ beforeSpawn(identity, leaseDigest) {
53
+ if (started || spent || !isDeepStrictEqual(identity, binding) || leaseDigest !== lease || !current()) {
54
+ dead = true;
55
+ throw denied();
56
+ }
57
+ const atSpawn = now();
58
+ if (!Number.isSafeInteger(atSpawn) || atSpawn < inventory.observedAt || atSpawn - inventory.observedAt > 60_000) {
59
+ dead = true;
60
+ throw denied();
61
+ }
62
+ spent = true;
63
+ started = true;
64
+ },
65
+ });
66
+ } });
67
+ }
@@ -25,17 +25,33 @@ export function validateNativeConfig(value, requirements, pin) {
25
25
  reject();
26
26
  if (!Array.isArray(result.layers) || !result.layers.length)
27
27
  reject();
28
- let flags = 0;
28
+ let flags = 0, users = 0;
29
29
  for (const layerValue of result.layers) {
30
30
  const layer = record(layerValue), name = record(layer.name);
31
31
  if (!['sessionFlags', 'user', 'system', 'project', 'mdm'].includes(String(name.type)))
32
32
  reject();
33
33
  if (name.type === 'sessionFlags')
34
34
  flags++;
35
- if (name.type !== 'sessionFlags' && Object.keys(record(layer.config)).length > 0)
35
+ if (name.type === 'user') {
36
+ if (++users > 1)
37
+ reject();
38
+ if (pin.accountSources) {
39
+ if (canonical(record(layer.config)) !== canonical(pin.accountSources.userConfig))
40
+ reject();
41
+ if (Object.keys(pin.accountSources.userConfig).length > 0 && name.file !== pin.accountSources.configPath)
42
+ reject();
43
+ if (name.file !== undefined && name.file !== pin.accountSources.configPath)
44
+ reject();
45
+ if (name.profile !== undefined && name.profile !== null)
46
+ reject();
47
+ }
48
+ else if (Object.keys(record(layer.config)).length > 0)
49
+ reject();
50
+ }
51
+ else if (name.type !== 'sessionFlags' && Object.keys(record(layer.config)).length > 0)
36
52
  reject();
37
53
  }
38
- if (flags !== 1)
54
+ if (flags !== 1 || pin.accountSources && Object.keys(pin.accountSources.userConfig).length > 0 && users !== 1)
39
55
  reject();
40
56
  const req = record(requirements).requirements;
41
57
  if (req !== null) {
@@ -91,7 +107,7 @@ export function validateNativeConfig(value, requirements, pin) {
91
107
  for (const k of Object.keys(server))
92
108
  if (!['url', 'bearer_token_env_var', 'environment_id', 'enabled', 'required', 'tool_timeout_sec', 'default_tools_approval_mode', 'enabled_tools'].includes(k))
93
109
  reject();
94
- return fingerprint({ config: c, requirements: req });
110
+ return fingerprint({ config: c, requirements: req, ...(pin.accountSources ? { accountSources: pin.accountSources.fingerprint } : {}) });
95
111
  }
96
112
  export function validateNativeTools(value, pin) {
97
113
  const response = record(value);
@@ -1,5 +1,5 @@
1
1
  import { issueCodexNativeProof } from './native-proof.js';
2
- import { lstat } from 'node:fs/promises';
2
+ import { inspectCodexAccountSources, codexAccountSourceGuard } from './account-sources.js';
3
3
  import path from 'node:path';
4
4
  import { CodexStdioRpc, record } from './stdio-rpc.js';
5
5
  import { CodexAppServerEvents, CodexEventQueue } from './app-server-events.js';
@@ -11,27 +11,7 @@ import { localToolInventory } from '../../safety/local-tool-server.js';
11
11
  import { CODEX_REPORT_SCHEMA } from './prompt-compiler.js';
12
12
  import { codexControlDenials } from './scratch.js';
13
13
  export async function assertCodexSourceIsolation(home, cwd, codexHome = path.join(home, '.codex')) {
14
- const denied = ['/etc/codex/rules', path.join(codexHome, 'config.toml'), path.join(codexHome, 'rules'), path.join(codexHome, 'AGENTS.md')];
15
- let parent = cwd;
16
- while (true) {
17
- const candidate = path.join(parent, '.codex');
18
- if (path.resolve(candidate) !== path.resolve(codexHome))
19
- denied.push(candidate);
20
- const next = path.dirname(parent);
21
- if (next === parent)
22
- break;
23
- parent = next;
24
- }
25
- for (const entry of denied) {
26
- try {
27
- await lstat(entry);
28
- reject();
29
- }
30
- catch (error) {
31
- if (error.code !== 'ENOENT')
32
- throw error;
33
- }
34
- }
14
+ await inspectCodexAccountSources(home, cwd, codexHome);
35
15
  }
36
16
  /** Dedicated thread; the model is never given this RPC client. */
37
17
  export class CodexNativeSession {
@@ -306,7 +286,8 @@ export async function startNativeCodexSession(options, invocation, prompt, signa
306
286
  const preparationDeadline = Date.now() + 30_000;
307
287
  let supported = options.supportedAdmission ? { policy: options.supportedAdmission.policy, binding: Object.freeze(structuredClone(options.supportedAdmission.binding)) } : undefined;
308
288
  const native = buildNativeInvocation(invocation, options);
309
- const sourceCheck = () => assertCodexSourceIsolation(options.home, options.cwd, native.env.CODEX_HOME || undefined);
289
+ const guard = codexAccountSourceGuard(options.home, options.cwd, native.env.CODEX_HOME || undefined);
290
+ const sourceCheck = async () => { native.pin.accountSources = await guard(); };
310
291
  let earlyTimer;
311
292
  let earlyAbort;
312
293
  let earlyStopped = false;