mtech-ai-local-runner 0.1.45 → 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 CHANGED
@@ -1,3 +1,14 @@
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
+
1
12
  ## 0.1.45: pin official Codex alpha.11 outside the app
2
13
 
3
14
  Manual and daemon Codex registration now require the reviewed official standalone
@@ -640,3 +651,13 @@ advertises `local_fix_gateway_publish_v1`. Publishing/installation/DEV activatio
640
651
  and an exact approved Fix contract/launch are still required before real use.
641
652
  An unresolved canonical PR approval or publication receipt pauses delivery; do
642
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
- authenticated = !codex || /^Logged in using ChatGPT\s*$/i.test([auth.stdout, auth.stderr ?? ''].join('\n').trim());
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, authenticated: probe.available && probe.authenticated })),
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.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
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.available && current.authenticated && !!current.executableVersion && codex.capabilities.every(c => current.capabilities.includes(c));
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, authenticated: ready };
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 authenticated = probe.available && probe.authenticated;
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, authenticated, capabilities };
236
+ return { adapter: probe.id, version: probe.version, ...status, capabilities };
235
237
  });
236
238
  }
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {