mtech-ai-local-runner 0.1.23 → 0.1.25

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,29 @@ 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.25: scoped reads and blocked diagnostics
14
+
15
+ Codex receives explicit file-scoped ai_local read/search guidance; launch and
16
+ Gateway reporting remain host duties. Valid blocked reports now retain bounded
17
+ report counts and identity comparisons in events and terminal results. They
18
+ remain blocked, and multiple reports still fail closed. Fake-data integration
19
+ checks generated settings with the actual scoped server; native model-visible
20
+ tool exposure still needs a separately approved actual attempt. Foreground
21
+ installation does not run Codex or update the daemon.
22
+
23
+ ## 0.1.24: bounded report identity diagnostics
24
+
25
+ Failed report validation records pair counts for same/different wire item IDs
26
+ and exact text, using at most 64 transient keyed samples. Raw IDs, text, keys
27
+ and hashes are never included in these diagnostics. Sampling does not limit
28
+ actual task duration or messages. Multiple reports still fail; none are chosen,
29
+ merged or deduplicated.
30
+
31
+ These observations distinguish wire-output patterns, not model-generation
32
+ identity: the inspected exec serializer assigns fresh CLI IDs. Earlier failures
33
+ cannot be reclassified without missing identity evidence. Publication and
34
+ foreground installation do not rerun Codex or update the daemon.
35
+
13
36
  ## 0.1.23: separate progress from the final report
14
37
 
15
38
  Codex receives native-message instructions: optional progress is plain text and
@@ -1,6 +1,6 @@
1
1
  import { compilePrompt as compileBrief } from "../prompt-compiler.js";
2
2
  export function compileCodexPrompt(taskPackage, label) {
3
- return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. Native filesystem, commands, web, images and delegation are unavailable or denied. A denied tool is a scope boundary, never a reason to try another path. If required work cannot proceed, return the structured blocked report.`;
3
+ return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. For reading, use server ai_local's local_read({path: "docs/example.md"}) or local_search({path: "docs/example.md", query: "text"}) with checkout-relative paths. In a read-only task, use only read/search tools; the Runner enforces the approved paths and operations. A denied native cwd is intentional: it is a private control directory, not the source checkout. Native filesystem, commands, web, images and delegation are unavailable or denied. A denied tool is a scope boundary, never a reason to try another path. Do not repeat host orchestration: claim, shadow, fingerprints, binding, administrator checks, launches, Gateway usage and Job reporting belong to the Runner/host, even when described in the objective. Perform only the assigned repository work and report evidence you actually observed. Do not claim that no model call occurred. If the required tool is unavailable, emit one final structured blocked report naming the missing tool and unfinished repository work; do not request or attempt a new launch.`;
4
4
  }
5
5
  export const CODEX_REPORT_SCHEMA = {
6
6
  type: "object", additionalProperties: false, required: ["status", "report", "evidence", "pending_work"],
@@ -1,3 +1,4 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
1
2
  import { completionEnvelope, completionEvent } from "../completion-report.js";
2
3
  function record(value) {
3
4
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -22,6 +23,8 @@ export class CodexStreamParser {
22
23
  report;
23
24
  messages = 0;
24
25
  candidates = 0;
26
+ identityKey = randomBytes(32);
27
+ reportSamples = [];
25
28
  parse(line) {
26
29
  if (this.stopped)
27
30
  return [];
@@ -50,8 +53,17 @@ export class CodexStreamParser {
50
53
  if (event.type === "item.completed" && item.type === "agent_message") {
51
54
  this.messages += 1;
52
55
  this.report = finalReport(item.text);
53
- if (["completed", "blocked"].includes(this.report.eventType))
56
+ if (["completed", "blocked"].includes(this.report.eventType)) {
54
57
  this.candidates += 1;
58
+ // Bounded diagnostic sample only. Neither raw IDs nor text digests
59
+ // leave this parser, and sampling never changes report acceptance.
60
+ if (this.reportSamples.length < 64) {
61
+ const digest = (domain, value) => createHmac("sha256", this.identityKey).update(domain).update(value).digest("hex");
62
+ const id = typeof item.id === "string" && item.id.length > 0 && item.id.length <= 256
63
+ ? digest("id:", item.id) : undefined;
64
+ this.reportSamples.push({ id, text: digest("text:", String(item.text)) });
65
+ }
66
+ }
55
67
  }
56
68
  }
57
69
  if (event.type !== "turn.completed")
@@ -63,6 +75,11 @@ export class CodexStreamParser {
63
75
  : this.candidates > 1 ? this.reportFailure("CODEX_COMPLETION_REPORT_MULTIPLE")
64
76
  : this.report && ["completed", "blocked"].includes(this.report.eventType) ? this.report
65
77
  : this.reportFailure("CODEX_COMPLETION_REPORT_NOT_FINAL");
78
+ if (terminal.eventType === "blocked")
79
+ terminal.payload = { ...terminal.payload, completion_report: {
80
+ version: "codex-completion-report-v1", code: "CODEX_COMPLETION_REPORT_BLOCKED", agent_message_count: this.messages,
81
+ valid_report_count: this.candidates, invalid_message_count: this.messages - this.candidates, identity: this.reportIdentity(),
82
+ } };
66
83
  const usage = record(event.usage);
67
84
  const observed = {};
68
85
  for (const field of ["input_tokens", "cached_input_tokens", "output_tokens"]) {
@@ -89,11 +106,41 @@ export class CodexStreamParser {
89
106
  return { eventType: "failed", summary: summaries[code], payload: { completion_report: {
90
107
  version: "codex-completion-report-v1", code, agent_message_count: this.messages,
91
108
  valid_report_count: this.candidates, invalid_message_count: this.messages - this.candidates,
109
+ identity: this.reportIdentity(),
92
110
  } } };
93
111
  }
112
+ reportIdentity() {
113
+ const counts = { same_item_same_text_pairs: 0, same_item_different_text_pairs: 0,
114
+ different_item_same_text_pairs: 0, different_item_different_text_pairs: 0 };
115
+ for (let i = 0; i < this.reportSamples.length; i++) {
116
+ const a = this.reportSamples[i];
117
+ if (!a.id)
118
+ continue;
119
+ for (let j = i + 1; j < this.reportSamples.length; j++) {
120
+ const b = this.reportSamples[j];
121
+ if (!b.id)
122
+ continue;
123
+ if (a.id === b.id) {
124
+ if (a.text === b.text)
125
+ counts.same_item_same_text_pairs++;
126
+ else
127
+ counts.same_item_different_text_pairs++;
128
+ }
129
+ else if (a.text === b.text)
130
+ counts.different_item_same_text_pairs++;
131
+ else
132
+ counts.different_item_different_text_pairs++;
133
+ }
134
+ }
135
+ return { version: "codex-report-identity-v1", sampled_reports: this.reportSamples.length,
136
+ unsampled_reports: this.candidates - this.reportSamples.length,
137
+ missing_item_id_reports: this.reportSamples.filter(sample => !sample.id).length, ...counts };
138
+ }
94
139
  finish(event) {
95
140
  this.stopped = true;
96
141
  this.report = undefined;
142
+ this.reportSamples.length = 0;
143
+ this.identityKey.fill(0);
97
144
  return [event];
98
145
  }
99
146
  }
@@ -3,22 +3,44 @@ const MAX_EVIDENCE_ITEMS = 20;
3
3
  const MAX_EVIDENCE_CHARS = 500;
4
4
  const MAX_REPORT_ENVELOPE_CHARS = 16_000;
5
5
  const REPORT_FAILURE_CODES = ["CODEX_COMPLETION_REPORT_MISSING", "CODEX_COMPLETION_REPORT_INVALID", "CODEX_COMPLETION_REPORT_MULTIPLE", "CODEX_COMPLETION_REPORT_NOT_FINAL", "CODEX_TURN_INCOMPLETE"];
6
+ const REPORT_DIAGNOSTIC_CODES = [...REPORT_FAILURE_CODES, "CODEX_COMPLETION_REPORT_BLOCKED"];
7
+ /** Pair counts only; no item IDs, text, stable digests or arbitrary keys. */
8
+ function reportIdentityDiagnostic(value, valid) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value))
10
+ return undefined;
11
+ const d = value;
12
+ const keys = ["sampled_reports", "unsampled_reports", "missing_item_id_reports", "same_item_same_text_pairs",
13
+ "same_item_different_text_pairs", "different_item_same_text_pairs", "different_item_different_text_pairs"];
14
+ if (d.version !== "codex-report-identity-v1" || keys.some(key => typeof d[key] !== "number" || !Number.isSafeInteger(d[key]) || Number(d[key]) < 0))
15
+ return undefined;
16
+ const sampled = Number(d.sampled_reports), missing = Number(d.missing_item_id_reports);
17
+ if (sampled !== Math.min(valid, 64) || Number(d.unsampled_reports) !== valid - sampled || missing > sampled)
18
+ return undefined;
19
+ const pairs = keys.slice(3).map(key => Number(d[key]));
20
+ const identified = sampled - missing, expected = identified * (identified - 1) / 2;
21
+ if (pairs.some(count => count > expected) || pairs.reduce((sum, count) => sum + count, 0) !== expected)
22
+ return undefined;
23
+ return { version: "codex-report-identity-v1", ...Object.fromEntries(keys.map(key => [key, d[key]])) };
24
+ }
6
25
  /** Diagnostic metadata only, never completion authority. Copy no provider text. */
7
26
  export function completionReportDiagnostic(value) {
8
27
  if (!value || typeof value !== "object" || Array.isArray(value))
9
28
  return undefined;
10
29
  const d = value, code = d.code;
11
30
  const messages = d.agent_message_count, valid = d.valid_report_count, invalid = d.invalid_message_count;
12
- if (d.version !== "codex-completion-report-v1" || typeof code !== "string" || !REPORT_FAILURE_CODES.includes(code)
31
+ if (d.version !== "codex-completion-report-v1" || typeof code !== "string" || !REPORT_DIAGNOSTIC_CODES.includes(code)
13
32
  || typeof messages !== "number" || !Number.isSafeInteger(messages) || messages < 0
14
33
  || typeof valid !== "number" || !Number.isSafeInteger(valid) || valid < 0 || valid > messages
15
34
  || typeof invalid !== "number" || !Number.isSafeInteger(invalid) || invalid !== messages - valid
16
35
  || code === "CODEX_COMPLETION_REPORT_MISSING" && messages !== 0
17
36
  || code === "CODEX_COMPLETION_REPORT_INVALID" && (messages === 0 || valid !== 0)
18
37
  || code === "CODEX_COMPLETION_REPORT_MULTIPLE" && valid < 2
19
- || code === "CODEX_COMPLETION_REPORT_NOT_FINAL" && (valid !== 1 || invalid === 0))
38
+ || code === "CODEX_COMPLETION_REPORT_NOT_FINAL" && (valid !== 1 || invalid === 0)
39
+ || code === "CODEX_COMPLETION_REPORT_BLOCKED" && valid !== 1)
20
40
  return undefined;
21
- return { version: "codex-completion-report-v1", code, agent_message_count: messages, valid_report_count: valid, invalid_message_count: invalid };
41
+ const identity = reportIdentityDiagnostic(d.identity, valid);
42
+ return { version: "codex-completion-report-v1", code, agent_message_count: messages, valid_report_count: valid, invalid_message_count: invalid,
43
+ ...(identity ? { identity } : {}) };
22
44
  }
23
45
  export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the available tools; do not delegate or start background work. " +
24
46
  "Do not report completed while waiting for results, tests, or another agent. " +
@@ -269,7 +269,9 @@ export class ExecutionCoordinator {
269
269
  clearInterval(heartbeatTimer);
270
270
  }
271
271
  const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
272
- const reportDiagnostic = status === "failed" ? completionReportDiagnostic(terminal.payload?.completion_report) : undefined;
272
+ const diagnostic = status === "failed" || terminal.eventType === "blocked" ? completionReportDiagnostic(terminal.payload?.completion_report) : undefined;
273
+ const reportDiagnostic = diagnostic && (terminal.eventType === "blocked"
274
+ ? diagnostic.code === "CODEX_COMPLETION_REPORT_BLOCKED" : diagnostic.code !== "CODEX_COMPLETION_REPORT_BLOCKED") ? diagnostic : undefined;
273
275
  await this.deps.gateway.complete({ 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 } : {}), ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
274
276
  await this.deps.state.clear();
275
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {