mtech-ai-local-runner 0.1.15 → 0.1.16

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
@@ -10,6 +10,21 @@ The package stores safe runner configuration locally and keeps OAuth refresh cre
10
10
 
11
11
  Public npm installation requires no npm account or publishing OTP. Setup requires Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
12
12
 
13
+ ## 0.1.16: bounded startup and MCP failure observations
14
+
15
+ Failed Codex preparation now preserves fixed native error markers and separates
16
+ local MCP request arrival, rejected credentials/Origin/Host, and authenticated
17
+ initialize receipt. Raw stderr, tokens, request bodies, paths and URLs are not
18
+ included. Observations do not authorize tools, attest native policy, or prove a
19
+ root cause. A missing successful initialize response does not prove that the
20
+ client sent no HTTP request. Existing trusted failure stages/codes remain intact,
21
+ including when new observations are added to an already classified gate error.
22
+
23
+ This patch fixes diagnostic evidence loss. It does not establish successful
24
+ real-account MCP connectivity, change permissions/deadlines/task limits, update
25
+ the daemon, or authorize another actual model attempt. Publication and verified
26
+ foreground installation are separate from a subsequently approved execution.
27
+
13
28
  ## 0.1.15: startup output before MCP readiness
14
29
 
15
30
  The process host drains stdout from spawn, before the adapter starts consuming
@@ -1,3 +1,5 @@
1
+ import { localEndpointObservations } from '../../safety/local-tool-server.js';
2
+ import { processStartupObservations } from '../../process/node-process-group-host.js';
1
3
  import { executionError } from '../../safety/execution-failure.js';
2
4
  import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
3
5
  import { fingerprint as taskFingerprint } from '../../daemon/execution-coordinator.js';
@@ -65,7 +67,8 @@ export class CodexExecLaunchHost {
65
67
  let stage = 'codex_context';
66
68
  let interruption;
67
69
  const mcpDetail = () => stage === 'codex_mcp' ? this.deps.execTools?.discoveryDetail() : undefined;
68
- const interrupt = (code) => { interruption ??= executionError(fail(), stage, code, mcpDetail()); };
70
+ const observations = () => { const invocation = requireOfficialExecPlan(this.plan); return [...(this.handle ? processStartupObservations(this.handle) : []), ...localEndpointObservations(this.plan.endpoint, invocation.env.AI_GATEWAY_JOB_MCP_TOKEN)]; };
71
+ const interrupt = (code) => { interruption ??= executionError(fail(), stage, code, mcpDetail(), observations()); };
69
72
  let timer, external;
70
73
  try {
71
74
  this.assertContext(ctx, jobId);
@@ -158,7 +161,7 @@ export class CodexExecLaunchHost {
158
161
  return await Promise.race([preparation, stopped]);
159
162
  }
160
163
  catch (error) {
161
- const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail());
164
+ const safe = executionError(interruption ?? error, stage, 'CODEX_EXEC_PREFLIGHT_REJECTED', mcpDetail(), observations());
162
165
  await this.cancel().catch(() => { });
163
166
  throw safe;
164
167
  }
@@ -1,3 +1,6 @@
1
+ const startup = new WeakMap();
2
+ /** Fixed text observations from this owned host only. Never raw stderr or authority. */
3
+ export function processStartupObservations(handle) { return [...(startup.get(handle) ?? [])]; }
1
4
  import { spawn } from "node:child_process";
2
5
  /** Each invocation owns one new process group; no name-based process sweeping. */
3
6
  export class NodeProcessGroupHost {
@@ -53,8 +56,42 @@ export class NodeProcessGroupHost {
53
56
  terminated(null, null);
54
57
  notify();
55
58
  });
56
- // Provider stderr can contain account details. Consume without publishing it.
57
- child.stderr.resume();
59
+ // Keep fixed observations and fixed marker prefixes only: no raw stderr,
60
+ // account text, paths or URLs survive a data callback, even for split chunks.
61
+ const observations = new Set();
62
+ const markers = [
63
+ { name: 'native_stdin_read', text: 'Reading prompt from stdin...', prefix: '' },
64
+ { name: 'native_model_catalog_failed', text: 'codex_models_manager::manager: failed to refresh available models:', prefix: '' },
65
+ { name: 'native_cloud_policy_failed', text: 'Failed to fetch cloud config bundle', prefix: '' },
66
+ { name: 'native_cloud_policy_failed', text: 'Failed to load cloud config bundle', prefix: '' },
67
+ { name: 'native_cloud_policy_timeout', text: 'Timed out waiting for cloud config bundle after ', prefix: '' },
68
+ { name: 'native_cloud_policy_timeout', text: 'timed out waiting for cloud config bundle after ', prefix: '' },
69
+ { name: 'native_required_mcp_failed', text: 'required MCP servers failed to initialize: ai_local:', prefix: '' },
70
+ { name: 'native_config_failed', text: 'Error loading config:', prefix: '' },
71
+ { name: 'native_auth_refresh_unsupported', text: 'chatgpt auth token refresh is not supported in exec mode', prefix: '' },
72
+ ];
73
+ child.stderr.on('data', (chunk) => {
74
+ const data = chunk.toString('utf8');
75
+ for (const marker of markers) {
76
+ if (observations.has(marker.name))
77
+ continue;
78
+ const text = marker.prefix + data;
79
+ marker.prefix = '';
80
+ if (text.includes(marker.text)) {
81
+ observations.add(marker.name);
82
+ continue;
83
+ }
84
+ for (let n = Math.min(text.length, marker.text.length - 1); n > 0; n--) {
85
+ const fixed = marker.text.slice(0, n);
86
+ if (text.endsWith(fixed)) {
87
+ marker.prefix = fixed;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ });
93
+ child.stderr.once('end', () => { for (const marker of markers)
94
+ marker.prefix = ''; });
58
95
  child.once("exit", (code, signal) => {
59
96
  try {
60
97
  kill();
@@ -106,7 +143,7 @@ export class NodeProcessGroupHost {
106
143
  notify();
107
144
  }
108
145
  })();
109
- return {
146
+ const handle = {
110
147
  stdin: { write: (text) => { child.stdin.write(text); }, end: () => { child.stdin.end(); } },
111
148
  stdout: { async *[Symbol.asyncIterator]() {
112
149
  if (consumed)
@@ -141,5 +178,7 @@ export class NodeProcessGroupHost {
141
178
  else
142
179
  exitCallbacks.push(callback); },
143
180
  };
181
+ startup.set(handle, observations);
182
+ return handle;
144
183
  }
145
184
  }
@@ -1,10 +1,12 @@
1
1
  import { GatewayToolError } from '../gateway-client.js';
2
+ /** Observations only: provider text cannot authorize a tool or certify a cause. */
3
+ export const STARTUP_OBSERVATIONS = Object.freeze(['native_stdin_read', 'native_model_catalog_failed', 'native_cloud_policy_failed', 'native_cloud_policy_timeout', 'native_required_mcp_failed', 'native_config_failed', 'native_auth_refresh_unsupported', 'local_mcp_request_received', 'local_mcp_auth_rejected', 'local_mcp_origin_or_host_rejected', 'local_mcp_initialize_received']);
2
4
  const issued = new WeakMap();
3
5
  export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
4
6
  if (error && typeof error === 'object') {
5
7
  const trusted = issued.get(error);
6
8
  if (trusted)
7
- return { ...trusted };
9
+ return { ...trusted, ...(trusted.observations ? { observations: [...trusted.observations] } : {}) };
8
10
  try {
9
11
  if (error instanceof GatewayToolError && (error.code === 'GATEWAY_TOOL_REJECTED' || error.code === 'GATEWAY_TRANSPORT_ERROR' || error.code === 'WORK_LOOP_CANCELLED')) {
10
12
  return { stage, code: error.code };
@@ -24,12 +26,17 @@ export function executionFailure(error, stage, fallback = 'EXECUTION_FAILED') {
24
26
  }
25
27
  return { stage, code: fallback };
26
28
  }
27
- export function executionError(error, stage, fallback, detail) {
29
+ export function executionError(error, stage, fallback, detail, observations) {
28
30
  const diagnostic = executionFailure(error, stage, fallback);
29
31
  // Only trusted callers supply this fixed vocabulary. Serialized provider
30
32
  // errors, messages and stderr never become diagnostic authority.
31
33
  if (!issued.has(error) && ['mcp_initialize_missing', 'mcp_initialized_missing', 'mcp_inventory_missing', 'mcp_discovery_complete'].includes(detail))
32
34
  diagnostic.detail = detail;
35
+ if (Array.isArray(observations)) {
36
+ const fixed = STARTUP_OBSERVATIONS.filter(value => diagnostic.observations?.includes(value) || observations.includes(value));
37
+ if (fixed.length)
38
+ diagnostic.observations = fixed;
39
+ }
33
40
  const safe = new Error(fallback);
34
41
  issued.set(safe, Object.freeze(diagnostic));
35
42
  return safe;
@@ -8,6 +8,12 @@ export function localDiscoveryScopeMatches(scope, endpoint, token) {
8
8
  const data = discoveryScopes.get(scope);
9
9
  return !!data && data.endpoint === endpoint && data.credentialHash === createHash('sha256').update(token).digest('hex');
10
10
  }
11
+ // Bounded diagnostic metadata; possession grants neither discovery nor tool authority.
12
+ const endpointObservations = new Map();
13
+ export function localEndpointObservations(url, token) {
14
+ const record = endpointObservations.get(url);
15
+ return record?.credentialHash === createHash('sha256').update(token).digest('hex') ? [...record.values] : [];
16
+ }
11
17
  const MAX_BODY = 1024 * 1024;
12
18
  const schemas = {
13
19
  local_read: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false },
@@ -22,6 +28,7 @@ export function localToolInventory() {
22
28
  /** Job-only JSON MCP endpoint; intentionally has no Gateway transport or discovery. */
23
29
  export class LocalToolServer {
24
30
  options;
31
+ observations = new Set();
25
32
  server = null;
26
33
  revoked = false;
27
34
  starting = null;
@@ -57,6 +64,7 @@ export class LocalToolServer {
57
64
  throw new Error('Local tool server already started');
58
65
  this.token = randomBytes(32).toString('hex');
59
66
  this.server = createServer(async (req, res) => {
67
+ this.observations.add('local_mcp_request_received');
60
68
  const reply = (status, body) => {
61
69
  if (res.writableEnded)
62
70
  return;
@@ -72,11 +80,13 @@ export class LocalToolServer {
72
80
  return;
73
81
  }
74
82
  if (req.headers.origin !== undefined || req.headers.host !== new URL(this.url).host) {
83
+ this.observations.add('local_mcp_origin_or_host_rejected');
75
84
  reply(403, { error: 'Forbidden' });
76
85
  return;
77
86
  }
78
87
  const token = this.token, expected = Buffer.from(token ? `Bearer ${token}` : ''), supplied = Buffer.from(req.headers.authorization ?? '');
79
88
  if (!token || expected.length !== supplied.length || !timingSafeEqual(expected, supplied)) {
89
+ this.observations.add('local_mcp_auth_rejected');
80
90
  reply(401, { error: 'Unauthorized' });
81
91
  return;
82
92
  }
@@ -128,6 +138,7 @@ export class LocalToolServer {
128
138
  }
129
139
  const send = (result) => reply(200, { jsonrpc: '2.0', id: rpc.id, result });
130
140
  if (rpc.method === 'initialize') {
141
+ this.observations.add('local_mcp_initialize_received');
131
142
  res.once('finish', () => { if (!this.revoked)
132
143
  this.observe('initialize_response'); });
133
144
  send({ protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'runner-local-tools', version: '1' } });
@@ -190,10 +201,13 @@ export class LocalToolServer {
190
201
  if (!address || typeof address === 'string')
191
202
  throw new Error('Failed to bind local tool server');
192
203
  this.url = `http://127.0.0.1:${address.port}/mcp`;
204
+ endpointObservations.set(this.url, { credentialHash: createHash('sha256').update(this.token).digest('hex'), values: this.observations });
193
205
  discoveryScopes.set(this.discoveryScope, { endpoint: this.url, credentialHash: createHash('sha256').update(this.token).digest('hex') });
194
206
  return { url: this.url, token: this.token };
195
207
  }
196
208
  async close() {
209
+ if (this.url)
210
+ endpointObservations.delete(this.url);
197
211
  this.revoke();
198
212
  const server = this.server;
199
213
  this.server = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {