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
|
|
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
|
-
//
|
|
57
|
-
|
|
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
|
-
|
|
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;
|