From bf09429d92cfe7e3bcdb619fb69a1ecd494a0192 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 09:26:19 +0200 Subject: [PATCH 01/15] fix(pi): support Codex subscription auth --- package.json | 4 + patches/@earendil-works__pi-ai@0.83.0.patch | 64 ++++++++++ ...rendil-works__pi-coding-agent@0.83.0.patch | 36 ++++++ pnpm-lock.yaml | 119 +++++++++++------- software/pi/package.json | 4 +- software/pi/patches/pi-acp-node-options.patch | 30 +++++ software/pi/scripts/build-pi-acp.mjs | 12 ++ software/pi/scripts/finalize-package.mjs | 67 ++++++++++ software/pi/tests/package.test.mjs | 94 +++++++++++++- 9 files changed, 377 insertions(+), 53 deletions(-) create mode 100644 patches/@earendil-works__pi-ai@0.83.0.patch create mode 100644 patches/@earendil-works__pi-coding-agent@0.83.0.patch create mode 100644 software/pi/patches/pi-acp-node-options.patch create mode 100644 software/pi/scripts/finalize-package.mjs diff --git a/package.json b/package.json index 90388822ac..4848076dcf 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,10 @@ "@rivetkit/rivetkit-wasm": "2.3.9", "@mariozechner/pi-tui": "0.60.0", "@mariozechner/pi-agent-core": "0.60.0" + }, + "patchedDependencies": { + "@earendil-works/pi-coding-agent@0.83.0": "patches/@earendil-works__pi-coding-agent@0.83.0.patch", + "@earendil-works/pi-ai@0.83.0": "patches/@earendil-works__pi-ai@0.83.0.patch" } } } diff --git a/patches/@earendil-works__pi-ai@0.83.0.patch b/patches/@earendil-works__pi-ai@0.83.0.patch new file mode 100644 index 0000000000..d7bd0314f3 --- /dev/null +++ b/patches/@earendil-works__pi-ai@0.83.0.patch @@ -0,0 +1,64 @@ +diff --git a/dist/env-api-keys.js b/dist/env-api-keys.js +index 942d6300b85f479b3b6f6eaad57296c5136b7f2c..ad7bae31f92aeb19a53d11dea245b2419c2ddaec 100644 +--- a/dist/env-api-keys.js ++++ b/dist/env-api-keys.js +@@ -97,6 +97,7 @@ function getApiKeyEnvVars(provider) { + together: "TOGETHER_API_KEY", + opencode: "OPENCODE_API_KEY", + "opencode-go": "OPENCODE_API_KEY", ++ "openai-codex": "OPENAI_CODEX_ACCESS_TOKEN", + "kimi-coding": "KIMI_API_KEY", + "cloudflare-workers-ai": "CLOUDFLARE_API_KEY", + "cloudflare-ai-gateway": "CLOUDFLARE_API_KEY", +diff --git a/dist/providers/openai-codex.js b/dist/providers/openai-codex.js +--- a/dist/providers/openai-codex.js ++++ b/dist/providers/openai-codex.js +@@ -3,12 +3,36 @@ import { lazyOAuth } from "../auth/helpers.js"; + import { loadOpenAICodexOAuth } from "../auth/oauth/load.js"; + import { createProvider } from "../models.js"; + import { OPENAI_CODEX_MODELS } from "./openai-codex.models.js"; ++function externalCodexAuth() { ++ return { ++ name: "externally managed Codex access token", ++ login: async (interaction) => ({ ++ type: "api_key", ++ key: await interaction.prompt({ type: "secret", message: "Enter externally managed Codex access token" }), ++ }), ++ resolve: async ({ ctx, credential }) => { ++ const apiKey = credential?.key ?? (await ctx.env("OPENAI_CODEX_ACCESS_TOKEN")); ++ if (!apiKey) ++ return undefined; ++ const accountId = credential?.env?.OPENAI_CODEX_ACCOUNT_ID ?? (await ctx.env("OPENAI_CODEX_ACCOUNT_ID")); ++ return { ++ auth: { ++ apiKey, ++ ...(accountId ? { headers: { "chatgpt-account-id": accountId } } : {}), ++ }, ++ env: credential?.env, ++ source: credential?.key ? "stored credential" : "OPENAI_CODEX_ACCESS_TOKEN", ++ }; ++ }, ++ }; ++} + export function openaiCodexProvider() { + return createProvider({ + id: "openai-codex", + name: "OpenAI Codex", + baseUrl: "https://chatgpt.com/backend-api", + auth: { ++ apiKey: externalCodexAuth(), + oauth: lazyOAuth({ name: "OpenAI (ChatGPT Plus/Pro)", load: loadOpenAICodexOAuth }), + }, + models: Object.values(OPENAI_CODEX_MODELS), +diff --git a/dist/api/openai-codex-responses.js b/dist/api/openai-codex-responses.js +--- a/dist/api/openai-codex-responses.js ++++ b/dist/api/openai-codex-responses.js +@@ -171,7 +171,7 @@ export const streamOpenAICodexResponses = (model, context, options) => { + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } +- const accountId = extractAccountId(apiKey); ++ const accountId = options?.headers?.["chatgpt-account-id"]?.trim() || extractAccountId(apiKey); + const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, model.compat?.supportsOpenAIGrammarTools ?? false); + const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId; + const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId); diff --git a/patches/@earendil-works__pi-coding-agent@0.83.0.patch b/patches/@earendil-works__pi-coding-agent@0.83.0.patch new file mode 100644 index 0000000000..9c5b172d02 --- /dev/null +++ b/patches/@earendil-works__pi-coding-agent@0.83.0.patch @@ -0,0 +1,36 @@ +diff --git a/dist/core/http-dispatcher.js b/dist/core/http-dispatcher.js +--- a/dist/core/http-dispatcher.js ++++ b/dist/core/http-dispatcher.js +@@ -10,4 +10,22 @@ export const HTTP_IDLE_TIMEOUT_CHOICES = [ + ]; + const originalGlobalFetch = globalThis.fetch; + let installedGlobalFetch; ++const UNDICI_GLOBAL_NAMES = [ ++ "fetch", ++ "Headers", ++ "Response", ++ "Request", ++ "FormData", ++ "WebSocket", ++ "CloseEvent", ++ "ErrorEvent", ++ "MessageEvent", ++ "EventSource", ++]; ++function canInstallUndiciGlobals() { ++ return UNDICI_GLOBAL_NAMES.every((name) => { ++ const descriptor = Object.getOwnPropertyDescriptor(globalThis, name); ++ return !descriptor || descriptor.writable === true || typeof descriptor.set === "function"; ++ }); ++} + export function parseHttpIdleTimeoutMs(value) { +@@ -83,6 +101,8 @@ export function configureHttpDispatcher(timeoutMs = DEFAULT_HTTP_IDLE_TIMEOUT_M + const shouldInstallGlobals = installedGlobalFetch === undefined + ? globalThis.fetch === originalGlobalFetch + : globalThis.fetch === installedGlobalFetch; +- if (shouldInstallGlobals) { ++ // Embedded runtimes can own non-writable Web APIs. Preserve that surface ++ // instead of letting undici.install() fail partway through its assignments. ++ if (shouldInstallGlobals && canInstallUndiciGlobals()) { + undici.install?.(); + installedGlobalFetch = globalThis.fetch; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74afb72b75..d6c45e3e38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,14 @@ overrides: pnpmfileChecksum: sha256-SEnUwm6AFeob5DIJB/q7nwJGPinO5RVveyVjDzNYuIM= +patchedDependencies: + '@earendil-works/pi-ai@0.83.0': + hash: 775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2 + path: patches/@earendil-works__pi-ai@0.83.0.patch + '@earendil-works/pi-coding-agent@0.83.0': + hash: 1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366 + path: patches/@earendil-works__pi-coding-agent@0.83.0.patch + importers: .: @@ -936,10 +944,10 @@ importers: dependencies: '@flue/cli': specifier: npm:@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2 - version: '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.6)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + version: '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.7)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' '@flue/runtime': specifier: npm:@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2 - version: '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.6)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + version: '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.7)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -948,7 +956,7 @@ importers: version: link:../../packages/flue '@rivet-dev/flue': specifier: 2.3.9 - version: 2.3.9(@opentelemetry/api@1.9.0)(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(better-sqlite3@12.8.0)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(tsx@4.21.0)(typebox@1.3.6)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + version: 2.3.9(@opentelemetry/api@1.9.0)(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(better-sqlite3@12.8.0)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(tsx@4.21.0)(typebox@1.3.7)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) rivetkit: specifier: 2.3.9 version: 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(ws@8.21.0(bufferutil@4.1.0)) @@ -3189,7 +3197,7 @@ importers: devDependencies: '@flue/runtime': specifier: npm:@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2 - version: '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.6)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + version: '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.7)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' '@rivet-dev/agentos': specifier: workspace:* version: link:../agentos @@ -4144,8 +4152,8 @@ importers: specifier: 1.2.1 version: 1.2.1(zod@3.25.76) '@earendil-works/pi-coding-agent': - specifier: 0.80.6 - version: 0.80.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + specifier: 0.83.0 + version: 0.83.0(patch_hash=1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) pi-mcp-adapter: specifier: 2.11.0 version: 2.11.0(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) @@ -5921,6 +5929,10 @@ packages: resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} engines: {node: '>=22.19.0'} + '@earendil-works/pi-agent-core@0.83.0': + resolution: {integrity: sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g==} + engines: {node: '>=22.19.0'} + '@earendil-works/pi-ai@0.74.2': resolution: {integrity: sha512-ukQBHGDm20k9ZUS2cGjNN9vDJp/48r35xmvgSx3paCaC06r2N/PLuRZoJmwQ1ZM7f8T3072odv9YPWn+77w0LA==} engines: {node: '>=20.0.0'} @@ -5936,8 +5948,13 @@ packages: engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.80.6': - resolution: {integrity: sha512-vcfD6tOk402isLl3Cm/qbn2O10TvgroMp1+/fEGM24ZdvETFCdOYv5VZ7m59EI5fPsjfSJh+CpQ5bhBrhfOg7g==} + '@earendil-works/pi-ai@0.83.0': + resolution: {integrity: sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.83.0': + resolution: {integrity: sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==} engines: {node: '>=22.19.0'} hasBin: true @@ -5945,8 +5962,8 @@ packages: resolution: {integrity: sha512-valQPz74qbdydRqII6t9rJ46YANMOOJeDhKm25a1ZrWvWwdjAaAEu6s3ur/LWz84Wkkwcbub2ZkVjzCZi8gFGA==} engines: {node: '>=20.0.0'} - '@earendil-works/pi-tui@0.80.10': - resolution: {integrity: sha512-c2JO29PbhKPEQ6fgHQKAl0WhwuFqzWfzspMmP+8B5tpDuP+0mvarRbKKg8gq4b+pQx/QX+6aVS4ko7deoyjQjg==} + '@earendil-works/pi-tui@0.83.0': + resolution: {integrity: sha512-IoYrb0rORjELmEpNtoCA/U8je3KopMkRAVJRdSzvXRvgb+Huo1gNh8Q5CSZvNOiYtDxJdj2tYZZHZ4B3+IN3hA==} engines: {node: '>=22.19.0'} '@emnapi/core@1.11.1': @@ -14233,6 +14250,9 @@ packages: typebox@1.3.6: resolution: {integrity: sha512-Sc8RA0NCMEFmApHNU9ZMzqcpQj46She44J8ffpLM/bdhLNUZKq7DJumcLcsFx1gRmDfQPgCgOmFFJ7rcnfWNyA==} + typebox@1.3.7: + resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -16710,9 +16730,9 @@ snapshots: '@microsoft/fetch-event-source': 2.0.1 fastq: 1.20.1 - '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': dependencies: - '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -16724,11 +16744,12 @@ snapshots: - ws - zod - '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': + '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: - '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) + '@earendil-works/pi-ai': 0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + diff: 8.0.4 ignore: 7.0.5 - typebox: 1.1.38 + typebox: 1.3.7 yaml: 2.9.0 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -16758,17 +16779,17 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) + '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)(bufferutil@4.1.0) '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) partial-json: 0.1.7 typebox: 1.1.38 transitivePeerDependencies: @@ -16779,7 +16800,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': + '@earendil-works/pi-ai@0.81.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -16800,19 +16821,19 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.81.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': + '@earendil-works/pi-ai@0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) + '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)(bufferutil@4.1.0) '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) + openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) partial-json: 0.1.7 - typebox: 1.1.38 + typebox: 1.3.7 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -16821,11 +16842,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.80.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-coding-agent@0.83.0(patch_hash=1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: - '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) - '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) - '@earendil-works/pi-tui': 0.80.10 + '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-tui': 0.83.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -16838,7 +16859,7 @@ snapshots: minimatch: 10.2.5 proper-lockfile: 4.1.2 semver: 7.8.0 - typebox: 1.1.38 + typebox: 1.3.7 undici: 8.5.0 yaml: 2.9.0 optionalDependencies: @@ -16858,7 +16879,7 @@ snapshots: optionalDependencies: koffi: 2.15.2 - '@earendil-works/pi-tui@0.80.10': + '@earendil-works/pi-tui@0.83.0': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.5 @@ -19548,10 +19569,10 @@ snapshots: optionalDependencies: pyodide: 0.28.3(bufferutil@4.1.0) - '@rivet-dev/flue@2.3.9(@opentelemetry/api@1.9.0)(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(better-sqlite3@12.8.0)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(tsx@4.21.0)(typebox@1.3.6)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + '@rivet-dev/flue@2.3.9(@opentelemetry/api@1.9.0)(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(better-sqlite3@12.8.0)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(tsx@4.21.0)(typebox@1.3.7)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': dependencies: - '@flue/cli': '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.6)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' - '@flue/runtime': '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.6)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + '@flue/cli': '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.7)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + '@flue/runtime': '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.7)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' rivetkit: 2.3.9(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(pyodide@0.28.3(bufferutil@4.1.0))(sql.js@1.14.1)(ws@8.21.0(bufferutil@4.1.0)) transitivePeerDependencies: - '@aws-sdk/client-rds-data' @@ -19615,10 +19636,10 @@ snapshots: - zod-openapi - zod-to-json-schema - '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.6)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + '@rivet-dev/labs-flue-cli@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@types/node@24.13.2)(bufferutil@4.1.0)(esbuild@0.28.1)(hono@4.12.9)(jiti@2.7.0)(tsx@4.21.0)(typebox@1.3.7)(typescript@5.9.3)(wrangler@4.113.0(bufferutil@4.1.0))(ws@8.21.0(bufferutil@4.1.0))(yaml@2.9.0)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': dependencies: '@cloudflare/vite-plugin': 1.46.0(bufferutil@4.1.0)(vite@8.1.5(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.113.0(bufferutil@4.1.0)) - '@flue/runtime': '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.6)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' + '@flue/runtime': '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.7)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)' '@flue/sdk': '@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2' '@hono/node-server': 2.0.11(hono@4.12.9) '@vercel/detect-agent': 1.2.3 @@ -19659,7 +19680,7 @@ snapshots: - zod-openapi - zod-to-json-schema - '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.6)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + '@rivet-dev/labs-flue-runtime@1.0.0-beta.9-rivet.2(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(bufferutil@4.1.0)(typebox@1.3.7)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': dependencies: '@cfworker/json-schema': 4.1.1 '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) @@ -19667,11 +19688,11 @@ snapshots: '@hono/node-server': 2.0.11(hono@4.12.9) '@hono/standard-validator': 0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.9) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) - '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3)) hono: 4.12.9 - hono-openapi: 1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.9))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.9)(openapi-types@12.1.3) + hono-openapi: 1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.9))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.9)(openapi-types@12.1.3) js-yaml: 4.3.0 just-bash: 3.1.0 openapi-types: 12.1.3 @@ -20780,25 +20801,25 @@ snapshots: '@stablelib/base64@1.0.1': {} - '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)': dependencies: '@standard-schema/spec': 1.1.0 '@types/json-schema': 7.0.15 quansync: 0.2.11 optionalDependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3)) - typebox: 1.3.6 + typebox: 1.3.7 valibot: 1.4.2(typescript@5.9.3) zod: 4.3.6 zod-to-json-schema: 3.25.2(zod@4.3.6) - '@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6)': + '@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6)': dependencies: - '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) '@standard-schema/spec': 1.1.0 openapi-types: 12.1.3 optionalDependencies: - typebox: 1.3.6 + typebox: 1.3.7 valibot: 1.4.2(typescript@5.9.3) zod: 4.3.6 @@ -23741,10 +23762,10 @@ snapshots: dependencies: react-is: 16.13.1 - hono-openapi@1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.9))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.9)(openapi-types@12.1.3): + hono-openapi@1.3.1(@hono/standard-validator@0.2.3(@standard-schema/spec@1.1.0)(hono@4.12.9))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.9)(openapi-types@12.1.3): dependencies: - '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) - '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.6)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) + '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(quansync@0.2.11)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.3.7)(valibot@1.4.2(typescript@5.9.3))(zod@4.3.6) '@types/json-schema': 7.0.15 openapi-types: 12.1.3 optionalDependencies: @@ -27119,6 +27140,8 @@ snapshots: typebox@1.3.6: {} + typebox@1.3.7: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 diff --git a/software/pi/package.json b/software/pi/package.json index e0967f4b22..0184630870 100644 --- a/software/pi/package.json +++ b/software/pi/package.json @@ -24,13 +24,13 @@ "agentos-package.json" ], "scripts": { - "build": "rm -rf dist && node scripts/build-pi-acp.mjs && tsc && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native --omit-optional", + "build": "rm -rf dist && node scripts/build-pi-acp.mjs && tsc && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native --omit-optional && node scripts/finalize-package.mjs", "check-types": "tsc --noEmit", "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" }, "dependencies": { "@agentclientprotocol/sdk": "1.2.1", - "@earendil-works/pi-coding-agent": "0.80.6", + "@earendil-works/pi-coding-agent": "0.83.0", "pi-mcp-adapter": "2.11.0", "zod": "3.25.76" }, diff --git a/software/pi/patches/pi-acp-node-options.patch b/software/pi/patches/pi-acp-node-options.patch new file mode 100644 index 0000000000..d420381265 --- /dev/null +++ b/software/pi/patches/pi-acp-node-options.patch @@ -0,0 +1,30 @@ +diff --git a/src/pi-rpc/process.ts b/src/pi-rpc/process.ts +index 035e591..4c784ad 100644 +--- a/src/pi-rpc/process.ts ++++ b/src/pi-rpc/process.ts +@@ -178,6 +178,7 @@ export class PiRpcProcess { + ? [ + '-e', +- `process.chdir(${JSON.stringify(params.cwd)}); process.argv.splice(1, 0, ${JSON.stringify(params.piEntrypoint)}); globalThis.__piAcpMain = import(${JSON.stringify(params.piEntrypoint)}).catch(error => { console.error(error); process.exitCode = 1; });`, ++ `process.chdir(${JSON.stringify(params.cwd)}); if (process.argv[1] === '--') process.argv.splice(1, 1); process.argv.splice(1, 0, ${JSON.stringify(params.piEntrypoint)}); globalThis.__piAcpMain = import(${JSON.stringify(params.piEntrypoint)}).catch(error => { console.error(error); process.exitCode = 1; });`, ++ '--', + ...piArgs + ] + : piArgs +@@ -189,6 +190,7 @@ export class PiRpcProcess { + shell: shouldUseShellForPiCommand(cmd), + signal: params.signal + }) ++ const proc = new PiRpcProcess(child, cleanupLaunchResources) + + // Ensure spawn failures (e.g. ENOENT when pi isn't installed) are surfaced as a + // deterministic error instead of later EPIPE/internal-error noise. +@@ -233,8 +235,6 @@ export class PiRpcProcess { + throw new PiRpcSpawnError(`Could not start pi (command: ${cmd}).`, { code, cause: e }) + } + +- const proc = new PiRpcProcess(child, cleanupLaunchResources) +- + // Best-effort handshake. + // Important: pi may emit a get_state response pointing at a sessionFile in a directory + // that is created lazily. Create the parent dir up-front to avoid later parse errors diff --git a/software/pi/scripts/build-pi-acp.mjs b/software/pi/scripts/build-pi-acp.mjs index 39838840d5..f7f96e7c19 100644 --- a/software/pi/scripts/build-pi-acp.mjs +++ b/software/pi/scripts/build-pi-acp.mjs @@ -26,6 +26,11 @@ const tarballPath = resolve(cacheDir, `pi-acp-${SOURCE_COMMIT}.tar.gz`); const sourceRoot = resolve(cacheDir, `pi-acp-${SOURCE_COMMIT}`); const outputDir = resolve(packageDir, "dist", "pi-acp"); const manifestPath = resolve(packageDir, "dist", "pi-acp-upstream.json"); +const compatibilityPatchPath = resolve( + packageDir, + "patches", + "pi-acp-node-options.patch", +); function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); @@ -94,6 +99,7 @@ for (const [name, version] of Object.entries(expectedDependencies)) { } run("npm", ["ci"], { cwd: sourceRoot }); +run("patch", ["-p1", "-i", compatibilityPatchPath], { cwd: sourceRoot }); run("npm", ["run", "build"], { cwd: sourceRoot }); const sourceEntrypoint = resolve(sourceRoot, "dist", "index.js"); @@ -120,6 +126,12 @@ writeFileSync( sourceCommit: SOURCE_COMMIT, sourceTarballSha256: SOURCE_TARBALL_SHA256, sourcePackageVersion: sourcePackage.version, + compatibilityPatches: [ + { + path: "patches/pi-acp-node-options.patch", + sha256: sha256(compatibilityPatchPath), + }, + ], buildCommands: ["npm ci", "npm run build"], entrypoint: "./pi-acp/index.js", entrypointSha256: sha256(sourceEntrypoint), diff --git a/software/pi/scripts/finalize-package.mjs b/software/pi/scripts/finalize-package.mjs new file mode 100644 index 0000000000..07ecc33aa0 --- /dev/null +++ b/software/pi/scripts/finalize-package.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import { cpSync, realpathSync, rmSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const stagedPackageDir = resolve(packageDir, "dist", "package"); +const codingAgentDir = resolve( + packageDir, + "node_modules", + "@earendil-works", + "pi-coding-agent", +); +const piAiDir = realpathSync( + resolve(dirname(realpathSync(codingAgentDir)), "pi-ai"), +); + +const patchedRuntimeFiles = [ + { + source: resolve(piAiDir, "dist", "env-api-keys.js"), + targets: [ + "@earendil-works/pi-ai/dist/env-api-keys.js", + "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js", + ], + }, + { + source: resolve(piAiDir, "dist", "providers", "openai-codex.js"), + targets: [ + "@earendil-works/pi-ai/dist/providers/openai-codex.js", + "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.js", + ], + }, + { + source: resolve(piAiDir, "dist", "api", "openai-codex-responses.js"), + targets: [ + "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js", + ], + }, + { + source: resolve(codingAgentDir, "dist", "core", "http-dispatcher.js"), + targets: ["@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js"], + }, +]; + +for (const file of patchedRuntimeFiles) { + for (const target of file.targets) { + cpSync(file.source, resolve(stagedPackageDir, "node_modules", target)); + } +} + +function run(command, args) { + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.status !== 0) { + throw new Error( + `Command failed (${result.status ?? "unknown"}): ${command} ${args.join(" ")}`, + ); + } +} + +const packageTar = resolve(packageDir, "dist", "package.tar"); +const packageAospkg = resolve(packageDir, "dist", "package.aospkg"); +rmSync(packageTar, { force: true }); +rmSync(packageAospkg, { force: true }); +run("tar", ["-cf", packageTar, "-C", stagedPackageDir, "."]); +run("agentos-toolchain", ["pack-aospkg", packageTar, packageAospkg]); diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index a547f920dc..be12309368 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -1,6 +1,8 @@ import assert from "node:assert/strict"; import { spawn } from "node:child_process"; -import { readFile } from "node:fs/promises"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import test from "node:test"; test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", async () => { @@ -46,7 +48,7 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", ); assert.equal( packageJson.dependencies["@earendil-works/pi-coding-agent"], - "0.80.6", + "0.83.0", ); assert.equal(packageJson.dependencies["pi-acp"], undefined); assert.equal(packageJson.dependencies["pi-mcp-adapter"], "2.11.0"); @@ -68,7 +70,55 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(packageJson.dependencies["@mariozechner/pi-coding-agent"], undefined); }); +test("Pi accepts external Codex auth without persisting it", async (t) => { + const { ModelRuntime } = await import( + new URL( + "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/index.js", + import.meta.url, + ) + ); + const directory = await mkdtemp(join(tmpdir(), "agentos-pi-codex-")); + const authPath = join(directory, "auth.json"); + const token = "external-codex-access-token"; + const accountId = "external-codex-account-id"; + t.after(() => rm(directory, { recursive: true, force: true })); + + const runtime = await ModelRuntime.create({ + authPath, + modelsPath: null, + allowModelNetwork: false, + }); + const auth = await runtime.getAuth("openai-codex", { + env: { + OPENAI_CODEX_ACCESS_TOKEN: token, + OPENAI_CODEX_ACCOUNT_ID: accountId, + }, + }); + + assert.equal(auth?.auth.apiKey, token); + assert.deepEqual(auth?.auth.headers, { "chatgpt-account-id": accountId }); + assert.equal(auth?.source, "OPENAI_CODEX_ACCESS_TOKEN"); + assert.equal(runtime.isUsingOAuth("openai-codex"), false); + assert.ok(runtime.getModel("openai-codex", "gpt-5.6-terra")); + assert.doesNotMatch(await readFile(authPath, "utf8"), /external-codex-access-token/); + assert.doesNotMatch(await readFile(authPath, "utf8"), /external-codex-account-id/); + assert.match( + await readFile( + new URL( + "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js", + import.meta.url, + ), + "utf8", + ), + /canInstallUndiciGlobals/, + ); +}); + test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { + const piEntrypoint = new URL( + "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + import.meta.url, + ).pathname; const child = spawn( new URL("../dist/package/bin/pi-acp", import.meta.url).pathname, [], @@ -76,7 +126,9 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, - PI_ACP_PI_COMMAND: "/opt/agentos/bin/pi", + PI_ACP_PI_ENTRYPOINT: piEntrypoint, + OPENAI_CODEX_ACCESS_TOKEN: "external-codex-access-token", + OPENAI_CODEX_ACCOUNT_ID: "external-codex-account-id", }, }, ); @@ -135,4 +187,40 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie response.result.agentCapabilities.sessionCapabilities.close, {}, ); + + const session = await new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error(`session/new timed out: ${stderr}`)), + 10_000, + ); + let buffer = ""; + child.stdout.on("data", (chunk) => { + buffer += chunk; + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + if (!line.trim()) continue; + const message = JSON.parse(line); + if (message.id !== 2) continue; + clearTimeout(timeout); + resolve(message); + } + }); + child.stdin.write( + `${JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "session/new", + params: { cwd: new URL("..", import.meta.url).pathname, mcpServers: [] }, + })}\n`, + ); + }); + + assert.equal(session.error, undefined); + assert.equal(typeof session.result.sessionId, "string"); + assert.ok( + session.result.models.availableModels.some( + (model) => model.modelId === "openai-codex/gpt-5.6-terra", + ), + ); }); From e86b9f249f0cc67b706fa225f956a598190c4a78 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 14:24:16 +0200 Subject: [PATCH 02/15] refactor(pi): provide Codex auth as extension --- patches/@earendil-works__pi-ai@0.83.0.patch | 52 ----------------- pnpm-lock.yaml | 8 +-- software/pi/agentos-package.json | 3 +- software/pi/extensions/codex-auth.mjs | 10 ++++ software/pi/patches/pi-acp-node-options.patch | 11 +++- software/pi/scripts/finalize-package.mjs | 21 ++----- software/pi/tests/package.test.mjs | 56 ++++++++----------- 7 files changed, 56 insertions(+), 105 deletions(-) create mode 100644 software/pi/extensions/codex-auth.mjs diff --git a/patches/@earendil-works__pi-ai@0.83.0.patch b/patches/@earendil-works__pi-ai@0.83.0.patch index d7bd0314f3..679137df90 100644 --- a/patches/@earendil-works__pi-ai@0.83.0.patch +++ b/patches/@earendil-works__pi-ai@0.83.0.patch @@ -1,55 +1,3 @@ -diff --git a/dist/env-api-keys.js b/dist/env-api-keys.js -index 942d6300b85f479b3b6f6eaad57296c5136b7f2c..ad7bae31f92aeb19a53d11dea245b2419c2ddaec 100644 ---- a/dist/env-api-keys.js -+++ b/dist/env-api-keys.js -@@ -97,6 +97,7 @@ function getApiKeyEnvVars(provider) { - together: "TOGETHER_API_KEY", - opencode: "OPENCODE_API_KEY", - "opencode-go": "OPENCODE_API_KEY", -+ "openai-codex": "OPENAI_CODEX_ACCESS_TOKEN", - "kimi-coding": "KIMI_API_KEY", - "cloudflare-workers-ai": "CLOUDFLARE_API_KEY", - "cloudflare-ai-gateway": "CLOUDFLARE_API_KEY", -diff --git a/dist/providers/openai-codex.js b/dist/providers/openai-codex.js ---- a/dist/providers/openai-codex.js -+++ b/dist/providers/openai-codex.js -@@ -3,12 +3,36 @@ import { lazyOAuth } from "../auth/helpers.js"; - import { loadOpenAICodexOAuth } from "../auth/oauth/load.js"; - import { createProvider } from "../models.js"; - import { OPENAI_CODEX_MODELS } from "./openai-codex.models.js"; -+function externalCodexAuth() { -+ return { -+ name: "externally managed Codex access token", -+ login: async (interaction) => ({ -+ type: "api_key", -+ key: await interaction.prompt({ type: "secret", message: "Enter externally managed Codex access token" }), -+ }), -+ resolve: async ({ ctx, credential }) => { -+ const apiKey = credential?.key ?? (await ctx.env("OPENAI_CODEX_ACCESS_TOKEN")); -+ if (!apiKey) -+ return undefined; -+ const accountId = credential?.env?.OPENAI_CODEX_ACCOUNT_ID ?? (await ctx.env("OPENAI_CODEX_ACCOUNT_ID")); -+ return { -+ auth: { -+ apiKey, -+ ...(accountId ? { headers: { "chatgpt-account-id": accountId } } : {}), -+ }, -+ env: credential?.env, -+ source: credential?.key ? "stored credential" : "OPENAI_CODEX_ACCESS_TOKEN", -+ }; -+ }, -+ }; -+} - export function openaiCodexProvider() { - return createProvider({ - id: "openai-codex", - name: "OpenAI Codex", - baseUrl: "https://chatgpt.com/backend-api", - auth: { -+ apiKey: externalCodexAuth(), - oauth: lazyOAuth({ name: "OpenAI (ChatGPT Plus/Pro)", load: loadOpenAICodexOAuth }), - }, - models: Object.values(OPENAI_CODEX_MODELS), diff --git a/dist/api/openai-codex-responses.js b/dist/api/openai-codex-responses.js --- a/dist/api/openai-codex-responses.js +++ b/dist/api/openai-codex-responses.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6c45e3e38..b45c783de0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ pnpmfileChecksum: sha256-SEnUwm6AFeob5DIJB/q7nwJGPinO5RVveyVjDzNYuIM= patchedDependencies: '@earendil-works/pi-ai@0.83.0': - hash: 775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2 + hash: fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08 path: patches/@earendil-works__pi-ai@0.83.0.patch '@earendil-works/pi-coding-agent@0.83.0': hash: 1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366 @@ -16746,7 +16746,7 @@ snapshots: '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: - '@earendil-works/pi-ai': 0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -16821,7 +16821,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-ai@0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -16845,7 +16845,7 @@ snapshots: '@earendil-works/pi-coding-agent@0.83.0(patch_hash=1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) - '@earendil-works/pi-ai': 0.83.0(patch_hash=775223cba59ad571af7d7fc5285a0750bdfc995e05180c4405bbc7d6959210b2)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) '@earendil-works/pi-tui': 0.83.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 diff --git a/software/pi/agentos-package.json b/software/pi/agentos-package.json index b378ea263f..96649c50f4 100644 --- a/software/pi/agentos-package.json +++ b/software/pi/agentos-package.json @@ -4,7 +4,8 @@ "acpEntrypoint": "pi-acp", "env": { "PI_ACP_PI_COMMAND": "/opt/agentos/bin/pi", - "PI_ACP_PI_ENTRYPOINT": "/opt/agentos/pkgs/pi/0.0.1/node_modules/@earendil-works/pi-coding-agent/dist/cli.js" + "PI_ACP_PI_ENTRYPOINT": "/opt/agentos/pkgs/pi/0.0.1/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + "PI_ACP_PI_EXTENSION": "/opt/agentos/pkgs/pi/0.0.1/extensions/codex-auth.mjs" } }, "registry": { diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs new file mode 100644 index 0000000000..1364ed25b9 --- /dev/null +++ b/software/pi/extensions/codex-auth.mjs @@ -0,0 +1,10 @@ +export const codexProviderConfig = { + apiKey: "$OPENAI_CODEX_ACCESS_TOKEN", + headers: { + "chatgpt-account-id": "$OPENAI_CODEX_ACCOUNT_ID", + }, +}; + +export default function codexAuthExtension(pi) { + pi.registerProvider("openai-codex", codexProviderConfig); +} diff --git a/software/pi/patches/pi-acp-node-options.patch b/software/pi/patches/pi-acp-node-options.patch index d420381265..ac44c0c1e3 100644 --- a/software/pi/patches/pi-acp-node-options.patch +++ b/software/pi/patches/pi-acp-node-options.patch @@ -1,7 +1,16 @@ diff --git a/src/pi-rpc/process.ts b/src/pi-rpc/process.ts -index 035e591..4c784ad 100644 +index 035e591..b59e591 100644 --- a/src/pi-rpc/process.ts +++ b/src/pi-rpc/process.ts +@@ -173,6 +173,8 @@ export class PiRpcProcess { + ...(mcpLaunch?.args ?? []), + ...(permissionGateLaunch?.args ?? []) + ] ++ const piExtension = process.env.PI_ACP_PI_EXTENSION?.trim() ++ if (piExtension) piArgs.push('--extension', piExtension) + if (params.sessionPath) piArgs.push('--session', params.sessionPath) + const args = params.piEntrypoint + ? [ @@ -178,6 +178,7 @@ export class PiRpcProcess { ? [ '-e', diff --git a/software/pi/scripts/finalize-package.mjs b/software/pi/scripts/finalize-package.mjs index 07ecc33aa0..4aadb2f628 100644 --- a/software/pi/scripts/finalize-package.mjs +++ b/software/pi/scripts/finalize-package.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import { cpSync, realpathSync, rmSync } from "node:fs"; +import { cpSync, mkdirSync, realpathSync, rmSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -16,22 +16,9 @@ const codingAgentDir = resolve( const piAiDir = realpathSync( resolve(dirname(realpathSync(codingAgentDir)), "pi-ai"), ); +const codexExtension = resolve(packageDir, "extensions", "codex-auth.mjs"); const patchedRuntimeFiles = [ - { - source: resolve(piAiDir, "dist", "env-api-keys.js"), - targets: [ - "@earendil-works/pi-ai/dist/env-api-keys.js", - "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js", - ], - }, - { - source: resolve(piAiDir, "dist", "providers", "openai-codex.js"), - targets: [ - "@earendil-works/pi-ai/dist/providers/openai-codex.js", - "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.js", - ], - }, { source: resolve(piAiDir, "dist", "api", "openai-codex-responses.js"), targets: [ @@ -50,6 +37,10 @@ for (const file of patchedRuntimeFiles) { } } +const stagedExtension = resolve(stagedPackageDir, "extensions", "codex-auth.mjs"); +mkdirSync(dirname(stagedExtension), { recursive: true }); +cpSync(codexExtension, stagedExtension); + function run(command, args) { const result = spawnSync(command, args, { stdio: "inherit" }); if (result.status !== 0) { diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index be12309368..e6adb0559e 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -1,8 +1,6 @@ import assert from "node:assert/strict"; import { spawn } from "node:child_process"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { readFile } from "node:fs/promises"; import test from "node:test"; test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", async () => { @@ -41,6 +39,10 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", manifest.agent.env.PI_ACP_PI_ENTRYPOINT, "/opt/agentos/pkgs/pi/0.0.1/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", ); + assert.equal( + manifest.agent.env.PI_ACP_PI_EXTENSION, + "/opt/agentos/pkgs/pi/0.0.1/extensions/codex-auth.mjs", + ); assert.equal(packageJson.bin["pi-acp"], "./dist/pi-acp/index.js"); assert.equal( packageJson.bin.pi, @@ -70,38 +72,23 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(packageJson.dependencies["@mariozechner/pi-coding-agent"], undefined); }); -test("Pi accepts external Codex auth without persisting it", async (t) => { - const { ModelRuntime } = await import( - new URL( - "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/index.js", - import.meta.url, - ) - ); - const directory = await mkdtemp(join(tmpdir(), "agentos-pi-codex-")); - const authPath = join(directory, "auth.json"); - const token = "external-codex-access-token"; - const accountId = "external-codex-account-id"; - t.after(() => rm(directory, { recursive: true, force: true })); - - const runtime = await ModelRuntime.create({ - authPath, - modelsPath: null, - allowModelNetwork: false, - }); - const auth = await runtime.getAuth("openai-codex", { - env: { - OPENAI_CODEX_ACCESS_TOKEN: token, - OPENAI_CODEX_ACCOUNT_ID: accountId, +test("Pi packages an AgentOS-owned external Codex auth extension", async () => { + const extensionUrl = new URL("../dist/package/extensions/codex-auth.mjs", import.meta.url); + const extension = await import(extensionUrl); + let registration; + extension.default({ + registerProvider(provider, config) { + registration = { provider, config }; }, }); - assert.equal(auth?.auth.apiKey, token); - assert.deepEqual(auth?.auth.headers, { "chatgpt-account-id": accountId }); - assert.equal(auth?.source, "OPENAI_CODEX_ACCESS_TOKEN"); - assert.equal(runtime.isUsingOAuth("openai-codex"), false); - assert.ok(runtime.getModel("openai-codex", "gpt-5.6-terra")); - assert.doesNotMatch(await readFile(authPath, "utf8"), /external-codex-access-token/); - assert.doesNotMatch(await readFile(authPath, "utf8"), /external-codex-account-id/); + assert.deepEqual(registration, { + provider: "openai-codex", + config: { + apiKey: "$OPENAI_CODEX_ACCESS_TOKEN", + headers: { "chatgpt-account-id": "$OPENAI_CODEX_ACCOUNT_ID" }, + }, + }); assert.match( await readFile( new URL( @@ -119,6 +106,10 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", import.meta.url, ).pathname; + const piExtension = new URL( + "../dist/package/extensions/codex-auth.mjs", + import.meta.url, + ).pathname; const child = spawn( new URL("../dist/package/bin/pi-acp", import.meta.url).pathname, [], @@ -127,6 +118,7 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie env: { ...process.env, PI_ACP_PI_ENTRYPOINT: piEntrypoint, + PI_ACP_PI_EXTENSION: piExtension, OPENAI_CODEX_ACCESS_TOKEN: "external-codex-access-token", OPENAI_CODEX_ACCOUNT_ID: "external-codex-account-id", }, From e5044ce5f9477bb7970b88022e06df2c9c1a028d Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 16:01:00 +0200 Subject: [PATCH 03/15] refactor(pi): remove dependency patches --- package.json | 4 - patches/@earendil-works__pi-ai@0.83.0.patch | 12 - ...rendil-works__pi-coding-agent@0.83.0.patch | 36 --- pnpm-lock.yaml | 250 ++++-------------- software/pi/agentos-package.json | 4 +- software/pi/extensions/codex-auth.mjs | 51 +++- software/pi/package.json | 4 +- software/pi/patches/pi-acp-node-options.patch | 39 --- software/pi/scripts/build-pi-acp.mjs | 17 +- software/pi/scripts/finalize-package.mjs | 58 ---- software/pi/tests/package.test.mjs | 75 ++++-- software/pi/wrappers/pi-agentos.mjs | 95 +++++++ 12 files changed, 243 insertions(+), 402 deletions(-) delete mode 100644 patches/@earendil-works__pi-ai@0.83.0.patch delete mode 100644 patches/@earendil-works__pi-coding-agent@0.83.0.patch delete mode 100644 software/pi/patches/pi-acp-node-options.patch delete mode 100644 software/pi/scripts/finalize-package.mjs create mode 100644 software/pi/wrappers/pi-agentos.mjs diff --git a/package.json b/package.json index 4848076dcf..90388822ac 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,6 @@ "@rivetkit/rivetkit-wasm": "2.3.9", "@mariozechner/pi-tui": "0.60.0", "@mariozechner/pi-agent-core": "0.60.0" - }, - "patchedDependencies": { - "@earendil-works/pi-coding-agent@0.83.0": "patches/@earendil-works__pi-coding-agent@0.83.0.patch", - "@earendil-works/pi-ai@0.83.0": "patches/@earendil-works__pi-ai@0.83.0.patch" } } } diff --git a/patches/@earendil-works__pi-ai@0.83.0.patch b/patches/@earendil-works__pi-ai@0.83.0.patch deleted file mode 100644 index 679137df90..0000000000 --- a/patches/@earendil-works__pi-ai@0.83.0.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/dist/api/openai-codex-responses.js b/dist/api/openai-codex-responses.js ---- a/dist/api/openai-codex-responses.js -+++ b/dist/api/openai-codex-responses.js -@@ -171,7 +171,7 @@ export const streamOpenAICodexResponses = (model, context, options) => { - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } -- const accountId = extractAccountId(apiKey); -+ const accountId = options?.headers?.["chatgpt-account-id"]?.trim() || extractAccountId(apiKey); - const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, model.compat?.supportsOpenAIGrammarTools ?? false); - const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId; - const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId); diff --git a/patches/@earendil-works__pi-coding-agent@0.83.0.patch b/patches/@earendil-works__pi-coding-agent@0.83.0.patch deleted file mode 100644 index 9c5b172d02..0000000000 --- a/patches/@earendil-works__pi-coding-agent@0.83.0.patch +++ /dev/null @@ -1,36 +0,0 @@ -diff --git a/dist/core/http-dispatcher.js b/dist/core/http-dispatcher.js ---- a/dist/core/http-dispatcher.js -+++ b/dist/core/http-dispatcher.js -@@ -10,4 +10,22 @@ export const HTTP_IDLE_TIMEOUT_CHOICES = [ - ]; - const originalGlobalFetch = globalThis.fetch; - let installedGlobalFetch; -+const UNDICI_GLOBAL_NAMES = [ -+ "fetch", -+ "Headers", -+ "Response", -+ "Request", -+ "FormData", -+ "WebSocket", -+ "CloseEvent", -+ "ErrorEvent", -+ "MessageEvent", -+ "EventSource", -+]; -+function canInstallUndiciGlobals() { -+ return UNDICI_GLOBAL_NAMES.every((name) => { -+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, name); -+ return !descriptor || descriptor.writable === true || typeof descriptor.set === "function"; -+ }); -+} - export function parseHttpIdleTimeoutMs(value) { -@@ -83,6 +101,8 @@ export function configureHttpDispatcher(timeoutMs = DEFAULT_HTTP_IDLE_TIMEOUT_M - const shouldInstallGlobals = installedGlobalFetch === undefined - ? globalThis.fetch === originalGlobalFetch - : globalThis.fetch === installedGlobalFetch; -- if (shouldInstallGlobals) { -+ // Embedded runtimes can own non-writable Web APIs. Preserve that surface -+ // instead of letting undici.install() fail partway through its assignments. -+ if (shouldInstallGlobals && canInstallUndiciGlobals()) { - undici.install?.(); - installedGlobalFetch = globalThis.fetch; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b45c783de0..8a986167cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,14 +21,6 @@ overrides: pnpmfileChecksum: sha256-SEnUwm6AFeob5DIJB/q7nwJGPinO5RVveyVjDzNYuIM= -patchedDependencies: - '@earendil-works/pi-ai@0.83.0': - hash: fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08 - path: patches/@earendil-works__pi-ai@0.83.0.patch - '@earendil-works/pi-coding-agent@0.83.0': - hash: 1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366 - path: patches/@earendil-works__pi-coding-agent@0.83.0.patch - importers: .: @@ -4151,9 +4143,12 @@ importers: '@agentclientprotocol/sdk': specifier: 1.2.1 version: 1.2.1(zod@3.25.76) + '@earendil-works/pi-ai': + specifier: 0.83.0 + version: 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) '@earendil-works/pi-coding-agent': specifier: 0.83.0 - version: 0.83.0(patch_hash=1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + version: 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) pi-mcp-adapter: specifier: 2.11.0 version: 2.11.0(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) @@ -5345,10 +5340,6 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-bedrock-runtime@3.1020.0': - resolution: {integrity: sha512-nqDCbaB05gRc3FuIEN74Mo04+k8RNI0YT2YBAU/9nioqgDyoqzMx8Ia2QWaw9UhUyIHMBjcFEfKIPfCZx7caCw==} - engines: {node: '>=20.0.0'} - '@aws-sdk/client-bedrock-runtime@3.1048.0': resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} engines: {node: '>=20.0.0'} @@ -5437,10 +5428,6 @@ packages: resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.972.12': - resolution: {integrity: sha512-ruyc/MNR6e+cUrGCth7fLQ12RXBZDy/bV06tgqB9Z5n/0SN/C0m6bsQEV8FF9zPI6VSAOaRd0rNgmpYVnGawrQ==} - engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.972.29': resolution: {integrity: sha512-t3tKQRTVXsI2QNPE3CaNjHl0wRO9Xi3acZkAyti2RQsiFmZ9Gi0kArX2ighlRJ1BtDVuul413gThAgzyTfgmWA==} engines: {node: '>=20.0.0'} @@ -5453,10 +5440,6 @@ packages: resolution: {integrity: sha512-oykin4mDWxNOuYQ7SF1cHzgYeuFEkF4cdRwgvjFFbIklkx09qIFBiOgsORafG9sXZFO3TayMmQuAQYgADXhI8w==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.972.8': - resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-expect-continue@3.972.8': resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} engines: {node: '>=20.0.0'} @@ -5493,10 +5476,6 @@ packages: resolution: {integrity: sha512-TIRLO5UR2+FVUGmhYoAwVkKhcVzywEDX/5LzR9tjy1h8FQAXOtFg2IqgmwvxU7y933rkTn9rl6AdgcAUgQ1/Kg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-websocket@3.972.14': - resolution: {integrity: sha512-qnfDlIHjm6DrTYNvWOUbnZdVKgtoKbO/Qzj+C0Wp5Y7VUrsvBRQtGKxD+hc+mRTS4N0kBJ6iZ3+zxm4N1OSyjg==} - engines: {node: '>= 14.0.0'} - '@aws-sdk/middleware-websocket@3.972.41': resolution: {integrity: sha512-LSbGvvYmjc4Br9BPYI2dTLnIclmrSiQbahkP4D6nRGVEv4qsCZ8csVuKBPVEEFCVD+EEngGh8ROls6XpumtwMg==} engines: {node: '>= 14.0.0'} @@ -5553,10 +5532,6 @@ packages: resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-format-url@3.972.8': - resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} - engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.965.5': resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} @@ -6670,15 +6645,6 @@ packages: '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} - '@google/genai@1.47.0': - resolution: {integrity: sha512-0VV7AaXm5rQu3oRHNZNEubRAOL2lv5u+YA72eWnDwcOx3B1jFRbvtgL4drRHlocRHOnludvr3xmbQGbR+/RQAQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - '@google/genai@1.52.0': resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} @@ -15533,13 +15499,13 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.974.2 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.974.2 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': @@ -15577,58 +15543,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.1020.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-node': 3.972.28 - '@aws-sdk/eventstream-handler-node': 3.972.12 - '@aws-sdk/middleware-eventstream': 3.972.8 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.27 - '@aws-sdk/middleware-websocket': 3.972.14 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/token-providers': 3.1020.0 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.13 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.45 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.9.7 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-bedrock-runtime@3.1048.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -15750,15 +15664,15 @@ snapshots: '@aws-sdk/crc64-nvme@3.972.5': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.972.24': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.972.59': @@ -15771,14 +15685,14 @@ snapshots: '@aws-sdk/credential-provider-http@3.972.26': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/fetch-http-handler': 5.3.15 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/fetch-http-handler': 5.6.7 '@smithy/node-http-handler': 4.9.7 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 '@smithy/util-stream': 4.5.21 tslib: 2.8.1 @@ -15794,7 +15708,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.972.27': dependencies: - '@aws-sdk/core': 3.973.26 + '@aws-sdk/core': 3.975.3 '@aws-sdk/credential-provider-env': 3.972.24 '@aws-sdk/credential-provider-http': 3.972.26 '@aws-sdk/credential-provider-login': 3.972.27 @@ -15802,11 +15716,11 @@ snapshots: '@aws-sdk/credential-provider-sso': 3.972.27 '@aws-sdk/credential-provider-web-identity': 3.972.27 '@aws-sdk/nested-clients': 3.996.17 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.974.2 '@smithy/credential-provider-imds': 4.2.12 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -15882,11 +15796,11 @@ snapshots: '@aws-sdk/credential-provider-process@3.972.24': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@aws-sdk/credential-provider-process@3.972.59': @@ -15899,13 +15813,13 @@ snapshots: '@aws-sdk/credential-provider-sso@3.972.27': dependencies: - '@aws-sdk/core': 3.973.26 + '@aws-sdk/core': 3.975.3 '@aws-sdk/nested-clients': 3.996.17 '@aws-sdk/token-providers': 3.1020.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.974.2 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -15922,12 +15836,12 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.972.27': dependencies: - '@aws-sdk/core': 3.973.26 + '@aws-sdk/core': 3.975.3 '@aws-sdk/nested-clients': 3.996.17 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.974.2 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -15941,13 +15855,6 @@ snapshots: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/eventstream-handler-node@3.972.12': - dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - '@aws-sdk/eventstream-handler-node@3.972.29': dependencies: '@aws-sdk/types': 3.974.2 @@ -15972,13 +15879,6 @@ snapshots: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.972.8': - dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -16064,21 +15964,6 @@ snapshots: '@smithy/util-retry': 4.2.12 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.972.14': - dependencies: - '@aws-sdk/types': 3.974.2 - '@aws-sdk/util-format-url': 3.972.8 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/fetch-http-handler': 5.6.7 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.16.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.972.41': dependencies: '@aws-sdk/core': 3.975.3 @@ -16228,13 +16113,6 @@ snapshots: '@smithy/util-endpoints': 3.3.3 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.972.8': - dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.965.5': dependencies: tslib: 2.8.1 @@ -16257,7 +16135,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.16': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 fast-xml-parser: 5.5.8 tslib: 2.8.1 @@ -16499,7 +16377,7 @@ snapshots: '@ai-sdk/provider': 2.0.3 '@anthropic-ai/sdk': 0.39.0 '@browserbasehq/sdk': 2.10.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) '@langchain/openai': 0.4.9(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)))(ws@8.21.0(bufferutil@4.1.0)) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) ai: 5.0.172(zod@4.3.6) @@ -16552,7 +16430,7 @@ snapshots: '@ai-sdk/provider': 2.0.3 '@anthropic-ai/sdk': 0.39.0 '@browserbasehq/sdk': 2.10.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) ai: 5.0.172(zod@4.3.6) devtools-protocol: 0.0.1642743 @@ -16746,7 +16624,7 @@ snapshots: '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: - '@earendil-works/pi-ai': 0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -16763,13 +16641,13 @@ snapshots: dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1089.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) '@mistralai/mistralai': 2.4.1(@opentelemetry/api@1.9.0)(bufferutil@4.1.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) partial-json: 0.1.7 - typebox: 1.3.6 + typebox: 1.3.7 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - '@opentelemetry/api' @@ -16821,7 +16699,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -16842,10 +16720,10 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.83.0(patch_hash=1d0a53322f46342d8f5e41228721849abb7b6babd98a6c2d1627d22bf2523366)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': + '@earendil-works/pi-coding-agent@0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) - '@earendil-works/pi-ai': 0.83.0(patch_hash=fddc05466df25c2f701127a5d630c1c6a64cd2157328e6a0616dac0ca6522f08)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) + '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) '@earendil-works/pi-tui': 0.83.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -17292,32 +17170,6 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@google/genai@1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.4 - ws: 8.21.0(bufferutil@4.1.0) - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@google/genai@1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.4 - ws: 8.21.0(bufferutil@4.1.0) - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)': dependencies: google-auth-library: 10.6.2 @@ -17800,7 +17652,6 @@ snapshots: '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -17812,7 +17663,6 @@ snapshots: '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -17822,8 +17672,8 @@ snapshots: '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) - '@aws-sdk/client-bedrock-runtime': 3.1020.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) + '@aws-sdk/client-bedrock-runtime': 3.1089.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) '@mistralai/mistralai': 1.14.1(bufferutil@4.1.0) '@sinclair/typebox': 0.34.49 ajv: 8.18.0 @@ -17836,7 +17686,6 @@ snapshots: zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -17846,8 +17695,8 @@ snapshots: '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) - '@aws-sdk/client-bedrock-runtime': 3.1020.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) + '@aws-sdk/client-bedrock-runtime': 3.1089.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0) '@mistralai/mistralai': 1.14.1(bufferutil@4.1.0) '@sinclair/typebox': 0.34.49 ajv: 8.18.0 @@ -17860,7 +17709,6 @@ snapshots: zod-to-json-schema: 3.25.2(zod@4.3.6) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -17892,7 +17740,6 @@ snapshots: '@mariozechner/clipboard': 0.3.2 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -17924,7 +17771,6 @@ snapshots: '@mariozechner/clipboard': 0.3.2 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - supports-color - utf-8-validate @@ -20466,7 +20312,7 @@ snapshots: dependencies: '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 '@smithy/url-parser': 4.2.12 tslib: 2.8.1 @@ -20503,7 +20349,7 @@ snapshots: '@smithy/eventstream-serde-universal@4.2.12': dependencies: '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@smithy/fetch-http-handler@5.3.15': @@ -20628,7 +20474,7 @@ snapshots: '@smithy/property-provider@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@smithy/protocol-http@5.3.12': @@ -20638,29 +20484,29 @@ snapshots: '@smithy/querystring-builder@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 '@smithy/querystring-parser@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@smithy/service-error-classification@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 '@smithy/shared-ini-file-loader@4.4.7': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 '@smithy/signature-v4@5.3.12': dependencies: '@smithy/is-array-buffer': 4.2.2 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.16.1 '@smithy/util-hex-encoding': 4.2.2 '@smithy/util-middleware': 4.2.12 '@smithy/util-uri-escape': 4.2.2 @@ -20766,7 +20612,7 @@ snapshots: '@smithy/util-stream@4.5.21': dependencies: '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 + '@smithy/node-http-handler': 4.9.7 '@smithy/types': 4.13.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 diff --git a/software/pi/agentos-package.json b/software/pi/agentos-package.json index 96649c50f4..bf29d4ff4e 100644 --- a/software/pi/agentos-package.json +++ b/software/pi/agentos-package.json @@ -3,9 +3,7 @@ "agent": { "acpEntrypoint": "pi-acp", "env": { - "PI_ACP_PI_COMMAND": "/opt/agentos/bin/pi", - "PI_ACP_PI_ENTRYPOINT": "/opt/agentos/pkgs/pi/0.0.1/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", - "PI_ACP_PI_EXTENSION": "/opt/agentos/pkgs/pi/0.0.1/extensions/codex-auth.mjs" + "PI_ACP_PI_COMMAND": "/opt/agentos/bin/pi-agentos" } }, "registry": { diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 1364ed25b9..26039ca796 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -1,10 +1,47 @@ -export const codexProviderConfig = { - apiKey: "$OPENAI_CODEX_ACCESS_TOKEN", - headers: { - "chatgpt-account-id": "$OPENAI_CODEX_ACCOUNT_ID", - }, -}; +import { stream as streamOpenAICodexResponses } from "@earendil-works/pi-ai/api/openai-codex-responses"; + +const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; +const SYNTHETIC_TOKEN_ENV = "AGENTOS_CODEX_ACCOUNT_TOKEN"; + +export function createAccountToken(accountId) { + const payload = Buffer.from( + JSON.stringify({ + "https://api.openai.com/auth": { chatgpt_account_id: accountId }, + }), + ).toString("base64url"); + return `e30.${payload}.agentos`; +} + +export function createCodexFetch(accessToken, accountId, upstreamFetch = globalThis.fetch) { + return (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); + + const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); + headers.set("authorization", `Bearer ${accessToken}`); + headers.set("chatgpt-account-id", accountId); + return upstreamFetch(input, { ...init, headers }); + }; +} + +function createCodexProviderConfig(accessToken, accountId) { + return { + api: "openai-codex-responses", + apiKey: `$${SYNTHETIC_TOKEN_ENV}`, + streamSimple: (model, context, options) => + streamOpenAICodexResponses(model, context, { + ...options, + fetch: createCodexFetch(accessToken, accountId, options?.fetch ?? globalThis.fetch), + transport: "sse", + }), + }; +} export default function codexAuthExtension(pi) { - pi.registerProvider("openai-codex", codexProviderConfig); + const accessToken = process.env.OPENAI_CODEX_ACCESS_TOKEN?.trim(); + const accountId = process.env.OPENAI_CODEX_ACCOUNT_ID?.trim(); + if (!accessToken || !accountId) return; + + process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(accountId); + pi.registerProvider("openai-codex", createCodexProviderConfig(accessToken, accountId)); } diff --git a/software/pi/package.json b/software/pi/package.json index 0184630870..8f45ca7bb7 100644 --- a/software/pi/package.json +++ b/software/pi/package.json @@ -7,6 +7,7 @@ "types": "./dist/index.d.ts", "bin": { "pi": "./node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + "pi-agentos": "./dist/pi-agentos.mjs", "pi-acp": "./dist/pi-acp/index.js" }, "exports": { @@ -24,12 +25,13 @@ "agentos-package.json" ], "scripts": { - "build": "rm -rf dist && node scripts/build-pi-acp.mjs && tsc && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native --omit-optional && node scripts/finalize-package.mjs", + "build": "rm -rf dist && node scripts/build-pi-acp.mjs && tsc && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native --omit-optional && agentos-toolchain pack-aospkg dist/package.tar dist/package.aospkg", "check-types": "tsc --noEmit", "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" }, "dependencies": { "@agentclientprotocol/sdk": "1.2.1", + "@earendil-works/pi-ai": "0.83.0", "@earendil-works/pi-coding-agent": "0.83.0", "pi-mcp-adapter": "2.11.0", "zod": "3.25.76" diff --git a/software/pi/patches/pi-acp-node-options.patch b/software/pi/patches/pi-acp-node-options.patch deleted file mode 100644 index ac44c0c1e3..0000000000 --- a/software/pi/patches/pi-acp-node-options.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/src/pi-rpc/process.ts b/src/pi-rpc/process.ts -index 035e591..b59e591 100644 ---- a/src/pi-rpc/process.ts -+++ b/src/pi-rpc/process.ts -@@ -173,6 +173,8 @@ export class PiRpcProcess { - ...(mcpLaunch?.args ?? []), - ...(permissionGateLaunch?.args ?? []) - ] -+ const piExtension = process.env.PI_ACP_PI_EXTENSION?.trim() -+ if (piExtension) piArgs.push('--extension', piExtension) - if (params.sessionPath) piArgs.push('--session', params.sessionPath) - const args = params.piEntrypoint - ? [ -@@ -178,6 +178,7 @@ export class PiRpcProcess { - ? [ - '-e', -- `process.chdir(${JSON.stringify(params.cwd)}); process.argv.splice(1, 0, ${JSON.stringify(params.piEntrypoint)}); globalThis.__piAcpMain = import(${JSON.stringify(params.piEntrypoint)}).catch(error => { console.error(error); process.exitCode = 1; });`, -+ `process.chdir(${JSON.stringify(params.cwd)}); if (process.argv[1] === '--') process.argv.splice(1, 1); process.argv.splice(1, 0, ${JSON.stringify(params.piEntrypoint)}); globalThis.__piAcpMain = import(${JSON.stringify(params.piEntrypoint)}).catch(error => { console.error(error); process.exitCode = 1; });`, -+ '--', - ...piArgs - ] - : piArgs -@@ -189,6 +190,7 @@ export class PiRpcProcess { - shell: shouldUseShellForPiCommand(cmd), - signal: params.signal - }) -+ const proc = new PiRpcProcess(child, cleanupLaunchResources) - - // Ensure spawn failures (e.g. ENOENT when pi isn't installed) are surfaced as a - // deterministic error instead of later EPIPE/internal-error noise. -@@ -233,8 +235,6 @@ export class PiRpcProcess { - throw new PiRpcSpawnError(`Could not start pi (command: ${cmd}).`, { code, cause: e }) - } - -- const proc = new PiRpcProcess(child, cleanupLaunchResources) -- - // Best-effort handshake. - // Important: pi may emit a get_state response pointing at a sessionFile in a directory - // that is created lazily. Create the parent dir up-front to avoid later parse errors diff --git a/software/pi/scripts/build-pi-acp.mjs b/software/pi/scripts/build-pi-acp.mjs index f7f96e7c19..961a71175b 100644 --- a/software/pi/scripts/build-pi-acp.mjs +++ b/software/pi/scripts/build-pi-acp.mjs @@ -26,11 +26,8 @@ const tarballPath = resolve(cacheDir, `pi-acp-${SOURCE_COMMIT}.tar.gz`); const sourceRoot = resolve(cacheDir, `pi-acp-${SOURCE_COMMIT}`); const outputDir = resolve(packageDir, "dist", "pi-acp"); const manifestPath = resolve(packageDir, "dist", "pi-acp-upstream.json"); -const compatibilityPatchPath = resolve( - packageDir, - "patches", - "pi-acp-node-options.patch", -); +const wrapperPath = resolve(packageDir, "wrappers", "pi-agentos.mjs"); +const extensionPath = resolve(packageDir, "extensions", "codex-auth.mjs"); function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); @@ -99,7 +96,6 @@ for (const [name, version] of Object.entries(expectedDependencies)) { } run("npm", ["ci"], { cwd: sourceRoot }); -run("patch", ["-p1", "-i", compatibilityPatchPath], { cwd: sourceRoot }); run("npm", ["run", "build"], { cwd: sourceRoot }); const sourceEntrypoint = resolve(sourceRoot, "dist", "index.js"); @@ -116,6 +112,9 @@ cpSync(sourceEntrypoint, resolve(outputDir, "index.js")); cpSync(resolve(sourceRoot, "package.json"), resolve(outputDir, "package.json")); const sourceMap = `${sourceEntrypoint}.map`; if (existsSync(sourceMap)) cpSync(sourceMap, resolve(outputDir, "index.js.map")); +cpSync(wrapperPath, resolve(packageDir, "dist", "pi-agentos.mjs")); +mkdirSync(resolve(packageDir, "dist", "extensions"), { recursive: true }); +cpSync(extensionPath, resolve(packageDir, "dist", "extensions", "codex-auth.mjs")); mkdirSync(dirname(manifestPath), { recursive: true }); writeFileSync( @@ -126,12 +125,6 @@ writeFileSync( sourceCommit: SOURCE_COMMIT, sourceTarballSha256: SOURCE_TARBALL_SHA256, sourcePackageVersion: sourcePackage.version, - compatibilityPatches: [ - { - path: "patches/pi-acp-node-options.patch", - sha256: sha256(compatibilityPatchPath), - }, - ], buildCommands: ["npm ci", "npm run build"], entrypoint: "./pi-acp/index.js", entrypointSha256: sha256(sourceEntrypoint), diff --git a/software/pi/scripts/finalize-package.mjs b/software/pi/scripts/finalize-package.mjs deleted file mode 100644 index 4aadb2f628..0000000000 --- a/software/pi/scripts/finalize-package.mjs +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env node - -import { spawnSync } from "node:child_process"; -import { cpSync, mkdirSync, realpathSync, rmSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const stagedPackageDir = resolve(packageDir, "dist", "package"); -const codingAgentDir = resolve( - packageDir, - "node_modules", - "@earendil-works", - "pi-coding-agent", -); -const piAiDir = realpathSync( - resolve(dirname(realpathSync(codingAgentDir)), "pi-ai"), -); -const codexExtension = resolve(packageDir, "extensions", "codex-auth.mjs"); - -const patchedRuntimeFiles = [ - { - source: resolve(piAiDir, "dist", "api", "openai-codex-responses.js"), - targets: [ - "@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js", - ], - }, - { - source: resolve(codingAgentDir, "dist", "core", "http-dispatcher.js"), - targets: ["@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js"], - }, -]; - -for (const file of patchedRuntimeFiles) { - for (const target of file.targets) { - cpSync(file.source, resolve(stagedPackageDir, "node_modules", target)); - } -} - -const stagedExtension = resolve(stagedPackageDir, "extensions", "codex-auth.mjs"); -mkdirSync(dirname(stagedExtension), { recursive: true }); -cpSync(codexExtension, stagedExtension); - -function run(command, args) { - const result = spawnSync(command, args, { stdio: "inherit" }); - if (result.status !== 0) { - throw new Error( - `Command failed (${result.status ?? "unknown"}): ${command} ${args.join(" ")}`, - ); - } -} - -const packageTar = resolve(packageDir, "dist", "package.tar"); -const packageAospkg = resolve(packageDir, "dist", "package.aospkg"); -rmSync(packageTar, { force: true }); -rmSync(packageAospkg, { force: true }); -run("tar", ["-cf", packageTar, "-C", stagedPackageDir, "."]); -run("agentos-toolchain", ["pack-aospkg", packageTar, packageAospkg]); diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index e6adb0559e..8acc409759 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -34,16 +34,11 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(manifest.agent.acpEntrypoint, "pi-acp"); assert.equal(manifest.agent.runtime, undefined); assert.equal(manifest.agent.snapshot, undefined); - assert.equal(manifest.agent.env.PI_ACP_PI_COMMAND, "/opt/agentos/bin/pi"); - assert.equal( - manifest.agent.env.PI_ACP_PI_ENTRYPOINT, - "/opt/agentos/pkgs/pi/0.0.1/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", - ); - assert.equal( - manifest.agent.env.PI_ACP_PI_EXTENSION, - "/opt/agentos/pkgs/pi/0.0.1/extensions/codex-auth.mjs", - ); + assert.equal(manifest.agent.env.PI_ACP_PI_COMMAND, "/opt/agentos/bin/pi-agentos"); + assert.equal(manifest.agent.env.PI_ACP_PI_ENTRYPOINT, undefined); + assert.equal(manifest.agent.env.PI_ACP_PI_EXTENSION, undefined); assert.equal(packageJson.bin["pi-acp"], "./dist/pi-acp/index.js"); + assert.equal(packageJson.bin["pi-agentos"], "./dist/pi-agentos.mjs"); assert.equal( packageJson.bin.pi, "./node_modules/@earendil-works/pi-coding-agent/dist/cli.js", @@ -64,6 +59,7 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", "85bc7e133d28e9d870ecad7aa3de9e6a17ffea142443a177d618597a56c72cd7", ); assert.equal(upstreamManifest.sourcePackageVersion, "0.0.31"); + assert.equal(upstreamManifest.compatibilityPatches, undefined); assert.deepEqual(upstreamManifest.buildCommands, ["npm ci", "npm run build"]); assert.ok(adapterEntrypoint.startsWith("#!/usr/bin/env node")); assert.equal(adapterPackageJson.name, "pi-acp"); @@ -72,24 +68,55 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(packageJson.dependencies["@mariozechner/pi-coding-agent"], undefined); }); -test("Pi packages an AgentOS-owned external Codex auth extension", async () => { - const extensionUrl = new URL("../dist/package/extensions/codex-auth.mjs", import.meta.url); +test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => { + const extensionUrl = new URL( + "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", + import.meta.url, + ); const extension = await import(extensionUrl); + const originalToken = process.env.OPENAI_CODEX_ACCESS_TOKEN; + const originalAccountId = process.env.OPENAI_CODEX_ACCOUNT_ID; + const accessToken = "external-codex-access-token"; + const accountId = "external-codex-account-id"; + let request; let registration; + const captureFetch = async (input, init) => { + request = { input, init }; + return new Response(null, { status: 204 }); + }; + process.env.OPENAI_CODEX_ACCESS_TOKEN = accessToken; + process.env.OPENAI_CODEX_ACCOUNT_ID = accountId; + t.after(() => { + if (originalToken === undefined) delete process.env.OPENAI_CODEX_ACCESS_TOKEN; + else process.env.OPENAI_CODEX_ACCESS_TOKEN = originalToken; + if (originalAccountId === undefined) delete process.env.OPENAI_CODEX_ACCOUNT_ID; + else process.env.OPENAI_CODEX_ACCOUNT_ID = originalAccountId; + delete process.env.AGENTOS_CODEX_ACCOUNT_TOKEN; + }); + extension.default({ registerProvider(provider, config) { registration = { provider, config }; }, }); - assert.deepEqual(registration, { - provider: "openai-codex", - config: { - apiKey: "$OPENAI_CODEX_ACCESS_TOKEN", - headers: { "chatgpt-account-id": "$OPENAI_CODEX_ACCOUNT_ID" }, + assert.equal(registration.provider, "openai-codex"); + assert.equal(registration.config.api, "openai-codex-responses"); + assert.equal(registration.config.apiKey, "$AGENTOS_CODEX_ACCOUNT_TOKEN"); + assert.equal(typeof registration.config.streamSimple, "function"); + assert.equal(process.env.AGENTOS_CODEX_ACCOUNT_TOKEN, extension.createAccountToken(accountId)); + assert.doesNotMatch(process.env.AGENTOS_CODEX_ACCOUNT_TOKEN, /external-codex-access-token/); + + await extension.createCodexFetch(accessToken, accountId, captureFetch)( + "https://chatgpt.com/backend-api/codex/responses", + { + headers: { authorization: "Bearer synthetic" }, }, - }); - assert.match( + ); + const headers = new Headers(request.init.headers); + assert.equal(headers.get("authorization"), `Bearer ${accessToken}`); + assert.equal(headers.get("chatgpt-account-id"), accountId); + assert.doesNotMatch( await readFile( new URL( "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js", @@ -102,14 +129,7 @@ test("Pi packages an AgentOS-owned external Codex auth extension", async () => { }); test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { - const piEntrypoint = new URL( - "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js", - import.meta.url, - ).pathname; - const piExtension = new URL( - "../dist/package/extensions/codex-auth.mjs", - import.meta.url, - ).pathname; + const piCommand = new URL("../dist/package/bin/pi-agentos", import.meta.url).pathname; const child = spawn( new URL("../dist/package/bin/pi-acp", import.meta.url).pathname, [], @@ -117,8 +137,7 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, - PI_ACP_PI_ENTRYPOINT: piEntrypoint, - PI_ACP_PI_EXTENSION: piExtension, + PI_ACP_PI_COMMAND: piCommand, OPENAI_CODEX_ACCESS_TOKEN: "external-codex-access-token", OPENAI_CODEX_ACCOUNT_ID: "external-codex-account-id", }, diff --git a/software/pi/wrappers/pi-agentos.mjs b/software/pi/wrappers/pi-agentos.mjs new file mode 100644 index 0000000000..e21a939e78 --- /dev/null +++ b/software/pi/wrappers/pi-agentos.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { isAbsolute, resolve } from "node:path"; +import { + createAgentSessionFromServices, + createAgentSessionRuntime, + createAgentSessionServices, + getAgentDir, + parseArgs, + runRpcMode, + SessionManager, + SettingsManager, +} from "@earendil-works/pi-coding-agent"; + +process.title = "pi-agentos-rpc"; +process.env.PI_CODING_AGENT = "true"; + +const launchCwd = process.cwd(); +const agentDir = getAgentDir(); +const parsed = parseArgs(process.argv.slice(2)); +const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.type === "error"); +if (parsed.mode && parsed.mode !== "rpc") { + errors.push({ type: "error", message: "pi-agentos only supports RPC mode" }); +} +if (errors.length > 0) throw new Error(errors.map((diagnostic) => diagnostic.message).join("\n")); + +const extensionPath = fileURLToPath(new URL("./extensions/codex-auth.mjs", import.meta.url)); +const extensionPaths = [extensionPath, ...(parsed.extensions ?? [])].map((path) => + isAbsolute(path) ? path : resolve(launchCwd, path), +); +const startupSettings = SettingsManager.create(launchCwd, agentDir, { projectTrusted: false }); +const sessionDir = parsed.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR ?? startupSettings.getSessionDir(); +const sessionManager = parsed.noSession + ? SessionManager.inMemory(launchCwd) + : parsed.session + ? SessionManager.open( + isAbsolute(parsed.session) ? parsed.session : resolve(launchCwd, parsed.session), + sessionDir, + ) + : SessionManager.create(launchCwd, sessionDir); + +const createRuntime = async ({ cwd, agentDir: runtimeAgentDir, sessionManager, sessionStartEvent }) => { + const settingsManager = SettingsManager.create(cwd, runtimeAgentDir, { projectTrusted: false }); + const services = await createAgentSessionServices({ + cwd, + agentDir: runtimeAgentDir, + settingsManager, + extensionFlagValues: parsed.unknownFlags, + resourceLoaderOptions: { + additionalExtensionPaths: extensionPaths, + noThemes: parsed.noThemes ?? true, + noExtensions: parsed.noExtensions, + noSkills: parsed.noSkills, + noPromptTemplates: parsed.noPromptTemplates, + noContextFiles: parsed.noContextFiles, + additionalSkillPaths: parsed.skills, + additionalPromptTemplatePaths: parsed.promptTemplates, + additionalThemePaths: parsed.themes, + systemPrompt: parsed.systemPrompt, + appendSystemPrompt: parsed.appendSystemPrompt, + }, + }); + const diagnostics = [ + ...services.diagnostics, + ...services.resourceLoader.getExtensions().errors.map(({ path, error }) => ({ + type: "error", + message: `Failed to load extension "${path}": ${error}`, + })), + ]; + const runtimeErrors = diagnostics.filter((diagnostic) => diagnostic.type === "error"); + if (runtimeErrors.length > 0) { + throw new Error(runtimeErrors.map((diagnostic) => diagnostic.message).join("\n")); + } + + return { + ...(await createAgentSessionFromServices({ + services, + sessionManager, + sessionStartEvent, + tools: parsed.tools, + excludeTools: parsed.excludeTools, + noTools: parsed.noTools ? "all" : parsed.noBuiltinTools ? "builtin" : undefined, + })), + services, + diagnostics, + }; +}; + +const runtime = await createAgentSessionRuntime(createRuntime, { + cwd: sessionManager.getCwd(), + agentDir, + sessionManager, +}); +await runRpcMode(runtime); From d9159bfd6cccd6dfa36534b93cf4c4e59b8433b5 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 17:25:16 +0200 Subject: [PATCH 04/15] fix(pi): refresh bound Codex credentials --- software/pi/extensions/codex-auth.mjs | 57 ++++++++++++++++++++++++--- software/pi/tests/package.test.mjs | 36 +++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 26039ca796..57668025d8 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -1,7 +1,14 @@ +import { execFile } from "node:child_process"; +import { accessSync, constants } from "node:fs"; +import { delimiter, resolve } from "node:path"; +import { promisify } from "node:util"; + import { stream as streamOpenAICodexResponses } from "@earendil-works/pi-ai/api/openai-codex-responses"; const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; const SYNTHETIC_TOKEN_ENV = "AGENTOS_CODEX_ACCOUNT_TOKEN"; +const CREDENTIAL_COMMAND = "agentos-codex-auth"; +const execFileAsync = promisify(execFile); export function createAccountToken(accountId) { const payload = Buffer.from( @@ -24,14 +31,50 @@ export function createCodexFetch(accessToken, accountId, upstreamFetch = globalT }; } -function createCodexProviderConfig(accessToken, accountId) { +export function parseCodexCredential(value) { + const parsed = typeof value === "string" ? JSON.parse(value) : value; + const accessToken = parsed?.accessToken?.trim(); + const accountId = parsed?.accountId?.trim(); + if (!accessToken || !accountId) { + throw new Error("Codex credential binding returned an invalid credential"); + } + return { accessToken, accountId }; +} + +export function createDynamicCodexFetch(resolveCredential, upstreamFetch = globalThis.fetch) { + return async (input, init) => { + const { accessToken, accountId } = parseCodexCredential(await resolveCredential()); + return createCodexFetch(accessToken, accountId, upstreamFetch)(input, init); + }; +} + +function commandExists(command) { + for (const directory of (process.env.PATH ?? "").split(delimiter)) { + if (!directory) continue; + try { + accessSync(resolve(directory, command), constants.X_OK); + return true; + } catch {} + } + return false; +} + +async function loadBoundCredential() { + const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["get"], { + timeout: 10_000, + maxBuffer: 1024 * 1024, + }); + return parseCodexCredential(stdout); +} + +function createCodexProviderConfig(resolveCredential) { return { api: "openai-codex-responses", apiKey: `$${SYNTHETIC_TOKEN_ENV}`, streamSimple: (model, context, options) => streamOpenAICodexResponses(model, context, { ...options, - fetch: createCodexFetch(accessToken, accountId, options?.fetch ?? globalThis.fetch), + fetch: createDynamicCodexFetch(resolveCredential, options?.fetch ?? globalThis.fetch), transport: "sse", }), }; @@ -40,8 +83,12 @@ function createCodexProviderConfig(accessToken, accountId) { export default function codexAuthExtension(pi) { const accessToken = process.env.OPENAI_CODEX_ACCESS_TOKEN?.trim(); const accountId = process.env.OPENAI_CODEX_ACCOUNT_ID?.trim(); - if (!accessToken || !accountId) return; + const staticCredential = accessToken && accountId ? { accessToken, accountId } : null; + if (!staticCredential && !commandExists(CREDENTIAL_COMMAND)) return; - process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(accountId); - pi.registerProvider("openai-codex", createCodexProviderConfig(accessToken, accountId)); + process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(staticCredential?.accountId ?? "agentos-bound-account"); + pi.registerProvider( + "openai-codex", + createCodexProviderConfig(staticCredential ? async () => staticCredential : loadBoundCredential), + ); } diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 8acc409759..746d0e3b79 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -128,6 +128,42 @@ test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => ); }); +test("Codex auth resolves a fresh bound credential for every request", async () => { + const extension = await import( + new URL( + "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", + import.meta.url, + ) + ); + let request; + let resolution = 0; + const dynamicFetch = extension.createDynamicCodexFetch( + async () => ({ + accessToken: `access-token-${++resolution}`, + accountId: `account-${resolution}`, + }), + async (input, init) => { + request = { input, init }; + return new Response(null, { status: 204 }); + }, + ); + + await dynamicFetch("https://chatgpt.com/backend-api/codex/responses"); + let headers = new Headers(request.init.headers); + assert.equal(headers.get("authorization"), "Bearer access-token-1"); + assert.equal(headers.get("chatgpt-account-id"), "account-1"); + + await dynamicFetch("https://chatgpt.com/backend-api/codex/responses"); + headers = new Headers(request.init.headers); + assert.equal(headers.get("authorization"), "Bearer access-token-2"); + assert.equal(headers.get("chatgpt-account-id"), "account-2"); + assert.equal(resolution, 2); + assert.throws( + () => extension.parseCodexCredential('{"accessToken":"missing-account"}'), + /invalid credential/, + ); +}); + test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { const piCommand = new URL("../dist/package/bin/pi-agentos", import.meta.url).pathname; const child = spawn( From 4d27bf5e9f9e4da34828d9eaef6e152ba02db5da Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 17:31:49 +0200 Subject: [PATCH 05/15] fix(pi): parse bound credential envelope --- software/pi/extensions/codex-auth.mjs | 5 +++-- software/pi/tests/package.test.mjs | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 57668025d8..7f18cf9cdd 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -33,8 +33,9 @@ export function createCodexFetch(accessToken, accountId, upstreamFetch = globalT export function parseCodexCredential(value) { const parsed = typeof value === "string" ? JSON.parse(value) : value; - const accessToken = parsed?.accessToken?.trim(); - const accountId = parsed?.accountId?.trim(); + const credential = parsed?.result ?? parsed; + const accessToken = credential?.accessToken?.trim(); + const accountId = credential?.accountId?.trim(); if (!accessToken || !accountId) { throw new Error("Codex credential binding returned an invalid credential"); } diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 746d0e3b79..069ec7d6cc 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -158,6 +158,13 @@ test("Codex auth resolves a fresh bound credential for every request", async () assert.equal(headers.get("authorization"), "Bearer access-token-2"); assert.equal(headers.get("chatgpt-account-id"), "account-2"); assert.equal(resolution, 2); + assert.deepEqual( + extension.parseCodexCredential({ + ok: true, + result: { accessToken: "bound-token", accountId: "bound-account" }, + }), + { accessToken: "bound-token", accountId: "bound-account" }, + ); assert.throws( () => extension.parseCodexCredential('{"accessToken":"missing-account"}'), /invalid credential/, From 4847350d6fec221a80e4c3e874aedcac1f7fda40 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 18:08:28 +0200 Subject: [PATCH 06/15] fix(pi): recover missing native sessions --- software/pi/package.json | 2 +- software/pi/scripts/build-pi-acp.mjs | 4 ++++ software/pi/tests/package.test.mjs | 28 ++++++++++++++++++++++++- software/pi/wrappers/acp-errors.mjs | 22 +++++++++++++++++++ software/pi/wrappers/pi-acp-agentos.mjs | 28 +++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 software/pi/wrappers/acp-errors.mjs create mode 100644 software/pi/wrappers/pi-acp-agentos.mjs diff --git a/software/pi/package.json b/software/pi/package.json index 8f45ca7bb7..a7f1cfa513 100644 --- a/software/pi/package.json +++ b/software/pi/package.json @@ -8,7 +8,7 @@ "bin": { "pi": "./node_modules/@earendil-works/pi-coding-agent/dist/cli.js", "pi-agentos": "./dist/pi-agentos.mjs", - "pi-acp": "./dist/pi-acp/index.js" + "pi-acp": "./dist/pi-acp-agentos.mjs" }, "exports": { ".": { diff --git a/software/pi/scripts/build-pi-acp.mjs b/software/pi/scripts/build-pi-acp.mjs index 961a71175b..7df74ff439 100644 --- a/software/pi/scripts/build-pi-acp.mjs +++ b/software/pi/scripts/build-pi-acp.mjs @@ -27,6 +27,8 @@ const sourceRoot = resolve(cacheDir, `pi-acp-${SOURCE_COMMIT}`); const outputDir = resolve(packageDir, "dist", "pi-acp"); const manifestPath = resolve(packageDir, "dist", "pi-acp-upstream.json"); const wrapperPath = resolve(packageDir, "wrappers", "pi-agentos.mjs"); +const acpWrapperPath = resolve(packageDir, "wrappers", "pi-acp-agentos.mjs"); +const acpErrorsPath = resolve(packageDir, "wrappers", "acp-errors.mjs"); const extensionPath = resolve(packageDir, "extensions", "codex-auth.mjs"); function sha256(path) { @@ -113,6 +115,8 @@ cpSync(resolve(sourceRoot, "package.json"), resolve(outputDir, "package.json")); const sourceMap = `${sourceEntrypoint}.map`; if (existsSync(sourceMap)) cpSync(sourceMap, resolve(outputDir, "index.js.map")); cpSync(wrapperPath, resolve(packageDir, "dist", "pi-agentos.mjs")); +cpSync(acpWrapperPath, resolve(packageDir, "dist", "pi-acp-agentos.mjs")); +cpSync(acpErrorsPath, resolve(packageDir, "dist", "acp-errors.mjs")); mkdirSync(resolve(packageDir, "dist", "extensions"), { recursive: true }); cpSync(extensionPath, resolve(packageDir, "dist", "extensions", "codex-auth.mjs")); diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 069ec7d6cc..a334ff4cec 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -37,7 +37,7 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(manifest.agent.env.PI_ACP_PI_COMMAND, "/opt/agentos/bin/pi-agentos"); assert.equal(manifest.agent.env.PI_ACP_PI_ENTRYPOINT, undefined); assert.equal(manifest.agent.env.PI_ACP_PI_EXTENSION, undefined); - assert.equal(packageJson.bin["pi-acp"], "./dist/pi-acp/index.js"); + assert.equal(packageJson.bin["pi-acp"], "./dist/pi-acp-agentos.mjs"); assert.equal(packageJson.bin["pi-agentos"], "./dist/pi-agentos.mjs"); assert.equal( packageJson.bin.pi, @@ -68,6 +68,32 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(packageJson.dependencies["@mariozechner/pi-coding-agent"], undefined); }); +test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => { + const { normalizeAcpResponse } = await import( + new URL( + "../dist/package/node_modules/@agentos-software/pi/dist/acp-errors.mjs", + import.meta.url, + ) + ); + const missing = JSON.stringify({ + jsonrpc: "2.0", + id: 7, + error: { code: -32602, message: "Invalid params: Unknown sessionId: old-id", data: {} }, + }); + assert.deepEqual(JSON.parse(normalizeAcpResponse(missing)), { + jsonrpc: "2.0", + id: 7, + error: { code: -32002, message: "Resource not found", data: { kind: "unknown_session" } }, + }); + const unrelated = JSON.stringify({ + jsonrpc: "2.0", + id: 8, + error: { code: -32602, message: "Invalid params: bad model", data: {} }, + }); + assert.equal(normalizeAcpResponse(unrelated), unrelated); + assert.equal(normalizeAcpResponse("not-json"), "not-json"); +}); + test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => { const extensionUrl = new URL( "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", diff --git a/software/pi/wrappers/acp-errors.mjs b/software/pi/wrappers/acp-errors.mjs new file mode 100644 index 0000000000..3effa93060 --- /dev/null +++ b/software/pi/wrappers/acp-errors.mjs @@ -0,0 +1,22 @@ +export function normalizeAcpResponse(line) { + let message; + try { + message = JSON.parse(line); + } catch { + return line; + } + if ( + message?.error?.code !== -32602 + || !/unknown sessionid:/i.test(message.error.message ?? "") + ) return line; + + return JSON.stringify({ + ...message, + error: { + ...message.error, + code: -32002, + message: "Resource not found", + data: { kind: "unknown_session" }, + }, + }); +} diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs new file mode 100644 index 0000000000..6f22fb052b --- /dev/null +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; + +import { normalizeAcpResponse } from "./acp-errors.mjs"; + +const upstreamEntrypoint = new URL("./pi-acp/index.js", import.meta.url).pathname; +const child = spawn(process.execPath, [upstreamEntrypoint, ...process.argv.slice(2)], { + env: process.env, + stdio: ["pipe", "pipe", "inherit"], +}); + +process.stdin.pipe(child.stdin); +const output = createInterface({ input: child.stdout, crlfDelay: Infinity }); +output.on("line", (line) => process.stdout.write(`${normalizeAcpResponse(line)}\n`)); + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => child.kill(signal)); +} +child.once("error", (error) => { + process.stderr.write(`${error.stack ?? error}\n`); + process.exitCode = 1; +}); +child.once("exit", (code, signal) => { + if (signal) process.kill(process.pid, signal); + else process.exitCode = code ?? 1; +}); From 7a4ee2866fee40e4aa902d19737bfd3b5fdf9157 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 18:13:43 +0200 Subject: [PATCH 07/15] fix(pi): relay guest ACP input --- software/pi/wrappers/pi-acp-agentos.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs index 6f22fb052b..3e89e52ae3 100644 --- a/software/pi/wrappers/pi-acp-agentos.mjs +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -11,7 +11,8 @@ const child = spawn(process.execPath, [upstreamEntrypoint, ...process.argv.slice stdio: ["pipe", "pipe", "inherit"], }); -process.stdin.pipe(child.stdin); +process.stdin.on("data", (chunk) => child.stdin.write(chunk)); +process.stdin.on("end", () => child.stdin.end()); const output = createInterface({ input: child.stdout, crlfDelay: Infinity }); output.on("line", (line) => process.stdout.write(`${normalizeAcpResponse(line)}\n`)); From 3adc4c99beb1ba42aa38bb9bf0e8d784d147b130 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 18:19:48 +0200 Subject: [PATCH 08/15] fix(pi): isolate guest version probe --- software/pi/package.json | 2 +- software/pi/scripts/build-pi-acp.mjs | 2 ++ software/pi/tests/package.test.mjs | 20 +++++++++++++++++++- software/pi/wrappers/pi-command.mjs | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 software/pi/wrappers/pi-command.mjs diff --git a/software/pi/package.json b/software/pi/package.json index a7f1cfa513..de0726818f 100644 --- a/software/pi/package.json +++ b/software/pi/package.json @@ -6,7 +6,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "bin": { - "pi": "./node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + "pi": "./dist/pi-command.mjs", "pi-agentos": "./dist/pi-agentos.mjs", "pi-acp": "./dist/pi-acp-agentos.mjs" }, diff --git a/software/pi/scripts/build-pi-acp.mjs b/software/pi/scripts/build-pi-acp.mjs index 7df74ff439..800fd85daa 100644 --- a/software/pi/scripts/build-pi-acp.mjs +++ b/software/pi/scripts/build-pi-acp.mjs @@ -29,6 +29,7 @@ const manifestPath = resolve(packageDir, "dist", "pi-acp-upstream.json"); const wrapperPath = resolve(packageDir, "wrappers", "pi-agentos.mjs"); const acpWrapperPath = resolve(packageDir, "wrappers", "pi-acp-agentos.mjs"); const acpErrorsPath = resolve(packageDir, "wrappers", "acp-errors.mjs"); +const piCommandPath = resolve(packageDir, "wrappers", "pi-command.mjs"); const extensionPath = resolve(packageDir, "extensions", "codex-auth.mjs"); function sha256(path) { @@ -117,6 +118,7 @@ if (existsSync(sourceMap)) cpSync(sourceMap, resolve(outputDir, "index.js.map")) cpSync(wrapperPath, resolve(packageDir, "dist", "pi-agentos.mjs")); cpSync(acpWrapperPath, resolve(packageDir, "dist", "pi-acp-agentos.mjs")); cpSync(acpErrorsPath, resolve(packageDir, "dist", "acp-errors.mjs")); +cpSync(piCommandPath, resolve(packageDir, "dist", "pi-command.mjs")); mkdirSync(resolve(packageDir, "dist", "extensions"), { recursive: true }); cpSync(extensionPath, resolve(packageDir, "dist", "extensions", "codex-auth.mjs")); diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index a334ff4cec..368f0d8c88 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -41,7 +41,7 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", assert.equal(packageJson.bin["pi-agentos"], "./dist/pi-agentos.mjs"); assert.equal( packageJson.bin.pi, - "./node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + "./dist/pi-command.mjs", ); assert.equal( packageJson.dependencies["@earendil-works/pi-coding-agent"], @@ -94,6 +94,24 @@ test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => assert.equal(normalizeAcpResponse("not-json"), "not-json"); }); +test("AgentOS Pi command answers the guest-safe version probe", async () => { + const command = new URL("../dist/package/bin/pi", import.meta.url).pathname; + const child = spawn(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { stdout += chunk; }); + child.stderr.on("data", (chunk) => { stderr += chunk; }); + const code = await new Promise((resolve, reject) => { + child.once("error", reject); + child.once("exit", resolve); + }); + assert.equal(code, 0); + assert.equal(stdout.trim(), "0.83.0"); + assert.equal(stderr, ""); +}); + test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => { const extensionUrl = new URL( "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", diff --git a/software/pi/wrappers/pi-command.mjs b/software/pi/wrappers/pi-command.mjs new file mode 100644 index 0000000000..75d4ba4a59 --- /dev/null +++ b/software/pi/wrappers/pi-command.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; + +if (process.argv.length === 3 && process.argv[2] === "--version") { + process.stdout.write("0.83.0\n"); + process.exit(0); +} + +const upstreamEntrypoint = new URL( + "../node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + import.meta.url, +).pathname; +const child = spawn(process.execPath, [upstreamEntrypoint, ...process.argv.slice(2)], { + env: process.env, + stdio: "inherit", +}); +for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => child.kill(signal)); +} +child.once("error", (error) => { + process.stderr.write(`${error.stack ?? error}\n`); + process.exitCode = 1; +}); +child.once("exit", (code, signal) => { + if (signal) process.kill(process.pid, signal); + else process.exitCode = code ?? 1; +}); From f4f79503a92f4fa6b7292a565574051eb0e96ae1 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 18:34:25 +0200 Subject: [PATCH 09/15] fix(pi): preserve ACP session cwd --- software/pi/tests/package.test.mjs | 14 ++++++++++++- software/pi/wrappers/acp-errors.mjs | 18 +++++++++++++---- software/pi/wrappers/pi-acp-agentos.mjs | 27 +++++++++++++++++++++---- software/pi/wrappers/pi-agentos.mjs | 8 +++++++- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 368f0d8c88..2428797e4d 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -69,7 +69,7 @@ test("Pi packages the commit-pinned rivet-dev ACP adapter and runtime closure", }); test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => { - const { normalizeAcpResponse } = await import( + const { acpRequestCwd, normalizeAcpResponse } = await import( new URL( "../dist/package/node_modules/@agentos-software/pi/dist/acp-errors.mjs", import.meta.url, @@ -85,6 +85,16 @@ test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => id: 7, error: { code: -32002, message: "Resource not found", data: { kind: "unknown_session" } }, }); + const wrongCwd = JSON.stringify({ + jsonrpc: "2.0", + id: 9, + error: { + code: -32602, + message: "Invalid params: cwd does not match persisted session: expected /, received /workspace", + data: {}, + }, + }); + assert.equal(JSON.parse(normalizeAcpResponse(wrongCwd)).error.code, -32002); const unrelated = JSON.stringify({ jsonrpc: "2.0", id: 8, @@ -92,6 +102,8 @@ test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => }); assert.equal(normalizeAcpResponse(unrelated), unrelated); assert.equal(normalizeAcpResponse("not-json"), "not-json"); + assert.equal(acpRequestCwd('{"method":"session/new","params":{"cwd":"/workspace"}}'), "/workspace"); + assert.equal(acpRequestCwd('{"method":"session/new","params":{"cwd":"relative"}}'), null); }); test("AgentOS Pi command answers the guest-safe version probe", async () => { diff --git a/software/pi/wrappers/acp-errors.mjs b/software/pi/wrappers/acp-errors.mjs index 3effa93060..3ffc13a0bc 100644 --- a/software/pi/wrappers/acp-errors.mjs +++ b/software/pi/wrappers/acp-errors.mjs @@ -5,10 +5,10 @@ export function normalizeAcpResponse(line) { } catch { return line; } - if ( - message?.error?.code !== -32602 - || !/unknown sessionid:/i.test(message.error.message ?? "") - ) return line; + const invalidSession = /unknown sessionid:|cwd does not match persisted session:/i.test( + message?.error?.message ?? "", + ); + if (message?.error?.code !== -32602 || !invalidSession) return line; return JSON.stringify({ ...message, @@ -20,3 +20,13 @@ export function normalizeAcpResponse(line) { }, }); } + +export function acpRequestCwd(line) { + try { + const message = JSON.parse(line); + const cwd = message?.params?.cwd; + return typeof cwd === "string" && cwd.startsWith("/") ? cwd : null; + } catch { + return null; + } +} diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs index 3e89e52ae3..1489fd87e9 100644 --- a/software/pi/wrappers/pi-acp-agentos.mjs +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -1,18 +1,36 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; +import { rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; import { createInterface } from "node:readline"; -import { normalizeAcpResponse } from "./acp-errors.mjs"; +import { acpRequestCwd, normalizeAcpResponse } from "./acp-errors.mjs"; const upstreamEntrypoint = new URL("./pi-acp/index.js", import.meta.url).pathname; +const cwdPath = resolve(tmpdir(), `agentos-pi-cwd-${process.pid}`); const child = spawn(process.execPath, [upstreamEntrypoint, ...process.argv.slice(2)], { - env: process.env, + env: { ...process.env, AGENTOS_PI_CWD_FILE: cwdPath }, stdio: ["pipe", "pipe", "inherit"], }); -process.stdin.on("data", (chunk) => child.stdin.write(chunk)); -process.stdin.on("end", () => child.stdin.end()); +let inputBuffer = ""; +function forwardInput(line, newline = true) { + const cwd = acpRequestCwd(line); + if (cwd) writeFileSync(cwdPath, `${cwd}\n`, { mode: 0o600 }); + child.stdin.write(`${line}${newline ? "\n" : ""}`); +} +process.stdin.on("data", (chunk) => { + inputBuffer += chunk.toString(); + const lines = inputBuffer.split("\n"); + inputBuffer = lines.pop() ?? ""; + for (const line of lines) forwardInput(line); +}); +process.stdin.on("end", () => { + if (inputBuffer) forwardInput(inputBuffer, false); + child.stdin.end(); +}); const output = createInterface({ input: child.stdout, crlfDelay: Infinity }); output.on("line", (line) => process.stdout.write(`${normalizeAcpResponse(line)}\n`)); @@ -24,6 +42,7 @@ child.once("error", (error) => { process.exitCode = 1; }); child.once("exit", (code, signal) => { + rmSync(cwdPath, { force: true }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; }); diff --git a/software/pi/wrappers/pi-agentos.mjs b/software/pi/wrappers/pi-agentos.mjs index e21a939e78..0146e07aa2 100644 --- a/software/pi/wrappers/pi-agentos.mjs +++ b/software/pi/wrappers/pi-agentos.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; import { isAbsolute, resolve } from "node:path"; import { createAgentSessionFromServices, @@ -16,7 +17,12 @@ import { process.title = "pi-agentos-rpc"; process.env.PI_CODING_AGENT = "true"; -const launchCwd = process.cwd(); +const configuredCwd = process.env.AGENTOS_PI_CWD_FILE + ? readFileSync(process.env.AGENTOS_PI_CWD_FILE, "utf8").trim() + : ""; +const launchCwd = isAbsolute(configuredCwd) ? configuredCwd : process.cwd(); +process.chdir(launchCwd); +process.env.PWD = launchCwd; const agentDir = getAgentDir(); const parsed = parseArgs(process.argv.slice(2)); const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.type === "error"); From 1ac107d255499688e749370b034cd2632ae2cb17 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 19:00:04 +0200 Subject: [PATCH 10/15] fix(pi): broker bound Codex requests --- software/pi/extensions/codex-auth.mjs | 35 +++++++++++++--- software/pi/tests/package.test.mjs | 35 ++++++++++++++++ software/pi/wrappers/acp-errors.mjs | 1 + software/pi/wrappers/pi-acp-agentos.mjs | 54 ++++++++++++++++++++----- 4 files changed, 111 insertions(+), 14 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 7f18cf9cdd..3bc8ad293f 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -36,16 +36,41 @@ export function parseCodexCredential(value) { const credential = parsed?.result ?? parsed; const accessToken = credential?.accessToken?.trim(); const accountId = credential?.accountId?.trim(); - if (!accessToken || !accountId) { - throw new Error("Codex credential binding returned an invalid credential"); + if (accessToken && accountId) return { accessToken, accountId }; + + const proxyUrl = credential?.proxyUrl?.trim(); + const proxyToken = credential?.proxyToken?.trim(); + if (proxyUrl && proxyToken && accountId) { + const parsedUrl = new URL(proxyUrl); + if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") { + return { proxyUrl: parsedUrl.toString(), proxyToken, accountId }; + } } - return { accessToken, accountId }; + throw new Error("Codex credential binding returned an invalid credential"); } export function createDynamicCodexFetch(resolveCredential, upstreamFetch = globalThis.fetch) { return async (input, init) => { - const { accessToken, accountId } = parseCodexCredential(await resolveCredential()); - return createCodexFetch(accessToken, accountId, upstreamFetch)(input, init); + const url = input instanceof Request ? input.url : String(input); + if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); + + const credential = parseCodexCredential(await resolveCredential()); + if (credential.accessToken) { + return createCodexFetch(credential.accessToken, credential.accountId, upstreamFetch)(input, init); + } + + const proxyUrl = new URL(credential.proxyUrl); + proxyUrl.searchParams.set("target", url); + const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); + headers.set("authorization", `Bearer ${credential.proxyToken}`); + headers.delete("chatgpt-account-id"); + return upstreamFetch(proxyUrl, { + ...(input instanceof Request + ? { method: input.method, body: input.body } + : {}), + ...init, + headers, + }); }; } diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 2428797e4d..c778bc64e0 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -103,6 +103,7 @@ test("AgentOS ACP shim normalizes only missing native Pi sessions", async () => assert.equal(normalizeAcpResponse(unrelated), unrelated); assert.equal(normalizeAcpResponse("not-json"), "not-json"); assert.equal(acpRequestCwd('{"method":"session/new","params":{"cwd":"/workspace"}}'), "/workspace"); + assert.equal(acpRequestCwd('{"method":"session/prompt","params":{"cwd":"/other"}}'), null); assert.equal(acpRequestCwd('{"method":"session/new","params":{"cwd":"relative"}}'), null); }); @@ -227,6 +228,40 @@ test("Codex auth resolves a fresh bound credential for every request", async () ); }); +test("Codex auth sends bound requests through an opaque proxy ticket", async () => { + const extension = await import( + new URL( + "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", + import.meta.url, + ) + ); + let request; + const proxyFetch = extension.createDynamicCodexFetch( + async () => ({ + proxyUrl: "http://gateway.internal/api/internal/codex-proxy", + proxyToken: "one-use-ticket", + accountId: "bound-account", + }), + async (input, init) => { + request = { input: String(input), init }; + return new Response(null, { status: 204 }); + }, + ); + + await proxyFetch("https://chatgpt.com/backend-api/codex/responses", { + method: "POST", + headers: { authorization: "Bearer synthetic", "content-type": "application/json" }, + body: "{}", + }); + const url = new URL(request.input); + const headers = new Headers(request.init.headers); + assert.equal(url.origin, "http://gateway.internal"); + assert.equal(url.pathname, "/api/internal/codex-proxy"); + assert.equal(url.searchParams.get("target"), "https://chatgpt.com/backend-api/codex/responses"); + assert.equal(headers.get("authorization"), "Bearer one-use-ticket"); + assert.equal(headers.get("chatgpt-account-id"), null); +}); + test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { const piCommand = new URL("../dist/package/bin/pi-agentos", import.meta.url).pathname; const child = spawn( diff --git a/software/pi/wrappers/acp-errors.mjs b/software/pi/wrappers/acp-errors.mjs index 3ffc13a0bc..54aef38931 100644 --- a/software/pi/wrappers/acp-errors.mjs +++ b/software/pi/wrappers/acp-errors.mjs @@ -24,6 +24,7 @@ export function normalizeAcpResponse(line) { export function acpRequestCwd(line) { try { const message = JSON.parse(line); + if (!["session/new", "session/load", "session/resume"].includes(message?.method)) return null; const cwd = message?.params?.cwd; return typeof cwd === "string" && cwd.startsWith("/") ? cwd : null; } catch { diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs index 1489fd87e9..370ede8cbe 100644 --- a/software/pi/wrappers/pi-acp-agentos.mjs +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -16,16 +16,40 @@ const child = spawn(process.execPath, [upstreamEntrypoint, ...process.argv.slice }); let inputBuffer = ""; +let sessionCwd = ""; +let finished = false; +function writeInput(value) { + if (child.stdin.write(value) || typeof process.stdin.pause !== "function") return; + process.stdin.pause(); + child.stdin.once("drain", () => process.stdin.resume?.()); +} function forwardInput(line, newline = true) { const cwd = acpRequestCwd(line); - if (cwd) writeFileSync(cwdPath, `${cwd}\n`, { mode: 0o600 }); - child.stdin.write(`${line}${newline ? "\n" : ""}`); + if (cwd && !sessionCwd) { + sessionCwd = cwd; + writeFileSync(cwdPath, `${cwd}\n`, { mode: 0o600 }); + } else if (cwd && cwd !== sessionCwd) { + throw new Error(`ACP adapter cannot change cwd from ${sessionCwd} to ${cwd}`); + } + writeInput(`${line}${newline ? "\n" : ""}`); } process.stdin.on("data", (chunk) => { + if (sessionCwd) { + writeInput(chunk); + return; + } inputBuffer += chunk.toString(); - const lines = inputBuffer.split("\n"); - inputBuffer = lines.pop() ?? ""; - for (const line of lines) forwardInput(line); + for (;;) { + const newline = inputBuffer.indexOf("\n"); + if (newline < 0) return; + const line = inputBuffer.slice(0, newline); + inputBuffer = inputBuffer.slice(newline + 1); + forwardInput(line); + if (!sessionCwd) continue; + if (inputBuffer) writeInput(inputBuffer); + inputBuffer = ""; + return; + } }); process.stdin.on("end", () => { if (inputBuffer) forwardInput(inputBuffer, false); @@ -35,14 +59,26 @@ const output = createInterface({ input: child.stdout, crlfDelay: Infinity }); output.on("line", (line) => process.stdout.write(`${normalizeAcpResponse(line)}\n`)); for (const signal of ["SIGINT", "SIGTERM"]) { - process.once(signal, () => child.kill(signal)); + process.once(signal, () => { + child.kill(signal); + setTimeout(() => child.kill("SIGKILL"), 2_000).unref?.(); + }); } child.once("error", (error) => { process.stderr.write(`${error.stack ?? error}\n`); - process.exitCode = 1; + finish(1); }); child.once("exit", (code, signal) => { + finish(code ?? 1, signal); +}); + +function finish(code, signal) { + if (finished) return; + finished = true; + process.stdin.removeAllListeners?.("data"); + process.stdin.removeAllListeners?.("end"); + output.close(); rmSync(cwdPath, { force: true }); if (signal) process.kill(process.pid, signal); - else process.exitCode = code ?? 1; -}); + else process.exit(code); +} From 24d934bc66f770312cf54c80995222f6200a526d Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 19:18:07 +0200 Subject: [PATCH 11/15] fix(pi): broker Codex over host bindings --- software/pi/extensions/codex-auth.mjs | 95 ++++++++++++++++++--------- software/pi/tests/package.test.mjs | 32 +++++---- 2 files changed, 79 insertions(+), 48 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 3bc8ad293f..6427507deb 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -1,7 +1,9 @@ import { execFile } from "node:child_process"; import { accessSync, constants } from "node:fs"; +import { rm, writeFile } from "node:fs/promises"; import { delimiter, resolve } from "node:path"; import { promisify } from "node:util"; +import { randomUUID } from "node:crypto"; import { stream as streamOpenAICodexResponses } from "@earendil-works/pi-ai/api/openai-codex-responses"; @@ -37,15 +39,6 @@ export function parseCodexCredential(value) { const accessToken = credential?.accessToken?.trim(); const accountId = credential?.accountId?.trim(); if (accessToken && accountId) return { accessToken, accountId }; - - const proxyUrl = credential?.proxyUrl?.trim(); - const proxyToken = credential?.proxyToken?.trim(); - if (proxyUrl && proxyToken && accountId) { - const parsedUrl = new URL(proxyUrl); - if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") { - return { proxyUrl: parsedUrl.toString(), proxyToken, accountId }; - } - } throw new Error("Codex credential binding returned an invalid credential"); } @@ -54,22 +47,52 @@ export function createDynamicCodexFetch(resolveCredential, upstreamFetch = globa const url = input instanceof Request ? input.url : String(input); if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); - const credential = parseCodexCredential(await resolveCredential()); - if (credential.accessToken) { - return createCodexFetch(credential.accessToken, credential.accountId, upstreamFetch)(input, init); - } + const { accessToken, accountId } = parseCodexCredential(await resolveCredential()); + return createCodexFetch(accessToken, accountId, upstreamFetch)(input, init); + }; +} + +async function requestBody(input, init) { + const body = init?.body; + if (typeof body === "string") return Buffer.from(body); + if (body instanceof URLSearchParams) return Buffer.from(body.toString()); + if (body instanceof ArrayBuffer) return Buffer.from(body); + if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength); + if (body instanceof Blob) return Buffer.from(await body.arrayBuffer()); + if (body) return Buffer.from(await new Response(body).arrayBuffer()); + if (input instanceof Request && input.body) return Buffer.from(await input.clone().arrayBuffer()); + return Buffer.alloc(0); +} + +function parseBoundResponse(value) { + const parsed = typeof value === "string" ? JSON.parse(value) : value; + const response = parsed?.result ?? parsed; + if ( + !Number.isInteger(response?.status) + || typeof response?.bodyBase64 !== "string" + || !response?.headers + || typeof response.headers !== "object" + ) throw new Error("Codex request binding returned an invalid response"); + return response; +} - const proxyUrl = new URL(credential.proxyUrl); - proxyUrl.searchParams.set("target", url); +export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis.fetch) { + return async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); - headers.set("authorization", `Bearer ${credential.proxyToken}`); + headers.delete("authorization"); headers.delete("chatgpt-account-id"); - return upstreamFetch(proxyUrl, { - ...(input instanceof Request - ? { method: input.method, body: input.body } - : {}), - ...init, - headers, + const response = parseBoundResponse(await invokeBinding({ + target: url, + method: init?.method ?? (input instanceof Request ? input.method : "GET"), + headers: Object.fromEntries(headers), + bodyBase64: (await requestBody(input, init)).toString("base64"), + })); + return new Response(Buffer.from(response.bodyBase64, "base64"), { + status: response.status, + statusText: typeof response.statusText === "string" ? response.statusText : "", + headers: response.headers, }); }; } @@ -85,22 +108,28 @@ function commandExists(command) { return false; } -async function loadBoundCredential() { - const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["get"], { - timeout: 10_000, - maxBuffer: 1024 * 1024, - }); - return parseCodexCredential(stdout); +async function invokeBoundRequest(request) { + const path = `/tmp/agentos-codex-request-${process.pid}-${randomUUID()}.json`; + try { + await writeFile(path, JSON.stringify(request), { mode: 0o600 }); + const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["request", "--json-file", path], { + timeout: 120_000, + maxBuffer: 64 * 1024 * 1024, + }); + return parseBoundResponse(stdout); + } finally { + await rm(path, { force: true }); + } } -function createCodexProviderConfig(resolveCredential) { +function createCodexProviderConfig(createFetch) { return { api: "openai-codex-responses", apiKey: `$${SYNTHETIC_TOKEN_ENV}`, streamSimple: (model, context, options) => streamOpenAICodexResponses(model, context, { ...options, - fetch: createDynamicCodexFetch(resolveCredential, options?.fetch ?? globalThis.fetch), + fetch: createFetch(options?.fetch ?? globalThis.fetch), transport: "sse", }), }; @@ -115,6 +144,10 @@ export default function codexAuthExtension(pi) { process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(staticCredential?.accountId ?? "agentos-bound-account"); pi.registerProvider( "openai-codex", - createCodexProviderConfig(staticCredential ? async () => staticCredential : loadBoundCredential), + createCodexProviderConfig( + staticCredential + ? (upstreamFetch) => createDynamicCodexFetch(async () => staticCredential, upstreamFetch) + : (upstreamFetch) => createBoundCodexFetch(invokeBoundRequest, upstreamFetch), + ), ); } diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index c778bc64e0..6188c6d44d 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -228,7 +228,7 @@ test("Codex auth resolves a fresh bound credential for every request", async () ); }); -test("Codex auth sends bound requests through an opaque proxy ticket", async () => { +test("Codex auth brokers bound requests without exposing the bearer token", async () => { const extension = await import( new URL( "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", @@ -236,15 +236,15 @@ test("Codex auth sends bound requests through an opaque proxy ticket", async () ) ); let request; - const proxyFetch = extension.createDynamicCodexFetch( - async () => ({ - proxyUrl: "http://gateway.internal/api/internal/codex-proxy", - proxyToken: "one-use-ticket", - accountId: "bound-account", - }), - async (input, init) => { - request = { input: String(input), init }; - return new Response(null, { status: 204 }); + const proxyFetch = extension.createBoundCodexFetch( + async (input) => { + request = input; + return { + status: 200, + statusText: "OK", + headers: { "content-type": "text/event-stream" }, + bodyBase64: Buffer.from("data: done\\n\\n").toString("base64"), + }; }, ); @@ -253,13 +253,11 @@ test("Codex auth sends bound requests through an opaque proxy ticket", async () headers: { authorization: "Bearer synthetic", "content-type": "application/json" }, body: "{}", }); - const url = new URL(request.input); - const headers = new Headers(request.init.headers); - assert.equal(url.origin, "http://gateway.internal"); - assert.equal(url.pathname, "/api/internal/codex-proxy"); - assert.equal(url.searchParams.get("target"), "https://chatgpt.com/backend-api/codex/responses"); - assert.equal(headers.get("authorization"), "Bearer one-use-ticket"); - assert.equal(headers.get("chatgpt-account-id"), null); + assert.equal(request.target, "https://chatgpt.com/backend-api/codex/responses"); + assert.equal(request.method, "POST"); + assert.equal(request.headers.authorization, undefined); + assert.equal(request.headers["chatgpt-account-id"], undefined); + assert.equal(Buffer.from(request.bodyBase64, "base64").toString(), "{}"); }); test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { From 6665ce27b7c1ee2104f5afba8a4be9db7d5abaa8 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 19:42:37 +0200 Subject: [PATCH 12/15] fix(pi): stream brokered Codex responses --- software/pi/extensions/codex-auth.mjs | 102 +++++++++++++++++++----- software/pi/tests/package.test.mjs | 31 +++++-- software/pi/wrappers/pi-acp-agentos.mjs | 2 +- software/pi/wrappers/pi-agentos.mjs | 11 ++- 4 files changed, 115 insertions(+), 31 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 6427507deb..2a8f3c895a 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -64,18 +64,30 @@ async function requestBody(input, init) { return Buffer.alloc(0); } -function parseBoundResponse(value) { +function bindingResult(value) { const parsed = typeof value === "string" ? JSON.parse(value) : value; - const response = parsed?.result ?? parsed; + return parsed?.result ?? parsed; +} + +function parseBoundStart(value) { + const response = bindingResult(value); if ( !Number.isInteger(response?.status) - || typeof response?.bodyBase64 !== "string" + || typeof response?.requestId !== "string" || !response?.headers || typeof response.headers !== "object" - ) throw new Error("Codex request binding returned an invalid response"); + ) throw new Error("Codex request binding returned invalid start metadata"); return response; } +function parseBoundChunk(value) { + const chunk = bindingResult(value); + if (typeof chunk?.done !== "boolean" || typeof chunk?.chunkBase64 !== "string") { + throw new Error("Codex request binding returned an invalid stream chunk"); + } + return chunk; +} + export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis.fetch) { return async (input, init) => { const url = input instanceof Request ? input.url : String(input); @@ -83,13 +95,49 @@ export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis. const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); headers.delete("authorization"); headers.delete("chatgpt-account-id"); - const response = parseBoundResponse(await invokeBinding({ + const response = parseBoundStart(await invokeBinding("start", { target: url, method: init?.method ?? (input instanceof Request ? input.method : "GET"), headers: Object.fromEntries(headers), bodyBase64: (await requestBody(input, init)).toString("base64"), })); - return new Response(Buffer.from(response.bodyBase64, "base64"), { + let closed = false; + const cancel = async () => { + if (closed) return; + closed = true; + await invokeBinding("cancel", { requestId: response.requestId }).catch(() => undefined); + }; + const stream = new ReadableStream({ + start(controller) { + if (!init?.signal) return; + if (init.signal.aborted) { + void cancel(); + controller.error(init.signal.reason); + return; + } + init.signal.addEventListener("abort", () => { + void cancel(); + controller.error(init.signal.reason); + }, { once: true }); + }, + async pull(controller) { + if (closed) return; + try { + const chunk = parseBoundChunk(await invokeBinding("read", { requestId: response.requestId })); + if (closed) return; + if (chunk.chunkBase64) controller.enqueue(Buffer.from(chunk.chunkBase64, "base64")); + if (chunk.done) { + closed = true; + controller.close(); + } + } catch (error) { + closed = true; + controller.error(error); + } + }, + cancel, + }); + return new Response(stream, { status: response.status, statusText: typeof response.statusText === "string" ? response.statusText : "", headers: response.headers, @@ -108,18 +156,26 @@ function commandExists(command) { return false; } -async function invokeBoundRequest(request) { - const path = `/tmp/agentos-codex-request-${process.pid}-${randomUUID()}.json`; - try { - await writeFile(path, JSON.stringify(request), { mode: 0o600 }); - const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["request", "--json-file", path], { - timeout: 120_000, - maxBuffer: 64 * 1024 * 1024, - }); - return parseBoundResponse(stdout); - } finally { - await rm(path, { force: true }); +async function invokeBoundCommand(command, input) { + if (command === "start") { + const path = `/tmp/agentos-codex-request-${process.pid}-${randomUUID()}.json`; + try { + await writeFile(path, JSON.stringify(input), { mode: 0o600 }); + const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["start", "--json-file", path], { + timeout: 120_000, + maxBuffer: 1024 * 1024, + }); + return bindingResult(stdout); + } finally { + await rm(path, { force: true }); + } } + const { stdout } = await execFileAsync( + CREDENTIAL_COMMAND, + [command, "--request-id", input.requestId], + { timeout: command === "read" ? 10 * 60_000 : 10_000, maxBuffer: 8 * 1024 * 1024 }, + ); + return bindingResult(stdout); } function createCodexProviderConfig(createFetch) { @@ -138,8 +194,14 @@ function createCodexProviderConfig(createFetch) { export default function codexAuthExtension(pi) { const accessToken = process.env.OPENAI_CODEX_ACCESS_TOKEN?.trim(); const accountId = process.env.OPENAI_CODEX_ACCOUNT_ID?.trim(); - const staticCredential = accessToken && accountId ? { accessToken, accountId } : null; - if (!staticCredential && !commandExists(CREDENTIAL_COMMAND)) return; + const bound = commandExists(CREDENTIAL_COMMAND); + const staticCredential = !bound + && process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH === "1" + && accessToken + && accountId + ? { accessToken, accountId } + : null; + if (!staticCredential && !bound) return; process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(staticCredential?.accountId ?? "agentos-bound-account"); pi.registerProvider( @@ -147,7 +209,7 @@ export default function codexAuthExtension(pi) { createCodexProviderConfig( staticCredential ? (upstreamFetch) => createDynamicCodexFetch(async () => staticCredential, upstreamFetch) - : (upstreamFetch) => createBoundCodexFetch(invokeBoundRequest, upstreamFetch), + : (upstreamFetch) => createBoundCodexFetch(invokeBoundCommand, upstreamFetch), ), ); } diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 6188c6d44d..e88b71e02e 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -133,6 +133,7 @@ test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => const extension = await import(extensionUrl); const originalToken = process.env.OPENAI_CODEX_ACCESS_TOKEN; const originalAccountId = process.env.OPENAI_CODEX_ACCOUNT_ID; + const originalAllowEnvAuth = process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH; const accessToken = "external-codex-access-token"; const accountId = "external-codex-account-id"; let request; @@ -143,11 +144,14 @@ test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => }; process.env.OPENAI_CODEX_ACCESS_TOKEN = accessToken; process.env.OPENAI_CODEX_ACCOUNT_ID = accountId; + process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH = "1"; t.after(() => { if (originalToken === undefined) delete process.env.OPENAI_CODEX_ACCESS_TOKEN; else process.env.OPENAI_CODEX_ACCESS_TOKEN = originalToken; if (originalAccountId === undefined) delete process.env.OPENAI_CODEX_ACCOUNT_ID; else process.env.OPENAI_CODEX_ACCOUNT_ID = originalAccountId; + if (originalAllowEnvAuth === undefined) delete process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH; + else process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH = originalAllowEnvAuth; delete process.env.AGENTOS_CODEX_ACCOUNT_TOKEN; }); @@ -236,19 +240,29 @@ test("Codex auth brokers bound requests without exposing the bearer token", asyn ) ); let request; + let reads = 0; const proxyFetch = extension.createBoundCodexFetch( - async (input) => { - request = input; + async (command, input) => { + if (command === "start") { + request = input; + return { + requestId: "00000000-0000-4000-8000-000000000001", + status: 200, + statusText: "OK", + headers: { "content-type": "text/event-stream" }, + }; + } + if (command === "cancel") return { cancelled: true }; + reads++; return { - status: 200, - statusText: "OK", - headers: { "content-type": "text/event-stream" }, - bodyBase64: Buffer.from("data: done\\n\\n").toString("base64"), + done: reads > 1, + chunkBase64: reads === 1 ? Buffer.from("data: done\\n\\n").toString("base64") : "", }; }, + async () => new Response(null, { status: 204 }), ); - await proxyFetch("https://chatgpt.com/backend-api/codex/responses", { + const response = await proxyFetch("https://chatgpt.com/backend-api/codex/responses", { method: "POST", headers: { authorization: "Bearer synthetic", "content-type": "application/json" }, body: "{}", @@ -258,6 +272,9 @@ test("Codex auth brokers bound requests without exposing the bearer token", asyn assert.equal(request.headers.authorization, undefined); assert.equal(request.headers["chatgpt-account-id"], undefined); assert.equal(Buffer.from(request.bodyBase64, "base64").toString(), "{}"); + assert.equal(await response.text(), "data: done\\n\\n"); + assert.equal(reads, 2); + assert.equal((await proxyFetch("https://example.com")).status, 204); }); test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs index 370ede8cbe..2e6bdf52ce 100644 --- a/software/pi/wrappers/pi-acp-agentos.mjs +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -68,7 +68,7 @@ child.once("error", (error) => { process.stderr.write(`${error.stack ?? error}\n`); finish(1); }); -child.once("exit", (code, signal) => { +child.once("close", (code, signal) => { finish(code ?? 1, signal); }); diff --git a/software/pi/wrappers/pi-agentos.mjs b/software/pi/wrappers/pi-agentos.mjs index 0146e07aa2..e8b83108cf 100644 --- a/software/pi/wrappers/pi-agentos.mjs +++ b/software/pi/wrappers/pi-agentos.mjs @@ -17,9 +17,14 @@ import { process.title = "pi-agentos-rpc"; process.env.PI_CODING_AGENT = "true"; -const configuredCwd = process.env.AGENTOS_PI_CWD_FILE - ? readFileSync(process.env.AGENTOS_PI_CWD_FILE, "utf8").trim() - : ""; +let configuredCwd = ""; +if (process.env.AGENTOS_PI_CWD_FILE) { + try { + configuredCwd = readFileSync(process.env.AGENTOS_PI_CWD_FILE, "utf8").trim(); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } +} const launchCwd = isAbsolute(configuredCwd) ? configuredCwd : process.cwd(); process.chdir(launchCwd); process.env.PWD = launchCwd; From cf25aecab623c29d979b5569a51800eca7d4b2af Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 19:54:32 +0200 Subject: [PATCH 13/15] fix(pi): honor binding timeout limit --- software/pi/extensions/codex-auth.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index 2a8f3c895a..ff3cf35764 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -173,7 +173,7 @@ async function invokeBoundCommand(command, input) { const { stdout } = await execFileAsync( CREDENTIAL_COMMAND, [command, "--request-id", input.requestId], - { timeout: command === "read" ? 10 * 60_000 : 10_000, maxBuffer: 8 * 1024 * 1024 }, + { timeout: command === "read" ? 5 * 60_000 : 10_000, maxBuffer: 8 * 1024 * 1024 }, ); return bindingResult(stdout); } From f0636c8c102ee653b6ec4b345d195a8d7adb5ed8 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 20:14:32 +0200 Subject: [PATCH 14/15] fix(pi): harden broker cancellation --- software/pi/extensions/codex-auth.mjs | 52 ++++++++++++++++--------- software/pi/tests/package.test.mjs | 2 +- software/pi/wrappers/pi-acp-agentos.mjs | 3 +- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs index ff3cf35764..6a7c142530 100644 --- a/software/pi/extensions/codex-auth.mjs +++ b/software/pi/extensions/codex-auth.mjs @@ -95,30 +95,44 @@ export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis. const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); headers.delete("authorization"); headers.delete("chatgpt-account-id"); - const response = parseBoundStart(await invokeBinding("start", { - target: url, - method: init?.method ?? (input instanceof Request ? input.method : "GET"), - headers: Object.fromEntries(headers), - bodyBase64: (await requestBody(input, init)).toString("base64"), - })); + const requestId = randomUUID(); let closed = false; + let streamController; const cancel = async () => { if (closed) return; closed = true; - await invokeBinding("cancel", { requestId: response.requestId }).catch(() => undefined); + await invokeBinding("cancel", { requestId }).catch(() => undefined); }; + const onAbort = () => { + void cancel(); + streamController?.error(init?.signal?.reason); + }; + if (init?.signal?.aborted) { + await cancel(); + throw init.signal.reason; + } + init?.signal?.addEventListener("abort", onAbort, { once: true }); + let response; + try { + response = parseBoundStart(await invokeBinding("start", { + requestId, + target: url, + method: init?.method ?? (input instanceof Request ? input.method : "GET"), + headers: Object.fromEntries(headers), + bodyBase64: (await requestBody(input, init)).toString("base64"), + })); + } catch (error) { + await cancel(); + init?.signal?.removeEventListener("abort", onAbort); + throw error; + } + if (closed) { + init?.signal?.removeEventListener("abort", onAbort); + throw init?.signal?.reason ?? new Error("Codex request was cancelled"); + } const stream = new ReadableStream({ start(controller) { - if (!init?.signal) return; - if (init.signal.aborted) { - void cancel(); - controller.error(init.signal.reason); - return; - } - init.signal.addEventListener("abort", () => { - void cancel(); - controller.error(init.signal.reason); - }, { once: true }); + streamController = controller; }, async pull(controller) { if (closed) return; @@ -128,10 +142,12 @@ export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis. if (chunk.chunkBase64) controller.enqueue(Buffer.from(chunk.chunkBase64, "base64")); if (chunk.done) { closed = true; + init?.signal?.removeEventListener("abort", onAbort); controller.close(); } } catch (error) { - closed = true; + await cancel(); + init?.signal?.removeEventListener("abort", onAbort); controller.error(error); } }, diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index e88b71e02e..107bbd19d5 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -246,7 +246,7 @@ test("Codex auth brokers bound requests without exposing the bearer token", asyn if (command === "start") { request = input; return { - requestId: "00000000-0000-4000-8000-000000000001", + requestId: input.requestId, status: 200, statusText: "OK", headers: { "content-type": "text/event-stream" }, diff --git a/software/pi/wrappers/pi-acp-agentos.mjs b/software/pi/wrappers/pi-acp-agentos.mjs index 2e6bdf52ce..2d6480a7ec 100644 --- a/software/pi/wrappers/pi-acp-agentos.mjs +++ b/software/pi/wrappers/pi-acp-agentos.mjs @@ -79,6 +79,5 @@ function finish(code, signal) { process.stdin.removeAllListeners?.("end"); output.close(); rmSync(cwdPath, { force: true }); - if (signal) process.kill(process.pid, signal); - else process.exit(code); + process.exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : code; } From 0335f9d635ebeae62bcfda8ac6331b8091f77fe3 Mon Sep 17 00:00:00 2001 From: Topper Bowers Date: Fri, 31 Jul 2026 23:20:35 +0200 Subject: [PATCH 15/15] refactor(pi): drop Codex auth broker for proxy-based subscription support The host-side streaming Codex proxy (custom baseUrl in models.json + OAuth credential in auth.json) makes the in-guest auth broker unnecessary. Stock Pi resolves the subscription through its normal openai-codex-responses provider, so remove the codex-auth extension, its launcher wiring, packaging, dependency, and tests. The Pi 0.83 packaging and durability wrappers (cwd preservation, guest-safe version probe, ACP session recovery) are retained. Co-Authored-By: Claude Fable 5 --- pnpm-lock.yaml | 3 - software/pi/extensions/codex-auth.mjs | 231 -------------------------- software/pi/package.json | 1 - software/pi/scripts/build-pi-acp.mjs | 3 - software/pi/tests/package.test.mjs | 154 ----------------- software/pi/wrappers/pi-agentos.mjs | 4 +- 6 files changed, 1 insertion(+), 395 deletions(-) delete mode 100644 software/pi/extensions/codex-auth.mjs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a986167cd..bb2255c09e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4143,9 +4143,6 @@ importers: '@agentclientprotocol/sdk': specifier: 1.2.1 version: 1.2.1(zod@3.25.76) - '@earendil-works/pi-ai': - specifier: 0.83.0 - version: 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) '@earendil-works/pi-coding-agent': specifier: 0.83.0 version: 0.83.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@3.25.76) diff --git a/software/pi/extensions/codex-auth.mjs b/software/pi/extensions/codex-auth.mjs deleted file mode 100644 index 6a7c142530..0000000000 --- a/software/pi/extensions/codex-auth.mjs +++ /dev/null @@ -1,231 +0,0 @@ -import { execFile } from "node:child_process"; -import { accessSync, constants } from "node:fs"; -import { rm, writeFile } from "node:fs/promises"; -import { delimiter, resolve } from "node:path"; -import { promisify } from "node:util"; -import { randomUUID } from "node:crypto"; - -import { stream as streamOpenAICodexResponses } from "@earendil-works/pi-ai/api/openai-codex-responses"; - -const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; -const SYNTHETIC_TOKEN_ENV = "AGENTOS_CODEX_ACCOUNT_TOKEN"; -const CREDENTIAL_COMMAND = "agentos-codex-auth"; -const execFileAsync = promisify(execFile); - -export function createAccountToken(accountId) { - const payload = Buffer.from( - JSON.stringify({ - "https://api.openai.com/auth": { chatgpt_account_id: accountId }, - }), - ).toString("base64url"); - return `e30.${payload}.agentos`; -} - -export function createCodexFetch(accessToken, accountId, upstreamFetch = globalThis.fetch) { - return (input, init) => { - const url = input instanceof Request ? input.url : String(input); - if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); - - const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); - headers.set("authorization", `Bearer ${accessToken}`); - headers.set("chatgpt-account-id", accountId); - return upstreamFetch(input, { ...init, headers }); - }; -} - -export function parseCodexCredential(value) { - const parsed = typeof value === "string" ? JSON.parse(value) : value; - const credential = parsed?.result ?? parsed; - const accessToken = credential?.accessToken?.trim(); - const accountId = credential?.accountId?.trim(); - if (accessToken && accountId) return { accessToken, accountId }; - throw new Error("Codex credential binding returned an invalid credential"); -} - -export function createDynamicCodexFetch(resolveCredential, upstreamFetch = globalThis.fetch) { - return async (input, init) => { - const url = input instanceof Request ? input.url : String(input); - if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); - - const { accessToken, accountId } = parseCodexCredential(await resolveCredential()); - return createCodexFetch(accessToken, accountId, upstreamFetch)(input, init); - }; -} - -async function requestBody(input, init) { - const body = init?.body; - if (typeof body === "string") return Buffer.from(body); - if (body instanceof URLSearchParams) return Buffer.from(body.toString()); - if (body instanceof ArrayBuffer) return Buffer.from(body); - if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength); - if (body instanceof Blob) return Buffer.from(await body.arrayBuffer()); - if (body) return Buffer.from(await new Response(body).arrayBuffer()); - if (input instanceof Request && input.body) return Buffer.from(await input.clone().arrayBuffer()); - return Buffer.alloc(0); -} - -function bindingResult(value) { - const parsed = typeof value === "string" ? JSON.parse(value) : value; - return parsed?.result ?? parsed; -} - -function parseBoundStart(value) { - const response = bindingResult(value); - if ( - !Number.isInteger(response?.status) - || typeof response?.requestId !== "string" - || !response?.headers - || typeof response.headers !== "object" - ) throw new Error("Codex request binding returned invalid start metadata"); - return response; -} - -function parseBoundChunk(value) { - const chunk = bindingResult(value); - if (typeof chunk?.done !== "boolean" || typeof chunk?.chunkBase64 !== "string") { - throw new Error("Codex request binding returned an invalid stream chunk"); - } - return chunk; -} - -export function createBoundCodexFetch(invokeBinding, upstreamFetch = globalThis.fetch) { - return async (input, init) => { - const url = input instanceof Request ? input.url : String(input); - if (url !== CODEX_BASE_URL && !url.startsWith(`${CODEX_BASE_URL}/`)) return upstreamFetch(input, init); - const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); - headers.delete("authorization"); - headers.delete("chatgpt-account-id"); - const requestId = randomUUID(); - let closed = false; - let streamController; - const cancel = async () => { - if (closed) return; - closed = true; - await invokeBinding("cancel", { requestId }).catch(() => undefined); - }; - const onAbort = () => { - void cancel(); - streamController?.error(init?.signal?.reason); - }; - if (init?.signal?.aborted) { - await cancel(); - throw init.signal.reason; - } - init?.signal?.addEventListener("abort", onAbort, { once: true }); - let response; - try { - response = parseBoundStart(await invokeBinding("start", { - requestId, - target: url, - method: init?.method ?? (input instanceof Request ? input.method : "GET"), - headers: Object.fromEntries(headers), - bodyBase64: (await requestBody(input, init)).toString("base64"), - })); - } catch (error) { - await cancel(); - init?.signal?.removeEventListener("abort", onAbort); - throw error; - } - if (closed) { - init?.signal?.removeEventListener("abort", onAbort); - throw init?.signal?.reason ?? new Error("Codex request was cancelled"); - } - const stream = new ReadableStream({ - start(controller) { - streamController = controller; - }, - async pull(controller) { - if (closed) return; - try { - const chunk = parseBoundChunk(await invokeBinding("read", { requestId: response.requestId })); - if (closed) return; - if (chunk.chunkBase64) controller.enqueue(Buffer.from(chunk.chunkBase64, "base64")); - if (chunk.done) { - closed = true; - init?.signal?.removeEventListener("abort", onAbort); - controller.close(); - } - } catch (error) { - await cancel(); - init?.signal?.removeEventListener("abort", onAbort); - controller.error(error); - } - }, - cancel, - }); - return new Response(stream, { - status: response.status, - statusText: typeof response.statusText === "string" ? response.statusText : "", - headers: response.headers, - }); - }; -} - -function commandExists(command) { - for (const directory of (process.env.PATH ?? "").split(delimiter)) { - if (!directory) continue; - try { - accessSync(resolve(directory, command), constants.X_OK); - return true; - } catch {} - } - return false; -} - -async function invokeBoundCommand(command, input) { - if (command === "start") { - const path = `/tmp/agentos-codex-request-${process.pid}-${randomUUID()}.json`; - try { - await writeFile(path, JSON.stringify(input), { mode: 0o600 }); - const { stdout } = await execFileAsync(CREDENTIAL_COMMAND, ["start", "--json-file", path], { - timeout: 120_000, - maxBuffer: 1024 * 1024, - }); - return bindingResult(stdout); - } finally { - await rm(path, { force: true }); - } - } - const { stdout } = await execFileAsync( - CREDENTIAL_COMMAND, - [command, "--request-id", input.requestId], - { timeout: command === "read" ? 5 * 60_000 : 10_000, maxBuffer: 8 * 1024 * 1024 }, - ); - return bindingResult(stdout); -} - -function createCodexProviderConfig(createFetch) { - return { - api: "openai-codex-responses", - apiKey: `$${SYNTHETIC_TOKEN_ENV}`, - streamSimple: (model, context, options) => - streamOpenAICodexResponses(model, context, { - ...options, - fetch: createFetch(options?.fetch ?? globalThis.fetch), - transport: "sse", - }), - }; -} - -export default function codexAuthExtension(pi) { - const accessToken = process.env.OPENAI_CODEX_ACCESS_TOKEN?.trim(); - const accountId = process.env.OPENAI_CODEX_ACCOUNT_ID?.trim(); - const bound = commandExists(CREDENTIAL_COMMAND); - const staticCredential = !bound - && process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH === "1" - && accessToken - && accountId - ? { accessToken, accountId } - : null; - if (!staticCredential && !bound) return; - - process.env[SYNTHETIC_TOKEN_ENV] = createAccountToken(staticCredential?.accountId ?? "agentos-bound-account"); - pi.registerProvider( - "openai-codex", - createCodexProviderConfig( - staticCredential - ? (upstreamFetch) => createDynamicCodexFetch(async () => staticCredential, upstreamFetch) - : (upstreamFetch) => createBoundCodexFetch(invokeBoundCommand, upstreamFetch), - ), - ); -} diff --git a/software/pi/package.json b/software/pi/package.json index de0726818f..59b51deda2 100644 --- a/software/pi/package.json +++ b/software/pi/package.json @@ -31,7 +31,6 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.2.1", - "@earendil-works/pi-ai": "0.83.0", "@earendil-works/pi-coding-agent": "0.83.0", "pi-mcp-adapter": "2.11.0", "zod": "3.25.76" diff --git a/software/pi/scripts/build-pi-acp.mjs b/software/pi/scripts/build-pi-acp.mjs index 800fd85daa..8aabb459a3 100644 --- a/software/pi/scripts/build-pi-acp.mjs +++ b/software/pi/scripts/build-pi-acp.mjs @@ -30,7 +30,6 @@ const wrapperPath = resolve(packageDir, "wrappers", "pi-agentos.mjs"); const acpWrapperPath = resolve(packageDir, "wrappers", "pi-acp-agentos.mjs"); const acpErrorsPath = resolve(packageDir, "wrappers", "acp-errors.mjs"); const piCommandPath = resolve(packageDir, "wrappers", "pi-command.mjs"); -const extensionPath = resolve(packageDir, "extensions", "codex-auth.mjs"); function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); @@ -119,8 +118,6 @@ cpSync(wrapperPath, resolve(packageDir, "dist", "pi-agentos.mjs")); cpSync(acpWrapperPath, resolve(packageDir, "dist", "pi-acp-agentos.mjs")); cpSync(acpErrorsPath, resolve(packageDir, "dist", "acp-errors.mjs")); cpSync(piCommandPath, resolve(packageDir, "dist", "pi-command.mjs")); -mkdirSync(resolve(packageDir, "dist", "extensions"), { recursive: true }); -cpSync(extensionPath, resolve(packageDir, "dist", "extensions", "codex-auth.mjs")); mkdirSync(dirname(manifestPath), { recursive: true }); writeFileSync( diff --git a/software/pi/tests/package.test.mjs b/software/pi/tests/package.test.mjs index 107bbd19d5..0066d35c35 100644 --- a/software/pi/tests/package.test.mjs +++ b/software/pi/tests/package.test.mjs @@ -125,158 +125,6 @@ test("AgentOS Pi command answers the guest-safe version probe", async () => { assert.equal(stderr, ""); }); -test("Pi packages an AgentOS-owned external Codex auth extension", async (t) => { - const extensionUrl = new URL( - "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", - import.meta.url, - ); - const extension = await import(extensionUrl); - const originalToken = process.env.OPENAI_CODEX_ACCESS_TOKEN; - const originalAccountId = process.env.OPENAI_CODEX_ACCOUNT_ID; - const originalAllowEnvAuth = process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH; - const accessToken = "external-codex-access-token"; - const accountId = "external-codex-account-id"; - let request; - let registration; - const captureFetch = async (input, init) => { - request = { input, init }; - return new Response(null, { status: 204 }); - }; - process.env.OPENAI_CODEX_ACCESS_TOKEN = accessToken; - process.env.OPENAI_CODEX_ACCOUNT_ID = accountId; - process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH = "1"; - t.after(() => { - if (originalToken === undefined) delete process.env.OPENAI_CODEX_ACCESS_TOKEN; - else process.env.OPENAI_CODEX_ACCESS_TOKEN = originalToken; - if (originalAccountId === undefined) delete process.env.OPENAI_CODEX_ACCOUNT_ID; - else process.env.OPENAI_CODEX_ACCOUNT_ID = originalAccountId; - if (originalAllowEnvAuth === undefined) delete process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH; - else process.env.AGENTOS_CODEX_ALLOW_ENV_AUTH = originalAllowEnvAuth; - delete process.env.AGENTOS_CODEX_ACCOUNT_TOKEN; - }); - - extension.default({ - registerProvider(provider, config) { - registration = { provider, config }; - }, - }); - - assert.equal(registration.provider, "openai-codex"); - assert.equal(registration.config.api, "openai-codex-responses"); - assert.equal(registration.config.apiKey, "$AGENTOS_CODEX_ACCOUNT_TOKEN"); - assert.equal(typeof registration.config.streamSimple, "function"); - assert.equal(process.env.AGENTOS_CODEX_ACCOUNT_TOKEN, extension.createAccountToken(accountId)); - assert.doesNotMatch(process.env.AGENTOS_CODEX_ACCOUNT_TOKEN, /external-codex-access-token/); - - await extension.createCodexFetch(accessToken, accountId, captureFetch)( - "https://chatgpt.com/backend-api/codex/responses", - { - headers: { authorization: "Bearer synthetic" }, - }, - ); - const headers = new Headers(request.init.headers); - assert.equal(headers.get("authorization"), `Bearer ${accessToken}`); - assert.equal(headers.get("chatgpt-account-id"), accountId); - assert.doesNotMatch( - await readFile( - new URL( - "../dist/package/node_modules/@earendil-works/pi-coding-agent/dist/core/http-dispatcher.js", - import.meta.url, - ), - "utf8", - ), - /canInstallUndiciGlobals/, - ); -}); - -test("Codex auth resolves a fresh bound credential for every request", async () => { - const extension = await import( - new URL( - "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", - import.meta.url, - ) - ); - let request; - let resolution = 0; - const dynamicFetch = extension.createDynamicCodexFetch( - async () => ({ - accessToken: `access-token-${++resolution}`, - accountId: `account-${resolution}`, - }), - async (input, init) => { - request = { input, init }; - return new Response(null, { status: 204 }); - }, - ); - - await dynamicFetch("https://chatgpt.com/backend-api/codex/responses"); - let headers = new Headers(request.init.headers); - assert.equal(headers.get("authorization"), "Bearer access-token-1"); - assert.equal(headers.get("chatgpt-account-id"), "account-1"); - - await dynamicFetch("https://chatgpt.com/backend-api/codex/responses"); - headers = new Headers(request.init.headers); - assert.equal(headers.get("authorization"), "Bearer access-token-2"); - assert.equal(headers.get("chatgpt-account-id"), "account-2"); - assert.equal(resolution, 2); - assert.deepEqual( - extension.parseCodexCredential({ - ok: true, - result: { accessToken: "bound-token", accountId: "bound-account" }, - }), - { accessToken: "bound-token", accountId: "bound-account" }, - ); - assert.throws( - () => extension.parseCodexCredential('{"accessToken":"missing-account"}'), - /invalid credential/, - ); -}); - -test("Codex auth brokers bound requests without exposing the bearer token", async () => { - const extension = await import( - new URL( - "../dist/package/node_modules/@agentos-software/pi/dist/extensions/codex-auth.mjs", - import.meta.url, - ) - ); - let request; - let reads = 0; - const proxyFetch = extension.createBoundCodexFetch( - async (command, input) => { - if (command === "start") { - request = input; - return { - requestId: input.requestId, - status: 200, - statusText: "OK", - headers: { "content-type": "text/event-stream" }, - }; - } - if (command === "cancel") return { cancelled: true }; - reads++; - return { - done: reads > 1, - chunkBase64: reads === 1 ? Buffer.from("data: done\\n\\n").toString("base64") : "", - }; - }, - async () => new Response(null, { status: 204 }), - ); - - const response = await proxyFetch("https://chatgpt.com/backend-api/codex/responses", { - method: "POST", - headers: { authorization: "Bearer synthetic", "content-type": "application/json" }, - body: "{}", - }); - assert.equal(request.target, "https://chatgpt.com/backend-api/codex/responses"); - assert.equal(request.method, "POST"); - assert.equal(request.headers.authorization, undefined); - assert.equal(request.headers["chatgpt-account-id"], undefined); - assert.equal(Buffer.from(request.bodyBase64, "base64").toString(), "{}"); - assert.equal(await response.text(), "data: done\\n\\n"); - assert.equal(reads, 2); - assert.equal((await proxyFetch("https://example.com")).status, 204); -}); - test("packaged pinned Pi adapter initializes with persistent session capabilities", async (t) => { const piCommand = new URL("../dist/package/bin/pi-agentos", import.meta.url).pathname; const child = spawn( @@ -287,8 +135,6 @@ test("packaged pinned Pi adapter initializes with persistent session capabilitie env: { ...process.env, PI_ACP_PI_COMMAND: piCommand, - OPENAI_CODEX_ACCESS_TOKEN: "external-codex-access-token", - OPENAI_CODEX_ACCOUNT_ID: "external-codex-account-id", }, }, ); diff --git a/software/pi/wrappers/pi-agentos.mjs b/software/pi/wrappers/pi-agentos.mjs index e8b83108cf..540b2e79a7 100644 --- a/software/pi/wrappers/pi-agentos.mjs +++ b/software/pi/wrappers/pi-agentos.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { fileURLToPath } from "node:url"; import { readFileSync } from "node:fs"; import { isAbsolute, resolve } from "node:path"; import { @@ -36,8 +35,7 @@ if (parsed.mode && parsed.mode !== "rpc") { } if (errors.length > 0) throw new Error(errors.map((diagnostic) => diagnostic.message).join("\n")); -const extensionPath = fileURLToPath(new URL("./extensions/codex-auth.mjs", import.meta.url)); -const extensionPaths = [extensionPath, ...(parsed.extensions ?? [])].map((path) => +const extensionPaths = (parsed.extensions ?? []).map((path) => isAbsolute(path) ? path : resolve(launchCwd, path), ); const startupSettings = SettingsManager.create(launchCwd, agentDir, { projectTrusted: false });