codex-computer-use-mcp 0.1.0 → 0.3.0
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/ARCHITECTURE.md +150 -0
- package/CONTRIBUTING.md +16 -10
- package/MIGRATION.md +94 -0
- package/PROOF.md +128 -54
- package/README.md +97 -115
- package/SECURITY.md +45 -22
- package/dist/audit.d.ts +15 -14
- package/dist/audit.js +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/config.d.ts +8 -8
- package/dist/config.js +8 -103
- package/dist/config.js.map +1 -1
- package/dist/direct-broker.d.ts +71 -0
- package/dist/direct-broker.js +652 -0
- package/dist/direct-broker.js.map +1 -0
- package/dist/direct-service.d.ts +33 -0
- package/dist/direct-service.js +350 -0
- package/dist/direct-service.js.map +1 -0
- package/dist/lock.d.ts +1 -0
- package/dist/lock.js +32 -3
- package/dist/lock.js.map +1 -1
- package/dist/mcp-elicitation.d.ts +8 -0
- package/dist/mcp-elicitation.js +47 -0
- package/dist/mcp-elicitation.js.map +1 -0
- package/dist/mcp-server.js +43 -100
- package/dist/mcp-server.js.map +1 -1
- package/dist/system.d.ts +0 -1
- package/dist/system.js +12 -37
- package/dist/system.js.map +1 -1
- package/dist/tools.d.ts +84 -0
- package/dist/tools.js +262 -0
- package/dist/tools.js.map +1 -0
- package/docs/adr/0001-standalone-code-mode-mcp.md +45 -0
- package/integrations/pi/README.md +51 -27
- package/integrations/pi/index.ts +237 -125
- package/integrations/pi/mcp.json.example +3 -3
- package/npm-shrinkwrap.json +3275 -48
- package/package.json +41 -13
- package/dist/policy.d.ts +0 -42
- package/dist/policy.js +0 -428
- package/dist/policy.js.map +0 -1
- package/dist/prompt.d.ts +0 -62
- package/dist/prompt.js +0 -119
- package/dist/prompt.js.map +0 -1
- package/dist/runner.d.ts +0 -55
- package/dist/runner.js +0 -494
- package/dist/runner.js.map +0 -1
- package/dist/service.d.ts +0 -17
- package/dist/service.js +0 -382
- package/dist/service.js.map +0 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { EXPECTED_OFFICIAL_INPUT_SCHEMAS, OFFICIAL_METHODS, } from "./tools.js";
|
|
6
|
+
export const CODEX_PATH = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
7
|
+
export const COMPUTER_USE_PLUGIN_ROOT = "/Applications/ChatGPT.app/Contents/Resources/plugins/openai-bundled/plugins/computer-use";
|
|
8
|
+
export const COMPUTER_USE_CLIENT_PATH = `${COMPUTER_USE_PLUGIN_ROOT}/Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient`;
|
|
9
|
+
const OPENAI_TEAM_ID = "2DC432GLL2";
|
|
10
|
+
const MAX_PROTOCOL_LINE_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const MAX_RESULT_BYTES = 25 * 1024 * 1024;
|
|
12
|
+
export class BrokerVerificationError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "BrokerVerificationError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class DirectBrokerCallError extends Error {
|
|
19
|
+
cleanupVerified;
|
|
20
|
+
directCalls;
|
|
21
|
+
modelTurnsStarted;
|
|
22
|
+
ephemeralThread;
|
|
23
|
+
elicitationRequests;
|
|
24
|
+
brokerVersion;
|
|
25
|
+
clientBuild;
|
|
26
|
+
constructor(message, cleanupVerified, cause, evidence = {}) {
|
|
27
|
+
super(message, { cause });
|
|
28
|
+
this.name = "DirectBrokerCallError";
|
|
29
|
+
this.cleanupVerified = cleanupVerified;
|
|
30
|
+
this.directCalls = evidence.directCalls ?? 0;
|
|
31
|
+
this.modelTurnsStarted = evidence.modelTurnsStarted ?? 0;
|
|
32
|
+
this.ephemeralThread = evidence.ephemeralThread ?? false;
|
|
33
|
+
this.elicitationRequests = evidence.elicitationRequests ?? 0;
|
|
34
|
+
this.brokerVersion = evidence.brokerVersion ?? "unknown";
|
|
35
|
+
this.clientBuild = evidence.clientBuild ?? "unknown";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function verifySignedBinary(binaryPath) {
|
|
39
|
+
const verify = spawnSync("/usr/bin/codesign", ["--verify", "--strict", binaryPath], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: 10_000,
|
|
42
|
+
});
|
|
43
|
+
if (verify.status !== 0)
|
|
44
|
+
throw new BrokerVerificationError(`Signature verification failed for ${path.basename(binaryPath)}`);
|
|
45
|
+
const details = spawnSync("/usr/bin/codesign", ["-dv", "--verbose=2", binaryPath], {
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
timeout: 10_000,
|
|
48
|
+
});
|
|
49
|
+
const output = `${details.stdout ?? ""}\n${details.stderr ?? ""}`;
|
|
50
|
+
if (details.status !== 0 || !output.includes(`TeamIdentifier=${OPENAI_TEAM_ID}`)) {
|
|
51
|
+
throw new BrokerVerificationError(`${path.basename(binaryPath)} is not signed by the expected OpenAI team`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function clientBuild() {
|
|
55
|
+
const plist = path.join(COMPUTER_USE_PLUGIN_ROOT, "Codex Computer Use.app", "Contents", "Info.plist");
|
|
56
|
+
const result = spawnSync("/usr/bin/plutil", ["-extract", "CFBundleVersion", "raw", plist], {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
timeout: 5000,
|
|
59
|
+
});
|
|
60
|
+
if (result.status !== 0 || !(result.stdout ?? "").trim()) {
|
|
61
|
+
throw new BrokerVerificationError("Could not verify the official Computer Use client build");
|
|
62
|
+
}
|
|
63
|
+
return (result.stdout ?? "").trim();
|
|
64
|
+
}
|
|
65
|
+
export function verifyOfficialDirectBroker() {
|
|
66
|
+
verifySignedBinary(CODEX_PATH);
|
|
67
|
+
verifySignedBinary(COMPUTER_USE_CLIENT_PATH);
|
|
68
|
+
const version = spawnSync(CODEX_PATH, ["--version"], { encoding: "utf8", timeout: 5000 });
|
|
69
|
+
if (version.status !== 0 || !/^codex-cli\s+\d+\./.test((version.stdout ?? "").trim())) {
|
|
70
|
+
throw new BrokerVerificationError("Could not verify the app-bundled Codex app-server version");
|
|
71
|
+
}
|
|
72
|
+
return { brokerVersion: (version.stdout ?? "").trim(), clientBuild: clientBuild() };
|
|
73
|
+
}
|
|
74
|
+
function buildBrokerEnv(codexHome, tempRoot) {
|
|
75
|
+
const env = {
|
|
76
|
+
HOME: tempRoot,
|
|
77
|
+
CODEX_HOME: codexHome,
|
|
78
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
79
|
+
TMPDIR: tempRoot,
|
|
80
|
+
NO_COLOR: "1",
|
|
81
|
+
CLICOLOR: "0",
|
|
82
|
+
};
|
|
83
|
+
for (const key of ["USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE", "SHELL", "TERM"]) {
|
|
84
|
+
if (process.env[key])
|
|
85
|
+
env[key] = process.env[key];
|
|
86
|
+
}
|
|
87
|
+
return env;
|
|
88
|
+
}
|
|
89
|
+
export function buildDirectAppServerArgs(mcpCwd = COMPUTER_USE_PLUGIN_ROOT) {
|
|
90
|
+
const mcpTable = `{"computer-use" = { command = ${JSON.stringify(COMPUTER_USE_CLIENT_PATH)}, args = ["mcp"], cwd = ${JSON.stringify(mcpCwd)}, enabled = true, startup_timeout_sec = 30, tool_timeout_sec = 120 }}`;
|
|
91
|
+
const disabledProvider = '{ name = "Direct dispatch disabled provider", base_url = "http://127.0.0.1:9/v1", wire_api = "responses", request_max_retries = 0, stream_max_retries = 0, supports_websockets = false, requires_openai_auth = false }';
|
|
92
|
+
return [
|
|
93
|
+
"-c", 'model_provider="direct_disabled"',
|
|
94
|
+
"-c", 'model="direct-disabled"',
|
|
95
|
+
"-c", `model_providers.direct_disabled=${disabledProvider}`,
|
|
96
|
+
"-c", "features.shell_tool=false",
|
|
97
|
+
"-c", "features.unified_exec=false",
|
|
98
|
+
"-c", "features.multi_agent=false",
|
|
99
|
+
"-c", "features.memories=false",
|
|
100
|
+
"-c", "memories.use_memories=false",
|
|
101
|
+
"-c", "memories.generate_memories=false",
|
|
102
|
+
"-c", "features.remote_plugin=false",
|
|
103
|
+
"-c", "features.plugins=false",
|
|
104
|
+
"-c", "features.remote_control=false",
|
|
105
|
+
"-c", "features.hooks=false",
|
|
106
|
+
"-c", "analytics.enabled=false",
|
|
107
|
+
"-c", 'otel.exporter="none"',
|
|
108
|
+
"-c", 'web_search="disabled"',
|
|
109
|
+
"-c", 'history.persistence="none"',
|
|
110
|
+
"-c", `mcp_servers=${mcpTable}`,
|
|
111
|
+
"-c", "plugins={}",
|
|
112
|
+
"app-server", "--stdio",
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function normalizeSchema(value) {
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map(normalizeSchema).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
118
|
+
}
|
|
119
|
+
if (value && typeof value === "object") {
|
|
120
|
+
return Object.fromEntries(Object.entries(value)
|
|
121
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
122
|
+
.map(([key, child]) => [key, normalizeSchema(child)]));
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function schemasEqual(left, right) {
|
|
127
|
+
return JSON.stringify(normalizeSchema(left)) === JSON.stringify(normalizeSchema(right));
|
|
128
|
+
}
|
|
129
|
+
function validateInventory(result) {
|
|
130
|
+
const data = result?.data;
|
|
131
|
+
if (!Array.isArray(data))
|
|
132
|
+
throw new BrokerVerificationError("App-server returned an invalid MCP inventory");
|
|
133
|
+
const server = data.find((entry) => entry?.name === "computer-use");
|
|
134
|
+
if (!server || !server.tools || typeof server.tools !== "object") {
|
|
135
|
+
throw new BrokerVerificationError("Official computer-use MCP server was not available");
|
|
136
|
+
}
|
|
137
|
+
const names = Object.keys(server.tools).sort();
|
|
138
|
+
const expectedNames = [...OFFICIAL_METHODS].sort();
|
|
139
|
+
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
|
|
140
|
+
throw new BrokerVerificationError("Official Computer Use tool inventory drifted; refusing direct dispatch");
|
|
141
|
+
}
|
|
142
|
+
for (const method of OFFICIAL_METHODS) {
|
|
143
|
+
const upstream = server.tools[method];
|
|
144
|
+
const schema = upstream?.inputSchema ?? upstream?.input_schema;
|
|
145
|
+
if (!schemasEqual(schema, EXPECTED_OFFICIAL_INPUT_SCHEMAS[method])) {
|
|
146
|
+
throw new BrokerVerificationError(`Official Computer Use schema drifted for ${method}; refusing direct dispatch`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function validateResult(value) {
|
|
151
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
152
|
+
throw new Error("Official Computer Use returned an invalid result");
|
|
153
|
+
const record = value;
|
|
154
|
+
if (!Array.isArray(record.content) || record.content.length > 100)
|
|
155
|
+
throw new Error("Official Computer Use returned invalid content blocks");
|
|
156
|
+
const content = record.content;
|
|
157
|
+
for (const item of content) {
|
|
158
|
+
if (!item || typeof item !== "object" || Array.isArray(item) || (item.type !== "text" && item.type !== "image")) {
|
|
159
|
+
throw new Error("Official Computer Use returned an unsupported content block");
|
|
160
|
+
}
|
|
161
|
+
if (item.type === "text" && typeof item.text !== "string")
|
|
162
|
+
throw new Error("Official Computer Use returned malformed text content");
|
|
163
|
+
if (item.type === "image" && (typeof item.data !== "string" || typeof item.mimeType !== "string")) {
|
|
164
|
+
throw new Error("Official Computer Use returned malformed image content");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
let encodedBytes = 0;
|
|
168
|
+
try {
|
|
169
|
+
encodedBytes = Buffer.byteLength(JSON.stringify({ content, structuredContent: record.structuredContent }), "utf8");
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error("Official Computer Use returned unserializable content");
|
|
173
|
+
}
|
|
174
|
+
if (encodedBytes > MAX_RESULT_BYTES)
|
|
175
|
+
throw new Error("Official Computer Use result exceeded the 25MB safety bound");
|
|
176
|
+
return {
|
|
177
|
+
content,
|
|
178
|
+
...(record.structuredContent !== undefined ? { structuredContent: record.structuredContent } : {}),
|
|
179
|
+
isError: record.isError === true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function processExists(pid) {
|
|
183
|
+
try {
|
|
184
|
+
process.kill(pid, 0);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return error?.code === "EPERM";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function processGroupExists(pid) {
|
|
192
|
+
try {
|
|
193
|
+
process.kill(-pid, 0);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return error?.code === "EPERM";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
class ProcessEnumerationError extends Error {
|
|
201
|
+
partialPids;
|
|
202
|
+
constructor(message, partialPids, cause) {
|
|
203
|
+
super(message, { cause });
|
|
204
|
+
this.name = "ProcessEnumerationError";
|
|
205
|
+
this.partialPids = new Set(partialPids);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function collectDescendants(rootPid, processEnumeratorCommand = "/usr/bin/pgrep") {
|
|
209
|
+
const descendants = new Set();
|
|
210
|
+
const queue = [rootPid];
|
|
211
|
+
while (queue.length > 0) {
|
|
212
|
+
if (descendants.size >= 256) {
|
|
213
|
+
throw new ProcessEnumerationError("Official app-server process tree exceeded the cleanup bound", descendants);
|
|
214
|
+
}
|
|
215
|
+
const parent = queue.shift();
|
|
216
|
+
const result = spawnSync(processEnumeratorCommand, ["-P", String(parent)], { encoding: "utf8", timeout: 2000 });
|
|
217
|
+
if (result.error
|
|
218
|
+
|| (result.status !== 0 && result.status !== 1)
|
|
219
|
+
|| (result.status === 1 && (result.stderr ?? "").trim().length > 0)) {
|
|
220
|
+
throw new ProcessEnumerationError("Could not enumerate the official app-server process tree", descendants, result.error);
|
|
221
|
+
}
|
|
222
|
+
for (const token of (result.stdout ?? "").trim().split(/\s+/)) {
|
|
223
|
+
const child = Number(token);
|
|
224
|
+
if (!Number.isSafeInteger(child) || child <= 1 || child === rootPid || descendants.has(child))
|
|
225
|
+
continue;
|
|
226
|
+
descendants.add(child);
|
|
227
|
+
queue.push(child);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return descendants;
|
|
231
|
+
}
|
|
232
|
+
function collectProcessesWithCwd(workDir, cwdEnumeratorCommand = "/usr/sbin/lsof") {
|
|
233
|
+
const result = spawnSync(cwdEnumeratorCommand, ["-a", "-d", "cwd", "+d", workDir, "-Fp"], {
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
timeout: 3000,
|
|
236
|
+
});
|
|
237
|
+
if (result.error
|
|
238
|
+
|| (result.status !== 0 && result.status !== 1)
|
|
239
|
+
|| (result.status === 1 && (result.stderr ?? "").trim().length > 0)) {
|
|
240
|
+
throw new ProcessEnumerationError("Could not enumerate processes owned by the private broker working directory", new Set(), result.error);
|
|
241
|
+
}
|
|
242
|
+
const pids = new Set();
|
|
243
|
+
for (const line of (result.stdout ?? "").split("\n")) {
|
|
244
|
+
const match = line.match(/^p(\d+)$/);
|
|
245
|
+
if (!match)
|
|
246
|
+
continue;
|
|
247
|
+
const pid = Number(match[1]);
|
|
248
|
+
if (Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)
|
|
249
|
+
pids.add(pid);
|
|
250
|
+
}
|
|
251
|
+
return pids;
|
|
252
|
+
}
|
|
253
|
+
async function terminateGroup(proc, workDir, processEnumeratorCommand = "/usr/bin/pgrep", cwdEnumeratorCommand = "/usr/sbin/lsof") {
|
|
254
|
+
const pid = proc?.pid;
|
|
255
|
+
if (!pid)
|
|
256
|
+
return;
|
|
257
|
+
const descendants = new Set();
|
|
258
|
+
const rootWasAlive = processExists(pid);
|
|
259
|
+
let cleanupError;
|
|
260
|
+
const enumerate = () => {
|
|
261
|
+
const found = new Set();
|
|
262
|
+
try {
|
|
263
|
+
for (const child of collectDescendants(pid, processEnumeratorCommand))
|
|
264
|
+
found.add(child);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error instanceof ProcessEnumerationError) {
|
|
268
|
+
for (const child of error.partialPids)
|
|
269
|
+
found.add(child);
|
|
270
|
+
}
|
|
271
|
+
cleanupError ??= error instanceof Error ? error : new Error(String(error));
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
for (const owned of collectProcessesWithCwd(workDir, cwdEnumeratorCommand))
|
|
275
|
+
found.add(owned);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof ProcessEnumerationError) {
|
|
279
|
+
for (const owned of error.partialPids)
|
|
280
|
+
found.add(owned);
|
|
281
|
+
}
|
|
282
|
+
cleanupError ??= error instanceof Error ? error : new Error(String(error));
|
|
283
|
+
}
|
|
284
|
+
return found;
|
|
285
|
+
};
|
|
286
|
+
for (const child of enumerate())
|
|
287
|
+
descendants.add(child);
|
|
288
|
+
let rootFrozen = false;
|
|
289
|
+
try {
|
|
290
|
+
process.kill(-pid, "SIGSTOP");
|
|
291
|
+
rootFrozen = true;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
try {
|
|
295
|
+
proc.kill("SIGSTOP");
|
|
296
|
+
rootFrozen = processExists(pid);
|
|
297
|
+
}
|
|
298
|
+
catch { /* already exited */ }
|
|
299
|
+
}
|
|
300
|
+
if (rootWasAlive && !rootFrozen)
|
|
301
|
+
cleanupError ??= new Error("Could not freeze the official app-server before cleanup");
|
|
302
|
+
let stable = false;
|
|
303
|
+
for (let pass = 0; pass < 16; pass += 1) {
|
|
304
|
+
let added = false;
|
|
305
|
+
for (const child of enumerate()) {
|
|
306
|
+
if (!descendants.has(child)) {
|
|
307
|
+
descendants.add(child);
|
|
308
|
+
added = true;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
process.kill(child, "SIGSTOP");
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
if (processExists(child))
|
|
315
|
+
cleanupError ??= new Error("Could not freeze an app-server-owned process");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!added && pass > 0) {
|
|
319
|
+
stable = true;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
323
|
+
}
|
|
324
|
+
if (!stable)
|
|
325
|
+
cleanupError ??= new Error("Official app-server process tree did not stabilize for cleanup");
|
|
326
|
+
for (const child of descendants) {
|
|
327
|
+
try {
|
|
328
|
+
process.kill(child, "SIGKILL");
|
|
329
|
+
}
|
|
330
|
+
catch { /* exited */ }
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
process.kill(-pid, "SIGKILL");
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
try {
|
|
337
|
+
proc.kill("SIGKILL");
|
|
338
|
+
}
|
|
339
|
+
catch { /* exited */ }
|
|
340
|
+
}
|
|
341
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
343
|
+
for (const owned of enumerate()) {
|
|
344
|
+
descendants.add(owned);
|
|
345
|
+
try {
|
|
346
|
+
process.kill(owned, "SIGKILL");
|
|
347
|
+
}
|
|
348
|
+
catch { /* exited */ }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (let elapsed = 0; elapsed < 1500; elapsed += 25) {
|
|
352
|
+
if (!processGroupExists(pid) && [...descendants].every((child) => !processExists(child)))
|
|
353
|
+
break;
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
355
|
+
}
|
|
356
|
+
let cwdSurvivors = new Set();
|
|
357
|
+
try {
|
|
358
|
+
cwdSurvivors = collectProcessesWithCwd(workDir, cwdEnumeratorCommand);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
cleanupError ??= error instanceof Error ? error : new Error(String(error));
|
|
362
|
+
}
|
|
363
|
+
for (const survivor of cwdSurvivors) {
|
|
364
|
+
try {
|
|
365
|
+
process.kill(survivor, "SIGKILL");
|
|
366
|
+
}
|
|
367
|
+
catch { /* exited */ }
|
|
368
|
+
}
|
|
369
|
+
if (processGroupExists(pid) || [...descendants, ...cwdSurvivors].some((child) => processExists(child))) {
|
|
370
|
+
cleanupError ??= new Error("Official app-server process tree did not terminate");
|
|
371
|
+
}
|
|
372
|
+
if (cleanupError)
|
|
373
|
+
throw cleanupError;
|
|
374
|
+
}
|
|
375
|
+
export async function callOfficialDirectTool(method, args, options = {}) {
|
|
376
|
+
const startedAt = Date.now();
|
|
377
|
+
const verification = options.skipSignatureVerification
|
|
378
|
+
? { brokerVersion: "test-app-server", clientBuild: "test-client" }
|
|
379
|
+
: verifyOfficialDirectBroker();
|
|
380
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pi-direct-computer-use."));
|
|
381
|
+
const codexHome = path.join(tempRoot, "codex-home");
|
|
382
|
+
const workDir = path.join(tempRoot, "work");
|
|
383
|
+
await mkdir(codexHome, { mode: 0o700 });
|
|
384
|
+
await mkdir(workDir, { mode: 0o700 });
|
|
385
|
+
await writeFile(path.join(codexHome, "config.toml"), "", { mode: 0o600 });
|
|
386
|
+
await chmod(codexHome, 0o700);
|
|
387
|
+
let proc;
|
|
388
|
+
let processClosed;
|
|
389
|
+
let termination;
|
|
390
|
+
let abortHandler;
|
|
391
|
+
let fatalError;
|
|
392
|
+
let stderr = "";
|
|
393
|
+
let nextId = 1;
|
|
394
|
+
let elicitationRequests = 0;
|
|
395
|
+
let modelTurnsStarted = 0;
|
|
396
|
+
let directCalls = 0;
|
|
397
|
+
let ephemeralThread = false;
|
|
398
|
+
let finalResult;
|
|
399
|
+
let primaryError;
|
|
400
|
+
let cleanupVerified = false;
|
|
401
|
+
const pending = new Map();
|
|
402
|
+
const rejectAll = (error) => {
|
|
403
|
+
for (const waiter of pending.values()) {
|
|
404
|
+
clearTimeout(waiter.timer);
|
|
405
|
+
waiter.reject(error);
|
|
406
|
+
}
|
|
407
|
+
pending.clear();
|
|
408
|
+
};
|
|
409
|
+
const ensureTerminated = () => {
|
|
410
|
+
termination ??= terminateGroup(proc, workDir, options.processEnumeratorCommand, options.cwdEnumeratorCommand);
|
|
411
|
+
return termination;
|
|
412
|
+
};
|
|
413
|
+
const fail = (error) => {
|
|
414
|
+
fatalError ??= error;
|
|
415
|
+
rejectAll(fatalError);
|
|
416
|
+
void ensureTerminated().catch(() => undefined);
|
|
417
|
+
};
|
|
418
|
+
const send = (message) => {
|
|
419
|
+
if (!proc?.stdin.writable)
|
|
420
|
+
throw new Error("Official app-server stdin is unavailable");
|
|
421
|
+
proc.stdin.write(`${JSON.stringify(message)}\n`, "utf8");
|
|
422
|
+
};
|
|
423
|
+
const request = (methodName, params, timeoutMs) => {
|
|
424
|
+
const id = String(nextId++);
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const timer = setTimeout(() => {
|
|
427
|
+
pending.delete(id);
|
|
428
|
+
reject(new Error(`Official app-server request timed out: ${methodName}`));
|
|
429
|
+
void ensureTerminated().catch(() => undefined);
|
|
430
|
+
}, timeoutMs);
|
|
431
|
+
pending.set(id, { resolve, reject, timer });
|
|
432
|
+
try {
|
|
433
|
+
send({ method: methodName, id, params });
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
pending.delete(id);
|
|
438
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
const command = options.appServerCommand ?? CODEX_PATH;
|
|
444
|
+
const commandArgs = options.appServerArgs ?? buildDirectAppServerArgs(workDir);
|
|
445
|
+
proc = spawn(command, commandArgs, {
|
|
446
|
+
cwd: workDir,
|
|
447
|
+
detached: true,
|
|
448
|
+
shell: false,
|
|
449
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
450
|
+
env: buildBrokerEnv(codexHome, tempRoot),
|
|
451
|
+
});
|
|
452
|
+
processClosed = new Promise((resolve) => proc.once("close", () => resolve()));
|
|
453
|
+
if (proc.pid)
|
|
454
|
+
options.onSpawn?.(proc.pid);
|
|
455
|
+
proc.stdin.on("error", (error) => {
|
|
456
|
+
if (error.code !== "EPIPE")
|
|
457
|
+
fail(error);
|
|
458
|
+
});
|
|
459
|
+
proc.stderr.setEncoding("utf8");
|
|
460
|
+
proc.stderr.on("data", (chunk) => {
|
|
461
|
+
if (stderr.length < 16_384)
|
|
462
|
+
stderr += chunk.slice(0, 16_384 - stderr.length);
|
|
463
|
+
});
|
|
464
|
+
proc.once("error", (error) => fail(error));
|
|
465
|
+
proc.once("close", (code) => {
|
|
466
|
+
if (pending.size > 0)
|
|
467
|
+
fail(new Error(`Official app-server exited before completing the request (${code ?? "unknown"})`));
|
|
468
|
+
});
|
|
469
|
+
const processProtocolLine = (line) => {
|
|
470
|
+
let message;
|
|
471
|
+
try {
|
|
472
|
+
message = JSON.parse(line);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
fail(new Error("Official app-server emitted malformed JSONL"));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (typeof message?.method === "string" && (message.method.startsWith("turn/") || message.method.startsWith("item/"))) {
|
|
479
|
+
modelTurnsStarted += 1;
|
|
480
|
+
fail(new Error("Official app-server unexpectedly emitted model-turn activity during direct dispatch"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (fatalError)
|
|
484
|
+
return;
|
|
485
|
+
try {
|
|
486
|
+
if (message?.id != null && (Object.prototype.hasOwnProperty.call(message, "result") || Object.prototype.hasOwnProperty.call(message, "error"))) {
|
|
487
|
+
const waiter = pending.get(String(message.id));
|
|
488
|
+
if (!waiter)
|
|
489
|
+
return;
|
|
490
|
+
pending.delete(String(message.id));
|
|
491
|
+
clearTimeout(waiter.timer);
|
|
492
|
+
if (message.error)
|
|
493
|
+
waiter.reject(new Error(`Official app-server error: ${String(message.error.message ?? "unknown")}`));
|
|
494
|
+
else
|
|
495
|
+
waiter.resolve(message.result);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (message?.id != null && typeof message.method === "string") {
|
|
499
|
+
if (message.method !== "mcpServer/elicitation/request") {
|
|
500
|
+
send({ id: message.id, error: { code: -32601, message: "Unsupported server request" } });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
elicitationRequests += 1;
|
|
504
|
+
const requestParams = message.params && typeof message.params === "object" && !Array.isArray(message.params)
|
|
505
|
+
? message.params
|
|
506
|
+
: {};
|
|
507
|
+
void (async () => {
|
|
508
|
+
let response = { action: "cancel" };
|
|
509
|
+
if (options.onElicitation) {
|
|
510
|
+
try {
|
|
511
|
+
const candidate = await options.onElicitation(requestParams);
|
|
512
|
+
if (candidate && ["accept", "decline", "cancel"].includes(candidate.action))
|
|
513
|
+
response = candidate;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
response = { action: "cancel" };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (fatalError || !proc?.stdin.writable)
|
|
520
|
+
return;
|
|
521
|
+
send({ id: message.id, result: response });
|
|
522
|
+
})().catch((error) => fail(error instanceof Error ? error : new Error(String(error))));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
let stdoutBuffer = "";
|
|
531
|
+
proc.stdout.setEncoding("utf8");
|
|
532
|
+
proc.stdout.on("data", (chunk) => {
|
|
533
|
+
let offset = 0;
|
|
534
|
+
while (offset < chunk.length) {
|
|
535
|
+
const newline = chunk.indexOf("\n", offset);
|
|
536
|
+
const end = newline === -1 ? chunk.length : newline;
|
|
537
|
+
const segment = chunk.slice(offset, end);
|
|
538
|
+
if (Buffer.byteLength(stdoutBuffer, "utf8") + Buffer.byteLength(segment, "utf8") > MAX_PROTOCOL_LINE_BYTES) {
|
|
539
|
+
fail(new Error("Official app-server protocol line exceeded the 8MB safety bound"));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (newline === -1) {
|
|
543
|
+
stdoutBuffer += segment;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const line = stdoutBuffer + segment;
|
|
547
|
+
stdoutBuffer = "";
|
|
548
|
+
if (line.length > 0)
|
|
549
|
+
processProtocolLine(line);
|
|
550
|
+
offset = newline + 1;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
proc.stdout.on("end", () => {
|
|
554
|
+
if (stdoutBuffer.length > 0)
|
|
555
|
+
processProtocolLine(stdoutBuffer);
|
|
556
|
+
stdoutBuffer = "";
|
|
557
|
+
});
|
|
558
|
+
if (options.signal) {
|
|
559
|
+
abortHandler = () => {
|
|
560
|
+
fail(new Error("Direct Computer Use request cancelled"));
|
|
561
|
+
};
|
|
562
|
+
if (options.signal.aborted)
|
|
563
|
+
abortHandler();
|
|
564
|
+
else
|
|
565
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
566
|
+
}
|
|
567
|
+
if (fatalError)
|
|
568
|
+
throw fatalError;
|
|
569
|
+
await request("initialize", {
|
|
570
|
+
clientInfo: { name: "pi_direct_computer_use", title: "Pi Direct Computer Use", version: "0.3.0" },
|
|
571
|
+
capabilities: { mcpServerOpenaiFormElicitation: options.supportsOpenAiFormElicitation === true && options.onElicitation !== undefined },
|
|
572
|
+
}, 15_000);
|
|
573
|
+
send({ method: "initialized" });
|
|
574
|
+
// Codex Full access requires both values: with `never` plus a restricted sandbox,
|
|
575
|
+
// core declines the signed service's app approval instead of prompting or accepting it.
|
|
576
|
+
const started = (await request("thread/start", { cwd: workDir, approvalPolicy: "never", sandbox: "danger-full-access", ephemeral: true, serviceName: "pi_direct_computer_use" }, 30_000));
|
|
577
|
+
const thread = started?.thread;
|
|
578
|
+
const threadId = thread?.id;
|
|
579
|
+
if (typeof threadId !== "string"
|
|
580
|
+
|| thread.ephemeral !== true
|
|
581
|
+
|| !Object.prototype.hasOwnProperty.call(thread, "path")
|
|
582
|
+
|| thread.path !== null
|
|
583
|
+
|| !Array.isArray(thread.turns)
|
|
584
|
+
|| thread.turns.length !== 0) {
|
|
585
|
+
throw new BrokerVerificationError("App-server did not attest an empty pathless ephemeral runtime context");
|
|
586
|
+
}
|
|
587
|
+
ephemeralThread = true;
|
|
588
|
+
const inventory = await request("mcpServerStatus/list", { threadId, detail: "toolsAndAuthOnly" }, 45_000);
|
|
589
|
+
validateInventory(inventory);
|
|
590
|
+
const raw = await request("mcpServer/tool/call", { threadId, server: "computer-use", tool: method, arguments: args }, options.timeoutMs ?? 120_000);
|
|
591
|
+
directCalls = 1;
|
|
592
|
+
if (modelTurnsStarted !== 0)
|
|
593
|
+
throw new BrokerVerificationError("Model-turn activity was observed during direct dispatch");
|
|
594
|
+
const result = validateResult(raw);
|
|
595
|
+
finalResult = {
|
|
596
|
+
...result,
|
|
597
|
+
brokerVersion: verification.brokerVersion,
|
|
598
|
+
clientBuild: verification.clientBuild,
|
|
599
|
+
durationMs: Date.now() - startedAt,
|
|
600
|
+
elicitationRequests,
|
|
601
|
+
modelTurnsStarted: 0,
|
|
602
|
+
ephemeralThread: true,
|
|
603
|
+
brokerCleanupVerified: true,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
const base = error instanceof Error ? error : new Error(String(error));
|
|
608
|
+
primaryError = /authentication|bearer|token/i.test(`${base.message}\n${stderr}`)
|
|
609
|
+
? new Error("Official direct Computer Use broker reported an authentication failure")
|
|
610
|
+
: base;
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
if (abortHandler && options.signal)
|
|
614
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
615
|
+
rejectAll(new Error("Official app-server closed"));
|
|
616
|
+
try {
|
|
617
|
+
await ensureTerminated();
|
|
618
|
+
if (processClosed) {
|
|
619
|
+
await new Promise((resolve, reject) => {
|
|
620
|
+
const timer = setTimeout(() => reject(new Error("Official app-server stdio did not close")), 1_000);
|
|
621
|
+
processClosed.then(() => {
|
|
622
|
+
clearTimeout(timer);
|
|
623
|
+
resolve();
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
628
|
+
if (fatalError || modelTurnsStarted !== 0) {
|
|
629
|
+
primaryError = fatalError ?? new BrokerVerificationError("Model-turn activity was observed during broker teardown");
|
|
630
|
+
}
|
|
631
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
632
|
+
cleanupVerified = true;
|
|
633
|
+
}
|
|
634
|
+
catch (cleanupError) {
|
|
635
|
+
primaryError = new Error("Official direct Computer Use broker cleanup failed", { cause: cleanupError });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const failureEvidence = {
|
|
639
|
+
directCalls,
|
|
640
|
+
modelTurnsStarted,
|
|
641
|
+
ephemeralThread,
|
|
642
|
+
elicitationRequests,
|
|
643
|
+
brokerVersion: verification.brokerVersion,
|
|
644
|
+
clientBuild: verification.clientBuild,
|
|
645
|
+
};
|
|
646
|
+
if (primaryError)
|
|
647
|
+
throw new DirectBrokerCallError(primaryError.message, cleanupVerified, primaryError, failureEvidence);
|
|
648
|
+
if (!finalResult)
|
|
649
|
+
throw new DirectBrokerCallError("Official direct Computer Use ended without a result", cleanupVerified, undefined, failureEvidence);
|
|
650
|
+
return finalResult;
|
|
651
|
+
}
|
|
652
|
+
//# sourceMappingURL=direct-broker.js.map
|