mtech-ai-local-runner 0.1.20 → 0.1.22
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,36 @@ 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.22: completion report classification and terminal diagnostics
|
|
14
|
+
|
|
15
|
+
Report validation distinguishes missing messages, zero valid reports, multiple
|
|
16
|
+
valid reports, an invalid final message and an incomplete turn. Runtime events
|
|
17
|
+
and terminal Job results retain only fixed codes and consistent message/report
|
|
18
|
+
counts, never raw provider output. Structured blocked reports use the same
|
|
19
|
+
bounded single-fence envelope rules as completed reports and remain blocked.
|
|
20
|
+
|
|
21
|
+
Exactly one valid final report and successful turn completion remain required;
|
|
22
|
+
multiple reports are never picked or merged. Existing evidence, scope, identity,
|
|
23
|
+
cancellation and unlimited admitted employee work remain unchanged. Earlier
|
|
24
|
+
0.1.21 report failure cannot be reclassified without its missing original
|
|
25
|
+
counts. Publication and foreground installation do not update the daemon or
|
|
26
|
+
rerun Codex. See [completion report validation](https://bitbucket.org/mtechbal/api-cloud-service-ai-gateway/src/development/docs/ai-ops-gateway/codex-completion-report-validation.md).
|
|
27
|
+
|
|
28
|
+
## 0.1.21: exclude personal history from isolated job startup
|
|
29
|
+
|
|
30
|
+
The job-only Codex wrapper denies reads of the original account's `sessions`
|
|
31
|
+
and `archived_sessions`. A fresh private job database therefore cannot re-index
|
|
32
|
+
unrelated personal rollouts before MCP startup. Existing CLI/login paths,
|
|
33
|
+
personal settings, required authenticated MCP and unlimited admitted employee
|
|
34
|
+
work remain unchanged. Nothing is removed from the original history.
|
|
35
|
+
|
|
36
|
+
Pinned native fake-data comparison verified zero imported personal-history rows
|
|
37
|
+
and successful MCP initialization with this restriction. Real-account no-model
|
|
38
|
+
evidence remains partial because the private collector did not confirm its
|
|
39
|
+
owned exit/cleanup; real model completion is not established. Publication and
|
|
40
|
+
foreground installation do not update the daemon or authorize an execution.
|
|
41
|
+
See [local host comparison](https://bitbucket.org/mtechbal/api-cloud-service-ai-gateway/src/development/docs/ai-ops-gateway/codex-local-host-comparison.md).
|
|
42
|
+
|
|
13
43
|
## 0.1.20: distinguish owned wrapper and target startup
|
|
14
44
|
|
|
15
45
|
Add bounded, read-only process-name/state/wait hints for the exact owned
|
|
@@ -14,12 +14,22 @@ export function accountAncestorPaths(account) {
|
|
|
14
14
|
}
|
|
15
15
|
return Object.freeze(parents);
|
|
16
16
|
}
|
|
17
|
+
/** New job processes must not re-index unrelated interactive history into
|
|
18
|
+
* their fresh private SQLite DB. Login stays at the original account root. */
|
|
19
|
+
export function personalHistoryReadDenial(account) {
|
|
20
|
+
accountAncestorPaths(account);
|
|
21
|
+
return '(deny file-read* ' + ['sessions', 'archived_sessions'].map(name => {
|
|
22
|
+
const root = JSON.stringify(path.join(account, name));
|
|
23
|
+
return `(literal ${root}) (subpath ${root})`;
|
|
24
|
+
}).join(' ') + ')';
|
|
25
|
+
}
|
|
17
26
|
/** Additional per-process denial, not a grant of authority or global policy edit.
|
|
18
27
|
* file-read* is intentional: data-only denial permits a final symlink alias.
|
|
19
28
|
* Ancestor symlink denials prevent a later account-directory redirect. */
|
|
20
29
|
export function globalInstructionProfile(account) {
|
|
21
30
|
const ancestors = accountAncestorPaths(account), literal = (p) => `(literal ${JSON.stringify(p)})`;
|
|
22
31
|
const profile = '(version 1)(allow default)'
|
|
32
|
+
+ personalHistoryReadDenial(account)
|
|
23
33
|
+ '(deny file-read* ' + ['AGENTS.md', 'AGENTS.override.md'].map(name => literal(path.join(account, name))).join(' ') + ')'
|
|
24
34
|
+ '(deny file-read* (require-all (vnode-type SYMLINK) (require-any ' + ancestors.map(literal).join(' ') + ')))';
|
|
25
35
|
if (profile.length > 64000)
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { completionEvent } from "../completion-report.js";
|
|
1
|
+
import { completionEnvelope, completionEvent } from "../completion-report.js";
|
|
2
2
|
function record(value) {
|
|
3
3
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
4
|
? value : undefined;
|
|
5
5
|
}
|
|
6
6
|
function finalReport(text) {
|
|
7
7
|
const complete = completionEvent(text);
|
|
8
|
-
if (complete.eventType === "completed"
|
|
8
|
+
if (complete.eventType === "completed")
|
|
9
9
|
return complete;
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
value = record(JSON.parse(text));
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return complete;
|
|
16
|
-
}
|
|
10
|
+
const value = completionEnvelope(text);
|
|
17
11
|
const bounded = (v, size) => typeof v === "string" && v.trim().length > 0 && v.length <= size;
|
|
18
12
|
const entries = (v, min) => Array.isArray(v) && v.length >= min && v.length <= 20 && v.every(item => bounded(item, 500));
|
|
19
13
|
if (!value || Object.keys(value).some(key => !["status", "report", "evidence", "pending_work"].includes(key))
|
|
@@ -26,6 +20,7 @@ function finalReport(text) {
|
|
|
26
20
|
export class CodexStreamParser {
|
|
27
21
|
stopped = false;
|
|
28
22
|
report;
|
|
23
|
+
messages = 0;
|
|
29
24
|
candidates = 0;
|
|
30
25
|
parse(line) {
|
|
31
26
|
if (this.stopped)
|
|
@@ -53,15 +48,21 @@ export class CodexStreamParser {
|
|
|
53
48
|
return this.finish({ eventType: "scope_conflict", summary: "Codex emitted a native tool operation outside the job bridge" });
|
|
54
49
|
}
|
|
55
50
|
if (event.type === "item.completed" && item.type === "agent_message") {
|
|
56
|
-
this.
|
|
57
|
-
|
|
51
|
+
this.messages += 1;
|
|
52
|
+
this.report = finalReport(item.text);
|
|
53
|
+
if (["completed", "blocked"].includes(this.report.eventType))
|
|
58
54
|
this.candidates += 1;
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
57
|
if (event.type !== "turn.completed")
|
|
62
58
|
return [];
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
// Keep final-message validity and uniqueness independent. Never select an
|
|
60
|
+
// earlier report over an invalid final message, or pick between two reports.
|
|
61
|
+
const terminal = this.candidates === 0
|
|
62
|
+
? this.reportFailure(this.messages === 0 ? "CODEX_COMPLETION_REPORT_MISSING" : "CODEX_COMPLETION_REPORT_INVALID")
|
|
63
|
+
: this.candidates > 1 ? this.reportFailure("CODEX_COMPLETION_REPORT_MULTIPLE")
|
|
64
|
+
: this.report && ["completed", "blocked"].includes(this.report.eventType) ? this.report
|
|
65
|
+
: this.reportFailure("CODEX_COMPLETION_REPORT_NOT_FINAL");
|
|
65
66
|
const usage = record(event.usage);
|
|
66
67
|
const observed = {};
|
|
67
68
|
for (const field of ["input_tokens", "cached_input_tokens", "output_tokens"]) {
|
|
@@ -75,7 +76,20 @@ export class CodexStreamParser {
|
|
|
75
76
|
return this.finish(terminal);
|
|
76
77
|
}
|
|
77
78
|
end() {
|
|
78
|
-
return this.stopped ? [] : this.finish(
|
|
79
|
+
return this.stopped ? [] : this.finish(this.reportFailure("CODEX_TURN_INCOMPLETE"));
|
|
80
|
+
}
|
|
81
|
+
reportFailure(code) {
|
|
82
|
+
const summaries = {
|
|
83
|
+
CODEX_COMPLETION_REPORT_MISSING: "Codex returned no completed agent message",
|
|
84
|
+
CODEX_COMPLETION_REPORT_INVALID: "Codex returned no valid completion report",
|
|
85
|
+
CODEX_COMPLETION_REPORT_MULTIPLE: "Codex returned multiple valid completion reports",
|
|
86
|
+
CODEX_COMPLETION_REPORT_NOT_FINAL: "Codex's final message was not the unique valid completion report",
|
|
87
|
+
CODEX_TURN_INCOMPLETE: "Codex ended before successful turn completion",
|
|
88
|
+
};
|
|
89
|
+
return { eventType: "failed", summary: summaries[code], payload: { completion_report: {
|
|
90
|
+
version: "codex-completion-report-v1", code, agent_message_count: this.messages,
|
|
91
|
+
valid_report_count: this.candidates, invalid_message_count: this.messages - this.candidates,
|
|
92
|
+
} } };
|
|
79
93
|
}
|
|
80
94
|
finish(event) {
|
|
81
95
|
this.stopped = true;
|
|
@@ -2,6 +2,24 @@ import { MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
|
2
2
|
const MAX_EVIDENCE_ITEMS = 20;
|
|
3
3
|
const MAX_EVIDENCE_CHARS = 500;
|
|
4
4
|
const MAX_REPORT_ENVELOPE_CHARS = 16_000;
|
|
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
|
+
/** Diagnostic metadata only, never completion authority. Copy no provider text. */
|
|
7
|
+
export function completionReportDiagnostic(value) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
9
|
+
return undefined;
|
|
10
|
+
const d = value, code = d.code;
|
|
11
|
+
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)
|
|
13
|
+
|| typeof messages !== "number" || !Number.isSafeInteger(messages) || messages < 0
|
|
14
|
+
|| typeof valid !== "number" || !Number.isSafeInteger(valid) || valid < 0 || valid > messages
|
|
15
|
+
|| typeof invalid !== "number" || !Number.isSafeInteger(invalid) || invalid !== messages - valid
|
|
16
|
+
|| code === "CODEX_COMPLETION_REPORT_MISSING" && messages !== 0
|
|
17
|
+
|| code === "CODEX_COMPLETION_REPORT_INVALID" && (messages === 0 || valid !== 0)
|
|
18
|
+
|| code === "CODEX_COMPLETION_REPORT_MULTIPLE" && valid < 2
|
|
19
|
+
|| code === "CODEX_COMPLETION_REPORT_NOT_FINAL" && (valid !== 1 || invalid === 0))
|
|
20
|
+
return undefined;
|
|
21
|
+
return { version: "codex-completion-report-v1", code, agent_message_count: messages, valid_report_count: valid, invalid_message_count: invalid };
|
|
22
|
+
}
|
|
5
23
|
export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the available tools; do not delegate or start background work. " +
|
|
6
24
|
"Do not report completed while waiting for results, tests, or another agent. " +
|
|
7
25
|
"Your final response must be one JSON object, without Markdown fences: " +
|
|
@@ -15,40 +33,43 @@ export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the
|
|
|
15
33
|
function boundedText(value, max) {
|
|
16
34
|
return typeof value === "string" && value.trim().length > 0 && value.length <= max;
|
|
17
35
|
}
|
|
18
|
-
/**
|
|
19
|
-
export function
|
|
20
|
-
const usage = typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0
|
|
21
|
-
? { cost_usd: costUsd }
|
|
22
|
-
: {};
|
|
23
|
-
const failed = {
|
|
24
|
-
eventType: "failed",
|
|
25
|
-
summary: "completion report is missing, incomplete or invalid",
|
|
26
|
-
payload: usage,
|
|
27
|
-
};
|
|
36
|
+
/** Decode bounded framing only. Callers must still validate status and evidence. */
|
|
37
|
+
export function completionEnvelope(result) {
|
|
28
38
|
if (typeof result !== "string" || result.length > MAX_REPORT_ENVELOPE_CHARS)
|
|
29
|
-
return
|
|
39
|
+
return undefined;
|
|
30
40
|
let report;
|
|
31
41
|
try {
|
|
32
42
|
report = JSON.parse(result);
|
|
33
43
|
}
|
|
34
44
|
catch {
|
|
35
|
-
// Claude may wrap the final report in one Markdown fence after a short
|
|
36
|
-
// explanation. Accept framing only; retain every completion/evidence check.
|
|
37
45
|
if ((result.match(/```/g) ?? []).length !== 2)
|
|
38
|
-
return
|
|
46
|
+
return undefined;
|
|
39
47
|
const framed = result.trim().match(/^(?:[\s\S]*?\n)?```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/);
|
|
40
48
|
if (!framed)
|
|
41
|
-
return
|
|
49
|
+
return undefined;
|
|
42
50
|
try {
|
|
43
51
|
report = JSON.parse(framed[1]);
|
|
44
52
|
}
|
|
45
53
|
catch {
|
|
46
|
-
return
|
|
54
|
+
return undefined;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
return report && typeof report === "object" && !Array.isArray(report)
|
|
58
|
+
? report : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Validate the full final result before truncation; never publish malformed/interim output. */
|
|
61
|
+
export function completionEvent(result, costUsd) {
|
|
62
|
+
const usage = typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0
|
|
63
|
+
? { cost_usd: costUsd }
|
|
64
|
+
: {};
|
|
65
|
+
const failed = {
|
|
66
|
+
eventType: "failed",
|
|
67
|
+
summary: "completion report is missing, incomplete or invalid",
|
|
68
|
+
payload: usage,
|
|
69
|
+
};
|
|
70
|
+
const value = completionEnvelope(result);
|
|
71
|
+
if (!value)
|
|
50
72
|
return failed;
|
|
51
|
-
const value = report;
|
|
52
73
|
if (Object.keys(value).some((key) => !["status", "report", "evidence", "pending_work"].includes(key))
|
|
53
74
|
|| value.status !== "completed"
|
|
54
75
|
|| !boundedText(value.report, MAX_EVENT_SUMMARY_CHARS)
|
|
@@ -2,6 +2,7 @@ import { executionError, executionFailure } from '../safety/execution-failure.js
|
|
|
2
2
|
import { requireSecurityRequirements } from '../safety/security-requirements.js';
|
|
3
3
|
import { GatewayToolError } from "../gateway-client.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { completionReportDiagnostic } from "../adapters/completion-report.js";
|
|
5
6
|
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
6
7
|
function canonical(value) {
|
|
7
8
|
if (Array.isArray(value))
|
|
@@ -268,7 +269,8 @@ export class ExecutionCoordinator {
|
|
|
268
269
|
clearInterval(heartbeatTimer);
|
|
269
270
|
}
|
|
270
271
|
const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
|
|
271
|
-
|
|
272
|
+
const reportDiagnostic = status === "failed" ? completionReportDiagnostic(terminal.payload?.completion_report) : undefined;
|
|
273
|
+
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() } : {}) } });
|
|
272
274
|
await this.deps.state.clear();
|
|
273
275
|
}
|
|
274
276
|
}
|