@ctliz/agent-intercom-codex 0.11.0-connect.2
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/.codex-plugin/plugin.json +39 -0
- package/.mcp.json +10 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +572 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/assets/logo-generated.png +0 -0
- package/assets/logo.svg +22 -0
- package/boss-control-outbox.ts +195 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +464 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +54 -0
- package/broker/boss-adapter.ts +531 -0
- package/broker/boss-control-ledger.ts +306 -0
- package/broker/broker.ts +2327 -0
- package/broker/client.ts +1111 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +453 -0
- package/codex/app-server-client.ts +562 -0
- package/codex/boss-client.ts +34 -0
- package/codex/bridge-config.ts +282 -0
- package/codex/bridge-daemon.ts +913 -0
- package/codex/clipboard.ts +142 -0
- package/codex/coi.ts +802 -0
- package/codex/contact.ts +54 -0
- package/codex/mcp-protocol.ts +248 -0
- package/codex/runtime.ts +488 -0
- package/codex/server.ts +60 -0
- package/codex/team.ts +249 -0
- package/codex/tui-input.ts +174 -0
- package/config.ts +195 -0
- package/dist/bridge-daemon.mjs +3781 -0
- package/dist/broker.mjs +3012 -0
- package/dist/build-info.json +12 -0
- package/dist/codex-server.mjs +2890 -0
- package/dist/coi.mjs +4686 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +89 -0
- package/protocol-v4/contract.ts +27 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build-info.mjs +58 -0
- package/scripts/build.mjs +55 -0
- package/skills/codex-intercom/SKILL.md +104 -0
- package/tsconfig.json +16 -0
- package/types.ts +225 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { assertHardenedBossProviderAuthority, type HardenedBossClientKind } from "./boss-client.ts";
|
|
8
|
+
|
|
9
|
+
export interface JsonRpcMessage {
|
|
10
|
+
id?: string | number | null;
|
|
11
|
+
method?: string;
|
|
12
|
+
params?: unknown;
|
|
13
|
+
result?: unknown;
|
|
14
|
+
error?: { code: number; message: string; data?: unknown };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CodexAppServerClientOptions {
|
|
18
|
+
command?: string;
|
|
19
|
+
args?: string[];
|
|
20
|
+
transport?: "stdio" | "unix-websocket";
|
|
21
|
+
socketPath?: string;
|
|
22
|
+
serverRequestHandler?: (message: JsonRpcMessage) => unknown | Promise<unknown>;
|
|
23
|
+
startDaemon?: boolean;
|
|
24
|
+
startDaemonCommand?: string;
|
|
25
|
+
startDaemonArgs?: string[];
|
|
26
|
+
requestTimeoutMs?: number;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PendingRequest {
|
|
31
|
+
resolve: (value: unknown) => void;
|
|
32
|
+
reject: (error: Error) => void;
|
|
33
|
+
timeout: NodeJS.Timeout;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
|
|
37
|
+
const MAX_WEBSOCKET_MESSAGE_BYTES = 16 * 1024 * 1024;
|
|
38
|
+
const UNIX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
39
|
+
|
|
40
|
+
export interface DecodedWebSocketFrame {
|
|
41
|
+
opcode: number;
|
|
42
|
+
payload: Buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asError(error: unknown): Error {
|
|
46
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function defaultServerRequestResponse(method: string): unknown {
|
|
50
|
+
switch (method) {
|
|
51
|
+
case "item/commandExecution/requestApproval":
|
|
52
|
+
return { decision: "decline" };
|
|
53
|
+
case "item/fileChange/requestApproval":
|
|
54
|
+
return { decision: "decline" };
|
|
55
|
+
case "item/permissions/requestApproval":
|
|
56
|
+
return { permissions: {}, scope: "turn", strictAutoReview: true };
|
|
57
|
+
case "item/tool/requestUserInput":
|
|
58
|
+
return { answers: {} };
|
|
59
|
+
case "mcpServer/elicitation/request":
|
|
60
|
+
return { action: "decline", content: null, _meta: null };
|
|
61
|
+
case "item/tool/call":
|
|
62
|
+
return { contentItems: [{ type: "text", text: "Background bridge declined tool call." }], success: false };
|
|
63
|
+
case "applyPatchApproval":
|
|
64
|
+
case "execCommandApproval":
|
|
65
|
+
return { decision: "denied" };
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`Unsupported app-server request: ${method}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class CodexAppServerClient extends EventEmitter {
|
|
72
|
+
private proc: ChildProcessWithoutNullStreams | null = null;
|
|
73
|
+
private socket: net.Socket | null = null;
|
|
74
|
+
private wsDecoder = new WebSocketFrameDecoder();
|
|
75
|
+
private rl: readline.Interface | null = null;
|
|
76
|
+
private nextId = 1;
|
|
77
|
+
private pending = new Map<string | number, PendingRequest>();
|
|
78
|
+
private initialized = false;
|
|
79
|
+
private options: Required<CodexAppServerClientOptions>;
|
|
80
|
+
|
|
81
|
+
constructor(options: CodexAppServerClientOptions = {}, protectedBossClient?: HardenedBossClientKind) {
|
|
82
|
+
super();
|
|
83
|
+
// Keep the provider ceiling at the lowest shared process boundary too.
|
|
84
|
+
// The second argument is deny-only: it cannot convey provider authority.
|
|
85
|
+
assertHardenedBossProviderAuthority(protectedBossClient);
|
|
86
|
+
this.options = {
|
|
87
|
+
command: options.command ?? "codex",
|
|
88
|
+
args: options.args ?? ["app-server"],
|
|
89
|
+
transport: options.transport ?? "stdio",
|
|
90
|
+
socketPath: options.socketPath ?? "",
|
|
91
|
+
serverRequestHandler: options.serverRequestHandler ?? defaultServerRequestResponseFromMessage,
|
|
92
|
+
startDaemon: options.startDaemon ?? false,
|
|
93
|
+
startDaemonCommand: options.startDaemonCommand ?? "codex",
|
|
94
|
+
startDaemonArgs: options.startDaemonArgs ?? ["app-server", "daemon", "start"],
|
|
95
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
96
|
+
env: options.env ?? process.env,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setServerRequestHandler(handler: (message: JsonRpcMessage) => unknown | Promise<unknown>): void {
|
|
101
|
+
this.options.serverRequestHandler = handler;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async connect(): Promise<void> {
|
|
105
|
+
if (this.proc) return;
|
|
106
|
+
|
|
107
|
+
if (this.options.startDaemon) {
|
|
108
|
+
const started = spawnSync(this.options.startDaemonCommand, this.options.startDaemonArgs, {
|
|
109
|
+
encoding: "utf8",
|
|
110
|
+
env: this.options.env,
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
if (started.status !== 0) {
|
|
114
|
+
throw new Error(`Failed to start Codex app-server daemon: ${started.stderr || started.stdout || `exit ${started.status}`}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.options.transport === "unix-websocket") {
|
|
119
|
+
await this.connectUnixWebSocket();
|
|
120
|
+
await this.initialize();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const proc = spawn(this.options.command, this.options.args, {
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
126
|
+
env: this.options.env,
|
|
127
|
+
});
|
|
128
|
+
this.proc = proc;
|
|
129
|
+
this.rl = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
130
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
131
|
+
proc.stderr.on("data", (chunk) => this.emit("stderr", String(chunk)));
|
|
132
|
+
proc.once("error", (error) => this.failAll(asError(error)));
|
|
133
|
+
proc.once("exit", (code, signal) => {
|
|
134
|
+
this.failAll(new Error(`Codex app-server proxy exited (${signal ?? code ?? "unknown"})`));
|
|
135
|
+
this.proc = null;
|
|
136
|
+
this.initialized = false;
|
|
137
|
+
this.emit("exit", { code, signal });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await this.initialize();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async disconnect(): Promise<void> {
|
|
144
|
+
const socket = this.socket;
|
|
145
|
+
if (socket) {
|
|
146
|
+
this.socket = null;
|
|
147
|
+
this.initialized = false;
|
|
148
|
+
this.failAll(new Error("Codex app-server client disconnected"));
|
|
149
|
+
await new Promise<void>((resolve) => {
|
|
150
|
+
const timeout = setTimeout(() => {
|
|
151
|
+
socket.destroy();
|
|
152
|
+
resolve();
|
|
153
|
+
}, 1000);
|
|
154
|
+
socket.once("close", () => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
if (!socket.destroyed) {
|
|
159
|
+
this.writeWebSocketFrame(0x8, Buffer.alloc(0));
|
|
160
|
+
socket.end();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const proc = this.proc;
|
|
167
|
+
this.rl?.close();
|
|
168
|
+
this.rl = null;
|
|
169
|
+
this.proc = null;
|
|
170
|
+
this.initialized = false;
|
|
171
|
+
this.failAll(new Error("Codex app-server client disconnected"));
|
|
172
|
+
if (!proc) return;
|
|
173
|
+
|
|
174
|
+
await new Promise<void>((resolve) => {
|
|
175
|
+
const timeout = setTimeout(() => {
|
|
176
|
+
proc.kill("SIGKILL");
|
|
177
|
+
resolve();
|
|
178
|
+
}, 2000);
|
|
179
|
+
proc.once("exit", () => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
resolve();
|
|
182
|
+
});
|
|
183
|
+
proc.stdin.end();
|
|
184
|
+
proc.kill("SIGTERM");
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async initialize(): Promise<void> {
|
|
189
|
+
if (this.initialized) return;
|
|
190
|
+
await this.request("initialize", {
|
|
191
|
+
clientInfo: {
|
|
192
|
+
name: "codex_intercom_bridge",
|
|
193
|
+
title: "Codex Intercom Bridge",
|
|
194
|
+
version: "0.1.0",
|
|
195
|
+
},
|
|
196
|
+
capabilities: { experimentalApi: true },
|
|
197
|
+
});
|
|
198
|
+
this.notify("initialized", {});
|
|
199
|
+
this.initialized = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
request(method: string, params?: unknown, timeoutMs = this.options.requestTimeoutMs): Promise<unknown> {
|
|
203
|
+
if (!this.canWrite()) {
|
|
204
|
+
return Promise.reject(new Error("Codex app-server client is not connected"));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const id = this.nextId++;
|
|
208
|
+
const payload = params === undefined ? { id, method } : { id, method, params };
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const timeout = setTimeout(() => {
|
|
211
|
+
this.pending.delete(id);
|
|
212
|
+
reject(new Error(`Codex app-server request timed out: ${method}`));
|
|
213
|
+
}, timeoutMs);
|
|
214
|
+
this.pending.set(id, { resolve, reject, timeout });
|
|
215
|
+
this.writePayload(payload);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
notify(method: string, params?: unknown): void {
|
|
220
|
+
if (!this.canWrite()) {
|
|
221
|
+
throw new Error("Codex app-server client is not connected");
|
|
222
|
+
}
|
|
223
|
+
const payload = params === undefined ? { method } : { method, params };
|
|
224
|
+
this.writePayload(payload);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private respond(id: string | number | null | undefined, result: unknown): void {
|
|
228
|
+
if (id === undefined || id === null) return;
|
|
229
|
+
this.writePayload({ id, result });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private respondError(id: string | number | null | undefined, code: number, message: string): void {
|
|
233
|
+
if (id === undefined || id === null) return;
|
|
234
|
+
this.writePayload({ id, error: { code, message } });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private canWrite(): boolean {
|
|
238
|
+
if (this.options.transport === "unix-websocket") {
|
|
239
|
+
return Boolean(this.socket && !this.socket.destroyed && this.socket.writable);
|
|
240
|
+
}
|
|
241
|
+
return Boolean(this.proc && this.proc.stdin.writable);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private writePayload(payload: unknown): void {
|
|
245
|
+
const json = JSON.stringify(payload);
|
|
246
|
+
if (this.options.transport === "unix-websocket") {
|
|
247
|
+
this.writeWebSocketFrame(0x1, Buffer.from(json, "utf8"));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.proc?.stdin.write(`${json}\n`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async connectUnixWebSocket(): Promise<void> {
|
|
254
|
+
const deadline = Date.now() + UNIX_WEBSOCKET_CONNECT_TIMEOUT_MS;
|
|
255
|
+
let lastError: Error | null = null;
|
|
256
|
+
let attempt = 0;
|
|
257
|
+
|
|
258
|
+
while (Date.now() < deadline) {
|
|
259
|
+
try {
|
|
260
|
+
await this.connectUnixWebSocketOnce();
|
|
261
|
+
return;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
lastError = asError(error);
|
|
264
|
+
this.socket?.destroy();
|
|
265
|
+
this.socket = null;
|
|
266
|
+
const code = (lastError as NodeJS.ErrnoException).code;
|
|
267
|
+
if (code !== "ENOENT" && code !== "ECONNREFUSED") throw lastError;
|
|
268
|
+
const backoffMs = Math.min(250, 25 * (2 ** attempt));
|
|
269
|
+
attempt += 1;
|
|
270
|
+
await delay(backoffMs);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw new Error(`Codex app-server WebSocket did not become ready within ${Math.round(UNIX_WEBSOCKET_CONNECT_TIMEOUT_MS / 1000)} seconds${lastError ? `: ${lastError.message}` : ""}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private connectUnixWebSocketOnce(): Promise<void> {
|
|
278
|
+
const socketPath = this.options.socketPath;
|
|
279
|
+
if (!socketPath) return Promise.reject(new Error("socketPath is required for unix-websocket transport"));
|
|
280
|
+
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const key = randomBytes(16).toString("base64");
|
|
283
|
+
const expectedAccept = createHash("sha1")
|
|
284
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
285
|
+
.digest("base64");
|
|
286
|
+
const socket = net.createConnection(socketPath);
|
|
287
|
+
this.socket = socket;
|
|
288
|
+
this.wsDecoder = new WebSocketFrameDecoder();
|
|
289
|
+
let handshake = Buffer.alloc(0);
|
|
290
|
+
let settled = false;
|
|
291
|
+
|
|
292
|
+
const fail = (error: Error) => {
|
|
293
|
+
if (!settled) {
|
|
294
|
+
settled = true;
|
|
295
|
+
cleanupHandshake();
|
|
296
|
+
reject(error);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.failAll(error);
|
|
300
|
+
this.emit("exit", { code: null, signal: null });
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const onData = (chunk: Buffer) => {
|
|
304
|
+
if (settled) {
|
|
305
|
+
this.handleWebSocketData(chunk);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
handshake = Buffer.concat([handshake, chunk]);
|
|
310
|
+
const headerEnd = handshake.indexOf("\r\n\r\n");
|
|
311
|
+
if (headerEnd < 0) return;
|
|
312
|
+
|
|
313
|
+
const head = handshake.subarray(0, headerEnd).toString("utf8");
|
|
314
|
+
const rest = handshake.subarray(headerEnd + 4);
|
|
315
|
+
if (!/^HTTP\/1\.1 101\b/im.test(head) || !head.toLowerCase().includes(`sec-websocket-accept: ${expectedAccept.toLowerCase()}`)) {
|
|
316
|
+
fail(new Error(`Codex app-server WebSocket handshake failed: ${head.split("\r\n")[0] || "invalid response"}`));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
settled = true;
|
|
321
|
+
cleanupHandshake();
|
|
322
|
+
socket.on("data", (data) => this.handleWebSocketData(data));
|
|
323
|
+
socket.on("error", (error) => fail(error));
|
|
324
|
+
socket.on("close", () => fail(new Error("Codex app-server WebSocket closed")));
|
|
325
|
+
if (rest.length) this.handleWebSocketData(rest);
|
|
326
|
+
resolve();
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const cleanupHandshake = () => {
|
|
330
|
+
socket.off("data", onData);
|
|
331
|
+
socket.off("error", fail);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
socket.once("connect", () => {
|
|
335
|
+
socket.write([
|
|
336
|
+
"GET / HTTP/1.1",
|
|
337
|
+
"Host: localhost",
|
|
338
|
+
"Upgrade: websocket",
|
|
339
|
+
"Connection: Upgrade",
|
|
340
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
341
|
+
"Sec-WebSocket-Version: 13",
|
|
342
|
+
"\r\n",
|
|
343
|
+
].join("\r\n"));
|
|
344
|
+
});
|
|
345
|
+
socket.on("data", onData);
|
|
346
|
+
socket.once("error", fail);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private writeWebSocketFrame(opcode: number, payload: Buffer): void {
|
|
351
|
+
const socket = this.socket;
|
|
352
|
+
if (!socket || socket.destroyed || !socket.writable) return;
|
|
353
|
+
|
|
354
|
+
const length = payload.length;
|
|
355
|
+
const lengthBytes = length < 126 ? 0 : length <= 0xffff ? 2 : 8;
|
|
356
|
+
const header = Buffer.alloc(2 + lengthBytes + 4);
|
|
357
|
+
header[0] = 0x80 | opcode;
|
|
358
|
+
if (length < 126) {
|
|
359
|
+
header[1] = 0x80 | length;
|
|
360
|
+
} else if (length <= 0xffff) {
|
|
361
|
+
header[1] = 0x80 | 126;
|
|
362
|
+
header.writeUInt16BE(length, 2);
|
|
363
|
+
} else {
|
|
364
|
+
header[1] = 0x80 | 127;
|
|
365
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const maskOffset = 2 + lengthBytes;
|
|
369
|
+
const mask = randomBytes(4);
|
|
370
|
+
mask.copy(header, maskOffset);
|
|
371
|
+
const masked = Buffer.alloc(length);
|
|
372
|
+
for (let index = 0; index < length; index += 1) {
|
|
373
|
+
masked[index] = payload[index] ^ mask[index % 4];
|
|
374
|
+
}
|
|
375
|
+
socket.write(Buffer.concat([header, masked]));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private handleWebSocketData(chunk: Buffer): void {
|
|
379
|
+
let frames: DecodedWebSocketFrame[];
|
|
380
|
+
try {
|
|
381
|
+
frames = this.wsDecoder.push(chunk);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.emit("protocolError", asError(error));
|
|
384
|
+
this.socket?.destroy(asError(error));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const { opcode, payload } of frames) {
|
|
388
|
+
if (opcode === 0x1) {
|
|
389
|
+
this.handleLine(payload.toString("utf8"));
|
|
390
|
+
} else if (opcode === 0x8) {
|
|
391
|
+
this.socket?.end();
|
|
392
|
+
} else if (opcode === 0x9) {
|
|
393
|
+
this.writeWebSocketFrame(0xA, payload);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private handleLine(line: string): void {
|
|
399
|
+
const trimmed = line.trim();
|
|
400
|
+
if (!trimmed) return;
|
|
401
|
+
|
|
402
|
+
let message: JsonRpcMessage;
|
|
403
|
+
try {
|
|
404
|
+
message = JSON.parse(trimmed) as JsonRpcMessage;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.emit("protocolError", asError(error));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (message.id !== undefined && !message.method) {
|
|
411
|
+
const pending = this.pending.get(message.id);
|
|
412
|
+
if (!pending) return;
|
|
413
|
+
this.pending.delete(message.id);
|
|
414
|
+
clearTimeout(pending.timeout);
|
|
415
|
+
if (message.error) {
|
|
416
|
+
pending.reject(new Error(message.error.message));
|
|
417
|
+
} else {
|
|
418
|
+
pending.resolve(message.result);
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (message.method && message.id !== undefined) {
|
|
424
|
+
void this.handleServerRequest(message);
|
|
425
|
+
this.emit("serverRequest", message);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (message.method) {
|
|
430
|
+
this.emit("notification", message);
|
|
431
|
+
if (message.method === "error") {
|
|
432
|
+
this.emit("serverError", message.params);
|
|
433
|
+
} else {
|
|
434
|
+
this.emit(message.method, message.params);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private failAll(error: Error): void {
|
|
440
|
+
for (const pending of this.pending.values()) {
|
|
441
|
+
clearTimeout(pending.timeout);
|
|
442
|
+
pending.reject(error);
|
|
443
|
+
}
|
|
444
|
+
this.pending.clear();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private async handleServerRequest(message: JsonRpcMessage): Promise<void> {
|
|
448
|
+
try {
|
|
449
|
+
if (process.env.CODEX_INTERCOM_DEBUG_TOOL_CALLS) {
|
|
450
|
+
process.stderr.write(`app-server request ${message.method ?? "unknown"}: ${JSON.stringify(message.params ?? {})}\n`);
|
|
451
|
+
}
|
|
452
|
+
this.respond(message.id, await this.options.serverRequestHandler(message));
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (process.env.CODEX_INTERCOM_DEBUG_TOOL_CALLS) {
|
|
455
|
+
process.stderr.write(`app-server request failed ${message.method ?? "unknown"}: ${asError(error).message}\n`);
|
|
456
|
+
}
|
|
457
|
+
this.respondError(message.id, -32601, asError(error).message);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function defaultServerRequestResponseFromMessage(message: JsonRpcMessage): unknown {
|
|
463
|
+
if (!message.method) throw new Error("Unsupported app-server request");
|
|
464
|
+
return defaultServerRequestResponse(message.method);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export class WebSocketFrameDecoder {
|
|
468
|
+
private buffer = Buffer.alloc(0);
|
|
469
|
+
private continuationOpcode: number | null = null;
|
|
470
|
+
private continuationParts: Buffer[] = [];
|
|
471
|
+
private continuationBytes = 0;
|
|
472
|
+
|
|
473
|
+
push(chunk: Buffer): DecodedWebSocketFrame[] {
|
|
474
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
475
|
+
const frames: DecodedWebSocketFrame[] = [];
|
|
476
|
+
|
|
477
|
+
while (this.buffer.length >= 2) {
|
|
478
|
+
const first = this.buffer[0];
|
|
479
|
+
const second = this.buffer[1];
|
|
480
|
+
const fin = Boolean(first & 0x80);
|
|
481
|
+
const rsv = first & 0x70;
|
|
482
|
+
const opcode = first & 0x0f;
|
|
483
|
+
const masked = Boolean(second & 0x80);
|
|
484
|
+
let length = second & 0x7f;
|
|
485
|
+
let offset = 2;
|
|
486
|
+
|
|
487
|
+
if (rsv !== 0) throw new Error("Unsupported WebSocket RSV bits");
|
|
488
|
+
|
|
489
|
+
if (length === 126) {
|
|
490
|
+
if (this.buffer.length < offset + 2) break;
|
|
491
|
+
length = this.buffer.readUInt16BE(offset);
|
|
492
|
+
offset += 2;
|
|
493
|
+
} else if (length === 127) {
|
|
494
|
+
if (this.buffer.length < offset + 8) break;
|
|
495
|
+
const bigLength = this.buffer.readBigUInt64BE(offset);
|
|
496
|
+
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("WebSocket frame too large");
|
|
497
|
+
length = Number(bigLength);
|
|
498
|
+
offset += 8;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const maskOffset = masked ? offset : -1;
|
|
502
|
+
if (masked) offset += 4;
|
|
503
|
+
if (this.buffer.length < offset + length) break;
|
|
504
|
+
|
|
505
|
+
const mask = masked ? Buffer.from(this.buffer.subarray(maskOffset, maskOffset + 4)) : null;
|
|
506
|
+
const payload = Buffer.from(this.buffer.subarray(offset, offset + length));
|
|
507
|
+
this.buffer = this.buffer.subarray(offset + length);
|
|
508
|
+
if (masked) {
|
|
509
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
510
|
+
payload[index] ^= mask![index % 4];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.acceptFrame(frames, opcode, fin, payload);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return frames;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private acceptFrame(frames: DecodedWebSocketFrame[], opcode: number, fin: boolean, payload: Buffer): void {
|
|
521
|
+
if (opcode >= 0x8) {
|
|
522
|
+
if (!fin) throw new Error("Fragmented WebSocket control frame");
|
|
523
|
+
if (payload.length > 125) throw new Error("Oversized WebSocket control frame");
|
|
524
|
+
frames.push({ opcode, payload });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (opcode === 0x0) {
|
|
529
|
+
if (this.continuationOpcode === null) throw new Error("Unexpected WebSocket continuation frame");
|
|
530
|
+
this.appendContinuation(payload);
|
|
531
|
+
if (fin) {
|
|
532
|
+
frames.push({ opcode: this.continuationOpcode, payload: Buffer.concat(this.continuationParts, this.continuationBytes) });
|
|
533
|
+
this.clearContinuation();
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (opcode !== 0x1 && opcode !== 0x2) throw new Error(`Unsupported WebSocket opcode: ${opcode}`);
|
|
539
|
+
if (this.continuationOpcode !== null) throw new Error("New WebSocket data frame before continuation completed");
|
|
540
|
+
if (fin) {
|
|
541
|
+
frames.push({ opcode, payload });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.continuationOpcode = opcode;
|
|
546
|
+
this.continuationParts = [];
|
|
547
|
+
this.continuationBytes = 0;
|
|
548
|
+
this.appendContinuation(payload);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private appendContinuation(payload: Buffer): void {
|
|
552
|
+
this.continuationBytes += payload.length;
|
|
553
|
+
if (this.continuationBytes > MAX_WEBSOCKET_MESSAGE_BYTES) throw new Error("WebSocket message too large");
|
|
554
|
+
this.continuationParts.push(payload);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private clearContinuation(): void {
|
|
558
|
+
this.continuationOpcode = null;
|
|
559
|
+
this.continuationParts = [];
|
|
560
|
+
this.continuationBytes = 0;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restricted Boss operation clients are intentionally unavailable in this
|
|
3
|
+
* adapter release. A safe client must be provisioned by the protected broker
|
|
4
|
+
* from its own authenticated binding and transport; accepting either from a
|
|
5
|
+
* caller would let untrusted code manufacture authority.
|
|
6
|
+
*/
|
|
7
|
+
export type HardenedBossClientKind = "boss_participant" | "boss_reviewer";
|
|
8
|
+
|
|
9
|
+
export const HARDENED_BOSS_CODEX_DEFAULTS = Object.freeze({
|
|
10
|
+
boss_participant: Object.freeze({ approvalPolicy: "untrusted", sandbox: "workspace-write" }),
|
|
11
|
+
boss_reviewer: Object.freeze({ approvalPolicy: "untrusted", sandbox: "read-only" }),
|
|
12
|
+
} as const);
|
|
13
|
+
|
|
14
|
+
export const BOSS_RESTRICTED_CLIENT_AVAILABILITY = "unavailable_without_protected_factory" as const;
|
|
15
|
+
|
|
16
|
+
export const PROVIDER_AUTHORITY_UNAVAILABLE = "PROVIDER_AUTHORITY_UNAVAILABLE" as const;
|
|
17
|
+
|
|
18
|
+
export class ProviderAuthorityUnavailableError extends Error {
|
|
19
|
+
readonly code = PROVIDER_AUTHORITY_UNAVAILABLE;
|
|
20
|
+
|
|
21
|
+
constructor(readonly bossClient: HardenedBossClientKind) {
|
|
22
|
+
super(`${PROVIDER_AUTHORITY_UNAVAILABLE}: ${bossClient} requires a broker-owned, artifact-attested Codex provider executable`);
|
|
23
|
+
this.name = "ProviderAuthorityUnavailableError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Protected Boss launches must not resolve `codex` through caller PATH or
|
|
29
|
+
* accept a caller-selected executable. No broker-owned provider attestation is
|
|
30
|
+
* available in this adapter yet, so every production spawn remains dormant.
|
|
31
|
+
*/
|
|
32
|
+
export function assertHardenedBossProviderAuthority(bossClient: HardenedBossClientKind | undefined): void {
|
|
33
|
+
if (bossClient !== undefined) throw new ProviderAuthorityUnavailableError(bossClient);
|
|
34
|
+
}
|