mtech-ai-local-runner 0.1.44 → 0.1.46
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 +46 -0
- package/dist/src/adapters/claude-code/adapter.js +3 -3
- package/dist/src/adapters/discovery.js +39 -7
- package/dist/src/adapters/status.js +9 -0
- package/dist/src/cli.js +2 -1
- package/dist/src/daemon/runtime.js +7 -5
- package/dist/src/platform/macos/launchd.js +4 -1
- package/dist/src/safety/codex-daemon-host.js +2 -2
- package/dist/src/safety/codex-daemon-releases.js +7 -7
- package/dist/src/safety/codex-exec-production-host.js +4 -1
- package/dist/src/safety/codex-manual-registered.js +5 -5
- package/dist/src/safety/codex-reviewed-manual-release.js +7 -7
- package/dist/src/safety/codex-standalone-binary.js +71 -0
- package/dist/src/safety/registered-command-integrity.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
## 0.1.46: login and execution are separate
|
|
2
|
+
|
|
3
|
+
A locally logged-in account may still be blocked by security admission. New
|
|
4
|
+
`login_status`, `execution_status` and `execution_reason_codes` show those facts
|
|
5
|
+
separately; the legacy `authenticated` field remains an eligibility conjunction.
|
|
6
|
+
Malformed/unreadable Claude auth output is unknown, not a request to log in again.
|
|
7
|
+
Claude also rechecks task source identity after MCP preparation before spawning.
|
|
8
|
+
This release does not enable Claude execution, update an installed daemon or
|
|
9
|
+
change existing account settings. See the Gateway repository's
|
|
10
|
+
`docs/ai-ops-gateway/claude-execution-admission-remaining.md` for remaining gates.
|
|
11
|
+
|
|
12
|
+
## 0.1.45: pin official Codex alpha.11 outside the app
|
|
13
|
+
|
|
14
|
+
Manual and daemon Codex registration now require the reviewed official standalone
|
|
15
|
+
`codex-cli 0.163.0-alpha.11` (SHA256
|
|
16
|
+
`80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466`) at
|
|
17
|
+
`<employee-home>/.local/share/mtec-ai-local-runner/codex/0.163.0-alpha.11/<sha256>/codex`.
|
|
18
|
+
There is no ChatGPT app, PATH or environment fallback. A missing, changed,
|
|
19
|
+
symlinked, foreign-owned or group/world-writable installation blocks Codex.
|
|
20
|
+
Existing login/settings, the Claude path, policy observer and execution limits
|
|
21
|
+
are unchanged. This package does not install the standalone automatically;
|
|
22
|
+
provisioning, exact Gateway runtime pins and daemon transition need their own
|
|
23
|
+
reviewed delivery plan. A package update alone is not a successful transition.
|
|
24
|
+
|
|
25
|
+
Executable integrity checks still hash every byte on every call, with a bounded
|
|
26
|
+
1 MiB read buffer to reduce repeated filesystem dispatch. Tests separate
|
|
27
|
+
independent scenarios and distinguish completion output from actual target exit.
|
|
28
|
+
No timeout was expanded. The historical MCP startup timeout is not declared fixed.
|
|
29
|
+
|
|
30
|
+
### Interactive LaunchAgent
|
|
31
|
+
|
|
32
|
+
`setup` writes the runner LaunchAgent with `ProcessType` `Interactive`. The `Background` class kept the daemon, Codex and
|
|
33
|
+
every verification command at priority 4 on the efficiency cores, so P1 verification took 12.5 minutes on a busy Mac;
|
|
34
|
+
`Standard` was barely faster. An installed agent changes class only when its plist is rewritten and reloaded;
|
|
35
|
+
`launchctl print gui/$(id -u)/com.mtec.ai-local-runner` should then show `spawn type = interactive (4)`.
|
|
36
|
+
|
|
1
37
|
## 0.1.44: stop repeating whole-checkout checks in P1 verification
|
|
2
38
|
|
|
3
39
|
The P1 verifier rereads the whole model checkout for every source check and every candidate check. On the AI
|
|
@@ -615,3 +651,13 @@ advertises `local_fix_gateway_publish_v1`. Publishing/installation/DEV activatio
|
|
|
615
651
|
and an exact approved Fix contract/launch are still required before real use.
|
|
616
652
|
An unresolved canonical PR approval or publication receipt pauses delivery; do
|
|
617
653
|
not rerun the model or delete recovery state to force a PR.
|
|
654
|
+
|
|
655
|
+
### Login and execution diagnostics
|
|
656
|
+
|
|
657
|
+
`login_status` reports local account evidence (`authenticated`, `unauthenticated`,
|
|
658
|
+
`unknown`); it does not certify remote token validity. `execution_status` reports
|
|
659
|
+
`ready` or `blocked`, with bounded `execution_reason_codes`. For compatibility,
|
|
660
|
+
the older `authenticated` field still means execution eligibility. A logged-in
|
|
661
|
+
Claude account can remain blocked by unverified security admission. Do not log out
|
|
662
|
+
or change credentials just because that legacy field is false. Unknown status is
|
|
663
|
+
not evidence of logout. These diagnostics do not bypass per-job admission.
|
|
@@ -78,6 +78,9 @@ export class ClaudeCodeAdapter {
|
|
|
78
78
|
if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
|
|
79
79
|
throw new Error("JOB_SECURITY_REJECTED");
|
|
80
80
|
const assertAdmission = () => {
|
|
81
|
+
const repositories = ctx.taskPackage?.repositories;
|
|
82
|
+
if (!Array.isArray(repositories) || repositories.length !== 1 || repositories[0]?.base_commit_sha !== this.options.securityPolicy?.pin.sourceSha)
|
|
83
|
+
throw new Error('JOB_SECURITY_REJECTED');
|
|
81
84
|
requireJobSecurity(this.options.securityPolicy, { adapterId: "claude_code_cli", adapterVersion: "1", jobId: this.options.jobId ?? ctx.bindingId, modelId: resolvedModelId, generation });
|
|
82
85
|
if (ctx.approvedTaskPackage?.resolved_provider !== "anthropic" || ctx.approvedTaskPackage?.resolved_model_id !== resolvedModelId
|
|
83
86
|
|| ctx.approvedTaskPackage?.repository?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
|
|
@@ -89,9 +92,6 @@ export class ClaudeCodeAdapter {
|
|
|
89
92
|
throw new Error('SECURITY_REQUIREMENTS_REJECTED');
|
|
90
93
|
this.receipt = createJobSecurityReceipt(this.options.securityPolicy, { jobId: this.options.jobId ?? ctx.bindingId, generation, modelId: resolvedModelId, sourceSha: this.options.securityPolicy.pin.sourceSha, ...ctx.securityBinding, requirements: ctx.approvedTaskPackage?.security_requirements });
|
|
91
94
|
const pkg = ctx.taskPackage;
|
|
92
|
-
const repos = pkg.repositories;
|
|
93
|
-
if (!Array.isArray(repos) || repos.length !== 1 || repos[0]?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
|
|
94
|
-
throw new Error("JOB_SECURITY_REJECTED");
|
|
95
95
|
const cwd = this.options.cwd;
|
|
96
96
|
const rel = cwd ? path.relative(path.resolve(this.options.worktreeRoot), path.resolve(cwd)) : "";
|
|
97
97
|
if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adapterStatus } from './status.js';
|
|
1
2
|
import { createCodexProductionHost } from '../safety/codex-production-host.js';
|
|
2
3
|
import { execFile } from 'node:child_process';
|
|
3
4
|
import { promisify } from 'node:util';
|
|
@@ -20,22 +21,51 @@ export async function discoverAdapters(env, command = run, adapterIds = ['claude
|
|
|
20
21
|
if (!executableVersion)
|
|
21
22
|
throw new Error('invalid version');
|
|
22
23
|
let authenticated = false;
|
|
24
|
+
let loginStatus = 'unknown';
|
|
25
|
+
const parseClaudeStatus = (stdout) => {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(stdout);
|
|
28
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.loggedIn === 'boolean') {
|
|
29
|
+
authenticated = value.loggedIn;
|
|
30
|
+
loginStatus = authenticated ? 'authenticated' : 'unauthenticated';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* Unknown evidence stays closed; never expose raw output. */ }
|
|
34
|
+
};
|
|
23
35
|
try {
|
|
24
|
-
const auth = await command(binary, codex ? ['login', 'status'] : ['auth', 'status']);
|
|
25
|
-
|
|
36
|
+
const auth = await command(binary, codex ? ['login', 'status'] : ['auth', 'status', '--json']);
|
|
37
|
+
if (codex) {
|
|
38
|
+
authenticated = /^Logged in using ChatGPT\s*$/i.test([auth.stdout, auth.stderr ?? ''].join('\n').trim());
|
|
39
|
+
loginStatus = authenticated ? 'authenticated' : 'unauthenticated';
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
parseClaudeStatus(auth.stdout);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Claude exits 1 for a known logged-out JSON response. Other failures
|
|
46
|
+
// (including timeout/permission errors) do not prove logout.
|
|
47
|
+
if (!codex && error && typeof error === 'object' && error.code === 1) {
|
|
48
|
+
const stdout = error.stdout;
|
|
49
|
+
if (typeof stdout === 'string') {
|
|
50
|
+
parseClaudeStatus(stdout);
|
|
51
|
+
if (authenticated) {
|
|
52
|
+
authenticated = false;
|
|
53
|
+
loginStatus = 'unknown';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
26
57
|
}
|
|
27
|
-
catch { /* Missing login is diagnostic, never an API-key fallback. */ }
|
|
28
58
|
if (codex) {
|
|
29
59
|
const compatibility = inspectCodexCompatibility({ cliVersion: executableVersion });
|
|
30
60
|
const admission = await diagnoseCliHostAdmission(id, binary, env);
|
|
31
|
-
return { ...base, available: compatibility.available, authenticated, executableVersion, capabilities: compatibility.capabilities, admission,
|
|
61
|
+
return { ...base, available: compatibility.available, authenticated, loginStatus, executableVersion, capabilities: compatibility.capabilities, admission,
|
|
32
62
|
reason: !authenticated ? 'Codex requires a ChatGPT account login; API-key authentication is unsupported.' : compatibility.reason };
|
|
33
63
|
}
|
|
34
64
|
const admission = await diagnoseCliHostAdmission(id, binary, env);
|
|
35
|
-
return { ...base, available: admission.available, authenticated, executableVersion, capabilities: admission.capabilities, admission, reason: !authenticated ? 'Claude CLI login is required.' : 'Claude CLI security isolation is unverified; execution is unsupported.' };
|
|
65
|
+
return { ...base, available: admission.available, authenticated, loginStatus, executableVersion, capabilities: admission.capabilities, admission, reason: loginStatus === 'unknown' ? 'Claude CLI login status could not be verified.' : !authenticated ? 'Claude CLI login is required.' : 'Claude CLI security isolation is unverified; execution is unsupported.' };
|
|
36
66
|
}
|
|
37
67
|
catch {
|
|
38
|
-
return { ...base, available: false, authenticated: false, capabilities: [], reason: codex ? 'Codex CLI is missing or its version could not be verified.' : 'Claude CLI is missing or its version could not be verified.' };
|
|
68
|
+
return { ...base, available: false, authenticated: false, loginStatus: 'unknown', capabilities: [], reason: codex ? 'Codex CLI is missing or its version could not be verified.' : 'Claude CLI is missing or its version could not be verified.' };
|
|
39
69
|
}
|
|
40
70
|
}));
|
|
41
71
|
}
|
|
@@ -56,6 +86,8 @@ export function adapterDiagnosticsWithRemedies(adapters) {
|
|
|
56
86
|
else
|
|
57
87
|
action = 'Codex is ready for an explicitly selected approved launch.';
|
|
58
88
|
}
|
|
89
|
+
else if (adapter.loginStatus === 'unknown')
|
|
90
|
+
action = 'Claude login status is unknown; inspect the local status diagnostic before changing credentials. Execution remains blocked.';
|
|
59
91
|
else if (!adapter.available && adapter.executableVersion && adapter.authenticated)
|
|
60
92
|
action = 'Claude CLI security isolation is unverified; wait for a reviewed release. Login alone does not grant execution.';
|
|
61
93
|
else if (!adapter.available)
|
|
@@ -64,7 +96,7 @@ export function adapterDiagnosticsWithRemedies(adapters) {
|
|
|
64
96
|
action = 'Run claude auth login, then rerun doctor.';
|
|
65
97
|
else
|
|
66
98
|
action = 'Claude is ready for an explicitly selected approved launch.';
|
|
67
|
-
return { ...adapter, action };
|
|
99
|
+
return { ...adapter, ...adapterStatus(adapter), action };
|
|
68
100
|
});
|
|
69
101
|
}
|
|
70
102
|
/** Setup/doctor use the same reviewed Codex probe as daemon; no legacy vendor
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Diagnostics never grant capabilities or replace the host admission gate. */
|
|
2
|
+
export function adapterStatus(probe) {
|
|
3
|
+
const login_status = probe.loginStatus ?? (probe.authenticated ? 'authenticated' : 'unknown');
|
|
4
|
+
const ready = probe.available && probe.authenticated && login_status === 'authenticated';
|
|
5
|
+
const codes = probe.admission?.checks.filter(c => c.status === 'blocked').map(c => c.code).filter(c => /^[A-Z][A-Z0-9_]{0,95}$/.test(c)).slice(0, 20) ?? [];
|
|
6
|
+
if (!ready && !codes.length)
|
|
7
|
+
codes.push(!probe.available ? 'EXECUTION_ADMISSION_UNAVAILABLE' : login_status === 'unknown' ? 'LOGIN_STATUS_UNVERIFIED' : 'LOGIN_REQUIRED');
|
|
8
|
+
return { authenticated: ready, login_status, execution_status: ready ? 'ready' : 'blocked', execution_reason_codes: ready ? [] : [...new Set(codes)] };
|
|
9
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { adapterStatus } from './adapters/status.js';
|
|
2
3
|
import { recoverLocalFixPublication } from './publication/recovery.js';
|
|
3
4
|
import { loadInstalledLocalFixHost } from './publication/installed-host.js';
|
|
4
5
|
import { FileDaemonStateStore } from './state-store.js';
|
|
@@ -194,7 +195,7 @@ export async function main(argv, env, io = {}) {
|
|
|
194
195
|
const registered = await gateway.registerLocalRunner({
|
|
195
196
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
|
|
196
197
|
displayName: runnerDisplayName(), packageVersion: "0.1.11",
|
|
197
|
-
capabilities: adapters.map(probe => ({ adapter: probe.id, version: probe.version, capabilities: probe.capabilities,
|
|
198
|
+
capabilities: adapters.map(probe => ({ adapter: probe.id, version: probe.version, capabilities: probe.capabilities, ...adapterStatus(probe) })),
|
|
198
199
|
});
|
|
199
200
|
await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
|
|
200
201
|
await launchd.install(process.argv[1], config.stateRoot);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adapterStatus } from '../adapters/status.js';
|
|
1
2
|
import { loadInstalledLocalFixHost } from '../publication/installed-host.js';
|
|
2
3
|
import { createCodexDaemonHost } from '../safety/codex-daemon-host.js';
|
|
3
4
|
import { reconcileTerminalState } from './terminal-recovery.js';
|
|
@@ -64,7 +65,7 @@ export async function buildDaemon(input) {
|
|
|
64
65
|
});
|
|
65
66
|
const registry = new AdapterRegistry([plugin, codex]);
|
|
66
67
|
const probes = await registry.probeAll();
|
|
67
|
-
const versions = Object.fromEntries(probes.filter((probe) => probe.
|
|
68
|
+
const versions = Object.fromEntries(probes.filter((probe) => adapterStatus(probe).authenticated).map((probe) => [probe.id, probe.version]));
|
|
68
69
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
69
70
|
const coordinator = new ExecutionCoordinator({
|
|
70
71
|
localFixHost,
|
|
@@ -115,10 +116,10 @@ export async function buildDaemon(input) {
|
|
|
115
116
|
catch {
|
|
116
117
|
current = { available: false, authenticated: false, capabilities: [] };
|
|
117
118
|
}
|
|
118
|
-
const ready = current.
|
|
119
|
+
const ready = adapterStatus(current).authenticated && !!current.executableVersion && codex.capabilities.every(c => current.capabilities.includes(c));
|
|
119
120
|
const index = probes.findIndex(p => p.id === 'codex_cli');
|
|
120
121
|
if (index >= 0)
|
|
121
|
-
probes[index] = { id: 'codex_cli', version: '1', ...current,
|
|
122
|
+
probes[index] = { id: 'codex_cli', version: '1', ...current, available: ready };
|
|
122
123
|
if (ready)
|
|
123
124
|
versions.codex_cli = '1';
|
|
124
125
|
else
|
|
@@ -222,7 +223,8 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
222
223
|
/** Vendor capability text cannot claim a host-owned publication facility. */
|
|
223
224
|
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode) {
|
|
224
225
|
return probes.map(probe => {
|
|
225
|
-
const
|
|
226
|
+
const status = adapterStatus(probe);
|
|
227
|
+
const authenticated = status.authenticated;
|
|
226
228
|
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1' && value !== 'local_fix_gateway_publish_v1');
|
|
227
229
|
if (trustedLocalFixHost && authenticated && probe.id === 'codex_cli') {
|
|
228
230
|
capabilities.push('local_fix_publication_v1');
|
|
@@ -231,6 +233,6 @@ export function localRunnerCapabilities(probes, trustedLocalFixHost = false, ver
|
|
|
231
233
|
if (verificationMode === 'gateway_publish_v1')
|
|
232
234
|
capabilities.push('local_fix_gateway_publish_v1');
|
|
233
235
|
}
|
|
234
|
-
return { adapter: probe.id, version: probe.version,
|
|
236
|
+
return { adapter: probe.id, version: probe.version, ...status, capabilities };
|
|
235
237
|
});
|
|
236
238
|
}
|
|
@@ -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>
|
|
40
|
+
<key>ProcessType</key><string>Interactive</string>
|
|
38
41
|
</dict></plist>
|
|
39
42
|
`;
|
|
40
43
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CODEX_DAEMON_EXEC_RELEASES } from './codex-daemon-releases.js';
|
|
2
|
-
import { registeredPrelaunchDependencies
|
|
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
|
|
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": "
|
|
11
|
+
"binarySha256": "80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466",
|
|
12
12
|
"modelIds": [
|
|
13
13
|
"gpt-5.6-sol"
|
|
14
14
|
],
|
|
15
|
-
"decisionId": "
|
|
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": "
|
|
30
|
-
"official_permissions": "
|
|
29
|
+
"binary_release": "00bfa3a8438d10787c6b18f6d56fde9dc37b1f4ae64c13ac723a5e472e54498a",
|
|
30
|
+
"official_permissions": "ae0906f5302344f41ae06aee053764ba98511fbd5072af81b5ec857a2d6c42c0",
|
|
31
31
|
"managed_scope_exclusion": "a559199e229e153759f7a7cbd682aae423c0e3d33ef2a692fcccb5eed7c57ed3",
|
|
32
|
-
"required_mcp_startup": "
|
|
33
|
-
"source_exclusion": "
|
|
32
|
+
"required_mcp_startup": "d2f482b5dd5e74684d7c77652b803314b1555354970d67b795dbefcdddcf7bc2",
|
|
33
|
+
"source_exclusion": "c44cf5ca899e42349c17ea37524984eb8aa6ffe505494ff6a253b159a0cada64",
|
|
34
34
|
"auth_backend_compatibility": "7c42b882a52698fef3e14b4b956ede91ed1e3b29511fbff3176387063b8d74f0"
|
|
35
35
|
},
|
|
36
|
-
"fixturesFingerprint": "
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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": "
|
|
10
|
+
"binarySha256": "80df47f6ca9d316c732014bc5f6e6514696fafb5593e08e064332a1d3718b466",
|
|
11
11
|
"modelIds": [
|
|
12
12
|
"gpt-5.6-sol"
|
|
13
13
|
],
|
|
14
|
-
"decisionId": "
|
|
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": "
|
|
29
|
-
"official_permissions": "
|
|
28
|
+
"binary_release": "00bfa3a8438d10787c6b18f6d56fde9dc37b1f4ae64c13ac723a5e472e54498a",
|
|
29
|
+
"official_permissions": "ae0906f5302344f41ae06aee053764ba98511fbd5072af81b5ec857a2d6c42c0",
|
|
30
30
|
"managed_scope_exclusion": "3bb8b2ad50a44cdea5a2a3f388f8009b51d8c60d46230fb3d0b7fed0b9f7452d",
|
|
31
|
-
"required_mcp_startup": "
|
|
32
|
-
"source_exclusion": "
|
|
31
|
+
"required_mcp_startup": "d2f482b5dd5e74684d7c77652b803314b1555354970d67b795dbefcdddcf7bc2",
|
|
32
|
+
"source_exclusion": "c44cf5ca899e42349c17ea37524984eb8aa6ffe505494ff6a253b159a0cada64",
|
|
33
33
|
"auth_backend_compatibility": "7c42b882a52698fef3e14b4b956ede91ed1e3b29511fbff3176387063b8d74f0"
|
|
34
34
|
},
|
|
35
|
-
"fixturesFingerprint": "
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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();
|