mtech-ai-local-runner 0.1.8 → 0.1.9

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.
Files changed (54) hide show
  1. package/README.md +45 -4
  2. package/dist/src/adapters/claude-code/adapter.js +157 -98
  3. package/dist/src/adapters/claude-code/completion-report.js +2 -64
  4. package/dist/src/adapters/claude-code/prompt-compiler.js +1 -60
  5. package/dist/src/adapters/claude-code/runtime-options.js +72 -0
  6. package/dist/src/adapters/codex/adapter.js +170 -0
  7. package/dist/src/adapters/codex/app-server-events.js +84 -0
  8. package/dist/src/adapters/codex/compatibility.js +45 -0
  9. package/dist/src/adapters/codex/config-builder.js +40 -0
  10. package/dist/src/adapters/codex/host-admission.js +3 -0
  11. package/dist/src/adapters/codex/inspection-rpc.js +48 -0
  12. package/dist/src/adapters/codex/job-rpc.js +189 -0
  13. package/dist/src/adapters/codex/native-policy.js +112 -0
  14. package/dist/src/adapters/codex/native-proof.js +14 -0
  15. package/dist/src/adapters/codex/native-session.js +375 -0
  16. package/dist/src/adapters/codex/plugin.js +26 -0
  17. package/dist/src/adapters/codex/pre-model-gate.js +112 -0
  18. package/dist/src/adapters/codex/prompt-compiler.js +13 -0
  19. package/dist/src/adapters/codex/runtime-options.js +163 -0
  20. package/dist/src/adapters/codex/scratch.js +28 -0
  21. package/dist/src/adapters/codex/stdio-rpc.js +113 -0
  22. package/dist/src/adapters/codex/stream-parser.js +85 -0
  23. package/dist/src/adapters/codex/supported-admission.js +97 -0
  24. package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
  25. package/dist/src/adapters/completion-report.js +64 -0
  26. package/dist/src/adapters/discovery.js +76 -0
  27. package/dist/src/adapters/prompt-compiler.js +60 -0
  28. package/dist/src/adapters/registry.js +10 -5
  29. package/dist/src/cli.js +50 -58
  30. package/dist/src/commands/doctor.js +7 -1
  31. package/dist/src/commands/setup.js +3 -0
  32. package/dist/src/daemon/execution-coordinator.js +19 -2
  33. package/dist/src/daemon/runtime.js +34 -41
  34. package/dist/src/process/node-process-group-host.js +74 -0
  35. package/dist/src/safety/cli-host-admission.js +74 -0
  36. package/dist/src/safety/cli-security-modes.js +4 -0
  37. package/dist/src/safety/codex-production-host.js +267 -0
  38. package/dist/src/safety/codex-supported-releases.js +1 -0
  39. package/dist/src/safety/hook-cli.js +21 -7
  40. package/dist/src/safety/hook-policy-builder.js +4 -11
  41. package/dist/src/safety/job-security-policy.js +62 -0
  42. package/dist/src/safety/job-security-receipt.js +20 -0
  43. package/dist/src/safety/job-security-session.js +133 -0
  44. package/dist/src/safety/job-tool-dispatcher.js +52 -0
  45. package/dist/src/safety/job-tool-policy.js +61 -0
  46. package/dist/src/safety/job-tool-session.js +82 -0
  47. package/dist/src/safety/local-tool-policy.js +101 -0
  48. package/dist/src/safety/local-tool-server.js +174 -0
  49. package/dist/src/safety/local-tool-worker.js +57 -0
  50. package/dist/src/safety/pre-tool-use-hook.js +3 -39
  51. package/dist/src/safety/registered-command-integrity.js +52 -0
  52. package/dist/src/safety/security-requirements.js +38 -0
  53. package/dist/src/safety/subprocess-sandbox.js +62 -0
  54. package/package.json +1 -1
@@ -0,0 +1,170 @@
1
+ import { requireSecurityRequirements } from '../../safety/security-requirements.js';
2
+ import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
3
+ import { requireJobSecurity } from '../../safety/job-security-policy.js';
4
+ import { CODEX_UNSUPPORTED_DIAGNOSTIC } from "./compatibility.js";
5
+ import { buildCodexInvocation } from "./config-builder.js";
6
+ import { compileCodexPrompt, CODEX_REPORT_SCHEMA } from "./prompt-compiler.js";
7
+ import { CodexStreamParser } from "./stream-parser.js";
8
+ function object(value) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value))
10
+ throw new Error("Codex task package is invalid");
11
+ return value;
12
+ }
13
+ export class CodexAdapter {
14
+ options;
15
+ handle;
16
+ tools;
17
+ started = false;
18
+ receipt;
19
+ securityReceipt() { return this.receipt ? structuredClone(this.receipt) : undefined; }
20
+ cancelled = false;
21
+ parser = new CodexStreamParser();
22
+ startupAbort = new AbortController();
23
+ securityPolicy;
24
+ constructor(options) {
25
+ this.options = options;
26
+ }
27
+ async start(ctx) {
28
+ if (this.started || this.cancelled || ctx.resumeSessionId)
29
+ throw new Error("Codex requires one fresh approved launch");
30
+ this.started = true;
31
+ if (!await this.options.verifyCompatibility())
32
+ throw new Error(CODEX_UNSUPPORTED_DIAGNOSTIC);
33
+ this.assertNotCancelled();
34
+ if (!this.options.startVerifiedSession)
35
+ throw new Error("CODEX_PRE_MODEL_UNAVAILABLE");
36
+ const approved = object(ctx.approvedTaskPackage);
37
+ const pkg = object(ctx.taskPackage);
38
+ const execution = object(pkg.execution);
39
+ const source = object(approved.repository);
40
+ const repositories = pkg.repositories;
41
+ const repository = object(Array.isArray(repositories) ? repositories[0] : undefined);
42
+ if (approved.runtime_adapter !== "codex_cli" || approved.runtime_adapter_version !== "1"
43
+ || approved.resolved_provider !== "openai" || !ctx.resolvedModelId
44
+ || approved.resolved_model_id !== ctx.resolvedModelId
45
+ || !/^[a-f0-9]{40}$/.test(String(source.base_commit_sha))
46
+ || source.base_commit_sha !== repository.base_commit_sha
47
+ || execution.mode !== "employee_ai_work_session" || execution.runtime_adapter !== "codex_cli"
48
+ || execution.limits_mode !== "unbounded")
49
+ throw new Error("Codex launch model, revision or execution policy differs from approval");
50
+ const admission = { adapterId: 'codex_cli', adapterVersion: '1', jobId: this.options.jobId, modelId: ctx.resolvedModelId, sourceSha: String(source.base_commit_sha), generation: ctx.generation };
51
+ if (!ctx.securityBinding)
52
+ throw new Error('SECURITY_REQUIREMENTS_REJECTED');
53
+ try {
54
+ this.securityPolicy = this.options.prepareSecurity ? await this.options.prepareSecurity(ctx) : this.options.securityPolicy;
55
+ this.assertNotCancelled();
56
+ }
57
+ catch (error) {
58
+ await this.cancel();
59
+ throw error;
60
+ }
61
+ const deferred = this.options.deferSupportedSecurity === true;
62
+ const assertAdmission = () => { if (!this.securityPolicy && deferred)
63
+ this.securityPolicy = this.options.securityPolicy; if (!Number.isSafeInteger(admission.generation) || Number(admission.generation) < 1)
64
+ throw new Error('JOB_SECURITY_REJECTED'); requireJobSecurity(this.securityPolicy, admission); requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: "codex_cli", adapterVersion: "1", requirementsSha256: this.securityPolicy.pin.requirementsSha256 }); };
65
+ try {
66
+ if (deferred) {
67
+ const r = requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: 'codex_cli', adapterVersion: '1' });
68
+ if (r.security_mode !== 'supported_cli_policy_v1' || this.securityPolicy || !Number.isSafeInteger(ctx.generation) || Number(ctx.generation) < 1 || Object.values(ctx.securityBinding).some(v => !/^[a-f0-9]{64}$/.test(v)))
69
+ throw new Error('SECURITY_REQUIREMENTS_REJECTED');
70
+ }
71
+ else {
72
+ assertAdmission();
73
+ this.receipt = createJobSecurityReceipt(this.securityPolicy, { jobId: this.options.jobId, generation: ctx.generation, modelId: ctx.resolvedModelId, sourceSha: String(source.base_commit_sha), ...ctx.securityBinding, requirements: ctx.approvedTaskPackage?.security_requirements });
74
+ }
75
+ }
76
+ catch (error) {
77
+ await this.cancel();
78
+ throw error;
79
+ }
80
+ try {
81
+ const prompt = compileCodexPrompt(pkg, this.options.jobId);
82
+ this.tools = this.options.createTools(pkg, approved);
83
+ const endpoint = await this.tools.start();
84
+ this.assertNotCancelled();
85
+ const schemaPath = await this.options.writeSchema(CODEX_REPORT_SCHEMA);
86
+ this.assertNotCancelled();
87
+ const invocation = buildCodexInvocation({ modelId: ctx.resolvedModelId,
88
+ worktreeRoot: this.options.cwd ?? this.options.worktreeRoot, sourceRoot: this.options.worktreeRoot, schemaPath, endpoint, env: this.options.env, scratch: this.options.scratch });
89
+ if (!deferred)
90
+ assertAdmission();
91
+ const handle = await this.options.startVerifiedSession(invocation, prompt, this.startupAbort.signal, assertAdmission);
92
+ try {
93
+ assertAdmission();
94
+ }
95
+ catch (error) {
96
+ this.tools.revoke();
97
+ handle.kill('SIGKILL');
98
+ throw error;
99
+ }
100
+ // Cancellation may race with a transport returning its newly created handle.
101
+ if (this.cancelled) {
102
+ handle.kill("SIGKILL");
103
+ this.assertNotCancelled();
104
+ }
105
+ this.handle = handle;
106
+ if (deferred)
107
+ this.receipt = createJobSecurityReceipt(this.securityPolicy, { jobId: this.options.jobId, generation: ctx.generation, modelId: ctx.resolvedModelId, sourceSha: String(source.base_commit_sha), ...ctx.securityBinding, requirements: ctx.approvedTaskPackage?.security_requirements });
108
+ }
109
+ catch {
110
+ const wasCancelled = this.cancelled;
111
+ await this.cancel();
112
+ throw new Error(wasCancelled ? "Codex launch cancelled during startup" : "Codex job setup failed before execution could be confirmed");
113
+ }
114
+ }
115
+ async *events() {
116
+ if (!this.handle)
117
+ throw new Error("Codex has not started");
118
+ try {
119
+ for await (const line of this.handle.stdout) {
120
+ if (this.cancelled)
121
+ return;
122
+ const events = this.parser.parse(line);
123
+ for (const event of events) {
124
+ if (["completed", "failed", "scope_conflict", "blocked"].includes(event.eventType)) {
125
+ await this.cancel();
126
+ yield event;
127
+ return;
128
+ }
129
+ yield event;
130
+ }
131
+ }
132
+ for (const event of this.parser.end())
133
+ yield event;
134
+ }
135
+ finally {
136
+ await this.cancel();
137
+ }
138
+ }
139
+ async cancel() {
140
+ this.cancelled = true;
141
+ try {
142
+ this.options.revokeSecurity?.();
143
+ }
144
+ finally {
145
+ try {
146
+ this.tools?.revoke();
147
+ }
148
+ finally {
149
+ this.startupAbort.abort();
150
+ try {
151
+ this.handle?.kill("SIGKILL");
152
+ }
153
+ finally {
154
+ await this.tools?.close();
155
+ }
156
+ }
157
+ }
158
+ }
159
+ assertNotCancelled() {
160
+ if (this.cancelled)
161
+ throw new Error("Codex launch cancelled during startup");
162
+ }
163
+ async command(command) {
164
+ if (["cancel", "pause", "request_replan"].includes(command.command))
165
+ return this.cancel();
166
+ if (command.command === "request_checkpoint")
167
+ return;
168
+ throw new Error("Codex cannot resume or change an active approved launch");
169
+ }
170
+ }
@@ -0,0 +1,84 @@
1
+ import { record } from './stdio-rpc.js';
2
+ /** Only provider completion/evidence events cross the runner boundary. */
3
+ export class CodexAppServerEvents {
4
+ threadId;
5
+ turnId;
6
+ usage;
7
+ stopped = false;
8
+ constructor(threadId, turnId) {
9
+ this.threadId = threadId;
10
+ this.turnId = turnId;
11
+ }
12
+ map(message) {
13
+ if (this.stopped)
14
+ return [];
15
+ const method = message.method;
16
+ if (method === 'error')
17
+ return [{ type: 'turn.failed' }];
18
+ const params = record(message.params ?? {});
19
+ const relevant = method === 'turn/completed' || method === 'turn/started' || method === 'thread/tokenUsage/updated' || method === 'item/started' || method === 'item/completed';
20
+ if (relevant) {
21
+ const turn = method === 'turn/completed' || method === 'turn/started' ? record(params.turn).id : params.turnId;
22
+ if (params.threadId !== this.threadId || turn !== this.turnId) {
23
+ this.stopped = true;
24
+ return [{ type: 'turn.failed' }];
25
+ }
26
+ }
27
+ if (method === 'thread/tokenUsage/updated') {
28
+ const last = record(record(params.tokenUsage).last);
29
+ this.usage = { input_tokens: last.inputTokens, cached_input_tokens: last.cachedInputTokens, output_tokens: last.outputTokens };
30
+ return [];
31
+ }
32
+ if (method === 'turn/completed') {
33
+ this.stopped = true;
34
+ if (record(params.turn).status !== 'completed')
35
+ return [{ type: 'turn.failed' }];
36
+ return [{ type: 'turn.completed', ...(this.usage ? { usage: this.usage, usage_source: 'codex_app_server.thread/tokenUsage/updated' } : {}) }];
37
+ }
38
+ if (method === 'item/started' || method === 'item/completed') {
39
+ const item = record(params.item);
40
+ const forbidden = { commandExecution: 'command_execution', fileChange: 'file_change', webSearch: 'web_search' };
41
+ if (typeof item.type === 'string' && forbidden[item.type])
42
+ return [{ type: 'item.started', item: { type: forbidden[item.type] } }];
43
+ if (method === 'item/completed' && item.type === 'agentMessage' && item.phase !== 'commentary') {
44
+ return [{ type: 'item.completed', item: { type: 'agent_message', text: item.text } }];
45
+ }
46
+ }
47
+ return [];
48
+ }
49
+ }
50
+ /** Bounded normalized-event buffer; no raw protocol/credentials are retained. */
51
+ export class CodexEventQueue {
52
+ lines = [];
53
+ bytes = 0;
54
+ done = false;
55
+ wake;
56
+ push(event) {
57
+ if (this.done)
58
+ return;
59
+ const line = JSON.stringify(event);
60
+ if (line.length > 256_000 || this.lines.length >= 128 || this.bytes + line.length > 1_000_000) {
61
+ this.lines = [JSON.stringify({ type: 'turn.failed' })];
62
+ this.bytes = this.lines[0].length;
63
+ this.done = true;
64
+ this.wake?.();
65
+ return;
66
+ }
67
+ this.lines.push(line);
68
+ this.bytes += line.length;
69
+ this.wake?.();
70
+ }
71
+ end() { this.done = true; this.wake?.(); }
72
+ async *[Symbol.asyncIterator]() {
73
+ while (true) {
74
+ while (this.lines.length) {
75
+ const line = this.lines.shift();
76
+ this.bytes -= line.length;
77
+ yield line;
78
+ }
79
+ if (this.done)
80
+ return;
81
+ await new Promise(resolve => { this.wake = resolve; });
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,45 @@
1
+ /** Candidate controls observed against the installed CLI using a loopback fixture.
2
+ * These are invocation inputs, not a certificate of compatibility. In particular,
3
+ * model metadata can still advertise apply_patch: an explicit deny profile and
4
+ * native-tool regression checks are required in addition to these switches.
5
+ */
6
+ export const CODEX_ISOLATION_EXEC_FLAGS = Object.freeze([
7
+ "--ignore-user-config", "--ignore-rules", "--ephemeral", "--strict-config", "--json",
8
+ ]);
9
+ export const CODEX_ISOLATION_CONFIG_OVERRIDES = Object.freeze([
10
+ "analytics.enabled=false",
11
+ "project_doc_max_bytes=0",
12
+ 'web_search="disabled"',
13
+ "features.shell_tool=false",
14
+ "features.multi_agent=false",
15
+ "features.hooks=false",
16
+ "features.apps=false",
17
+ "features.plugins=false",
18
+ "features.view_image=false",
19
+ "features.goals=false",
20
+ "features.browser_use=false",
21
+ "features.computer_use=false",
22
+ "features.in_app_browser=false",
23
+ "features.image_generation=false",
24
+ "features.workspace_dependencies=false",
25
+ "features.remote_plugin=false",
26
+ "features.skill_search=false",
27
+ "features.skill_mcp_dependency_install=false",
28
+ "features.skip_host_skill_discovery=true",
29
+ ]);
30
+ export const CODEX_UNSUPPORTED_DIAGNOSTIC = "Codex CLI isolation is unverified; this adapter is unsupported and cannot execute jobs.";
31
+ /**
32
+ * Admission is intentionally closed for every currently observed CLI version.
33
+ * Help flags, protocol schemas and sandbox subprocess fixtures do not establish
34
+ * native file-tool enforcement, isolated config loading, or bridge failure safety.
35
+ * Observations are not certificates: accepting caller-supplied booleans or hashes
36
+ * here would let an adapter self-attest to the authorization boundary it must meet.
37
+ * A supported path requires a reviewed verifier for all design controls, pinned to
38
+ * the actual binary, effective configuration and managed requirements. None exists.
39
+ */
40
+ export function inspectCodexCompatibility(_observation = {}) {
41
+ return {
42
+ status: "unsupported", available: false, executable: false,
43
+ capabilities: [], reason: CODEX_UNSUPPORTED_DIAGNOSTIC,
44
+ };
45
+ }
@@ -0,0 +1,40 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { CODEX_ISOLATION_EXEC_FLAGS, CODEX_ISOLATION_CONFIG_OVERRIDES } from "./compatibility.js";
3
+ import { codexControlDenials } from './scratch.js';
4
+ /** Candidate invocation; execution still requires independent compatibility admission. */
5
+ export function buildCodexInvocation(input) {
6
+ if (!input.modelId.trim() || /[\r\n\0]/.test(input.modelId)
7
+ || !isAbsolute(input.worktreeRoot) || input.sourceRoot !== undefined && !isAbsolute(input.sourceRoot) || !isAbsolute(input.schemaPath)
8
+ || !input.env.HOME || !isAbsolute(input.env.HOME))
9
+ throw new Error("Codex requires an exact model and absolute job/control/home paths");
10
+ const endpoint = new URL(input.endpoint.url);
11
+ if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port
12
+ || endpoint.pathname !== "/mcp" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash
13
+ || !input.endpoint.token || /[\r\n\0]/.test(input.endpoint.token))
14
+ throw new Error("Codex requires the authenticated job loopback endpoint");
15
+ const deniedFiles = codexControlDenials({ home: input.env.HOME, codexHome: input.env.CODEX_HOME, cwd: input.worktreeRoot, sourceRoot: input.sourceRoot, schemaPath: input.schemaPath, scratch: input.scratch }).map(file => `${JSON.stringify(file)}="deny"`).join(',');
16
+ const overrides = [
17
+ ...CODEX_ISOLATION_CONFIG_OVERRIDES,
18
+ ...(input.scratch ? [`log_dir=${JSON.stringify(input.scratch.logDir)}`, `sqlite_home=${JSON.stringify(input.scratch.sqliteHome)}`] : []),
19
+ 'forced_login_method="chatgpt"', 'model_provider="openai"', 'approval_policy="never"',
20
+ 'shell_environment_policy.inherit="none"',
21
+ 'default_permissions="local_job"',
22
+ `permissions.local_job.filesystem={":minimal"="read",${deniedFiles}}`,
23
+ "permissions.local_job.network.enabled=false",
24
+ // Only this job server is preauthorized: every operation is checked by Runner policy.
25
+ `mcp_servers={ai_local={url=${JSON.stringify(endpoint.toString())},bearer_token_env_var="AI_GATEWAY_JOB_MCP_TOKEN",required=true,default_tools_approval_mode="approve",enabled_tools=["local_read","local_search","local_edit","local_test"]}}`,
26
+ ];
27
+ const env = { HOME: input.env.HOME, AI_GATEWAY_JOB_MCP_TOKEN: input.endpoint.token };
28
+ for (const key of ["PATH", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "CODEX_HOME"]) {
29
+ if (input.env[key] !== undefined)
30
+ env[key] = input.env[key];
31
+ }
32
+ if (input.scratch)
33
+ env.TMPDIR = input.scratch.tmpDir;
34
+ return {
35
+ args: ["--no-daemon", "exec", ...CODEX_ISOLATION_EXEC_FLAGS,
36
+ ...overrides.flatMap((setting) => ["-c", setting]),
37
+ "--model", input.modelId, "-C", input.worktreeRoot, "--output-schema", input.schemaPath, "-"],
38
+ env,
39
+ };
40
+ }
@@ -0,0 +1,3 @@
1
+ import { diagnoseCliHostAdmission } from '../../safety/cli-host-admission.js';
2
+ /** Compatibility wrapper; all CLIs share the same closed release admission. */
3
+ export const diagnoseCodexHostAdmission = (binary, env) => diagnoseCliHostAdmission('codex_cli', binary, env);
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ const rejected = () => new Error('CODEX_INSPECTION_REJECTED');
3
+ function exact(value, keys) {
4
+ return !!value && typeof value === 'object' && !Array.isArray(value)
5
+ && Object.keys(value).sort().join(',') === [...keys].sort().join(',');
6
+ }
7
+ const text = (value) => typeof value === 'string' && !!value && value.length <= 256 && !/[\r\n\0]/.test(value);
8
+ /** No-model diagnostics only. This allowlist is not an isolation certificate or
9
+ * an authentication interface. The wrapped owned transport is never exposed. */
10
+ export class CodexInspectionRpc {
11
+ rpc;
12
+ #closed = false;
13
+ constructor(rpc) {
14
+ this.rpc = rpc;
15
+ }
16
+ async request(method, params) {
17
+ let allowed = false;
18
+ if (method === 'initialize' && exact(params, ['clientInfo', 'capabilities'])) {
19
+ const client = params.clientInfo, cap = params.capabilities;
20
+ allowed = exact(client, ['name', 'version']) && text(client.name) && text(client.version)
21
+ && exact(cap, ['experimentalApi']) && cap.experimentalApi === true;
22
+ }
23
+ else if (method === 'config/read' && exact(params, ['includeLayers', 'cwd'])) {
24
+ allowed = params.includeLayers === true && typeof params.cwd === 'string' && path.isAbsolute(params.cwd) && !/[\r\n\0]/.test(params.cwd);
25
+ }
26
+ else if (method === 'configRequirements/read')
27
+ allowed = exact(params, []);
28
+ else if (method === 'account/read')
29
+ allowed = exact(params, ['refreshToken']) && params.refreshToken === false;
30
+ if (this.#closed || !allowed)
31
+ throw rejected();
32
+ try {
33
+ return await this.rpc.request(method, structuredClone(params));
34
+ }
35
+ catch {
36
+ throw rejected();
37
+ }
38
+ }
39
+ initialized() { if (this.#closed)
40
+ throw rejected(); try {
41
+ this.rpc.notify('initialized');
42
+ }
43
+ catch {
44
+ throw rejected();
45
+ } }
46
+ close() { if (this.#closed)
47
+ return; this.#closed = true; this.rpc.close(); }
48
+ }
@@ -0,0 +1,189 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { CODEX_REPORT_SCHEMA } from './prompt-compiler.js';
3
+ const rejected = () => new Error('CODEX_JOB_RPC_REJECTED');
4
+ const exact = (value, keys) => !!value && typeof value === 'object' && !Array.isArray(value)
5
+ && isDeepStrictEqual(Object.keys(value).sort(), [...keys].sort());
6
+ const text = (v) => typeof v === 'string' && v.length > 0 && !/[\r\n\0]/.test(v);
7
+ /** Reject non-JSON authority before copying it; never invoke caller getters. */
8
+ function dataOnly(value, depth = 0, budget = { left: 10_000 }) {
9
+ if (--budget.left < 0 || depth > 32)
10
+ return false;
11
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
12
+ return true;
13
+ if (typeof value === 'number')
14
+ return Number.isFinite(value);
15
+ if (!value || typeof value !== 'object')
16
+ return false;
17
+ const array = Array.isArray(value);
18
+ if (!array && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
19
+ return false;
20
+ const descriptors = Object.getOwnPropertyDescriptors(value);
21
+ if (array && Object.keys(value).length !== value.length)
22
+ return false;
23
+ return Reflect.ownKeys(descriptors).every(key => {
24
+ if (typeof key !== 'string')
25
+ return false;
26
+ if (array && key === 'length')
27
+ return true;
28
+ const d = descriptors[key];
29
+ return d.enumerable === true && Object.hasOwn(d, 'value') && (!array || /^(0|[1-9]\d*)$/.test(key))
30
+ && dataOnly(d.value, depth + 1, budget);
31
+ });
32
+ }
33
+ /** Limits our owned control channel, not all vendor capabilities or account I/O.
34
+ * No caller receives the raw transport from this object. */
35
+ export class CodexJobRpc {
36
+ rpc;
37
+ #phase = 'new';
38
+ #threadId;
39
+ #cleanupFailed = false;
40
+ #requestPending = false;
41
+ #listeners = new Set();
42
+ #startup;
43
+ #schema = structuredClone(CODEX_REPORT_SCHEMA);
44
+ constructor(rpc, pin) {
45
+ this.rpc = rpc;
46
+ this.#startup = { cwd: pin.cwd, model: pin.model, modelProvider: pin.provider, ephemeral: true, approvalPolicy: 'never', permissions: 'local_job', allowProviderModelFallback: false, environments: [] };
47
+ rpc.onNotification(message => {
48
+ if (this.#phase === 'closed')
49
+ return;
50
+ const method = message.method;
51
+ if (typeof method !== 'string' || method.startsWith('thread/environment/') || method === 'thread/settings/updated') {
52
+ this.deny();
53
+ return;
54
+ }
55
+ for (const listener of this.#listeners) {
56
+ try {
57
+ listener(message);
58
+ }
59
+ catch {
60
+ this.deny();
61
+ return;
62
+ }
63
+ }
64
+ });
65
+ }
66
+ async request(method, params, timeoutMs) {
67
+ try {
68
+ if (this.#phase === 'closed' || this.#requestPending || !dataOnly(params))
69
+ throw rejected();
70
+ this.#requestPending = true;
71
+ const args = structuredClone(params);
72
+ if (method !== 'turn/start' && timeoutMs !== undefined && (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000))
73
+ throw rejected();
74
+ if (method === 'initialize') {
75
+ if (this.#phase !== 'new' || !isDeepStrictEqual(args, { clientInfo: { name: 'ai_gateway_local_codex', version: '1' }, capabilities: { experimentalApi: true } }))
76
+ throw rejected();
77
+ this.#phase = 'initializing';
78
+ }
79
+ else if (method === 'config/read') {
80
+ if (!this.inspectable() || !isDeepStrictEqual(args, { includeLayers: true, cwd: this.#startup.cwd }))
81
+ throw rejected();
82
+ }
83
+ else if (method === 'configRequirements/read') {
84
+ if (!this.inspectable() || !exact(args, []))
85
+ throw rejected();
86
+ }
87
+ else if (method === 'thread/start') {
88
+ if (this.#phase !== 'preparing' || !isDeepStrictEqual(args, this.#startup))
89
+ throw rejected();
90
+ this.#phase = 'startingThread';
91
+ }
92
+ else if (method === 'mcpServerStatus/list') {
93
+ if (this.#phase !== 'ready' || !isDeepStrictEqual(args, { threadId: this.#threadId, limit: 100, detail: 'full' }))
94
+ throw rejected();
95
+ }
96
+ else if (method === 'turn/start') {
97
+ if (this.#phase !== 'ready' || timeoutMs !== null || !exact(args, ['threadId', 'input', 'outputSchema']) || args.threadId !== this.#threadId
98
+ || !isDeepStrictEqual(args.outputSchema, this.#schema) || !Array.isArray(args.input) || args.input.length !== 1)
99
+ throw rejected();
100
+ const item = args.input[0];
101
+ if (!exact(item, ['type', 'text', 'text_elements']) || item.type !== 'text' || typeof item.text !== 'string' || !item.text || item.text.includes('\0')
102
+ || !isDeepStrictEqual(item.text_elements, []))
103
+ throw rejected();
104
+ this.#phase = 'startingTurn';
105
+ }
106
+ else
107
+ throw rejected();
108
+ const result = await this.rpc.request(method, args, timeoutMs);
109
+ // A concurrent violation, close or environment notification cannot revive the channel.
110
+ if (this.isClosed())
111
+ throw rejected();
112
+ if (method === 'initialize')
113
+ this.#phase = 'initialized';
114
+ if (method === 'thread/start') {
115
+ if (!result || typeof result !== 'object' || Array.isArray(result))
116
+ throw rejected();
117
+ const thread = result.thread;
118
+ if (!thread || typeof thread !== 'object' || Array.isArray(thread))
119
+ throw rejected();
120
+ const t = thread;
121
+ if (!text(t.id) || !isDeepStrictEqual(t.environments, []))
122
+ throw rejected();
123
+ this.#threadId = t.id;
124
+ this.#phase = 'ready';
125
+ }
126
+ if (method === 'turn/start')
127
+ this.#phase = 'running';
128
+ return result;
129
+ }
130
+ catch {
131
+ this.deny();
132
+ throw rejected();
133
+ }
134
+ finally {
135
+ this.#requestPending = false;
136
+ }
137
+ }
138
+ notify(method, params) {
139
+ if (this.#phase !== 'initialized' || method !== 'initialized' || params !== undefined) {
140
+ this.deny();
141
+ throw rejected();
142
+ }
143
+ this.#phase = 'preparing';
144
+ try {
145
+ this.rpc.notify('initialized');
146
+ }
147
+ catch {
148
+ this.deny();
149
+ throw rejected();
150
+ }
151
+ }
152
+ onNotification(listener) {
153
+ if (this.#phase === 'closed')
154
+ throw rejected();
155
+ this.#listeners.add(listener);
156
+ return () => this.#listeners.delete(listener);
157
+ }
158
+ close() {
159
+ if (this.#phase === 'closed')
160
+ return;
161
+ this.#phase = 'closed';
162
+ try {
163
+ this.rpc.close();
164
+ if (this.rpc.cleanupConfirmed?.() === false)
165
+ this.#cleanupFailed = true;
166
+ }
167
+ catch {
168
+ this.#cleanupFailed = true;
169
+ }
170
+ finally {
171
+ this.#listeners.clear();
172
+ }
173
+ }
174
+ cleanupConfirmed() { return !this.#cleanupFailed; }
175
+ isClosed() { return this.#phase === 'closed'; }
176
+ inspectable() { return this.#phase === 'preparing' || this.#phase === 'ready'; }
177
+ deny() {
178
+ if (this.#phase === 'closed')
179
+ return;
180
+ const listeners = [...this.#listeners];
181
+ this.close();
182
+ for (const listener of listeners) {
183
+ try {
184
+ listener({ method: 'error', params: { code: 'CODEX_JOB_RPC_REJECTED' } });
185
+ }
186
+ catch { /* Fixed errors only. */ }
187
+ }
188
+ }
189
+ }