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 CHANGED
@@ -1,4 +1,17 @@
1
- ## 0.1.29 candidate: reviewed daemon Codex admission
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 Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
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 completionRequest = structuredClone({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(securityReceipt ? { security_receipt: securityReceipt } : {}) } });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {