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.
Files changed (66) hide show
  1. package/README.md +25 -4
  2. package/dist/actor-contract.d.ts +3 -1
  3. package/dist/actor-contract.js +6 -3
  4. package/dist/actor-contract.js.map +1 -1
  5. package/dist/computer-use.js +4 -3
  6. package/dist/computer-use.js.map +1 -1
  7. package/dist/cua-provider-error.d.ts +5 -1
  8. package/dist/cua-provider-error.js +9 -2
  9. package/dist/cua-provider-error.js.map +1 -1
  10. package/dist/doctor-lab.d.ts +8 -0
  11. package/dist/doctor-lab.js +9 -4
  12. package/dist/doctor-lab.js.map +1 -1
  13. package/dist/first-run-path.d.ts +3 -0
  14. package/dist/first-run-path.js +22 -1
  15. package/dist/first-run-path.js.map +1 -1
  16. package/dist/guest-bootstrap.d.ts +12 -4
  17. package/dist/guest-bootstrap.js +49 -17
  18. package/dist/guest-bootstrap.js.map +1 -1
  19. package/dist/guest-runtime-desktop.d.ts +5 -2
  20. package/dist/guest-runtime-desktop.js +30 -6
  21. package/dist/guest-runtime-desktop.js.map +1 -1
  22. package/dist/guest-runtime-main.js +2 -1
  23. package/dist/guest-runtime-main.js.map +1 -1
  24. package/dist/guest-runtime.d.ts +1 -1
  25. package/dist/guest-runtime.js +5 -3
  26. package/dist/guest-runtime.js.map +1 -1
  27. package/dist/init-templates.d.ts +6 -1
  28. package/dist/init-templates.js +39 -3
  29. package/dist/init-templates.js.map +1 -1
  30. package/dist/init.d.ts +6 -1
  31. package/dist/init.js +35 -3
  32. package/dist/init.js.map +1 -1
  33. package/dist/lab-engine.js +1 -1
  34. package/dist/lab-engine.js.map +1 -1
  35. package/dist/lab-summary.d.ts +4 -0
  36. package/dist/lab-summary.js +5 -1
  37. package/dist/lab-summary.js.map +1 -1
  38. package/dist/local-firecracker-desktop.js +13 -10
  39. package/dist/local-firecracker-desktop.js.map +1 -1
  40. package/dist/local-runtime-release.js +8 -8
  41. package/dist/observer-app.html +1 -1
  42. package/dist/program.js +9 -1
  43. package/dist/program.js.map +1 -1
  44. package/dist/restricted-codex-participant-policy.d.ts +0 -2
  45. package/dist/restricted-codex-participant-policy.js +2 -3
  46. package/dist/restricted-codex-participant-policy.js.map +1 -1
  47. package/dist/restricted-codex-participant.d.ts +2 -2
  48. package/dist/restricted-codex-participant.js +46 -39
  49. package/dist/restricted-codex-participant.js.map +1 -1
  50. package/dist/restricted-codex-policy.d.ts +1 -0
  51. package/dist/restricted-codex-policy.js.map +1 -1
  52. package/dist/restricted-codex-session.d.ts +6 -2
  53. package/dist/restricted-codex-session.js +279 -206
  54. package/dist/restricted-codex-session.js.map +1 -1
  55. package/dist/restricted-codex-transport.d.ts +4 -2
  56. package/dist/restricted-codex-transport.js +14 -2
  57. package/dist/restricted-codex-transport.js.map +1 -1
  58. package/dist/tui-app.js +16 -16
  59. package/docs/architecture/local-browser-runtime.md +25 -5
  60. package/docs/architecture/restricted-codex-analysis.md +14 -5
  61. package/docs/contracts/schemas.md +8 -1
  62. package/docs/goals/current.md +3 -3
  63. package/docs/ramp/README.md +7 -1
  64. package/docs/release/0.100.0-local-setup.md +31 -0
  65. package/docs/release/0.100.1-participant-continuity.md +20 -0
  66. 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
- /** Each call owns a separate process, home and thread. An unresolved child blocks
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 error = restrictedCodexRequestError(request);
123
- if (error)
124
- return restrictedCodexFailure(error);
125
- if (unclosedChildren.size)
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
- /** Exactly one fresh thread/turn; private host auth, no participant state reuse. */
167
- async function executeRestrictedCodexSession(request, options = {}, readinessOnly = false) {
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, turnId, earlyTurnId;
179
- let dispatched = false, completed = false, usage = null;
180
- let generatedDeltaBytes = 0;
181
- let outputItem;
182
- let result = restrictedCodexFailure("codex_process_failed");
183
- const early = [];
184
- let resolveTurn;
185
- const finished = new Promise(resolve => { resolveTurn = resolve; });
186
- const handleTurnEvent = (method, params) => {
187
- if (!hasScopedIdentity(method, params, threadId, turnId)) {
188
- deadline.stop("codex_protocol_error");
189
- return;
190
- }
191
- const item = codexRecord(params.item);
192
- if (method === "rawResponseItem/completed") {
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
- if (item.type === "agentMessage") {
209
- if (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)) {
210
- deadline.stop("codex_tool_call");
211
- return;
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
- if (method === "item/completed" && item.phase !== "commentary") {
214
- if (typeof item.id !== "string" || typeof item.text !== "string" || Buffer.byteLength(item.text) > CODEX_MAX_OUTPUT_BYTES
215
- || (item.phase !== null && item.phase !== "final_answer") || (item.delivery !== null && item.delivery !== undefined)
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
- if (method === "turn/completed") {
225
- const turn = codexRecord(params.turn);
226
- if (turn.id !== turnId || completed) {
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
- completed = true;
231
- if (turn.status === "interrupted") {
232
- deadline.stop("cancelled");
233
- return;
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 (turn.status !== "completed" || turn.error !== null || !outputItem) {
236
- deadline.stop("invalid_response");
237
- return;
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
- try {
240
- resolveTurn({ status: "completed", output: JSON.parse(outputItem.text), usage,
241
- usageComplete: usage !== null, dispatched: true, errorCode: null });
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
- catch {
244
- deadline.stop("invalid_response");
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" && !["message", "reasoning"].includes(String(item.type)))
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
- const initialize = await transport.rpc("initialize", {
325
- clientInfo: { name: "humanish_analysis", version: "1.0.0" }, capabilities: { experimentalApi: true }
326
- });
327
- if (typeof initialize.userAgent !== "string" || !initialize.userAgent.includes(`/0.154.0 `)
328
- || initialize.codexHome !== home || initialize.platformOs !== (platform === "darwin" ? "macos" : "linux") || initialize.platformFamily !== "unix")
329
- throw new RestrictedCodexStop("codex_unsupported_version");
330
- transport.notify("initialized", {});
331
- const effective = await transport.rpc("config/read", { includeLayers: true, cwd });
332
- if (!admitsRestrictedCodexConfig(effective, configPath, request.model))
333
- throw new RestrictedCodexStop("codex_unsafe_configuration");
334
- const account = await transport.rpc("account/read", { refreshToken: false });
335
- if (account.account === null)
336
- throw new RestrictedCodexStop("codex_login_required");
337
- if (codexRecord(account.account).type !== "chatgpt" || account.requiresOpenaiAuth !== true)
338
- throw new RestrictedCodexStop("codex_unsupported_auth");
339
- const thread = await transport.rpc("thread/start", { cwd, ephemeral: true, experimentalRawEvents: true,
340
- approvalPolicy: "never", sandbox: "read-only", model: request.model, modelProvider: "openai", allowProviderModelFallback: false,
341
- environments: [], runtimeWorkspaceRoots: [], dynamicTools: [], baseInstructions: request.instructions, config: config.overrides });
342
- if (!admitsRestrictedCodexThread(thread, request.model, cwd))
343
- throw new RestrictedCodexStop("codex_unsafe_configuration");
344
- threadId = String(codexRecord(thread.thread).id);
345
- const mcp = await transport.rpc("mcpServerStatus/list", { limit: 100 });
346
- if (!Array.isArray(mcp.data) || mcp.data.length !== 0 || mcp.nextCursor !== null)
347
- throw new RestrictedCodexStop("codex_unsafe_configuration");
348
- if (readinessOnly) {
349
- result = { status: "completed", output: null, usage: null, usageComplete: false, dispatched: false, errorCode: null };
350
- }
351
- else {
352
- const input = [{ type: "text", text: request.evidence, text_elements: [] }];
353
- for (const [index, image] of request.images.entries()) {
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 match = CODEX_IMAGE.exec(image.dataUrl);
356
- const imagePath = path.join(scratch, `evidence-${index}.${match[1] === "jpeg" ? "jpg" : match[1]}`);
357
- await writeFile(imagePath, Buffer.from(match[2], "base64"), { mode: 0o600, flag: "wx" });
358
- input.push({ type: "text", text: JSON.stringify({ captureEvidenceId: image.evidenceId }), text_elements: [] }, { type: "localImage", path: imagePath });
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
- deadline.check();
361
- // Even a lost acknowledgment may have dispatched the request. Never claim zero cost.
362
- dispatched = true;
363
- const turn = await transport.rpc("turn/start", { threadId, cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly" },
364
- environments: [], runtimeWorkspaceRoots: [], effort: "low", model: request.model, outputSchema: request.schema, input });
365
- const returnedTurnId = codexRecord(turn.turn).id;
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
- catch (error) {
401
- if (codexRecord(error).code !== "ENOENT")
402
- cleaned = false;
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
- // An unexpected replacement may contain rotated login state. Do not discard it
406
- // or overwrite the host login. Leave this private control-plane directory for
407
- // recovery and fail closed; no path or credential is included in the result.
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
- if (work && cleaned && !authReplaced)
414
- await rm(work, { recursive: true, force: true }).catch(() => { cleaned = false; });
415
- if (work && !cleaned && !authReplaced)
416
- await writeRecoveryMarker(work, sourceEnv, "process_cleanup_unconfirmed").catch(() => undefined);
417
- if (!cleaned)
418
- result = restrictedCodexFailure("codex_cleanup_failed", dispatched, usage);
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 result;
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.",