agencyx-cli 0.0.0-stage → 0.1.0-dev.4

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 (39) hide show
  1. package/dist/adapters/agent.js +73 -0
  2. package/dist/adapters/ambient.js +35 -0
  3. package/dist/adapters/instructions.js +102 -0
  4. package/dist/adapters/mcp.js +237 -0
  5. package/dist/adapters/permissions.js +42 -0
  6. package/dist/adapters/plugins.js +484 -0
  7. package/dist/agency/invocation.js +97 -0
  8. package/dist/agency/prepared-input.js +124 -0
  9. package/dist/bridge/execution.js +119 -0
  10. package/dist/bridge/main.js +26 -0
  11. package/dist/bridge/plan.js +268 -0
  12. package/dist/bridge/version.js +2 -0
  13. package/dist/cli-options.js +96 -0
  14. package/dist/cli.js +41 -0
  15. package/dist/codex/agents.js +72 -0
  16. package/dist/codex/app-server.js +116 -0
  17. package/dist/codex/inspection.js +217 -0
  18. package/dist/codex/mcp-readiness.js +45 -0
  19. package/dist/codex/native-plugins.js +109 -0
  20. package/dist/codex/options.js +170 -0
  21. package/dist/codex/plugin-hooks.js +93 -0
  22. package/dist/codex/rpc.js +170 -0
  23. package/dist/codex/session-proxy.js +258 -0
  24. package/dist/codex/skill-policy.js +69 -0
  25. package/dist/codex/skill-views.js +104 -0
  26. package/dist/codex/toml.js +46 -0
  27. package/dist/commands/codex.js +118 -0
  28. package/dist/commands/doctor.js +58 -0
  29. package/dist/commands/passthrough.js +14 -0
  30. package/dist/diagnostics/plan.js +87 -0
  31. package/dist/diagnostics.js +39 -0
  32. package/dist/model.js +2 -0
  33. package/dist/runtime/ipc.js +316 -0
  34. package/dist/runtime/process.js +286 -0
  35. package/dist/runtime/resolve-command.js +38 -0
  36. package/dist/runtime/session.js +107 -0
  37. package/dist/validation.js +31 -0
  38. package/package.json +41 -4
  39. package/README.md +0 -3
@@ -0,0 +1,170 @@
1
+ import path from "node:path";
2
+ import { fail } from "../diagnostics.js";
3
+ import { configArguments, parseOverride } from "./toml.js";
4
+ const CONFIG_FLAGS = {
5
+ "--model": "model",
6
+ "-m": "model",
7
+ "--sandbox": "sandbox_mode",
8
+ "-s": "sandbox_mode",
9
+ "--ask-for-approval": "approval_policy",
10
+ "-a": "approval_policy",
11
+ };
12
+ const VALUE_FLAGS = new Set([
13
+ "--config",
14
+ "-c",
15
+ "--profile",
16
+ "-p",
17
+ "--model",
18
+ "-m",
19
+ "--sandbox",
20
+ "-s",
21
+ "--ask-for-approval",
22
+ "-a",
23
+ "--enable",
24
+ "--disable",
25
+ "--cd",
26
+ "-C",
27
+ "--add-dir",
28
+ "--image",
29
+ "-i",
30
+ "--output-last-message",
31
+ "-o",
32
+ "--output-schema",
33
+ "--color",
34
+ ]);
35
+ const SWITCHES = new Set([
36
+ "--no-alt-screen",
37
+ "--no-daemon",
38
+ "--search",
39
+ "--json",
40
+ "--ephemeral",
41
+ "--skip-git-repo-check",
42
+ "--help",
43
+ "-h",
44
+ "--version",
45
+ "-V",
46
+ "--dangerously-bypass-approvals-and-sandbox",
47
+ "--approve-for-me",
48
+ "--dangerously-bypass-hook-trust",
49
+ ]);
50
+ export function parseCodexOptions(args, cwd) {
51
+ const options = {
52
+ original: [...args],
53
+ configArgs: [],
54
+ entry: "interactive",
55
+ frontendArgs: [],
56
+ addDirs: [],
57
+ bypassHookTrust: false,
58
+ };
59
+ let promptCount = 0;
60
+ for (let i = 0; i < args.length; i++) {
61
+ const raw = args[i];
62
+ if (raw === "exec" && i === 0) {
63
+ options.entry = "exec";
64
+ continue;
65
+ }
66
+ if (!raw.startsWith("-") || raw === "-") {
67
+ if ([
68
+ "resume",
69
+ "fork",
70
+ "review",
71
+ "app-server",
72
+ "mcp",
73
+ "login",
74
+ "logout",
75
+ "plugin",
76
+ ].includes(raw) &&
77
+ promptCount === 0)
78
+ fail("CODEX_ENTRY_UNSUPPORTED", "arguments", "This wrapper currently supports new interactive sessions and the native exec entry.", {}, 2);
79
+ if (++promptCount > 1)
80
+ fail("CODEX_ARGUMENT_UNSUPPORTED", "arguments", "Use a single native prompt argument, or stdin with the native exec entry.", {}, 2);
81
+ options.frontendArgs.push(raw);
82
+ continue;
83
+ }
84
+ const equals = raw.indexOf("=");
85
+ const attachedShort = /^-[cmpisaC].+/.test(raw) && !raw.startsWith("--");
86
+ const flag = attachedShort
87
+ ? raw.slice(0, 2)
88
+ : equals < 0
89
+ ? raw
90
+ : raw.slice(0, equals);
91
+ const inline = attachedShort
92
+ ? raw.slice(2)
93
+ : equals < 0
94
+ ? undefined
95
+ : raw.slice(equals + 1);
96
+ if (VALUE_FLAGS.has(flag)) {
97
+ const value = inline ?? args[++i];
98
+ if (value === undefined || value.includes("\0"))
99
+ fail("CODEX_ARGUMENT_INVALID", "arguments", "A native option is missing a valid value.", { field: flag }, 2);
100
+ if (flag === "--config" || flag === "-c") {
101
+ parseOverride(value);
102
+ options.configArgs.push("-c", value);
103
+ }
104
+ else if (flag === "--profile" || flag === "-p") {
105
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
106
+ fail("CODEX_ARGUMENT_INVALID", "arguments", "The native Codex profile name contains unsupported characters.", { field: flag }, 2);
107
+ options.nativeProfile = value;
108
+ }
109
+ else if (CONFIG_FLAGS[flag])
110
+ options.configArgs.push(...configArguments({ [CONFIG_FLAGS[flag]]: value }));
111
+ else if (flag === "--enable" || flag === "--disable")
112
+ options.configArgs.push(...configArguments({ features: { [value]: flag === "--enable" } }));
113
+ else if (flag === "--cd" || flag === "-C") {
114
+ if (path.resolve(cwd, value) !== path.resolve(cwd))
115
+ fail("CWD_MISMATCH", "arguments", "Agency and Codex must resolve the same workspace. Run agencyx from the intended directory.", { field: flag }, 2);
116
+ }
117
+ else if (flag === "--add-dir")
118
+ options.addDirs.push(path.resolve(cwd, value));
119
+ else if (flag === "--image" || flag === "-i")
120
+ options.frontendArgs.push(flag, value);
121
+ }
122
+ else if (SWITCHES.has(flag) && inline === undefined) {
123
+ if (flag === "--search")
124
+ options.configArgs.push(...configArguments({ web_search: "live" }));
125
+ else if (flag === "--dangerously-bypass-approvals-and-sandbox")
126
+ options.configArgs.push(...configArguments({
127
+ sandbox_mode: "danger-full-access",
128
+ approval_policy: "never",
129
+ }));
130
+ else if (flag === "--approve-for-me")
131
+ options.configArgs.push(...configArguments({
132
+ sandbox_mode: "workspace-write",
133
+ approvals_reviewer: "auto_review",
134
+ }));
135
+ else if (flag === "--dangerously-bypass-hook-trust") {
136
+ options.bypassHookTrust = true;
137
+ options.configArgs.push(flag);
138
+ }
139
+ else if (flag === "--no-alt-screen")
140
+ options.frontendArgs.push(flag);
141
+ }
142
+ else {
143
+ fail("CODEX_ARGUMENT_UNSUPPORTED", "arguments", "This native option has not been validated with the wrapper configuration and session handoff.", { field: flag }, 2);
144
+ }
145
+ }
146
+ return options;
147
+ }
148
+ export function mergedOverrides(left, right) {
149
+ const result = { ...left };
150
+ for (const [key, value] of Object.entries(right)) {
151
+ const previous = Object.hasOwn(result, key) ? result[key] : undefined;
152
+ const next = typeof previous === "object" &&
153
+ previous !== null &&
154
+ !Array.isArray(previous) &&
155
+ typeof value === "object" &&
156
+ value !== null &&
157
+ !Array.isArray(value)
158
+ ? mergedOverrides(previous, value)
159
+ : value;
160
+ // Configuration names are data, including the literal key "__proto__".
161
+ Object.defineProperty(result, key, {
162
+ value: next,
163
+ enumerable: true,
164
+ configurable: true,
165
+ writable: true,
166
+ });
167
+ }
168
+ return result;
169
+ }
170
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ import { readJsonFile } from "../agency/prepared-input.js";
3
+ import { fail } from "../diagnostics.js";
4
+ import { isRecord, record } from "../validation.js";
5
+ export function sessionHookCommand(handler, resourceRoot, platform = process.platform) {
6
+ if (platform === "win32") {
7
+ const root = `'${resourceRoot.replaceAll("'", "''")}'`;
8
+ const command = handler.command_windows ?? handler.powershell ?? handler.command;
9
+ return `$env:CLAUDE_PLUGIN_ROOT = ${root}; $CLAUDE_PLUGIN_ROOT = $env:CLAUDE_PLUGIN_ROOT; $env:CODEX_PLUGIN_ROOT = $env:CLAUDE_PLUGIN_ROOT; $CODEX_PLUGIN_ROOT = $env:CLAUDE_PLUGIN_ROOT; ${String(command)}`;
10
+ }
11
+ const root = `'${resourceRoot.replaceAll("'", "'\\''")}'`;
12
+ return `export CLAUDE_PLUGIN_ROOT=${root} CODEX_PLUGIN_ROOT=${root}; ${String(handler.command)}`;
13
+ }
14
+ export async function prepareSessionHooks(packages, inspection) {
15
+ const bindings = [];
16
+ const groups = [];
17
+ if (!packages.some(({ spec }) => spec.hooks.some((hook) => hook.count > 0)))
18
+ return { bindings, overrides: {} };
19
+ if (isRecord(inspection.config?.features) &&
20
+ inspection.config.features.hooks === false)
21
+ fail("PLUGIN_HOOK_RUNTIME_DISABLED", "compatibility", "Native hooks are disabled in the effective Codex configuration.");
22
+ if (inspection.sessionOverrides === undefined)
23
+ fail("PLUGIN_HOOK_SOURCE_UNVERIFIED", "compatibility", "Codex must report its session configuration layer to preserve existing inline hooks.");
24
+ const previous = isRecord(inspection.sessionOverrides.hooks)
25
+ ? inspection.sessionOverrides.hooks.SessionStart
26
+ : undefined;
27
+ if (previous !== undefined) {
28
+ if (!Array.isArray(previous))
29
+ fail("PLUGIN_HOOK_SOURCE_UNVERIFIED", "compatibility", "The existing inline SessionStart hooks have an unsupported shape.");
30
+ groups.push(...previous.map((group) => record(group, "native inline hook group")));
31
+ }
32
+ for (const { spec, resourceRoot } of packages) {
33
+ for (const hook of spec.hooks) {
34
+ if (!hook.count)
35
+ continue;
36
+ const source = path.join(resourceRoot, path.relative(spec.root, hook.file));
37
+ const document = record(await readJsonFile(source), "plugin hooks");
38
+ const events = record(document.hooks, "plugin hook events");
39
+ for (const raw of events.SessionStart) {
40
+ const group = record(raw, "hook group");
41
+ const handlers = [];
42
+ for (const rawHandler of group.hooks) {
43
+ const handler = record(rawHandler, "hook handler");
44
+ const command = sessionHookCommand(handler, resourceRoot);
45
+ const { powershell: _powershell, command_windows: _windows, ...native } = handler;
46
+ handlers.push({ ...native, command });
47
+ bindings.push({
48
+ pluginName: spec.name,
49
+ sourcePath: hook.file,
50
+ command,
51
+ ...(typeof group.matcher === "string"
52
+ ? { matcher: group.matcher }
53
+ : {}),
54
+ });
55
+ }
56
+ groups.push({ ...group, hooks: handlers });
57
+ }
58
+ }
59
+ }
60
+ return { bindings, overrides: { hooks: { SessionStart: groups } } };
61
+ }
62
+ export function verifySessionHooks(bindings, inspection) {
63
+ const expected = new Map();
64
+ for (const binding of bindings) {
65
+ const key = JSON.stringify([binding.command, binding.matcher ?? null]);
66
+ const previous = expected.get(key);
67
+ expected.set(key, { binding, count: (previous?.count ?? 0) + 1 });
68
+ }
69
+ for (const { binding, count } of expected.values()) {
70
+ const hooks = inspection.hooks.filter((hook) => hook.pluginId === null &&
71
+ hook.eventName.toLowerCase() === "sessionstart" &&
72
+ hook.command === binding.command &&
73
+ (hook.matcher ?? null) === (binding.matcher ?? null));
74
+ if (hooks.length !== count || hooks.some((hook) => !hook.enabled))
75
+ fail("PLUGIN_HOOK_MISSING", "compatibility", "A required session hook was omitted, duplicated or disabled by native discovery.", { component: binding.pluginName, path: binding.sourcePath });
76
+ if (hooks.some((hook) => !["trusted", "managed"].includes(hook.trustStatus)))
77
+ fail("PLUGIN_HOOK_UNTRUSTED", "compatibility", "The required hook needs native Codex trust review. Review the hook first; an explicitly supplied native --dangerously-bypass-hook-trust applies only to that invocation. The wrapper never enables it automatically.", { component: binding.pluginName, path: binding.sourcePath });
78
+ }
79
+ }
80
+ export function hookTrustOverrides(inspection) {
81
+ const state = Object.create(null);
82
+ for (const hook of inspection.hooks) {
83
+ if (!hook.enabled ||
84
+ ["trusted", "managed"].includes(hook.trustStatus) ||
85
+ hook.isManaged)
86
+ continue;
87
+ if (!hook.key || !hook.currentHash)
88
+ fail("CODEX_HOOK_TRUST_OVERRIDE_UNSUPPORTED", "compatibility", "Codex did not report the native hook key and content hash required for an explicit session-only hook-trust override.");
89
+ state[hook.key] = { trusted_hash: hook.currentHash };
90
+ }
91
+ return Object.keys(state).length ? { hooks: { state } } : {};
92
+ }
93
+ //# sourceMappingURL=plugin-hooks.js.map
@@ -0,0 +1,170 @@
1
+ import { Duplex } from "node:stream";
2
+ import { EventEmitter } from "node:events";
3
+ import WebSocket from "ws";
4
+ import { AgencyxError } from "../diagnostics.js";
5
+ import { isRecord } from "../validation.js";
6
+ import { JsonLinePeer } from "../runtime/ipc.js";
7
+ export class RpcError extends AgencyxError {
8
+ rpcCode;
9
+ constructor(rpcCode, method) {
10
+ super({
11
+ code: rpcCode === -32601
12
+ ? "CODEX_PROTOCOL_UNSUPPORTED"
13
+ : "CODEX_REQUEST_FAILED",
14
+ stage: "codex",
15
+ reason: `The official app-server could not complete ${method}. Raw server error text was withheld.`,
16
+ }, 6);
17
+ this.rpcCode = rpcCode;
18
+ }
19
+ }
20
+ export class RpcClient extends EventEmitter {
21
+ connection;
22
+ nextId = 1;
23
+ pending = new Map();
24
+ closed = false;
25
+ constructor(connection) {
26
+ super();
27
+ this.connection = connection;
28
+ connection.events.on("message", (message) => this.receive(message));
29
+ connection.events.on("closed", () => this.disconnect());
30
+ connection.events.on("fault", () => this.disconnect());
31
+ }
32
+ receive(message) {
33
+ if (!isRecord(message)) {
34
+ this.disconnect();
35
+ return;
36
+ }
37
+ if (typeof message.method === "string") {
38
+ if (message.id !== undefined) {
39
+ // Never invent an approval decision while the original frontend is absent.
40
+ this.connection.send({
41
+ id: message.id,
42
+ error: {
43
+ code: -32601,
44
+ message: "No interactive frontend is attached to this preparation client.",
45
+ },
46
+ });
47
+ }
48
+ else
49
+ this.emit("notification", message);
50
+ return;
51
+ }
52
+ if (typeof message.id !== "number")
53
+ return;
54
+ const request = this.pending.get(message.id);
55
+ if (!request)
56
+ return;
57
+ clearTimeout(request.timer);
58
+ this.pending.delete(message.id);
59
+ if (isRecord(message.error))
60
+ request.reject(new RpcError(typeof message.error.code === "number"
61
+ ? message.error.code
62
+ : undefined, "request"));
63
+ else if (Object.hasOwn(message, "result"))
64
+ request.resolve(message.result);
65
+ else
66
+ request.reject(new RpcError(undefined, "malformed response"));
67
+ }
68
+ request(method, params, timeoutMs = 15_000) {
69
+ if (this.closed)
70
+ return Promise.reject(new RpcError(undefined, method));
71
+ const id = this.nextId++;
72
+ return new Promise((resolve, reject) => {
73
+ const timer = setTimeout(() => {
74
+ this.pending.delete(id);
75
+ reject(new AgencyxError({
76
+ code: "CODEX_REQUEST_TIMEOUT",
77
+ stage: "codex",
78
+ reason: `The official app-server did not answer ${method} in time.`,
79
+ }, 6));
80
+ }, timeoutMs);
81
+ this.pending.set(id, { resolve, reject, timer });
82
+ try {
83
+ this.connection.send({ id, method, params });
84
+ }
85
+ catch {
86
+ clearTimeout(timer);
87
+ this.pending.delete(id);
88
+ reject(new RpcError(undefined, method));
89
+ }
90
+ });
91
+ }
92
+ notify(method, params) {
93
+ if (!this.closed)
94
+ this.connection.send({ method, params });
95
+ }
96
+ async initialize() {
97
+ const result = await this.request("initialize", {
98
+ clientInfo: { name: "agencyx", version: "0.1.0" },
99
+ capabilities: { experimentalApi: true },
100
+ });
101
+ this.notify("initialized", {});
102
+ return result;
103
+ }
104
+ disconnect() {
105
+ if (this.closed)
106
+ return;
107
+ this.closed = true;
108
+ for (const request of this.pending.values()) {
109
+ clearTimeout(request.timer);
110
+ request.reject(new RpcError(undefined, "connection"));
111
+ }
112
+ this.pending.clear();
113
+ this.emit("closed");
114
+ }
115
+ close() {
116
+ this.disconnect();
117
+ this.connection.close();
118
+ }
119
+ }
120
+ export function stdioClient(running) {
121
+ const duplex = Duplex.from({
122
+ readable: running.child.stdout,
123
+ writable: running.child.stdin,
124
+ });
125
+ const peer = new JsonLinePeer(duplex);
126
+ void running.completion.then(() => duplex.destroy(), () => duplex.destroy());
127
+ return new RpcClient({
128
+ events: peer,
129
+ send: (value) => peer.send(value),
130
+ close: () => duplex.destroy(),
131
+ });
132
+ }
133
+ export async function websocketClient(endpoint, token, timeoutMs = 2_000) {
134
+ const socket = new WebSocket(endpoint, {
135
+ headers: { Authorization: `Bearer ${token}` },
136
+ handshakeTimeout: timeoutMs,
137
+ maxPayload: 16 * 1024 * 1024,
138
+ });
139
+ const events = new EventEmitter();
140
+ socket.on("error", () => events.emit("fault"));
141
+ socket.on("close", () => events.emit("closed"));
142
+ socket.on("message", (data) => {
143
+ try {
144
+ events.emit("message", JSON.parse(data.toString()));
145
+ }
146
+ catch {
147
+ events.emit("fault");
148
+ socket.terminate();
149
+ }
150
+ });
151
+ await new Promise((resolve, reject) => {
152
+ const onError = () => {
153
+ socket.off("open", onOpen);
154
+ socket.terminate();
155
+ reject(new RpcError(undefined, "WebSocket connection"));
156
+ };
157
+ const onOpen = () => {
158
+ socket.off("error", onError);
159
+ resolve();
160
+ };
161
+ socket.once("open", onOpen);
162
+ socket.once("error", onError);
163
+ });
164
+ return new RpcClient({
165
+ events,
166
+ send: (value) => socket.send(JSON.stringify(value)),
167
+ close: () => socket.close(),
168
+ });
169
+ }
170
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1,258 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { timingSafeEqual } from "node:crypto";
4
+ import WebSocket, { WebSocketServer } from "ws";
5
+ import { asDiagnostic, fail } from "../diagnostics.js";
6
+ import { isRecord } from "../validation.js";
7
+ import { combineInstructions } from "../adapters/instructions.js";
8
+ import { mergedOverrides } from "./options.js";
9
+ import { assertUserSkillInvocation, filterUserSkillCatalogue, } from "./skill-policy.js";
10
+ const MAX_BUFFER = 16 * 1024 * 1024;
11
+ async function sameDirectory(left, right) {
12
+ const [a, b] = await Promise.all([
13
+ realpath(left).catch(() => path.resolve(left)),
14
+ realpath(right).catch(() => path.resolve(right)),
15
+ ]);
16
+ return process.platform === "win32"
17
+ ? a.toLowerCase() === b.toLowerCase()
18
+ : a === b;
19
+ }
20
+ export async function injectSessionRequest(value, plan) {
21
+ if (!isRecord(value))
22
+ fail("CODEX_PROTOCOL_INVALID", "codex", "The frontend sent an invalid protocol message.", {}, 6);
23
+ assertUserSkillInvocation(value, plan.skillViews ?? []);
24
+ if (value.method === "skills/extraRoots/set" &&
25
+ (plan.skillViews?.length ?? 0) > 0) {
26
+ if (!isRecord(value.params) ||
27
+ !Array.isArray(value.params.extraRoots) ||
28
+ !value.params.extraRoots.every((root) => typeof root === "string"))
29
+ fail("CODEX_PROTOCOL_INVALID", "codex", "The frontend sent invalid native skill roots.", {}, 6);
30
+ return {
31
+ ...value,
32
+ params: {
33
+ ...value.params,
34
+ extraRoots: [
35
+ ...new Set([
36
+ ...value.params.extraRoots,
37
+ ...plan.skillViews.flatMap((view) => view.extraRoots),
38
+ ]),
39
+ ],
40
+ },
41
+ };
42
+ }
43
+ if (value.method === "thread/resume" || value.method === "thread/fork")
44
+ fail("CODEX_ENTRY_UNSUPPORTED", "compatibility", "This Agency session supports new Codex threads. Resume and fork require a separately verified resource contract.");
45
+ if (value.method !== "thread/start")
46
+ return value;
47
+ const params = value.params === null || value.params === undefined ? {} : value.params;
48
+ if (!isRecord(params) ||
49
+ (params.config !== undefined &&
50
+ params.config !== null &&
51
+ !isRecord(params.config)))
52
+ fail("CODEX_PROTOCOL_INVALID", "codex", "The frontend sent invalid thread configuration.", {}, 6);
53
+ if (params.cwd !== undefined &&
54
+ params.cwd !== null &&
55
+ (typeof params.cwd !== "string" ||
56
+ !(await sameDirectory(params.cwd, plan.cwd))))
57
+ fail("CWD_MISMATCH", "compatibility", "A new thread cannot change the workspace after Agency prepared its resources.");
58
+ const next = {
59
+ ...params,
60
+ cwd: plan.cwd,
61
+ config: mergedOverrides(isRecord(params.config) ? params.config : {}, plan.overrides),
62
+ };
63
+ if (plan.developerInstructions !== undefined) {
64
+ if (params.developerInstructions !== undefined &&
65
+ params.developerInstructions !== null &&
66
+ typeof params.developerInstructions !== "string")
67
+ fail("CODEX_PROTOCOL_INVALID", "codex", "The frontend sent invalid developer instructions.", {}, 6);
68
+ const instructions = typeof params.developerInstructions === "string"
69
+ ? combineInstructions(params.developerInstructions, plan.additionalDeveloperInstructions
70
+ ? [plan.additionalDeveloperInstructions]
71
+ : [], {})
72
+ : plan.developerInstructions;
73
+ if (instructions !== undefined)
74
+ Object.assign(next, { developerInstructions: instructions });
75
+ }
76
+ return { ...value, params: next };
77
+ }
78
+ export async function createSessionProxy(backendEndpoint, backendToken, frontendToken, plan) {
79
+ let failure;
80
+ let active = false;
81
+ const connections = new Set();
82
+ const expected = Buffer.from(`Bearer ${frontendToken}`);
83
+ const server = new WebSocketServer({
84
+ host: "127.0.0.1",
85
+ port: 0,
86
+ maxPayload: MAX_BUFFER,
87
+ verifyClient: ({ req }) => {
88
+ const actual = Buffer.from(req.headers.authorization ?? "");
89
+ return (!req.headers.origin &&
90
+ actual.length === expected.length &&
91
+ timingSafeEqual(actual, expected));
92
+ },
93
+ });
94
+ await new Promise((resolve, reject) => {
95
+ server.once("listening", resolve);
96
+ server.once("error", reject);
97
+ });
98
+ server.on("error", () => {
99
+ failure = {
100
+ code: "CODEX_PROXY_FAILED",
101
+ stage: "codex",
102
+ reason: "The private frontend connection failed.",
103
+ };
104
+ });
105
+ server.on("connection", (frontend) => {
106
+ if (active) {
107
+ frontend.close(1008, "One frontend per Agency session.");
108
+ return;
109
+ }
110
+ active = true;
111
+ connections.add(frontend);
112
+ const backend = new WebSocket(backendEndpoint, {
113
+ headers: { Authorization: `Bearer ${backendToken}` },
114
+ maxPayload: MAX_BUFFER,
115
+ handshakeTimeout: 5_000,
116
+ });
117
+ connections.add(backend);
118
+ const skillListRequests = new Set();
119
+ const opened = new Promise((resolve, reject) => {
120
+ backend.once("open", resolve);
121
+ backend.once("error", reject);
122
+ });
123
+ void opened.catch(() => undefined);
124
+ const end = () => {
125
+ frontend.terminate();
126
+ backend.terminate();
127
+ connections.delete(frontend);
128
+ connections.delete(backend);
129
+ };
130
+ frontend.on("error", end);
131
+ backend.on("error", () => {
132
+ failure = {
133
+ code: "CODEX_BACKEND_CONNECTION_FAILED",
134
+ stage: "codex",
135
+ reason: "The private backend connection failed.",
136
+ };
137
+ end();
138
+ });
139
+ frontend.on("close", end);
140
+ backend.on("close", end);
141
+ backend.on("message", (data, binary) => {
142
+ if (frontend.readyState !== WebSocket.OPEN)
143
+ return;
144
+ if (frontend.bufferedAmount > MAX_BUFFER) {
145
+ failure = {
146
+ code: "CODEX_PROXY_BUFFER_LIMIT",
147
+ stage: "codex",
148
+ reason: "The frontend exceeded its bounded transport buffer.",
149
+ };
150
+ end();
151
+ return;
152
+ }
153
+ // Only frontend skill-menu responses are adapted. Backend discovery,
154
+ // approvals, model events and tool results retain their native data.
155
+ let outgoing = data;
156
+ if (!binary && skillListRequests.size) {
157
+ let response;
158
+ try {
159
+ response = JSON.parse(data.toString());
160
+ }
161
+ catch {
162
+ response = undefined;
163
+ }
164
+ if (isRecord(response) &&
165
+ (typeof response.id === "number" ||
166
+ typeof response.id === "string") &&
167
+ skillListRequests.delete(response.id)) {
168
+ const filtered = filterUserSkillCatalogue(response, plan.skillViews ?? []);
169
+ if (filtered !== response)
170
+ outgoing = Buffer.from(JSON.stringify(filtered));
171
+ }
172
+ }
173
+ frontend.send(outgoing, { binary });
174
+ });
175
+ let ordered = Promise.resolve();
176
+ let queuedBytes = 0;
177
+ frontend.on("message", (data, binary) => {
178
+ const bytes = Buffer.byteLength(data.toString());
179
+ queuedBytes += bytes;
180
+ if (queuedBytes > MAX_BUFFER) {
181
+ failure = {
182
+ code: "CODEX_PROXY_BUFFER_LIMIT",
183
+ stage: "codex",
184
+ reason: "The frontend exceeded its bounded request queue.",
185
+ };
186
+ end();
187
+ return;
188
+ }
189
+ ordered = ordered
190
+ .then(async () => {
191
+ await opened;
192
+ if (binary)
193
+ fail("CODEX_PROTOCOL_INVALID", "codex", "Binary protocol frames are not supported.", {}, 6);
194
+ let value;
195
+ try {
196
+ value = JSON.parse(data.toString());
197
+ }
198
+ catch {
199
+ fail("CODEX_PROTOCOL_INVALID", "codex", "The frontend sent invalid JSON.", {}, 6);
200
+ }
201
+ try {
202
+ const injected = await injectSessionRequest(value, plan);
203
+ if (isRecord(value) &&
204
+ value.method === "skills/list" &&
205
+ (typeof value.id === "number" || typeof value.id === "string"))
206
+ skillListRequests.add(value.id);
207
+ if (backend.bufferedAmount > MAX_BUFFER)
208
+ fail("CODEX_PROXY_BUFFER_LIMIT", "codex", "The backend exceeded its bounded transport buffer.", {}, 6);
209
+ if (backend.readyState === WebSocket.OPEN)
210
+ backend.send(injected === value ? data : JSON.stringify(injected), { binary: false });
211
+ }
212
+ catch (error) {
213
+ const diagnostic = asDiagnostic(error);
214
+ if (isRecord(value) &&
215
+ value.id !== undefined &&
216
+ frontend.readyState === WebSocket.OPEN)
217
+ frontend.send(JSON.stringify({
218
+ id: value.id,
219
+ error: {
220
+ code: -32602,
221
+ message: `${diagnostic.code}: ${diagnostic.reason}`,
222
+ },
223
+ }));
224
+ else {
225
+ failure = diagnostic;
226
+ end();
227
+ }
228
+ }
229
+ })
230
+ .catch(() => {
231
+ failure = {
232
+ code: "CODEX_PROXY_FAILED",
233
+ stage: "codex",
234
+ reason: "The private frontend protocol connection failed.",
235
+ };
236
+ end();
237
+ })
238
+ .finally(() => {
239
+ queuedBytes -= bytes;
240
+ });
241
+ });
242
+ });
243
+ const address = server.address();
244
+ if (!address || typeof address === "string") {
245
+ server.close();
246
+ fail("BACKEND_ADDRESS_INVALID", "codex", "The private frontend proxy did not return a loopback address.", {}, 6);
247
+ }
248
+ return {
249
+ endpoint: `ws://127.0.0.1:${address.port}`,
250
+ failure: () => failure,
251
+ close: async () => {
252
+ for (const connection of connections)
253
+ connection.terminate();
254
+ await new Promise((resolve) => server.close(() => resolve()));
255
+ },
256
+ };
257
+ }
258
+ //# sourceMappingURL=session-proxy.js.map