mtech-ai-local-runner 0.1.29 → 0.1.30
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 +23 -2
- package/dist/src/adapters/claude-code/adapter.js +13 -0
- package/dist/src/adapters/codex/adapter.js +1 -0
- package/dist/src/adapters/codex/exec-launch-host.js +15 -1
- package/dist/src/daemon/execution-coordinator.js +13 -1
- package/dist/src/safety/local-read-observations.js +17 -0
- package/dist/src/safety/local-tool-server.js +36 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
## 0.1.
|
|
1
|
+
## 0.1.30: governed local alert investigation evidence
|
|
2
|
+
|
|
3
|
+
The Gateway candidate offers explicit Local Codex Runner or Local Claude Runner
|
|
4
|
+
selection for read-only Sentry investigation. It binds the chosen adapter/model
|
|
5
|
+
and employee device to the exact approved launch; it never falls back to another
|
|
6
|
+
adapter. Each adapter retains authenticated host-owned bounded read observations
|
|
7
|
+
through completion and cleanup. These counts are employee-runner evidence, not
|
|
8
|
+
independent root-cause verification.
|
|
9
|
+
|
|
10
|
+
This release does not remove Claude's current unsupported host-admission gate.
|
|
11
|
+
An installed or authenticated CLI alone is not execution permission. Gateway
|
|
12
|
+
deployment, feature activation and actual execution approvals remain separate.
|
|
13
|
+
|
|
14
|
+
## 0.1.29: reviewed daemon Codex admission
|
|
2
15
|
|
|
3
16
|
This candidate connects the installed, hash-pinned root policy observer to the
|
|
4
17
|
existing employee-privileged Codex CLI adapter. The observer accepts only its
|
|
@@ -35,7 +48,15 @@ npx mtech-ai-local-runner setup
|
|
|
35
48
|
|
|
36
49
|
The package stores safe runner configuration locally and keeps OAuth refresh credentials in macOS Keychain when available.
|
|
37
50
|
|
|
38
|
-
Public npm installation requires no npm account or publishing OTP. Setup requires
|
|
51
|
+
Public npm installation requires no npm account or publishing OTP. Setup requires
|
|
52
|
+
the employee's own Gateway OAuth login and a supported, authenticated selected
|
|
53
|
+
CLI. Codex uses the employee's ChatGPT account. Claude requires its own login
|
|
54
|
+
and a separately verified host admission; it is currently not admitted.
|
|
55
|
+
|
|
56
|
+
For the current unattended Codex pilot, an administrator must separately install
|
|
57
|
+
the reviewed, employee-UID-bound policy observer on that Mac. The npm setup
|
|
58
|
+
command does not install that privileged service. A fresh employee computer is
|
|
59
|
+
not ready merely because npm installation succeeds.
|
|
39
60
|
|
|
40
61
|
## 0.1.28: completion receipt wire compatibility
|
|
41
62
|
|
|
@@ -8,6 +8,7 @@ import { completionEvent } from "./completion-report.js";
|
|
|
8
8
|
import { requireJobSecurity } from "../../safety/job-security-policy.js";
|
|
9
9
|
import { HOOK_POLICY_ENV_VAR, buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
|
|
10
10
|
import { formatHookCommand } from "../../safety/hook-command.js";
|
|
11
|
+
import { localEndpointReadObservations } from '../../safety/local-tool-server.js';
|
|
11
12
|
const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV", "CLAUDE_CONFIG_DIR"];
|
|
12
13
|
const ALLOWED_SUPERVISOR_COMMANDS = new Set(["continue", "answer_question", "request_revision", "request_checkpoint", "submit_for_review"]);
|
|
13
14
|
const LOCAL_NAMES = ["local_read", "local_search", "local_edit", "local_test"].map(name => `mcp__ai_local__${name}`);
|
|
@@ -44,7 +45,16 @@ export class ClaudeCodeAdapter {
|
|
|
44
45
|
cancelled = false;
|
|
45
46
|
started = false;
|
|
46
47
|
receipt;
|
|
48
|
+
toolEndpoint;
|
|
49
|
+
readSnapshot;
|
|
47
50
|
securityReceipt() { return this.receipt ? structuredClone(this.receipt) : undefined; }
|
|
51
|
+
localReadObservations() {
|
|
52
|
+
const endpoint = this.toolEndpoint;
|
|
53
|
+
const observed = endpoint ? localEndpointReadObservations(endpoint.url, endpoint.token) : undefined;
|
|
54
|
+
if (observed)
|
|
55
|
+
this.readSnapshot = observed;
|
|
56
|
+
return this.readSnapshot ? structuredClone(this.readSnapshot) : undefined;
|
|
57
|
+
}
|
|
48
58
|
constructor(options) {
|
|
49
59
|
this.options = options;
|
|
50
60
|
}
|
|
@@ -97,6 +107,7 @@ export class ClaudeCodeAdapter {
|
|
|
97
107
|
if (this.cancelled)
|
|
98
108
|
throw new Error("adapter startup cancelled");
|
|
99
109
|
const endpoint = await this.tools.start();
|
|
110
|
+
this.toolEndpoint = endpoint;
|
|
100
111
|
if (this.cancelled)
|
|
101
112
|
throw new Error("adapter startup cancelled");
|
|
102
113
|
const mcpConfigPath = this.options.writeLocalMcpConfig(endpoint);
|
|
@@ -222,6 +233,8 @@ export class ClaudeCodeAdapter {
|
|
|
222
233
|
await this.cleanup();
|
|
223
234
|
}
|
|
224
235
|
async cleanup() {
|
|
236
|
+
this.localReadObservations();
|
|
237
|
+
this.toolEndpoint = undefined;
|
|
225
238
|
const tools = this.tools;
|
|
226
239
|
const handle = this.handle;
|
|
227
240
|
this.tools = undefined;
|
|
@@ -19,6 +19,7 @@ export class CodexAdapter {
|
|
|
19
19
|
started = false;
|
|
20
20
|
receipt;
|
|
21
21
|
terminalAudit;
|
|
22
|
+
localReadObservations() { return this.usingOfficialExec ? this.ownedExec?.localReadObservations() : undefined; }
|
|
22
23
|
securityReceipt() { return this.terminalAudit ? structuredClone(this.terminalAudit) : this.usingOfficialExec ? this.ownedExec?.receipt() : this.receipt ? structuredClone(this.receipt) : undefined; }
|
|
23
24
|
cancelled = false;
|
|
24
25
|
usingOfficialExec = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { PreparationClock, CODEX_PREPARATION_BUDGET_MS } from '../../safety/preparation-timing.js';
|
|
3
|
-
import { localEndpointObservations } from '../../safety/local-tool-server.js';
|
|
3
|
+
import { localEndpointObservations, localEndpointReadObservations } from '../../safety/local-tool-server.js';
|
|
4
4
|
import { processStartupObservations, freezeProcessLaunchObservation } from '../../process/node-process-group-host.js';
|
|
5
5
|
import { executionError } from '../../safety/execution-failure.js';
|
|
6
6
|
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
@@ -24,6 +24,7 @@ export class CodexExecLaunchHost {
|
|
|
24
24
|
handle;
|
|
25
25
|
inspection;
|
|
26
26
|
exitAudit;
|
|
27
|
+
readAudit;
|
|
27
28
|
inspectionFingerprint;
|
|
28
29
|
handshake;
|
|
29
30
|
receiptValue;
|
|
@@ -233,9 +234,22 @@ export class CodexExecLaunchHost {
|
|
|
233
234
|
catch {
|
|
234
235
|
return undefined;
|
|
235
236
|
} }
|
|
237
|
+
localReadObservations() {
|
|
238
|
+
if (!this.stopped) {
|
|
239
|
+
try {
|
|
240
|
+
const token = requireOfficialExecPlan(this.plan).env.AI_GATEWAY_JOB_MCP_TOKEN;
|
|
241
|
+
const current = token ? localEndpointReadObservations(this.plan.endpoint, token) : undefined;
|
|
242
|
+
if (current)
|
|
243
|
+
this.readAudit = current;
|
|
244
|
+
}
|
|
245
|
+
catch { /* Absent observation is never interpreted as successful reads. */ }
|
|
246
|
+
}
|
|
247
|
+
return this.readAudit ? structuredClone(this.readAudit) : undefined;
|
|
248
|
+
}
|
|
236
249
|
cancel() {
|
|
237
250
|
if (this.closePromise)
|
|
238
251
|
return this.closePromise;
|
|
252
|
+
this.localReadObservations();
|
|
239
253
|
this.stopped = true;
|
|
240
254
|
this.receiptValue = undefined;
|
|
241
255
|
if (!this.ownsAttempt) {
|
|
@@ -184,6 +184,16 @@ export class ExecutionCoordinator {
|
|
|
184
184
|
const evidence = this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha });
|
|
185
185
|
payload = { ...payload, diff_hash: evidence.diffHash, changed_file_manifest_hash: evidence.changedFileManifestHash };
|
|
186
186
|
}
|
|
187
|
+
// A vendor report cannot supply host observations, even when its shape looks valid.
|
|
188
|
+
if (payload) {
|
|
189
|
+
payload = { ...payload };
|
|
190
|
+
delete payload.local_read_observations;
|
|
191
|
+
}
|
|
192
|
+
if (isTerminalEventType(event.eventType) || event.eventType === 'blocked' || event.eventType === 'scope_conflict') {
|
|
193
|
+
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
194
|
+
if (reads)
|
|
195
|
+
payload = { ...payload, local_read_observations: reads };
|
|
196
|
+
}
|
|
187
197
|
const normalized = {
|
|
188
198
|
protocolVersion: PROTOCOL_VERSION, bindingId: binding.bindingId,
|
|
189
199
|
sequence: sequence + 1, eventType: event.eventType,
|
|
@@ -274,7 +284,8 @@ export class ExecutionCoordinator {
|
|
|
274
284
|
const reportDiagnostic = diagnostic && (terminal.eventType === "blocked"
|
|
275
285
|
? diagnostic.code === "CODEX_COMPLETION_REPORT_BLOCKED" : diagnostic.code !== "CODEX_COMPLETION_REPORT_BLOCKED") ? diagnostic : undefined;
|
|
276
286
|
const securityReceipt = adapter.securityReceipt?.();
|
|
277
|
-
const
|
|
287
|
+
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
288
|
+
const completionRequest = structuredClone({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(reads ? { local_read_observations: reads } : {}), ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(securityReceipt ? { security_receipt: securityReceipt } : {}) } });
|
|
278
289
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken,
|
|
279
290
|
phase: "terminal_pending", bindingId: binding.bindingId, terminalEventType: terminal.eventType,
|
|
280
291
|
completionRequest: structuredClone(completionRequest) });
|
|
@@ -282,3 +293,4 @@ export class ExecutionCoordinator {
|
|
|
282
293
|
await this.deps.state.clear();
|
|
283
294
|
}
|
|
284
295
|
}
|
|
296
|
+
import { safeLocalReadObservations } from '../safety/local-read-observations.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function safeLocalReadObservations(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3
|
+
return;
|
|
4
|
+
const v = value, c = v.classification_counts;
|
|
5
|
+
if (Object.keys(v).sort().join() !== ['version', 'successful_read_calls', 'unique_successful_paths', 'failed_read_calls', 'classification_counts', 'truncated'].sort().join()
|
|
6
|
+
|| v.version !== 'local-read-observations-v1' || typeof v.truncated !== 'boolean' || !c || typeof c !== 'object' || Array.isArray(c))
|
|
7
|
+
return;
|
|
8
|
+
const counts = c;
|
|
9
|
+
if (Object.keys(counts).sort().join() !== 'documentation,source')
|
|
10
|
+
return;
|
|
11
|
+
const numbers = [v.successful_read_calls, v.unique_successful_paths, v.failed_read_calls, counts.documentation, counts.source];
|
|
12
|
+
if (numbers.some(n => !Number.isSafeInteger(n) || Number(n) < 0 || Number(n) > 1_000_000)
|
|
13
|
+
|| Number(v.unique_successful_paths) > 1024 || Number(v.unique_successful_paths) > Number(v.successful_read_calls)
|
|
14
|
+
|| Number(counts.documentation) + Number(counts.source) !== v.successful_read_calls)
|
|
15
|
+
return;
|
|
16
|
+
return structuredClone(value);
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { LOCAL_TOOL_NAMES } from './local-tool-policy.js';
|
|
4
5
|
const discoveryScopes = new WeakMap();
|
|
5
6
|
export function localDiscoveryScopeMatches(scope, endpoint, token) {
|
|
@@ -10,6 +11,10 @@ export function localDiscoveryScopeMatches(scope, endpoint, token) {
|
|
|
10
11
|
}
|
|
11
12
|
// Bounded diagnostic metadata; possession grants neither discovery nor tool authority.
|
|
12
13
|
const endpointObservations = new Map();
|
|
14
|
+
export function localEndpointReadObservations(url, token) {
|
|
15
|
+
const record = endpointObservations.get(url);
|
|
16
|
+
return record?.credentialHash === createHash('sha256').update(token).digest('hex') ? record.reads() : undefined;
|
|
17
|
+
}
|
|
13
18
|
export function localEndpointObservations(url, token) {
|
|
14
19
|
const record = endpointObservations.get(url);
|
|
15
20
|
return record?.credentialHash === createHash('sha256').update(token).digest('hex') ? [...record.values] : [];
|
|
@@ -28,6 +33,32 @@ export function localToolInventory() {
|
|
|
28
33
|
/** Job-only JSON MCP endpoint; intentionally has no Gateway transport or discovery. */
|
|
29
34
|
export class LocalToolServer {
|
|
30
35
|
options;
|
|
36
|
+
readCounts = { version: 'local-read-observations-v1', successful_read_calls: 0, unique_successful_paths: 0, failed_read_calls: 0, classification_counts: { documentation: 0, source: 0 }, truncated: false };
|
|
37
|
+
readPathDigests = new Set();
|
|
38
|
+
/** Counts are server-authored observations, not model evidence or a policy attestation. */
|
|
39
|
+
readObservations() { return structuredClone(this.readCounts); }
|
|
40
|
+
recordRead(success, args) {
|
|
41
|
+
const count = success ? 'successful_read_calls' : 'failed_read_calls';
|
|
42
|
+
if (this.readCounts[count] >= 1_000_000) {
|
|
43
|
+
this.readCounts.truncated = true;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.readCounts[count]++;
|
|
47
|
+
if (!success)
|
|
48
|
+
return;
|
|
49
|
+
const value = (args && typeof args === 'object' && !Array.isArray(args) ? args.path : undefined);
|
|
50
|
+
const file = path.posix.normalize(typeof value === 'string' ? value : '');
|
|
51
|
+
const category = file.startsWith('docs/') ? 'documentation' : 'source';
|
|
52
|
+
this.readCounts.classification_counts[category]++;
|
|
53
|
+
const digest = createHash('sha256').update(file).digest('hex');
|
|
54
|
+
if (!this.readPathDigests.has(digest)) {
|
|
55
|
+
if (this.readPathDigests.size < 1024)
|
|
56
|
+
this.readPathDigests.add(digest);
|
|
57
|
+
else
|
|
58
|
+
this.readCounts.truncated = true;
|
|
59
|
+
}
|
|
60
|
+
this.readCounts.unique_successful_paths = this.readPathDigests.size;
|
|
61
|
+
}
|
|
31
62
|
observations = new Set();
|
|
32
63
|
server = null;
|
|
33
64
|
revoked = false;
|
|
@@ -174,6 +205,8 @@ export class LocalToolServer {
|
|
|
174
205
|
return;
|
|
175
206
|
}
|
|
176
207
|
send({ content: [{ type: 'text', text: JSON.stringify(result) }] });
|
|
208
|
+
if (params.name === 'local_read')
|
|
209
|
+
this.recordRead(true, params.arguments);
|
|
177
210
|
}
|
|
178
211
|
catch (error) {
|
|
179
212
|
if (this.token !== token || !this.allowed()) {
|
|
@@ -182,6 +215,8 @@ export class LocalToolServer {
|
|
|
182
215
|
}
|
|
183
216
|
// Only stable policy error identifiers cross this boundary, never raw paths/stderr.
|
|
184
217
|
const code = error instanceof Error ? error.message.match(/^(DENIED_[A-Z_]+|LOCAL_SANDBOX_UNAVAILABLE)/)?.[0] : undefined;
|
|
218
|
+
if (params.name === 'local_read')
|
|
219
|
+
this.recordRead(false, params.arguments);
|
|
185
220
|
send({ isError: true, content: [{ type: 'text', text: code ?? 'LOCAL_TOOL_FAILED' }] });
|
|
186
221
|
}
|
|
187
222
|
}
|
|
@@ -203,7 +238,7 @@ export class LocalToolServer {
|
|
|
203
238
|
if (!address || typeof address === 'string')
|
|
204
239
|
throw new Error('Failed to bind local tool server');
|
|
205
240
|
this.url = `http://127.0.0.1:${address.port}/mcp`;
|
|
206
|
-
endpointObservations.set(this.url, { credentialHash: createHash('sha256').update(this.token).digest('hex'), values: this.observations });
|
|
241
|
+
endpointObservations.set(this.url, { credentialHash: createHash('sha256').update(this.token).digest('hex'), values: this.observations, reads: () => this.readObservations() });
|
|
207
242
|
discoveryScopes.set(this.discoveryScope, { endpoint: this.url, credentialHash: createHash('sha256').update(this.token).digest('hex') });
|
|
208
243
|
return { url: this.url, token: this.token };
|
|
209
244
|
}
|