humanish 0.99.1 → 0.100.1
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/README.md +25 -4
- package/dist/actor-contract.d.ts +3 -1
- package/dist/actor-contract.js +6 -3
- package/dist/actor-contract.js.map +1 -1
- package/dist/computer-use.js +4 -3
- package/dist/computer-use.js.map +1 -1
- package/dist/cua-provider-error.d.ts +5 -1
- package/dist/cua-provider-error.js +9 -2
- package/dist/cua-provider-error.js.map +1 -1
- package/dist/doctor-lab.d.ts +8 -0
- package/dist/doctor-lab.js +9 -4
- package/dist/doctor-lab.js.map +1 -1
- package/dist/first-run-path.d.ts +3 -0
- package/dist/first-run-path.js +22 -1
- package/dist/first-run-path.js.map +1 -1
- package/dist/guest-bootstrap.d.ts +12 -4
- package/dist/guest-bootstrap.js +49 -17
- package/dist/guest-bootstrap.js.map +1 -1
- package/dist/guest-runtime-desktop.d.ts +5 -2
- package/dist/guest-runtime-desktop.js +30 -6
- package/dist/guest-runtime-desktop.js.map +1 -1
- package/dist/guest-runtime-main.js +2 -1
- package/dist/guest-runtime-main.js.map +1 -1
- package/dist/guest-runtime.d.ts +1 -1
- package/dist/guest-runtime.js +5 -3
- package/dist/guest-runtime.js.map +1 -1
- package/dist/init-templates.d.ts +6 -1
- package/dist/init-templates.js +39 -3
- package/dist/init-templates.js.map +1 -1
- package/dist/init.d.ts +6 -1
- package/dist/init.js +35 -3
- package/dist/init.js.map +1 -1
- package/dist/lab-engine.js +1 -1
- package/dist/lab-engine.js.map +1 -1
- package/dist/lab-summary.d.ts +4 -0
- package/dist/lab-summary.js +5 -1
- package/dist/lab-summary.js.map +1 -1
- package/dist/local-firecracker-desktop.js +13 -10
- package/dist/local-firecracker-desktop.js.map +1 -1
- package/dist/local-runtime-release.js +8 -8
- package/dist/observer-app.html +1 -1
- package/dist/program.js +9 -1
- package/dist/program.js.map +1 -1
- package/dist/restricted-codex-participant-policy.d.ts +0 -2
- package/dist/restricted-codex-participant-policy.js +2 -3
- package/dist/restricted-codex-participant-policy.js.map +1 -1
- package/dist/restricted-codex-participant.d.ts +2 -2
- package/dist/restricted-codex-participant.js +46 -39
- package/dist/restricted-codex-participant.js.map +1 -1
- package/dist/restricted-codex-policy.d.ts +1 -0
- package/dist/restricted-codex-policy.js.map +1 -1
- package/dist/restricted-codex-session.d.ts +6 -2
- package/dist/restricted-codex-session.js +279 -206
- package/dist/restricted-codex-session.js.map +1 -1
- package/dist/restricted-codex-transport.d.ts +4 -2
- package/dist/restricted-codex-transport.js +14 -2
- package/dist/restricted-codex-transport.js.map +1 -1
- package/dist/tui-app.js +16 -16
- package/docs/architecture/local-browser-runtime.md +25 -5
- package/docs/architecture/restricted-codex-analysis.md +14 -5
- package/docs/contracts/schemas.md +8 -1
- package/docs/goals/current.md +3 -3
- package/docs/ramp/README.md +7 -1
- package/docs/release/0.100.0-local-setup.md +31 -0
- package/docs/release/0.100.1-participant-continuity.md +20 -0
- package/package.json +1 -1
|
@@ -111,20 +111,19 @@ async function checkVersion(file, env, cwd, spawnFn, deadline) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
const rawCompactionTypes = ["compaction", "compaction_summary", "context_compaction"];
|
|
115
|
+
const allowedRawItemTypes = ["message", "reasoning", ...rawCompactionTypes];
|
|
114
116
|
const unclosedChildren = new Set();
|
|
115
117
|
function retainUnclosedChild(closed) {
|
|
116
118
|
unclosedChildren.add(closed);
|
|
117
119
|
void closed.then(() => { unclosedChildren.delete(closed); });
|
|
118
120
|
}
|
|
119
|
-
/**
|
|
120
|
-
* new work, but ordinary participant turns may run concurrently. */
|
|
121
|
+
/** Analysts/readiness keep their one-shot lifetime; participants own a session. */
|
|
121
122
|
export async function runRestrictedCodexSession(request, options = {}, readinessOnly = false) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return restrictedCodexFailure("codex_busy");
|
|
127
|
-
return executeRestrictedCodexSession(request, options, readinessOnly);
|
|
123
|
+
const session = createRestrictedCodexSession(options);
|
|
124
|
+
const result = await session.run(request, readinessOnly);
|
|
125
|
+
return await session.close() ? result
|
|
126
|
+
: { ...restrictedCodexFailure("codex_cleanup_failed", result.dispatched, result.usage), failurePhase: "cleanup" };
|
|
128
127
|
}
|
|
129
128
|
async function writeRecoveryMarker(work, sourceEnv, reason) {
|
|
130
129
|
const cache = sourceEnv.XDG_CACHE_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".cache");
|
|
@@ -163,127 +162,146 @@ function hasScopedIdentity(method, params, threadId, turnId) {
|
|
|
163
162
|
&& (turnId === undefined || params.turnId === turnId);
|
|
164
163
|
return (params.threadId === undefined || params.threadId === threadId) && (params.turnId === undefined || params.turnId === turnId);
|
|
165
164
|
}
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
const admissionError = restrictedCodexRequestError(request);
|
|
169
|
-
if (admissionError)
|
|
170
|
-
return restrictedCodexFailure(admissionError);
|
|
165
|
+
/** One private process and conversation per owner. Only completed turns may continue. */
|
|
166
|
+
export function createRestrictedCodexSession(options = {}) {
|
|
171
167
|
const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
|
|
172
|
-
if (!(platform === "linux" && arch === "x64") && !(platform === "darwin" && arch === "arm64"))
|
|
173
|
-
return restrictedCodexFailure("codex_unsupported_platform");
|
|
174
|
-
const deadline = new RestrictedCodexDeadline(request.timeoutMs, request.signal);
|
|
175
168
|
const sourceEnv = options.env ?? process.env;
|
|
176
169
|
const spawnFn = options.spawnFn ?? ((file, args, settings) => spawn(file, args, settings));
|
|
177
170
|
let work, authLink, transport;
|
|
178
|
-
let threadId,
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
let
|
|
182
|
-
let
|
|
183
|
-
|
|
184
|
-
let
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!["message", "reasoning"].includes(String(item.type))) {
|
|
194
|
-
deadline.stop("codex_tool_call");
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (item.type === "message" && Array.isArray(item.content)
|
|
198
|
-
&& item.content.some(content => codexRecord(content).type === "refusal"))
|
|
199
|
-
deadline.stop("refusal");
|
|
200
|
-
}
|
|
201
|
-
if (method === "thread/tokenUsage/updated")
|
|
202
|
-
usage = restrictedCodexUsage(params.tokenUsage);
|
|
203
|
-
if (method === "item/started" || method === "item/completed") {
|
|
204
|
-
if (!["userMessage", "agentMessage", "reasoning"].includes(String(item.type))) {
|
|
205
|
-
deadline.stop("codex_tool_call");
|
|
206
|
-
return;
|
|
171
|
+
let threadId, cwd = "", scratch = "";
|
|
172
|
+
let identity;
|
|
173
|
+
let previousUsage = { input: 0, output: 0, cachedInput: 0, cacheWriteInput: 0 };
|
|
174
|
+
let activeDeadline;
|
|
175
|
+
let interrupt;
|
|
176
|
+
let closed = false, cleanupTrusted = true;
|
|
177
|
+
let pending, closing, disposing;
|
|
178
|
+
const dispose = () => {
|
|
179
|
+
closed = true;
|
|
180
|
+
return disposing ??= (async () => {
|
|
181
|
+
let cleaned = cleanupTrusted;
|
|
182
|
+
if (transport) {
|
|
183
|
+
cleaned = await transport.close(interrupt).catch(() => false) && cleaned;
|
|
184
|
+
if (!cleaned)
|
|
185
|
+
retainUnclosedChild(transport.owned.closed);
|
|
207
186
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
187
|
+
let authReplaced = false;
|
|
188
|
+
if (authLink && cleaned) {
|
|
189
|
+
try {
|
|
190
|
+
authReplaced = !(await lstat(authLink)).isSymbolicLink();
|
|
191
|
+
if (!authReplaced)
|
|
192
|
+
await unlink(authLink);
|
|
212
193
|
}
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|| (outputItem && (outputItem.id !== item.id || outputItem.text !== item.text))) {
|
|
217
|
-
deadline.stop("invalid_response");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
outputItem = { id: item.id, text: item.text };
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (codexRecord(error).code !== "ENOENT")
|
|
196
|
+
cleaned = false;
|
|
221
197
|
}
|
|
222
198
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
199
|
+
// Preserve unexpected login rotation for private recovery, never overwrite host auth.
|
|
200
|
+
if (authReplaced) {
|
|
201
|
+
cleaned = false;
|
|
202
|
+
if (work)
|
|
203
|
+
await preserveUnexpectedAuth(work, sourceEnv).catch(() => undefined);
|
|
204
|
+
}
|
|
205
|
+
if (work && cleaned)
|
|
206
|
+
await rm(work, { recursive: true, force: true }).catch(() => { cleaned = false; });
|
|
207
|
+
if (work && !cleaned && !authReplaced)
|
|
208
|
+
await writeRecoveryMarker(work, sourceEnv, "process_cleanup_unconfirmed").catch(() => undefined);
|
|
209
|
+
return cleaned;
|
|
210
|
+
})();
|
|
211
|
+
};
|
|
212
|
+
async function execute(request, readinessOnly) {
|
|
213
|
+
const deadline = new RestrictedCodexDeadline(request.timeoutMs, request.signal);
|
|
214
|
+
activeDeadline = deadline;
|
|
215
|
+
let turnId, earlyTurnId;
|
|
216
|
+
let dispatched = false, usage = null;
|
|
217
|
+
let completed = false, compacted = false;
|
|
218
|
+
let latestUsage = null;
|
|
219
|
+
let generatedDeltaBytes = 0;
|
|
220
|
+
let outputItem;
|
|
221
|
+
let result = restrictedCodexFailure("codex_process_failed");
|
|
222
|
+
let phase = "startup";
|
|
223
|
+
const early = [];
|
|
224
|
+
let resolveTurn;
|
|
225
|
+
const finished = new Promise(resolve => { resolveTurn = resolve; });
|
|
226
|
+
const handleTurnEvent = (method, params) => {
|
|
227
|
+
if (!hasScopedIdentity(method, params, threadId, turnId)) {
|
|
227
228
|
deadline.stop("codex_protocol_error");
|
|
228
229
|
return;
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
const item = codexRecord(params.item);
|
|
232
|
+
// CLI 0.154 thread totals omit compaction requests. Retain known usage,
|
|
233
|
+
// but never label it complete when native compaction occurred in this turn.
|
|
234
|
+
if (method === "thread/compacted" || item.type === "contextCompaction" || rawCompactionTypes.includes(String(item.type)))
|
|
235
|
+
compacted = true;
|
|
236
|
+
if (method === "rawResponseItem/completed") {
|
|
237
|
+
if (!allowedRawItemTypes.includes(String(item.type))) {
|
|
238
|
+
deadline.stop("codex_tool_call");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (item.type === "message" && Array.isArray(item.content)
|
|
242
|
+
&& item.content.some(content => codexRecord(content).type === "refusal"))
|
|
243
|
+
deadline.stop("refusal");
|
|
234
244
|
}
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
if (method === "thread/tokenUsage/updated") {
|
|
246
|
+
const total = restrictedCodexUsage(params.tokenUsage);
|
|
247
|
+
// App-server reports cumulative thread usage. Receipts must charge only this turn.
|
|
248
|
+
if (total && previousUsage) {
|
|
249
|
+
const delta = { input: total.input - previousUsage.input, output: total.output - previousUsage.output,
|
|
250
|
+
cachedInput: (total.cachedInput ?? 0) - (previousUsage.cachedInput ?? 0),
|
|
251
|
+
cacheWriteInput: (total.cacheWriteInput ?? 0) - (previousUsage.cacheWriteInput ?? 0) };
|
|
252
|
+
usage = Object.values(delta).every(value => Number.isSafeInteger(value) && value >= 0)
|
|
253
|
+
&& delta.cachedInput + delta.cacheWriteInput <= delta.input ? delta : null;
|
|
254
|
+
}
|
|
255
|
+
else
|
|
256
|
+
usage = null;
|
|
257
|
+
latestUsage = total;
|
|
238
258
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
259
|
+
if (method === "item/started" || method === "item/completed") {
|
|
260
|
+
if (!["userMessage", "agentMessage", "reasoning", "contextCompaction"].includes(String(item.type))) {
|
|
261
|
+
deadline.stop("codex_tool_call");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (item.type === "agentMessage") {
|
|
265
|
+
if (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)) {
|
|
266
|
+
deadline.stop("codex_tool_call");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (method === "item/completed" && item.phase !== "commentary") {
|
|
270
|
+
if (typeof item.id !== "string" || typeof item.text !== "string" || Buffer.byteLength(item.text) > CODEX_MAX_OUTPUT_BYTES
|
|
271
|
+
|| (item.phase !== null && item.phase !== "final_answer") || (item.delivery !== null && item.delivery !== undefined)
|
|
272
|
+
|| (outputItem && (outputItem.id !== item.id || outputItem.text !== item.text))) {
|
|
273
|
+
deadline.stop("invalid_response");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
outputItem = { id: item.id, text: item.text };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
242
279
|
}
|
|
243
|
-
|
|
244
|
-
|
|
280
|
+
if (method === "turn/completed") {
|
|
281
|
+
const turn = codexRecord(params.turn);
|
|
282
|
+
if (turn.id !== turnId || completed) {
|
|
283
|
+
deadline.stop("codex_protocol_error");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
completed = true;
|
|
287
|
+
if (turn.status === "interrupted") {
|
|
288
|
+
deadline.stop("cancelled");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (turn.status !== "completed" || turn.error !== null || !outputItem) {
|
|
292
|
+
deadline.stop("invalid_response");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
resolveTurn({ status: "completed", output: JSON.parse(outputItem.text), usage,
|
|
297
|
+
usageComplete: usage !== null && !compacted, dispatched: true, errorCode: null });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
deadline.stop("invalid_response");
|
|
301
|
+
}
|
|
245
302
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
deadline.check();
|
|
250
|
-
const file = await resolveExecutable(options, sourceEnv);
|
|
251
|
-
deadline.check();
|
|
252
|
-
work = await mkdtemp(path.join(options.tempRoot ?? tmpdir(), "humanish-codex-analysis-"));
|
|
253
|
-
await chmod(work, 0o700);
|
|
254
|
-
work = await realpath(work);
|
|
255
|
-
const home = path.join(work, "home"), cwd = path.join(work, "cwd"), scratch = path.join(work, "scratch");
|
|
256
|
-
for (const directory of [home, cwd, scratch])
|
|
257
|
-
await mkdir(directory, { mode: 0o700 });
|
|
258
|
-
const env = childEnvironment(sourceEnv, home, scratch);
|
|
259
|
-
await checkVersion(file, env, cwd, spawnFn, deadline);
|
|
260
|
-
const config = restrictedCodexConfig(request.model), configPath = path.join(home, "config.toml");
|
|
261
|
-
await writeFile(configPath, config.toml, { mode: 0o600, flag: "wx" });
|
|
262
|
-
const authHome = options.authHome ?? sourceEnv.CODEX_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".codex");
|
|
263
|
-
if (!path.isAbsolute(authHome))
|
|
264
|
-
throw new RestrictedCodexStop("codex_unsupported_auth");
|
|
265
|
-
let authFile;
|
|
266
|
-
try {
|
|
267
|
-
authFile = await realpath(path.join(authHome, "auth.json"));
|
|
268
|
-
if (!(await stat(authFile)).isFile())
|
|
269
|
-
throw new Error();
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
throw new RestrictedCodexStop("codex_login_required");
|
|
273
|
-
}
|
|
274
|
-
deadline.check();
|
|
275
|
-
authLink = path.join(home, "auth.json");
|
|
276
|
-
await symlink(authFile, authLink);
|
|
277
|
-
deadline.check();
|
|
278
|
-
const owned = ownCodexProcess(spawnFn(file, ["app-server", "--strict-config"], {
|
|
279
|
-
cwd, env, detached: false, stdio: ["pipe", "pipe", "pipe"]
|
|
280
|
-
}));
|
|
281
|
-
// Raw input notifications echo supplied image data. Bound that wire separately
|
|
282
|
-
// from the 2 MiB generated report, without dropping admitted evidence.
|
|
283
|
-
const frameLimit = Math.max(CODEX_MAX_OUTPUT_BYTES, Buffer.byteLength(JSON.stringify({ instructions: request.instructions,
|
|
284
|
-
evidence: request.evidence, images: request.images, schema: request.schema })) + 1024 * 1024);
|
|
285
|
-
transport = new RestrictedCodexTransport(owned, deadline, frameLimit);
|
|
286
|
-
transport.onNotification = (method, params) => {
|
|
303
|
+
};
|
|
304
|
+
const onNotification = (method, params) => {
|
|
287
305
|
if (!dispatched)
|
|
288
306
|
return;
|
|
289
307
|
if (!hasScopedIdentity(method, params, threadId, turnId ?? earlyTurnId)) {
|
|
@@ -303,7 +321,7 @@ async function executeRestrictedCodexSession(request, options = {}, readinessOnl
|
|
|
303
321
|
}
|
|
304
322
|
const item = codexRecord(params.item);
|
|
305
323
|
// Tool requests must fail even if the turn-start acknowledgment is lost.
|
|
306
|
-
if ((method === "rawResponseItem/completed" && !
|
|
324
|
+
if ((method === "rawResponseItem/completed" && !allowedRawItemTypes.includes(String(item.type)))
|
|
307
325
|
|| (["item/started", "item/completed"].includes(method) && item.type === "agentMessage"
|
|
308
326
|
&& (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)))) {
|
|
309
327
|
deadline.stop("codex_tool_call");
|
|
@@ -313,111 +331,166 @@ async function executeRestrictedCodexSession(request, options = {}, readinessOnl
|
|
|
313
331
|
const value = codexRecord(params.turn).id;
|
|
314
332
|
if (typeof value !== "string" || value.length === 0 || (earlyTurnId !== undefined && value !== earlyTurnId))
|
|
315
333
|
deadline.stop("codex_protocol_error");
|
|
316
|
-
else
|
|
334
|
+
else {
|
|
317
335
|
earlyTurnId = value;
|
|
336
|
+
interrupt = { threadId: threadId, turnId: value };
|
|
337
|
+
}
|
|
318
338
|
}
|
|
319
339
|
if (turnId === undefined)
|
|
320
340
|
early.push({ method, params });
|
|
321
341
|
else
|
|
322
342
|
handleTurnEvent(method, params);
|
|
323
343
|
};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
344
|
+
try {
|
|
345
|
+
deadline.check();
|
|
346
|
+
const frameLimit = Math.max(CODEX_MAX_OUTPUT_BYTES, Buffer.byteLength(JSON.stringify({ instructions: request.instructions,
|
|
347
|
+
evidence: request.evidence, images: request.images, schema: request.schema })) + 1024 * 1024);
|
|
348
|
+
if (!transport) {
|
|
349
|
+
const file = await resolveExecutable(options, sourceEnv);
|
|
350
|
+
deadline.check();
|
|
351
|
+
work = await mkdtemp(path.join(options.tempRoot ?? tmpdir(), "humanish-codex-analysis-"));
|
|
352
|
+
await chmod(work, 0o700);
|
|
353
|
+
work = await realpath(work);
|
|
354
|
+
const home = path.join(work, "home");
|
|
355
|
+
cwd = path.join(work, "cwd");
|
|
356
|
+
scratch = path.join(work, "scratch");
|
|
357
|
+
for (const directory of [home, cwd, scratch])
|
|
358
|
+
await mkdir(directory, { mode: 0o700 });
|
|
359
|
+
const env = childEnvironment(sourceEnv, home, scratch);
|
|
360
|
+
await checkVersion(file, env, cwd, spawnFn, deadline);
|
|
361
|
+
const config = restrictedCodexConfig(request.model), configPath = path.join(home, "config.toml");
|
|
362
|
+
await writeFile(configPath, config.toml, { mode: 0o600, flag: "wx" });
|
|
363
|
+
const authHome = options.authHome ?? sourceEnv.CODEX_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".codex");
|
|
364
|
+
if (!path.isAbsolute(authHome))
|
|
365
|
+
throw new RestrictedCodexStop("codex_unsupported_auth");
|
|
366
|
+
let authFile;
|
|
367
|
+
try {
|
|
368
|
+
authFile = await realpath(path.join(authHome, "auth.json"));
|
|
369
|
+
if (!(await stat(authFile)).isFile())
|
|
370
|
+
throw new Error();
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
throw new RestrictedCodexStop("codex_login_required");
|
|
374
|
+
}
|
|
375
|
+
deadline.check();
|
|
376
|
+
authLink = path.join(home, "auth.json");
|
|
377
|
+
await symlink(authFile, authLink);
|
|
354
378
|
deadline.check();
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
const owned = ownCodexProcess(spawnFn(file, ["app-server", "--strict-config"], {
|
|
380
|
+
cwd, env, detached: false, stdio: ["pipe", "pipe", "pipe"]
|
|
381
|
+
}));
|
|
382
|
+
transport = new RestrictedCodexTransport(owned, deadline, frameLimit);
|
|
383
|
+
transport.onNotification = onNotification;
|
|
384
|
+
phase = "initialize";
|
|
385
|
+
const initialize = await transport.rpc("initialize", {
|
|
386
|
+
clientInfo: { name: "humanish_analysis", version: "1.0.0" }, capabilities: { experimentalApi: true }
|
|
387
|
+
});
|
|
388
|
+
if (typeof initialize.userAgent !== "string" || !initialize.userAgent.includes(`/0.154.0 `)
|
|
389
|
+
|| initialize.codexHome !== home || initialize.platformOs !== (platform === "darwin" ? "macos" : "linux") || initialize.platformFamily !== "unix")
|
|
390
|
+
throw new RestrictedCodexStop("codex_unsupported_version");
|
|
391
|
+
transport.notify("initialized", {});
|
|
392
|
+
phase = "config/read";
|
|
393
|
+
const effective = await transport.rpc("config/read", { includeLayers: true, cwd });
|
|
394
|
+
if (!admitsRestrictedCodexConfig(effective, configPath, request.model))
|
|
395
|
+
throw new RestrictedCodexStop("codex_unsafe_configuration");
|
|
396
|
+
phase = "account/read";
|
|
397
|
+
const account = await transport.rpc("account/read", { refreshToken: false });
|
|
398
|
+
if (account.account === null)
|
|
399
|
+
throw new RestrictedCodexStop("codex_login_required");
|
|
400
|
+
if (codexRecord(account.account).type !== "chatgpt" || account.requiresOpenaiAuth !== true)
|
|
401
|
+
throw new RestrictedCodexStop("codex_unsupported_auth");
|
|
402
|
+
phase = "thread/start";
|
|
403
|
+
const thread = await transport.rpc("thread/start", { cwd, ephemeral: true, experimentalRawEvents: true,
|
|
404
|
+
approvalPolicy: "never", sandbox: "read-only", model: request.model, modelProvider: "openai", allowProviderModelFallback: false,
|
|
405
|
+
environments: [], runtimeWorkspaceRoots: [], dynamicTools: [], baseInstructions: request.instructions, config: config.overrides });
|
|
406
|
+
if (!admitsRestrictedCodexThread(thread, request.model, cwd))
|
|
407
|
+
throw new RestrictedCodexStop("codex_unsafe_configuration");
|
|
408
|
+
threadId = String(codexRecord(thread.thread).id);
|
|
409
|
+
phase = "mcpServerStatus/list";
|
|
410
|
+
const mcp = await transport.rpc("mcpServerStatus/list", { limit: 100 });
|
|
411
|
+
if (!Array.isArray(mcp.data) || mcp.data.length !== 0 || mcp.nextCursor !== null)
|
|
412
|
+
throw new RestrictedCodexStop("codex_unsafe_configuration");
|
|
413
|
+
identity = { model: request.model, instructions: request.instructions };
|
|
359
414
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (typeof returnedTurnId !== "string" || returnedTurnId.length === 0 || returnedTurnId.length > 200
|
|
367
|
-
|| (earlyTurnId !== undefined && earlyTurnId !== returnedTurnId))
|
|
368
|
-
throw new RestrictedCodexStop("codex_protocol_error");
|
|
369
|
-
turnId = returnedTurnId;
|
|
370
|
-
for (const event of early)
|
|
371
|
-
handleTurnEvent(event.method, event.params);
|
|
372
|
-
early.length = 0;
|
|
373
|
-
result = await deadline.wait(finished);
|
|
374
|
-
deadline.check();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
const code = error instanceof RestrictedCodexStop ? error.code : "codex_process_failed";
|
|
379
|
-
result = restrictedCodexFailure(code === "codex_cleanup_failed" ? code : deadline.code ?? code, dispatched, usage);
|
|
380
|
-
}
|
|
381
|
-
finally {
|
|
382
|
-
// Deadline remains authoritative until acceptance; teardown has its own small grace.
|
|
383
|
-
deadline.close();
|
|
384
|
-
let cleaned = result.errorCode !== "codex_cleanup_failed";
|
|
385
|
-
if (transport) {
|
|
386
|
-
const cleanupTurn = turnId ?? earlyTurnId;
|
|
387
|
-
cleaned = await transport.close(dispatched && !completed && threadId && cleanupTurn
|
|
388
|
-
? { threadId, turnId: cleanupTurn } : undefined).catch(() => false);
|
|
389
|
-
if (!cleaned)
|
|
390
|
-
retainUnclosedChild(transport.owned.closed);
|
|
391
|
-
}
|
|
392
|
-
let authReplaced = false;
|
|
393
|
-
if (authLink && cleaned) {
|
|
394
|
-
try {
|
|
395
|
-
const info = await lstat(authLink);
|
|
396
|
-
authReplaced = !info.isSymbolicLink();
|
|
397
|
-
if (!authReplaced)
|
|
398
|
-
await unlink(authLink);
|
|
415
|
+
else {
|
|
416
|
+
transport.beginRequest(deadline, frameLimit);
|
|
417
|
+
transport.onNotification = onNotification;
|
|
418
|
+
}
|
|
419
|
+
if (readinessOnly) {
|
|
420
|
+
result = { status: "completed", output: null, usage: null, usageComplete: false, dispatched: false, errorCode: null };
|
|
399
421
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
422
|
+
else {
|
|
423
|
+
phase = "turn/start";
|
|
424
|
+
const input = [{ type: "text", text: request.evidence, text_elements: [] }];
|
|
425
|
+
for (const [index, image] of request.images.entries()) {
|
|
426
|
+
deadline.check();
|
|
427
|
+
const match = CODEX_IMAGE.exec(image.dataUrl);
|
|
428
|
+
const imagePath = path.join(scratch, `evidence-${index}.${match[1] === "jpeg" ? "jpg" : match[1]}`);
|
|
429
|
+
await writeFile(imagePath, Buffer.from(match[2], "base64"), { mode: 0o600 });
|
|
430
|
+
input.push({ type: "text", text: JSON.stringify({ captureEvidenceId: image.evidenceId }), text_elements: [] }, { type: "localImage", path: imagePath });
|
|
431
|
+
}
|
|
432
|
+
deadline.check();
|
|
433
|
+
// Even a lost acknowledgment may have dispatched the request. Never claim zero cost.
|
|
434
|
+
dispatched = true;
|
|
435
|
+
const turn = await transport.rpc("turn/start", { threadId, cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly" },
|
|
436
|
+
environments: [], runtimeWorkspaceRoots: [], effort: "low", model: request.model, outputSchema: request.schema, input });
|
|
437
|
+
const returnedTurnId = codexRecord(turn.turn).id;
|
|
438
|
+
if (typeof returnedTurnId !== "string" || returnedTurnId.length === 0 || returnedTurnId.length > 200
|
|
439
|
+
|| (earlyTurnId !== undefined && earlyTurnId !== returnedTurnId))
|
|
440
|
+
throw new RestrictedCodexStop("codex_protocol_error");
|
|
441
|
+
turnId = returnedTurnId;
|
|
442
|
+
interrupt = { threadId: threadId, turnId };
|
|
443
|
+
for (const event of early)
|
|
444
|
+
handleTurnEvent(event.method, event.params);
|
|
445
|
+
early.length = 0;
|
|
446
|
+
phase = "response";
|
|
447
|
+
result = await deadline.wait(finished);
|
|
448
|
+
deadline.check();
|
|
449
|
+
previousUsage = latestUsage;
|
|
450
|
+
interrupt = undefined;
|
|
403
451
|
}
|
|
404
452
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (authReplaced) {
|
|
409
|
-
cleaned = false;
|
|
410
|
-
if (work)
|
|
411
|
-
await preserveUnexpectedAuth(work, sourceEnv).catch(() => undefined);
|
|
453
|
+
catch (error) {
|
|
454
|
+
const code = error instanceof RestrictedCodexStop ? error.code : "codex_process_failed";
|
|
455
|
+
result = { ...restrictedCodexFailure(code === "codex_cleanup_failed" ? code : deadline.code ?? code, dispatched, usage), failurePhase: phase };
|
|
412
456
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
finally {
|
|
458
|
+
deadline.close();
|
|
459
|
+
activeDeadline = undefined;
|
|
460
|
+
if (transport)
|
|
461
|
+
transport.onNotification = () => undefined;
|
|
462
|
+
if (result.errorCode !== null) {
|
|
463
|
+
if (result.errorCode === "codex_cleanup_failed")
|
|
464
|
+
cleanupTrusted = false;
|
|
465
|
+
if (!await dispose())
|
|
466
|
+
result = { ...restrictedCodexFailure("codex_cleanup_failed", dispatched, usage), failurePhase: "cleanup" };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return result.errorCode !== null && result.failurePhase === undefined ? { ...result, failurePhase: phase } : result;
|
|
419
470
|
}
|
|
420
|
-
return
|
|
471
|
+
return {
|
|
472
|
+
run(request, readinessOnly = false) {
|
|
473
|
+
const error = restrictedCodexRequestError(request);
|
|
474
|
+
if (error)
|
|
475
|
+
return Promise.resolve(restrictedCodexFailure(error));
|
|
476
|
+
if (closed || (identity && (identity.model !== request.model || identity.instructions !== request.instructions)))
|
|
477
|
+
return Promise.resolve(restrictedCodexFailure("invalid_request"));
|
|
478
|
+
if (pending || unclosedChildren.size)
|
|
479
|
+
return Promise.resolve(restrictedCodexFailure("codex_busy"));
|
|
480
|
+
if (!(platform === "linux" && arch === "x64") && !(platform === "darwin" && arch === "arm64"))
|
|
481
|
+
return Promise.resolve(restrictedCodexFailure("codex_unsupported_platform"));
|
|
482
|
+
const task = execute(request, readinessOnly);
|
|
483
|
+
pending = task;
|
|
484
|
+
void task.finally(() => { if (pending === task)
|
|
485
|
+
pending = undefined; }).catch(() => undefined);
|
|
486
|
+
return task;
|
|
487
|
+
},
|
|
488
|
+
close() {
|
|
489
|
+
closed = true;
|
|
490
|
+
activeDeadline?.stop("cancelled");
|
|
491
|
+
return closing ??= (async () => { await pending; return dispose(); })();
|
|
492
|
+
}
|
|
493
|
+
};
|
|
421
494
|
}
|
|
422
495
|
export async function checkRestrictedCodexSessionReadiness(input = {}, options = {}) {
|
|
423
496
|
return runRestrictedCodexSession({ model: RESTRICTED_CODEX_ANALYSIS_MODELS[0], instructions: "Analyze only supplied study evidence.",
|