@paperclipai/server 2026.830.0-canary.4 → 2026.830.0-canary.6
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/dist/build-info.json +1 -1
- package/dist/vendor/paperclip-runner/drivers/codex/app-server-transport.d.ts +108 -0
- package/dist/vendor/paperclip-runner/drivers/codex/app-server-transport.d.ts.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/app-server-transport.js +565 -0
- package/dist/vendor/paperclip-runner/drivers/codex/app-server-transport.js.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-question-adapter.d.ts +25 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-question-adapter.d.ts.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-question-adapter.js +486 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-question-adapter.js.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-security-config.d.ts +4 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-security-config.d.ts.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-security-config.js +105 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-security-config.js.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-turn-diff.d.ts +12 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-turn-diff.d.ts.map +1 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-turn-diff.js +196 -0
- package/dist/vendor/paperclip-runner/drivers/codex/codex-turn-diff.js.map +1 -0
- package/package.json +18 -18
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
|
|
2
|
+
export interface CodexRpcNotification {
|
|
3
|
+
method: string;
|
|
4
|
+
params: Record<string, unknown>;
|
|
5
|
+
/** Internal-only correlation retained outside provider params and canonical PRP payloads. */
|
|
6
|
+
paperclipTrace?: {
|
|
7
|
+
sourceEventId: string;
|
|
8
|
+
sourceEventType: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface CodexTraceInterpretation {
|
|
12
|
+
sourceEventId: string;
|
|
13
|
+
sourceEventType: string;
|
|
14
|
+
providerMethod: string;
|
|
15
|
+
disposition: "mapped" | "ignored" | "rejected";
|
|
16
|
+
emittedEventIds: string[];
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CodexRpcServerRequest extends CodexRpcNotification {
|
|
20
|
+
id: string | number;
|
|
21
|
+
}
|
|
22
|
+
export type CodexServerRequestHandler = (request: CodexRpcServerRequest) => Promise<Record<string, unknown>>;
|
|
23
|
+
export interface CodexAppServerTransport {
|
|
24
|
+
request(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
25
|
+
notify(method: string, params?: Record<string, unknown>): void;
|
|
26
|
+
notifications(): AsyncIterable<CodexRpcNotification>;
|
|
27
|
+
setServerRequestHandler(handler: CodexServerRequestHandler): void;
|
|
28
|
+
/**
|
|
29
|
+
* Optional provider-neutral resolution path used when a transport has
|
|
30
|
+
* already normalized a native server request behind another PRP boundary.
|
|
31
|
+
*/
|
|
32
|
+
resolveRuntimeRequest?(input: {
|
|
33
|
+
requestId: string;
|
|
34
|
+
turnId: string;
|
|
35
|
+
resolution: HarnessRuntimeRequestResolution;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
processInfo?(): CodexTransportProcessInfo;
|
|
39
|
+
attachRun?(input: {
|
|
40
|
+
runId: string;
|
|
41
|
+
turnId: string;
|
|
42
|
+
itemId: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
/** Records post-rehydration driver mapping without affecting run authority. */
|
|
45
|
+
recordTraceInterpretation?(input: CodexTraceInterpretation): void;
|
|
46
|
+
}
|
|
47
|
+
export interface CodexTransportProcessInfo {
|
|
48
|
+
pid: number | null;
|
|
49
|
+
processGroupId: number | null;
|
|
50
|
+
startedAt: string;
|
|
51
|
+
exited: boolean;
|
|
52
|
+
exitCode: number | null;
|
|
53
|
+
signal: NodeJS.Signals | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Environment for the trusted app-server process itself. HOME and CODEX_HOME
|
|
57
|
+
* are retained so Codex can authenticate; model-issued commands receive a
|
|
58
|
+
* separate empty-by-default environment and filesystem permission profile.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createSanitizedCodexEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
61
|
+
export declare function sanitizedEnvironmentKeys(source?: NodeJS.ProcessEnv): string[];
|
|
62
|
+
export declare function redactCodexDiagnostic(message: string): string;
|
|
63
|
+
export declare const CODEX_METHOD_NOT_FOUND = -32601;
|
|
64
|
+
export declare const CODEX_INVALID_REQUEST = -32600;
|
|
65
|
+
/**
|
|
66
|
+
* A provider error response, carrying its JSON-RPC code. Callers need the code
|
|
67
|
+
* to tell "this provider build will not serve the method" apart from "the call
|
|
68
|
+
* failed this time", which are different facts about a capability.
|
|
69
|
+
*/
|
|
70
|
+
export declare class CodexRpcError extends Error {
|
|
71
|
+
readonly code: number | null;
|
|
72
|
+
constructor(message: string, code: number | null);
|
|
73
|
+
get methodUnavailable(): boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* True only when the provider answered and its answer denies the method. A
|
|
77
|
+
* plain `Error` here means the call never got an answer — a transport or
|
|
78
|
+
* protocol failure — which must not be read as a missing capability.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isCodexMethodUnavailable(error: unknown): boolean;
|
|
81
|
+
export interface ProcessCodexTransportOptions {
|
|
82
|
+
command?: string;
|
|
83
|
+
args?: string[];
|
|
84
|
+
environment?: NodeJS.ProcessEnv;
|
|
85
|
+
onDiagnostic?: (message: string) => void;
|
|
86
|
+
maxLineBytes?: number;
|
|
87
|
+
maxPendingRequests?: number;
|
|
88
|
+
maxQueuedNotifications?: number;
|
|
89
|
+
maxQueuedNotificationBytes?: number;
|
|
90
|
+
maxBufferedOutputBytes?: number;
|
|
91
|
+
maxInflightServerRequests?: number;
|
|
92
|
+
/** Starts a dedicated process group so runnerd and its Codex child are reaped together. */
|
|
93
|
+
processGroup?: boolean;
|
|
94
|
+
closeGraceMs?: number;
|
|
95
|
+
onProcess?: (info: CodexTransportProcessInfo) => void;
|
|
96
|
+
}
|
|
97
|
+
/** Local stdio JSON-RPC transport. App-server never becomes a WAN endpoint. */
|
|
98
|
+
export declare class ProcessCodexAppServerTransport implements CodexAppServerTransport {
|
|
99
|
+
#private;
|
|
100
|
+
constructor(options?: ProcessCodexTransportOptions);
|
|
101
|
+
request(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
102
|
+
notify(method: string, params?: Record<string, unknown>): void;
|
|
103
|
+
notifications(): AsyncIterable<CodexRpcNotification>;
|
|
104
|
+
setServerRequestHandler(handler: CodexServerRequestHandler): void;
|
|
105
|
+
close(): Promise<void>;
|
|
106
|
+
processInfo(): CodexTransportProcessInfo;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=app-server-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-server-transport.d.ts","sourceRoot":"","sources":["../../../src/drivers/codex/app-server-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
class BoundedAsyncQueue {
|
|
3
|
+
maxValues;
|
|
4
|
+
maxBytes;
|
|
5
|
+
#values = [];
|
|
6
|
+
#waiters = [];
|
|
7
|
+
#queuedBytes = 0;
|
|
8
|
+
#closed = false;
|
|
9
|
+
#error = null;
|
|
10
|
+
constructor(maxValues, maxBytes) {
|
|
11
|
+
this.maxValues = maxValues;
|
|
12
|
+
this.maxBytes = maxBytes;
|
|
13
|
+
}
|
|
14
|
+
push(value, bytes) {
|
|
15
|
+
if (this.#closed)
|
|
16
|
+
return false;
|
|
17
|
+
const waiter = this.#waiters.shift();
|
|
18
|
+
if (waiter !== undefined) {
|
|
19
|
+
waiter.resolve({ value, done: false });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (this.#values.length >= this.maxValues ||
|
|
23
|
+
this.#queuedBytes + bytes > this.maxBytes) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
this.#values.push({ value, bytes });
|
|
27
|
+
this.#queuedBytes += bytes;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
close(error) {
|
|
31
|
+
if (this.#closed)
|
|
32
|
+
return;
|
|
33
|
+
this.#closed = true;
|
|
34
|
+
this.#values = [];
|
|
35
|
+
this.#queuedBytes = 0;
|
|
36
|
+
this.#error = error ?? null;
|
|
37
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
38
|
+
if (error === undefined)
|
|
39
|
+
waiter.resolve({ value: undefined, done: true });
|
|
40
|
+
else
|
|
41
|
+
waiter.reject(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
[Symbol.asyncIterator]() {
|
|
45
|
+
return {
|
|
46
|
+
next: async () => {
|
|
47
|
+
const queued = this.#values.shift();
|
|
48
|
+
if (queued !== undefined) {
|
|
49
|
+
this.#queuedBytes -= queued.bytes;
|
|
50
|
+
return { value: queued.value, done: false };
|
|
51
|
+
}
|
|
52
|
+
if (this.#error !== null)
|
|
53
|
+
return Promise.reject(this.#error);
|
|
54
|
+
if (this.#closed)
|
|
55
|
+
return { value: undefined, done: true };
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
this.#waiters.push({ resolve, reject });
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class BoundedLineDecoder {
|
|
64
|
+
maxLineBytes;
|
|
65
|
+
onLine;
|
|
66
|
+
onError;
|
|
67
|
+
#buffer = Buffer.alloc(0);
|
|
68
|
+
#closed = false;
|
|
69
|
+
constructor(maxLineBytes, onLine, onError) {
|
|
70
|
+
this.maxLineBytes = maxLineBytes;
|
|
71
|
+
this.onLine = onLine;
|
|
72
|
+
this.onError = onError;
|
|
73
|
+
}
|
|
74
|
+
write(chunk) {
|
|
75
|
+
if (this.#closed)
|
|
76
|
+
return;
|
|
77
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
78
|
+
let offset = 0;
|
|
79
|
+
while (offset < incoming.length) {
|
|
80
|
+
const newline = incoming.indexOf(0x0a, offset);
|
|
81
|
+
const end = newline < 0 ? incoming.length : newline;
|
|
82
|
+
const segment = incoming.subarray(offset, end);
|
|
83
|
+
if (this.#buffer.length + segment.length > this.maxLineBytes) {
|
|
84
|
+
this.#fail(new Error(`codex app-server line exceeded ${this.maxLineBytes} bytes`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (segment.length > 0)
|
|
88
|
+
this.#buffer = Buffer.concat([this.#buffer, segment]);
|
|
89
|
+
if (newline < 0)
|
|
90
|
+
return;
|
|
91
|
+
const line = this.#buffer;
|
|
92
|
+
this.#buffer = Buffer.alloc(0);
|
|
93
|
+
this.onLine(line.toString("utf8"), line.length);
|
|
94
|
+
if (this.#closed)
|
|
95
|
+
return;
|
|
96
|
+
offset = newline + 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
end() {
|
|
100
|
+
if (this.#closed)
|
|
101
|
+
return;
|
|
102
|
+
if (this.#buffer.length > 0) {
|
|
103
|
+
this.#fail(new Error("codex app-server ended with an unterminated JSON-RPC line"));
|
|
104
|
+
}
|
|
105
|
+
this.#closed = true;
|
|
106
|
+
}
|
|
107
|
+
close() {
|
|
108
|
+
this.#closed = true;
|
|
109
|
+
this.#buffer = Buffer.alloc(0);
|
|
110
|
+
}
|
|
111
|
+
#fail(error) {
|
|
112
|
+
this.close();
|
|
113
|
+
this.onError(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const SAFE_ENVIRONMENT_KEYS = [
|
|
117
|
+
"ALL_PROXY",
|
|
118
|
+
"CODEX_HOME",
|
|
119
|
+
"HOME",
|
|
120
|
+
"HTTP_PROXY",
|
|
121
|
+
"HTTPS_PROXY",
|
|
122
|
+
"LANG",
|
|
123
|
+
"LC_ALL",
|
|
124
|
+
"NO_PROXY",
|
|
125
|
+
"NODE_EXTRA_CA_CERTS",
|
|
126
|
+
"PATH",
|
|
127
|
+
"PATHEXT",
|
|
128
|
+
"SSL_CERT_FILE",
|
|
129
|
+
"SystemRoot",
|
|
130
|
+
"TEMP",
|
|
131
|
+
"TMP",
|
|
132
|
+
"TMPDIR",
|
|
133
|
+
"WINDIR",
|
|
134
|
+
];
|
|
135
|
+
/**
|
|
136
|
+
* Environment for the trusted app-server process itself. HOME and CODEX_HOME
|
|
137
|
+
* are retained so Codex can authenticate; model-issued commands receive a
|
|
138
|
+
* separate empty-by-default environment and filesystem permission profile.
|
|
139
|
+
*/
|
|
140
|
+
export function createSanitizedCodexEnvironment(source = process.env) {
|
|
141
|
+
const environment = {};
|
|
142
|
+
for (const key of SAFE_ENVIRONMENT_KEYS) {
|
|
143
|
+
const value = source[key];
|
|
144
|
+
if (value === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
if (key.includes("PROXY") && proxyContainsCredentials(value))
|
|
147
|
+
continue;
|
|
148
|
+
environment[key] = value;
|
|
149
|
+
}
|
|
150
|
+
return environment;
|
|
151
|
+
}
|
|
152
|
+
export function sanitizedEnvironmentKeys(source = process.env) {
|
|
153
|
+
return Object.keys(createSanitizedCodexEnvironment(source)).sort();
|
|
154
|
+
}
|
|
155
|
+
export function redactCodexDiagnostic(message) {
|
|
156
|
+
return message
|
|
157
|
+
.replaceAll(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
158
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [REDACTED]")
|
|
159
|
+
.replace(/Basic\s+([A-Za-z0-9+/=]+)/gi, (match, encoded) => {
|
|
160
|
+
try {
|
|
161
|
+
// Only redact an actual RFC 7617 credential. Treating every word after
|
|
162
|
+
// “Basic” as base64 corrupted ordinary question copy such as
|
|
163
|
+
// “Basic API” before it entered the Paperclip protocol.
|
|
164
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
165
|
+
return decoded.includes(":") ? "Basic [REDACTED]" : match;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return match;
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1[REDACTED]@")
|
|
172
|
+
.replace(/([?&](?:api[_-]?key|token|secret|password)=)[^&#\s]+/gi, "$1[REDACTED]")
|
|
173
|
+
.replace(/(["'](?:api[_-]?key|token|secret|password|authorization)["']\s*:\s*["'])[^"']+/gi, "$1[REDACTED]")
|
|
174
|
+
.replace(/(api[_-]?key|token|secret|password)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]")
|
|
175
|
+
.replace(/(PAPERCLIP_API_KEY|OPENAI_API_KEY|OPENROUTER_API_KEY)=[^\s]+/g, "$1=[REDACTED]");
|
|
176
|
+
}
|
|
177
|
+
function proxyContainsCredentials(value) {
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(value);
|
|
180
|
+
return url.username.length > 0 || url.password.length > 0;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export const CODEX_METHOD_NOT_FOUND = -32_601;
|
|
187
|
+
export const CODEX_INVALID_REQUEST = -32_600;
|
|
188
|
+
/**
|
|
189
|
+
* Codes where the provider refused the call itself rather than failing while
|
|
190
|
+
* serving it: the method is absent (-32601), or the request was rejected
|
|
191
|
+
* outright, which is how the Codex app-server reports a feature it has
|
|
192
|
+
* switched off (-32600 `goals feature is disabled`). Server-error codes
|
|
193
|
+
* (-32603 and the -32000..-32099 implementation range) and transport failures
|
|
194
|
+
* describe one attempt, not what the build supports.
|
|
195
|
+
*/
|
|
196
|
+
const METHOD_UNAVAILABLE_CODES = [
|
|
197
|
+
CODEX_METHOD_NOT_FOUND,
|
|
198
|
+
CODEX_INVALID_REQUEST,
|
|
199
|
+
];
|
|
200
|
+
/**
|
|
201
|
+
* A provider error response, carrying its JSON-RPC code. Callers need the code
|
|
202
|
+
* to tell "this provider build will not serve the method" apart from "the call
|
|
203
|
+
* failed this time", which are different facts about a capability.
|
|
204
|
+
*/
|
|
205
|
+
export class CodexRpcError extends Error {
|
|
206
|
+
code;
|
|
207
|
+
constructor(message, code) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = "CodexRpcError";
|
|
210
|
+
this.code = code;
|
|
211
|
+
}
|
|
212
|
+
get methodUnavailable() {
|
|
213
|
+
return this.code !== null && METHOD_UNAVAILABLE_CODES.includes(this.code);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* True only when the provider answered and its answer denies the method. A
|
|
218
|
+
* plain `Error` here means the call never got an answer — a transport or
|
|
219
|
+
* protocol failure — which must not be read as a missing capability.
|
|
220
|
+
*/
|
|
221
|
+
export function isCodexMethodUnavailable(error) {
|
|
222
|
+
return error instanceof CodexRpcError && error.methodUnavailable;
|
|
223
|
+
}
|
|
224
|
+
function rpcErrorCode(value) {
|
|
225
|
+
if (typeof value !== "object" || value === null)
|
|
226
|
+
return null;
|
|
227
|
+
const code = value.code;
|
|
228
|
+
return typeof code === "number" && Number.isFinite(code) ? code : null;
|
|
229
|
+
}
|
|
230
|
+
const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
|
|
231
|
+
const DEFAULT_MAX_PENDING_REQUESTS = 64;
|
|
232
|
+
const DEFAULT_MAX_QUEUED_NOTIFICATIONS = 256;
|
|
233
|
+
const DEFAULT_MAX_QUEUED_NOTIFICATION_BYTES = 4 * 1024 * 1024;
|
|
234
|
+
const DEFAULT_MAX_BUFFERED_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
235
|
+
const DEFAULT_MAX_INFLIGHT_SERVER_REQUESTS = 32;
|
|
236
|
+
const MAX_DIAGNOSTIC_LINE_BYTES = 16 * 1024;
|
|
237
|
+
/** Local stdio JSON-RPC transport. App-server never becomes a WAN endpoint. */
|
|
238
|
+
export class ProcessCodexAppServerTransport {
|
|
239
|
+
#process;
|
|
240
|
+
#notifications;
|
|
241
|
+
#pending = new Map();
|
|
242
|
+
#nextId = 1;
|
|
243
|
+
#serverRequestHandler = async () => ({
|
|
244
|
+
success: false,
|
|
245
|
+
contentItems: [{ type: "input_text", text: "Unsupported client request." }],
|
|
246
|
+
});
|
|
247
|
+
#inflightServerRequests = 0;
|
|
248
|
+
#closed = false;
|
|
249
|
+
#exited = false;
|
|
250
|
+
#exitCode = null;
|
|
251
|
+
#exitSignal = null;
|
|
252
|
+
#resolveExit;
|
|
253
|
+
#exitPromise;
|
|
254
|
+
#processGroup;
|
|
255
|
+
#startedAt;
|
|
256
|
+
#closeGraceMs;
|
|
257
|
+
#onProcess;
|
|
258
|
+
#onDiagnostic;
|
|
259
|
+
#maxLineBytes;
|
|
260
|
+
#maxPendingRequests;
|
|
261
|
+
#maxInflightServerRequests;
|
|
262
|
+
#maxBufferedOutputBytes;
|
|
263
|
+
#stdoutDecoder;
|
|
264
|
+
#stderrDecoder;
|
|
265
|
+
constructor(options = {}) {
|
|
266
|
+
this.#onDiagnostic = options.onDiagnostic;
|
|
267
|
+
this.#maxLineBytes = positiveLimit(options.maxLineBytes, DEFAULT_MAX_LINE_BYTES);
|
|
268
|
+
this.#maxPendingRequests = positiveLimit(options.maxPendingRequests, DEFAULT_MAX_PENDING_REQUESTS);
|
|
269
|
+
this.#maxInflightServerRequests = positiveLimit(options.maxInflightServerRequests, DEFAULT_MAX_INFLIGHT_SERVER_REQUESTS);
|
|
270
|
+
this.#maxBufferedOutputBytes = positiveLimit(options.maxBufferedOutputBytes, DEFAULT_MAX_BUFFERED_OUTPUT_BYTES);
|
|
271
|
+
this.#processGroup =
|
|
272
|
+
options.processGroup === true && process.platform !== "win32";
|
|
273
|
+
this.#startedAt = new Date().toISOString();
|
|
274
|
+
this.#closeGraceMs = positiveLimit(options.closeGraceMs, 1_000);
|
|
275
|
+
this.#onProcess = options.onProcess;
|
|
276
|
+
this.#exitPromise = new Promise((resolve) => {
|
|
277
|
+
this.#resolveExit = resolve;
|
|
278
|
+
});
|
|
279
|
+
this.#notifications = new BoundedAsyncQueue(positiveLimit(options.maxQueuedNotifications, DEFAULT_MAX_QUEUED_NOTIFICATIONS), positiveLimit(options.maxQueuedNotificationBytes, DEFAULT_MAX_QUEUED_NOTIFICATION_BYTES));
|
|
280
|
+
this.#process = spawn(options.command ?? "codex", options.args ?? ["app-server"], {
|
|
281
|
+
env: options.environment ?? createSanitizedCodexEnvironment(),
|
|
282
|
+
stdio: "pipe",
|
|
283
|
+
detached: this.#processGroup,
|
|
284
|
+
});
|
|
285
|
+
this.#stdoutDecoder = new BoundedLineDecoder(this.#maxLineBytes, (line, bytes) => this.#onLine(line, bytes), (error) => this.#fatal(error));
|
|
286
|
+
this.#stderrDecoder = new BoundedLineDecoder(MAX_DIAGNOSTIC_LINE_BYTES, (line) => this.#onDiagnostic?.(redactCodexDiagnostic(line)), () => this.#onDiagnostic?.("codex app-server diagnostic line exceeded retention limit"));
|
|
287
|
+
this.#process.stdout.on("data", (chunk) => this.#stdoutDecoder.write(chunk));
|
|
288
|
+
this.#process.stdout.on("end", () => {
|
|
289
|
+
this.#stdoutDecoder.end();
|
|
290
|
+
this.#fatal(new Error("codex app-server stdout ended before transport closure"));
|
|
291
|
+
});
|
|
292
|
+
this.#process.stdout.on("error", (error) => this.#fatal(error));
|
|
293
|
+
this.#process.stdout.on("close", () => {
|
|
294
|
+
this.#fatal(new Error("codex app-server stdout closed before transport closure"));
|
|
295
|
+
});
|
|
296
|
+
this.#process.stderr.on("data", (chunk) => this.#stderrDecoder.write(chunk));
|
|
297
|
+
this.#process.stderr.on("end", () => this.#stderrDecoder.end());
|
|
298
|
+
this.#process.stdin.on("error", (error) => this.#fatal(error));
|
|
299
|
+
this.#process.on("error", (error) => this.#fatal(error));
|
|
300
|
+
this.#process.on("exit", (code, signal) => {
|
|
301
|
+
this.#exited = true;
|
|
302
|
+
this.#exitCode = code;
|
|
303
|
+
this.#exitSignal = signal;
|
|
304
|
+
this.#resolveExit();
|
|
305
|
+
this.#onProcess?.(this.processInfo());
|
|
306
|
+
if (!this.#closed) {
|
|
307
|
+
this.#fatal(new Error(`codex app-server exited (code=${code ?? "null"}, signal=${signal ?? "none"})`));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
this.#onProcess?.(this.processInfo());
|
|
311
|
+
}
|
|
312
|
+
request(method, params) {
|
|
313
|
+
if (this.#closed)
|
|
314
|
+
return Promise.reject(new Error("codex app-server transport is closed"));
|
|
315
|
+
if (this.#pending.size >= this.#maxPendingRequests) {
|
|
316
|
+
return Promise.reject(new Error(`codex app-server pending request limit ${this.#maxPendingRequests} exceeded`));
|
|
317
|
+
}
|
|
318
|
+
const id = this.#nextId++;
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
this.#pending.set(id, { resolve, reject });
|
|
321
|
+
try {
|
|
322
|
+
this.#write({ method, id, params });
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.#pending.delete(id);
|
|
326
|
+
reject(error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
notify(method, params) {
|
|
331
|
+
this.#write(params === undefined ? { method } : { method, params });
|
|
332
|
+
}
|
|
333
|
+
notifications() {
|
|
334
|
+
return this.#notifications;
|
|
335
|
+
}
|
|
336
|
+
setServerRequestHandler(handler) {
|
|
337
|
+
this.#serverRequestHandler = handler;
|
|
338
|
+
}
|
|
339
|
+
async close() {
|
|
340
|
+
const firstClose = !this.#closed;
|
|
341
|
+
if (firstClose) {
|
|
342
|
+
this.#closed = true;
|
|
343
|
+
this.#stdoutDecoder.close();
|
|
344
|
+
this.#stderrDecoder.close();
|
|
345
|
+
this.#notifications.close();
|
|
346
|
+
this.#failAll(new Error("codex app-server transport closed"));
|
|
347
|
+
}
|
|
348
|
+
if (!this.#exited) {
|
|
349
|
+
if (firstClose)
|
|
350
|
+
this.#signal("SIGTERM");
|
|
351
|
+
const exited = await Promise.race([
|
|
352
|
+
this.#exitPromise.then(() => true),
|
|
353
|
+
new Promise((resolve) => setTimeout(() => resolve(false), this.#closeGraceMs)),
|
|
354
|
+
]);
|
|
355
|
+
if (!exited && !this.#exited) {
|
|
356
|
+
this.#signal("SIGKILL");
|
|
357
|
+
await Promise.race([
|
|
358
|
+
this.#exitPromise,
|
|
359
|
+
new Promise((resolve) => setTimeout(resolve, this.#closeGraceMs)),
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
processInfo() {
|
|
365
|
+
return {
|
|
366
|
+
pid: this.#process.pid ?? null,
|
|
367
|
+
processGroupId: this.#processGroup ? (this.#process.pid ?? null) : null,
|
|
368
|
+
startedAt: this.#startedAt,
|
|
369
|
+
exited: this.#exited,
|
|
370
|
+
exitCode: this.#exitCode,
|
|
371
|
+
signal: this.#exitSignal,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
#signal(signal) {
|
|
375
|
+
const pid = this.#process.pid;
|
|
376
|
+
if (this.#processGroup && pid !== undefined) {
|
|
377
|
+
try {
|
|
378
|
+
process.kill(-pid, signal);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// The group may already be gone; the direct child kill below is idempotent.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.#process.kill(signal);
|
|
386
|
+
}
|
|
387
|
+
#write(message) {
|
|
388
|
+
if (this.#closed)
|
|
389
|
+
throw new Error("codex app-server transport is closed");
|
|
390
|
+
let serialized;
|
|
391
|
+
try {
|
|
392
|
+
serialized = JSON.stringify(message);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
396
|
+
this.#fatal(failure);
|
|
397
|
+
throw failure;
|
|
398
|
+
}
|
|
399
|
+
const line = `${serialized}\n`;
|
|
400
|
+
const lineBytes = Buffer.byteLength(line);
|
|
401
|
+
if (lineBytes - 1 > this.#maxLineBytes) {
|
|
402
|
+
const error = new Error(`outbound codex JSON-RPC line exceeded ${this.#maxLineBytes} bytes`);
|
|
403
|
+
this.#fatal(error);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
if (this.#process.stdin.writableLength + lineBytes >
|
|
407
|
+
this.#maxBufferedOutputBytes) {
|
|
408
|
+
const error = new Error(`outbound codex JSON-RPC buffer exceeded ${this.#maxBufferedOutputBytes} bytes`);
|
|
409
|
+
this.#fatal(error);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
this.#process.stdin.write(line);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
417
|
+
this.#fatal(failure);
|
|
418
|
+
throw failure;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
#onLine(line, bytes) {
|
|
422
|
+
let value;
|
|
423
|
+
try {
|
|
424
|
+
value = JSON.parse(line);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
this.#fatal(new Error("codex app-server emitted malformed JSON"));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!isRecord(value)) {
|
|
431
|
+
this.#fatal(new Error("codex app-server emitted a non-object JSON-RPC message"));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const message = value;
|
|
435
|
+
const hasResult = Object.hasOwn(message, "result");
|
|
436
|
+
const hasError = Object.hasOwn(message, "error");
|
|
437
|
+
if (typeof message.id === "number" && (hasResult || hasError)) {
|
|
438
|
+
if (!Number.isSafeInteger(message.id) ||
|
|
439
|
+
message.id <= 0 ||
|
|
440
|
+
hasResult === hasError) {
|
|
441
|
+
this.#fatal(new Error("codex app-server emitted a malformed JSON-RPC response"));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const pending = this.#pending.get(message.id);
|
|
445
|
+
if (pending === undefined) {
|
|
446
|
+
this.#fatal(new Error("codex app-server responded to an unknown request id"));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
this.#pending.delete(message.id);
|
|
450
|
+
if (hasError) {
|
|
451
|
+
pending.reject(new CodexRpcError(redactCodexDiagnostic(boundedJson(message.error)), rpcErrorCode(message.error)));
|
|
452
|
+
}
|
|
453
|
+
else if (isRecord(message.result)) {
|
|
454
|
+
pending.resolve(message.result);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const error = new Error("codex app-server response result was not an object");
|
|
458
|
+
pending.reject(error);
|
|
459
|
+
this.#fatal(error);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (typeof message.method !== "string" ||
|
|
464
|
+
message.method.length === 0 ||
|
|
465
|
+
hasResult ||
|
|
466
|
+
hasError) {
|
|
467
|
+
this.#fatal(new Error("codex app-server emitted a malformed JSON-RPC message"));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (message.params !== undefined && !isRecord(message.params)) {
|
|
471
|
+
this.#fatal(new Error("codex app-server message params were not an object"));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const params = message.params ?? {};
|
|
475
|
+
if (message.id !== undefined) {
|
|
476
|
+
if (!isValidServerRequestId(message.id)) {
|
|
477
|
+
this.#fatal(new Error("codex app-server request id had an invalid type"));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.#handleServerRequest({
|
|
481
|
+
id: message.id,
|
|
482
|
+
method: message.method,
|
|
483
|
+
params,
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (!this.#notifications.push({ method: message.method, params }, bytes)) {
|
|
488
|
+
this.#fatal(new Error("codex app-server notification queue limit exceeded"));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
#handleServerRequest(request) {
|
|
492
|
+
if (this.#inflightServerRequests >= this.#maxInflightServerRequests) {
|
|
493
|
+
this.#writeServerResponse({
|
|
494
|
+
id: request.id,
|
|
495
|
+
error: {
|
|
496
|
+
code: -32_001,
|
|
497
|
+
message: "Client request queue limit exceeded.",
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.#inflightServerRequests += 1;
|
|
503
|
+
void Promise.resolve()
|
|
504
|
+
.then(() => this.#serverRequestHandler(request))
|
|
505
|
+
.then((result) => {
|
|
506
|
+
this.#writeServerResponse({ id: request.id, result });
|
|
507
|
+
}, (error) => {
|
|
508
|
+
this.#writeServerResponse({
|
|
509
|
+
id: request.id,
|
|
510
|
+
error: {
|
|
511
|
+
code: -32_000,
|
|
512
|
+
message: redactCodexDiagnostic(String(error)),
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
})
|
|
516
|
+
.finally(() => {
|
|
517
|
+
this.#inflightServerRequests -= 1;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
#writeServerResponse(message) {
|
|
521
|
+
if (this.#closed)
|
|
522
|
+
return;
|
|
523
|
+
try {
|
|
524
|
+
this.#write(message);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
this.#fatal(error instanceof Error ? error : new Error(String(error)));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
#fatal(error) {
|
|
531
|
+
if (this.#closed)
|
|
532
|
+
return;
|
|
533
|
+
this.#closed = true;
|
|
534
|
+
this.#stdoutDecoder.close();
|
|
535
|
+
this.#stderrDecoder.close();
|
|
536
|
+
this.#signal("SIGKILL");
|
|
537
|
+
this.#notifications.close(error);
|
|
538
|
+
this.#failAll(error);
|
|
539
|
+
this.#onDiagnostic?.(redactCodexDiagnostic(error.message));
|
|
540
|
+
}
|
|
541
|
+
#failAll(error) {
|
|
542
|
+
for (const pending of this.#pending.values())
|
|
543
|
+
pending.reject(error);
|
|
544
|
+
this.#pending.clear();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function isRecord(value) {
|
|
548
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
549
|
+
}
|
|
550
|
+
function positiveLimit(value, fallback) {
|
|
551
|
+
return Number.isSafeInteger(value) && (value ?? 0) > 0 ? value : fallback;
|
|
552
|
+
}
|
|
553
|
+
function isValidServerRequestId(value) {
|
|
554
|
+
return ((typeof value === "number" && Number.isSafeInteger(value)) ||
|
|
555
|
+
(typeof value === "string" &&
|
|
556
|
+
value.length > 0 &&
|
|
557
|
+
Buffer.byteLength(value) <= 256));
|
|
558
|
+
}
|
|
559
|
+
function boundedJson(value) {
|
|
560
|
+
const serialized = JSON.stringify(value) ?? String(value);
|
|
561
|
+
return serialized.length <= MAX_DIAGNOSTIC_LINE_BYTES
|
|
562
|
+
? serialized
|
|
563
|
+
: `${serialized.slice(0, MAX_DIAGNOSTIC_LINE_BYTES)}...[truncated]`;
|
|
564
|
+
}
|
|
565
|
+
//# sourceMappingURL=app-server-transport.js.map
|