Claude Skill

add-codex

Use Codex (OpenAI's codex app-server) as a full agent provider — planning, tool orchestration, MCP tools, server-side history, session resume — alongside or instead of Claude. ChatGPT subscription or OpenAI API key, vault-only via OneCLI. Per-group via `ncl groups config update -

LLM Mart · 0 points · 15 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download nanocoai-nanoclaw-.claude_skills_add-codex-143db6c.zip · 14 KB
Part of nanocoai/nanoclaw — 49 skills

Install

skills CLI npx skills add https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-codex
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nanocoai-nanoclaw@llmmart
Git git clone https://github.com/nanocoai/nanoclaw.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nanocoai/nanoclaw collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Codex agent provider

Shortcut: pnpm exec tsx setup/index.ts --step provider-auth codex performs this whole install (manifest-driven from the providers branch: files, barrels, CLI manifest entry, image rebuild) plus auth in one command. The steps below are the same operations, for agent-driven or manual application.

NanoClaw selects each group's agent backend from container_configs.provider (default claude). This skill installs the Codex provider: copy the payload from the providers branch, append one import to each of the three provider barrels, add the pinned Codex CLI to the container manifest (container/cli-tools.json), rebuild, then run the vault auth walk-through.

The provider runs codex app-server as a child process speaking JSON-RPC over stdio: native streaming, MCP tools, server-side conversation history (the continuation is a thread id, no on-disk transcript). Credentials are vault-only: The selected gateway serves a sentinel auth.json stub into the container and swaps the real ChatGPT token or API key on the wire — no key in .env, nothing readable in the container.

The mechanical steps under Install carry nc: directive fences: an agent reads the prose and applies them, and a parser can apply them deterministically from the same document. Every directive is idempotent, so the whole skill is safe to re-run; anything a parser can't apply falls back to the prose beside it.

Install

Pre-flight

Requires src/project-doc-compose.ts on trunk. If it is missing, stop and tell the operator to run /update-nanoclaw first.

Check whether the payload is already wired (a prior apply, or a trunk that still carries it). All of these present means installed — skip to Authenticate:

  • src/providers/codex.ts and src/providers/codex-agents-md.ts
  • container/agent-runner/src/providers/codex.ts and codex-app-server.ts
  • setup/providers/codex.ts and both provider-contracts/codex.ts declarations (host and container)
  • import './codex.js'; in the three provider barrels and both contract barrels
  • an @openai/codex entry in container/cli-tools.json

1. Fetch and copy the payload

Fetch the providers branch and copy the Codex payload into all three trees (additive — overwrite each file, never merge the branch). The host files are the provider contribution + the AGENTS.md spec (composition itself lives in trunk's src/project-doc-compose.ts) + their guards; the container files are the provider runtime (turn loop, JSON-RPC wrapper, native memory SessionStart hook, per-exchange archiver) + their guards; the setup file is the picker entry + vault auth walk-through; container/AGENTS.md is the runtime-contract base the composed AGENTS.md embeds.

src/providers/codex.ts
src/providers/codex-agents-md.ts
src/providers/codex-registration.test.ts
src/providers/codex-host-contribution.test.ts
src/providers/codex-agents-md.test.ts
container/agent-runner/src/providers/codex.ts
container/agent-runner/src/providers/codex-app-server.ts
container/agent-runner/src/providers/exchange-archive.ts
container/agent-runner/src/providers/exchange-archive.test.ts
container/agent-runner/src/providers/codex-registration.test.ts
container/agent-runner/src/providers/codex.factory.test.ts
container/agent-runner/src/providers/codex.turns.test.ts
container/agent-runner/src/providers/codex-app-server.test.ts
container/agent-runner/src/providers/codex-contract-parity.test.ts
container/agent-runner/src/providers/codex.conformance.test.ts
container/agent-runner/src/providers/codex-cli-tools.test.ts
container/agent-runner/src/provider-contracts/codex.ts
setup/providers/codex-registration.test.ts
container/AGENTS.md

Use the selected gateway for authentication

Install the bundled Codex authentication hook alongside the registry payload. This keeps the same login choices while delegating custody to the selected gateway, and preserves the hook when a provider refresh copies registry files again. These two files are omitted from the registry copy so refresh stays idempotent. The setup screens and step sequence do not change.

payload/src/provider-contracts/codex.ts -> src/provider-contracts/codex.ts
payload/setup/providers/codex.ts -> setup/providers/codex.ts
payload/setup/providers/codex.test.ts -> setup/providers/codex.test.ts

2. Wire the barrels

Append the self-registration import to each provider and contract barrel (skipped if already present).

import './codex.js';
import './codex.js';
import './codex.js';
import './codex.js';
import './codex.js';

3. CLI manifest

The agent's global Node CLIs install from container/cli-tools.json (a json-merge seam), not hand-edited Dockerfile layers. Add Codex by appending one entry — idempotent on name, so a re-run is a no-op. @openai/codex has no native postinstall, so no onlyBuilt. The Dockerfile already installs every manifest entry via pinned pnpm install -g; no Dockerfile edit is needed.

{ "name": "@openai/codex", "version": "0.155.1" }

The version (0.155.1) is the canonical pin — this SKILL.md is the source of truth.

4. Build

pnpm run build
pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit
./container/build.sh

5. Validate

pnpm exec tsx scripts/provider-contract-verifier.ts --required-declared codex

The registration tests import only the real barrels — they go red if a barrel line is missing, a barrel fails to evaluate, or the payload is broken.

Authenticate

pnpm exec tsx setup/index.ts --step provider-auth codex

The same walk-through fresh installs get from the setup picker: ChatGPT subscription (browser login or device pairing) or an OpenAI API key, landed in the selected gateway’s vault. Idempotent — it short-circuits when a matching secret already exists. It finishes with the install check.

Use it

Per group:

ncl groups config update --id <group-id> --provider codex
ncl groups restart --id <group-id>

Switching is an operator action — run it from the host. Every provider uses the same memory/ tree, so memory carries across automatically. Run /migrate-memory only when upgrading a group that still has legacy .seed.md, CLAUDE.local.md, or unindexed imported memory. See docs/provider-migration.md.

Default new groups to codex (optional)

New groups are created on the instance default (DEFAULT_AGENT_PROVIDER in .env, or claude when unset). Installing this skill wires codex in but does NOT change that default — "installed" is not "authenticated", so the default stays claude until you opt in explicitly.

After install, ask the operator before flipping it:

"Codex is installed. Default new agent groups to codex? Existing groups keep their current provider."

On yes — set it, then restart the host so it takes effect:

pnpm exec tsx setup/index.ts --step set-env -- --key DEFAULT_AGENT_PROVIDER --value codex
launchctl kickstart -k gui/$(id -u)/com.nanoclaw   # macOS; Linux: systemctl --user restart nanoclaw

This affects only groups created afterward. Per-group ncl groups config update --provider still overrides the default in either direction. Creation itself stays provider-agnostic (no --provider flag — provider is a DB property stamped from the instance default at creation).

Troubleshooting

  • Container dies at boot, channel silent: grep 'Container exited non-zero' logs/nanoclaw.error.log — the stderrTail carries the reason (e.g. Unknown provider: codex. Registered: claude means the barrels aren't wired in the running build).
  • In-channel Error: spawn codex ENOENT on every message: the image predates the manifest entry — re-run ./container/build.sh.
  • Auth errors mid-conversation: the vault secret is missing or stale — re-run pnpm exec tsx setup/index.ts --step provider-auth codex (subscription re-login updates the vault copy).
Files (nanoclaw)
  • payload
    • setup
      • providers
        • codex.test.ts 10.4 KB
          import { EventEmitter } from 'events';
          import fs from 'fs';
          import os from 'os';
          import path from 'path';
          
          import { afterEach, describe, expect, it, vi } from 'vitest';
          
          // Mock child_process so runCodexLoginAuth never spawns a real codex CLI; the
          // spawn stand-in plays `codex login` writing auth.json into whatever
          // CODEX_HOME it was handed.
          const mockSpawn = vi.fn();
          const mockSpawnSync = vi.fn();
          const mockExecFileSync = vi.fn();
          vi.mock('child_process', () => ({
            spawn: (...args: unknown[]) => mockSpawn(...args),
            spawnSync: (...args: unknown[]) => mockSpawnSync(...args),
            execFileSync: (...args: unknown[]) => mockExecFileSync(...args),
          }));
          
          // Keep the auth flow's structured logging out of logs/setup.log.
          vi.mock('../logs.js', () => ({ step: vi.fn(), userInput: vi.fn() }));
          
          // The API-key path reads the key through clack's masked prompt; everything
          // else in the module keeps the real clack rendering.
          const mockPassword = vi.fn();
          vi.mock('@clack/prompts', async (original) => ({
            ...(await original<typeof import('@clack/prompts')>()),
            password: (...args: unknown[]) => mockPassword(...args),
          }));
          
          import * as setupLog from '../logs.js';
          import {
            buildCodexFailurePrompt,
            runCodexApiKeyAuth,
            runCodexInstallCheck,
            runCodexLoginAuth,
            storeFailureMessage,
            verifyCodexInstall,
          } from './codex.js';
          
          // Structural guard for the codex payload wiring: provider files, both barrel
          // imports, and the pinned Dockerfile install. Goes red if any of them is
          // removed without going through the /add-codex (or its REMOVE.md) path.
          describe('verifyCodexInstall', () => {
            it('passes on a tree with the codex payload wired', () => {
              const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-complete-check-'));
              try {
                for (const file of [
                  'src/providers/codex.ts',
                  'src/providers/codex-agents-md.ts',
                  'container/agent-runner/src/providers/codex.ts',
                  'container/agent-runner/src/providers/codex-app-server.ts',
                  'src/providers/index.ts',
                  'container/agent-runner/src/providers/index.ts',
                ]) {
                  const target = path.join(root, file);
                  fs.mkdirSync(path.dirname(target), { recursive: true });
                  fs.writeFileSync(target, "import './codex.js';\n");
                }
                fs.writeFileSync(path.join(root, 'container/cli-tools.json'), JSON.stringify([{ name: '@openai/codex' }]));
                expect(verifyCodexInstall(root)).toEqual({ ok: true, problems: [] });
              } finally {
                fs.rmSync(root, { recursive: true, force: true });
              }
            });
          
            it('blocks setup when the payload is incomplete', async () => {
              const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-install-check-'));
              try {
                await expect(runCodexInstallCheck(root)).rejects.toThrow(/Codex provider is not fully installed/);
              } finally {
                fs.rmSync(root, { recursive: true, force: true });
              }
            });
          });
          
          // Pure prompt builder for the failure-assist hook — no spawning involved.
          describe('buildCodexFailurePrompt', () => {
            it('carries the failure context and the de-duped reference list', () => {
              const projectRoot = '/repo';
              const prompt = buildCodexFailurePrompt(
                {
                  stepName: 'verify',
                  msg: 'first-chat ping timed out',
                  hint: 'check the container logs',
                  rawLogPath: '/repo/logs/setup-steps/verify.log',
                },
                projectRoot,
              );
          
              expect(prompt).toContain('Failed step: verify');
              expect(prompt).toContain('Error: first-chat ping timed out');
              expect(prompt).toContain('Hint: check the container logs');
              expect(prompt).toContain('README.md'); // BIG_PICTURE_FILES
              expect(prompt).toContain('setup/verify.ts'); // STEP_FILES['verify']
              expect(prompt).toContain('logs/setup.log');
              expect(prompt).toContain('logs/setup-steps/verify.log'); // relativized rawLogPath
            });
          
            it('falls back to the step-log directory when no raw log path is given', () => {
              const prompt = buildCodexFailurePrompt({ stepName: 'verify', msg: 'boom' }, '/repo');
              expect(prompt).toContain('logs/setup-steps/');
              expect(prompt).not.toContain('Hint:');
            });
          });
          
          // Session-isolation invariant: the ChatGPT session vaulted for the gateway
          // must never be the user's personal ~/.codex session — sharing one OAuth
          // session across two consumers gets the whole family invalidated server-side
          // when refresh tokens rotate (see the header of codex.ts).
          describe('runCodexLoginAuth', () => {
            it('logs in under an isolated CODEX_HOME, vaults from it, and deletes it', async () => {
              mockSpawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
              mockExecFileSync.mockReturnValue('');
          
              let loginEnv: NodeJS.ProcessEnv | undefined;
              mockSpawn.mockImplementation((...args: unknown[]) => {
                const opts = args[2] as { env?: NodeJS.ProcessEnv };
                loginEnv = opts.env;
                fs.writeFileSync(path.join(opts.env!.CODEX_HOME!, 'auth.json'), '{"tokens":{}}');
                const child = new EventEmitter();
                setImmediate(() => child.emit('close', 0));
                return child;
              });
          
              const save = vi.fn(async (_provider, credential) => {
                expect(fs.existsSync(credential.file)).toBe(true);
              });
              await runCodexLoginAuth('browser', { has: async () => false, save });
          
              // The login spawn ran under a CODEX_HOME that is not the personal one.
              const codexHome = loginEnv?.CODEX_HOME;
              expect(codexHome).toBeDefined();
              expect(codexHome).not.toBe(path.join(os.homedir(), '.codex'));
          
              // The vault snapshot was read from the isolated dir, not ~/.codex.
              expect(save).toHaveBeenCalledWith('codex', { kind: 'oauth', file: path.join(codexHome!, 'auth.json') });
          
              // The isolated dir holds a live credential — gone once vaulted.
              expect(fs.existsSync(codexHome!)).toBe(false);
            });
          });
          
          // A gateway store failure carries the adapter's own message beside the bare
          // `gateway_store_failed` code, on both save paths, so the operator and
          // logs/setup.log see the cause.
          describe('gateway store failures name their cause', () => {
            const failure = new Error('Provider codex does not declare its subscription endpoint');
          
            function stopOnExit(): { exit: ReturnType<typeof vi.spyOn>; log: ReturnType<typeof vi.spyOn> } {
              const exit = vi.spyOn(process, 'exit').mockImplementation((() => {
                throw new Error('setup stopped');
              }) as typeof process.exit);
              const log = vi.spyOn(console, 'log').mockImplementation(() => {});
              return { exit, log };
            }
          
            afterEach(() => {
              vi.restoreAllMocks();
              vi.mocked(setupLog.step).mockClear();
            });
          
            it('after a ChatGPT login, logs the adapter message and prints it under the friendly line', async () => {
              mockSpawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
              mockSpawn.mockImplementation((...args: unknown[]) => {
                const opts = args[2] as { env?: NodeJS.ProcessEnv };
                fs.writeFileSync(path.join(opts.env!.CODEX_HOME!, 'auth.json'), '{"tokens":{}}');
                const child = new EventEmitter();
                setImmediate(() => child.emit('close', 0));
                return child;
              });
              const { exit, log } = stopOnExit();
          
              const save = vi.fn(async () => {
                throw failure;
              });
              await expect(runCodexLoginAuth('device', { has: async () => false, save })).rejects.toThrow('setup stopped');
          
              expect(exit).toHaveBeenCalledWith(1);
              expect(setupLog.step).toHaveBeenCalledWith(
                'auth',
                'failed',
                expect.any(Number),
                expect.objectContaining({
                  PROVIDER: 'codex',
                  METHOD: 'device',
                  ERROR: 'gateway_store_failed',
                  MESSAGE: failure.message,
                }),
              );
              expect(log.mock.calls.some((call) => String(call[0]).includes(failure.message))).toBe(true);
            });
          
            it('after an API key paste, logs the adapter message and prints it under the friendly line', async () => {
              mockPassword.mockResolvedValue('sk-test-not-a-real-key');
              const { exit, log } = stopOnExit();
          
              const save = vi.fn(async () => {
                throw failure;
              });
              await expect(runCodexApiKeyAuth({ has: async () => false, save })).rejects.toThrow('setup stopped');
          
              expect(save).toHaveBeenCalledWith('codex', { kind: 'api-key', value: 'sk-test-not-a-real-key' });
              expect(exit).toHaveBeenCalledWith(1);
              expect(setupLog.step).toHaveBeenCalledWith(
                'auth',
                'failed',
                0,
                expect.objectContaining({
                  PROVIDER: 'codex',
                  METHOD: 'api',
                  ERROR: 'gateway_store_failed',
                  MESSAGE: failure.message,
                }),
              );
              expect(log.mock.calls.some((call) => String(call[0]).includes(failure.message))).toBe(true);
            });
          });
          
          describe('storeFailureMessage', () => {
            it('masks standard base64 tokens containing plus, slash and padding', () => {
              const token = 'Qm9vdHN0cmFwVG9r+ZW5WYWx1ZUhlcmUx/MjM0NTY3ODkw==';
              expect(storeFailureMessage(new Error(`Store rejected ${token}`))).toBe('Store rejected [redacted]');
            });
          
            it('keeps a plain adapter message intact', () => {
              expect(storeFailureMessage(new Error('Provider codex does not declare its subscription endpoint'))).toBe(
                'Provider codex does not declare its subscription endpoint',
              );
              expect(storeFailureMessage('not an Error')).toBe('not an Error');
            });
          
            it('withholds the excerpt a real JSON parse error quotes from auth.json', () => {
              let parse: unknown;
              try {
                JSON.parse('{"tokens":{"refresh_token":rt_FAKE_SECRET_VALUE}}');
              } catch (err) {
                parse = err;
              }
              const message = storeFailureMessage(parse);
              expect(message).toContain('SyntaxError');
              expect(message).not.toContain('rt_FAKE');
              expect(message).not.toContain('refresh_token');
              expect(message).not.toContain('sh_token');
            });
          
            it('masks token-shaped runs in any other message, and keeps one line', () => {
              const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0';
              const parse = new Error(`Unexpected token 's', ..."refresh_token":${token}}... is not valid JSON\nstack line`);
              const message = storeFailureMessage(parse);
              expect(message).not.toContain(token);
              expect(message).not.toContain('eyJhbGci');
              expect(message).toContain('[redacted]');
              expect(message).not.toContain('\n');
              expect(message.length).toBeLessThanOrEqual(300);
            });
          
            it('masks a token that straddles the length cut', () => {
              const key = `sk-proj-${'A'.repeat(64)}`;
              const message = storeFailureMessage(new Error(`${'x '.repeat(146)}${key}`));
              expect(message).not.toContain('sk-proj-AAAA');
              expect(message).not.toContain('AAAAAAAA');
              expect(message.length).toBeLessThanOrEqual(300);
            });
          });
          
        • codex.ts 14.9 KB
          /**
           * Codex owns its login prompts and dedicated OAuth session. The selected
           * gateway owns credential storage, injection, and refresh. No personal
           * ~/.codex/auth.json is copied: rotating one session from two consumers
           * would invalidate it. Both setup entry points use this provider hook.
           */
          import { spawn, spawnSync } from 'child_process';
          import { getCredentialStore, type ProviderCredentialStore } from '../gateways/credential-store.js';
          import fs from 'fs';
          import os from 'os';
          import path from 'path';
          
          import * as p from '@clack/prompts';
          import k from 'kleur';
          
          import { brightSelect } from '../lib/bright-select.js';
          import { type AssistContext, BIG_PICTURE_FILES, STEP_FILES } from '../lib/claude-assist.js';
          import { brandBody, note } from '../lib/theme.js';
          import * as setupLog from '../logs.js';
          import { type FailureAssistResult, registerSetupProvider } from './registry.js';
          
          // ─── auth step ───────────────────────────────────────────────────────────
          
          /**
           * The gateway adapter's own message, kept beside the friendly line so a store
           * failure names its cause in logs/setup.log and on screen. Adapters throw
           * plain messages, but a parse error quotes
           * its input (a malformed auth.json would put token text in the message), so
           * token-shaped runs are masked and the message is kept to its first line.
           */
          export function storeFailureMessage(err: unknown): string {
            // A JSON parse error quotes a short excerpt of its input (shorter than the
            // token mask below); the store may be parsing a credential file, so keep the
            // error class and drop the excerpt without naming the input.
            if (err instanceof SyntaxError) return `${err.name}: a JSON input could not be parsed (excerpt withheld)`;
            const raw = err instanceof Error ? err.message : String(err);
            // Mask before cutting: a cut could shorten a token below the mask threshold.
            return raw
              .replace(/[A-Za-z0-9_+/=-]{24,}/g, '[redacted]')
              .split('\n')[0]
              .slice(0, 300);
          }
          
          function ensureAnswer<T>(value: T | symbol): T {
            if (p.isCancel(value)) {
              p.cancel('Setup cancelled.');
              process.exit(1);
            }
            return value as T;
          }
          
          export async function runCodexAuthStep(): Promise<void> {
            const store = await getCredentialStore();
            if (await store.has('codex')) {
              p.log.success(brandBody('Your OpenAI account is already connected.'));
              setupLog.step('auth', 'skipped', 0, { REASON: 'openai-secret-already-present', PROVIDER: 'codex' });
              return;
            }
          
            const method = ensureAnswer(
              await brightSelect<'browser' | 'device' | 'api' | 'skip'>({
                message: 'How would you like to connect Codex?',
                options: [
                  {
                    value: 'browser',
                    label: 'Sign in with my ChatGPT subscription',
                    hint: 'recommended if you have Plus or Pro — opens a browser',
                  },
                  {
                    value: 'device',
                    label: 'ChatGPT device pairing',
                    hint: 'no browser handoff — shows a URL and a code',
                  },
                  {
                    value: 'api',
                    label: 'Paste an OpenAI API key',
                    hint: 'pay-per-use; stored in your gateway, never copied into the container',
                  },
                  {
                    value: 'skip',
                    label: "Skip — I'll connect later",
                    hint: 'Codex groups will start, but model calls will fail auth',
                  },
                ],
              }),
            );
            setupLog.userInput('codex_auth_method', method);
          
            if (method === 'skip') {
              const confirmed = ensureAnswer(
                await p.confirm({
                  message: "Skip Codex sign-in? Codex won't be able to answer until you connect an OpenAI account.",
                  initialValue: false,
                }),
              );
              if (!confirmed) return runCodexAuthStep();
              setupLog.step('auth', 'skipped', 0, { REASON: 'user-skipped', PROVIDER: 'codex' });
              p.log.warn(brandBody('Codex sign-in skipped. Add an OpenAI account to your gateway before using Codex groups.'));
              return;
            }
          
            if (method === 'api') {
              await runCodexApiKeyAuth(store);
              return;
            }
          
            await runCodexLoginAuth(method, store);
          }
          
          export async function runCodexApiKeyAuth(store: ProviderCredentialStore): Promise<void> {
            const key = ensureAnswer(
              await p.password({
                message: 'Paste your OpenAI API key (sk-…)',
                validate: (v) => (v && v.trim().startsWith('sk-') ? undefined : 'That does not look like an OpenAI API key.'),
              }),
            ) as string;
          
            try {
              await store.save('codex', { kind: 'api-key', value: key.trim() });
            } catch (err) {
              const message = storeFailureMessage(err);
              setupLog.step('auth', 'failed', 0, {
                PROVIDER: 'codex',
                METHOD: 'api',
                ERROR: 'gateway_store_failed',
                MESSAGE: message,
              });
              p.log.error(brandBody("Couldn't save your OpenAI key to the vault. Check the selected gateway, then retry."));
              console.log(k.dim(`   ${message}`));
              process.exit(1);
            }
            setupLog.step('auth', 'success', 0, { PROVIDER: 'codex', METHOD: 'api' });
            p.log.success(brandBody('OpenAI account connected.'));
          }
          
          export async function runCodexLoginAuth(method: 'browser' | 'device', store?: ProviderCredentialStore): Promise<void> {
            store ??= await getCredentialStore();
            const codexCheck = spawnSync('codex', ['--version'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
            if (codexCheck.status !== 0) {
              p.log.error(
                brandBody(
                  'The Codex CLI is not installed on this machine. Install it with `npm install -g @openai/codex`, then re-run setup — or choose the API key option instead.',
                ),
              );
              setupLog.step('auth', 'failed', 0, { PROVIDER: 'codex', METHOD: method, ERROR: 'codex_cli_missing' });
              process.exit(1);
            }
          
            if (method === 'browser') {
              p.log.step(brandBody('Opening the Codex sign-in flow…'));
              console.log(k.dim('   (a browser will open for sign-in; this part is interactive)'));
            } else {
              p.log.step(brandBody('Starting Codex device-code pairing…'));
              console.log(k.dim('   (a URL and code will appear below — open the URL and enter the code)'));
            }
            console.log();
          
            // Session-isolation invariant: the login runs under a
            // throwaway CODEX_HOME so the vaulted session is dedicated to the gateway
            // and never shared with the user's personal ~/.codex.
            const loginHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-vault-login-'));
            // Holds a live credential after login — must go on every exit path. The
            // failure branches call process.exit, which skips finally blocks, so each
            // removes it explicitly.
            const removeLoginHome = (): void => fs.rmSync(loginHome, { recursive: true, force: true });
          
            const args = method === 'device' ? ['login', '--device-auth'] : ['login'];
            const start = Date.now();
            const code = await runInherit('codex', args, { CODEX_HOME: loginHome });
            const durationMs = Date.now() - start;
            console.log();
          
            if (code !== 0) {
              removeLoginHome();
              setupLog.step('auth', 'failed', durationMs, { PROVIDER: 'codex', METHOD: method, EXIT_CODE: String(code) });
              p.log.error(
                brandBody(
                  "Couldn't complete the Codex sign-in. Re-run setup and try again, or choose the API key option instead.",
                ),
              );
              process.exit(1);
            }
          
            const authJsonPath = path.join(loginHome, 'auth.json');
            if (!fs.existsSync(authJsonPath)) {
              removeLoginHome();
              setupLog.step('auth', 'failed', durationMs, { PROVIDER: 'codex', METHOD: method, ERROR: 'auth_json_not_found' });
              p.log.error(
                brandBody('Codex login succeeded but no auth.json was written. Try again, or paste an API key instead.'),
              );
              process.exit(1);
            }
          
            try {
              await store.save('codex', { kind: 'oauth', file: authJsonPath });
            } catch (err) {
              removeLoginHome();
              const message = storeFailureMessage(err);
              setupLog.step('auth', 'failed', durationMs, {
                PROVIDER: 'codex',
                METHOD: method,
                ERROR: 'gateway_store_failed',
                MESSAGE: message,
              });
              p.log.error(
                brandBody("Couldn't save your Codex credentials to the vault. Check the selected gateway, then retry."),
              );
              console.log(k.dim(`   ${message}`));
              process.exit(1);
            }
            removeLoginHome();
            setupLog.step('auth', 'success', durationMs, { PROVIDER: 'codex', METHOD: method });
            p.log.success(
              brandBody('OpenAI account connected — credentials live in your selected gateway, never in the container.'),
            );
          }
          
          function runInherit(cmd: string, args: string[], extraEnv?: Record<string, string>): Promise<number> {
            return new Promise((resolve) => {
              const child = spawn(cmd, args, {
                stdio: 'inherit',
                env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
              });
              child.on('close', (code) => resolve(code ?? 1));
              child.on('error', () => resolve(1));
            });
          }
          
          // ─── failure assist ──────────────────────────────────────────────────────
          
          /**
           * The Codex CLI can debug a setup failure only if the binary runs AND
           * ~/.codex/auth.json exists (API-key-only installs keep the key in the
           * OneCLI vault, so the host-side CLI has nothing to authenticate with).
           */
          export function isCodexCliUsable(): boolean {
            const codexCheck = spawnSync('codex', ['--version'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
            if (codexCheck.status !== 0) return false;
            return fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'));
          }
          
          /**
           * Failure prompt handed to the interactive Codex session — same content as
           * the dispatcher's Claude system prompt: what failed, the job ("diagnose and
           * fix, be concise, exit when done"), and a de-duped file reference list.
           */
          export function buildCodexFailurePrompt(ctx: AssistContext, projectRoot: string): string {
            const stepRefs = STEP_FILES[ctx.stepName] ?? [];
            const references = [
              ...BIG_PICTURE_FILES,
              ...stepRefs,
              'logs/setup.log',
              ctx.rawLogPath ? path.relative(projectRoot, ctx.rawLogPath) : 'logs/setup-steps/',
            ].filter((v, i, a) => a.indexOf(v) === i);
          
            const lines: string[] = [
              "The user is running NanoClaw's interactive setup flow and hit a failure.",
              '',
              `Failed step: ${ctx.stepName}`,
              `Error: ${ctx.msg}`,
            ];
          
            if (ctx.hint) lines.push(`Hint: ${ctx.hint}`);
          
            lines.push(
              '',
              'Your job: help them diagnose and fix this issue. Read the referenced files',
              'and logs to understand what went wrong, then help them fix it. You can read',
              'files, run commands, check logs, and explain what happened. Be concise.',
              "When they're ready to resume setup, tell them to exit Codex.",
              '',
              'Relevant files (read as needed):',
            );
            for (const f of references) lines.push(`  - ${f}`);
          
            return lines.join('\n');
          }
          
          /**
           * Registry hook: offer to debug a setup failure with the Codex CLI. Returns
           * 'unavailable' when the CLI can't run here so the dispatcher can fall back
           * to its guarded Claude offer.
           */
          export async function offerCodexFailureAssist(ctx: AssistContext, projectRoot: string): Promise<FailureAssistResult> {
            if (!isCodexCliUsable()) return 'unavailable';
          
            const want = ensureAnswer(
              await p.confirm({
                message: 'Want to debug this with Codex?',
                initialValue: true,
              }),
            );
            if (!want) return 'declined';
          
            const prompt = buildCodexFailurePrompt(ctx, projectRoot);
          
            note(
              [
                'Launching Codex to help debug this failure.',
                'It has the context of what went wrong.',
                '',
                k.dim("Exit Codex (Ctrl-C or /quit) when you're ready to come back to setup."),
              ].join('\n'),
              'Handing off to Codex',
            );
          
            return new Promise<FailureAssistResult>((resolve) => {
              // codex accepts a positional initial prompt for the interactive TUI.
              const child = spawn('codex', [prompt], { cwd: projectRoot, stdio: 'inherit' });
              child.on('close', () => {
                p.log.success(brandBody("Back from Codex. Let's continue."));
                resolve('launched');
              });
              child.on('error', () => {
                p.log.error("Couldn't launch Codex.");
                resolve('unavailable');
              });
            });
          }
          
          // ─── install verification ────────────────────────────────────────────────
          
          /**
           * Verify the codex provider payload is fully wired — the same pre-flight the
           * /add-codex skill checks. While codex ships in trunk these always pass; once
           * the payload moves to the providers branch, a failed check means the install
           * step should run (or the user finishes via /add-codex).
           */
          export function verifyCodexInstall(root = process.cwd()): { ok: boolean; problems: string[] } {
            const problems: string[] = [];
          
            const requiredFiles = [
              'src/providers/codex.ts',
              'src/providers/codex-agents-md.ts',
              'container/agent-runner/src/providers/codex.ts',
              'container/agent-runner/src/providers/codex-app-server.ts',
            ];
            for (const file of requiredFiles) {
              if (!fs.existsSync(path.join(root, file))) problems.push(`missing file: ${file}`);
            }
          
            for (const barrel of ['src/providers/index.ts', 'container/agent-runner/src/providers/index.ts']) {
              const barrelPath = path.join(root, barrel);
              if (!fs.existsSync(barrelPath) || !fs.readFileSync(barrelPath, 'utf-8').includes("import './codex.js';")) {
                problems.push(`missing barrel import in ${barrel}`);
              }
            }
          
            const manifestPath = path.join(root, 'container', 'cli-tools.json');
            let hasCodexCli = false;
            if (fs.existsSync(manifestPath)) {
              try {
                const tools = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as Array<{ name?: string }>;
                hasCodexCli = Array.isArray(tools) && tools.some((t) => t.name === '@openai/codex');
              } catch {
                hasCodexCli = false;
              }
            }
            if (!hasCodexCli) {
              problems.push('container/cli-tools.json missing the @openai/codex CLI entry');
            }
          
            return { ok: problems.length === 0, problems };
          }
          
          export async function runCodexInstallCheck(root = process.cwd()): Promise<void> {
            p.log.step(brandBody('Checking the Codex provider install…'));
            const { ok, problems } = verifyCodexInstall(root);
            if (ok) {
              setupLog.step('codex-install', 'success', 0, {});
              p.log.success(brandBody('Codex installed properly.'));
              return;
            }
          
            setupLog.step('codex-install', 'failed', 0, { PROBLEMS: problems.join('; ') });
            p.log.warn(brandBody('The Codex provider is not fully installed:'));
            for (const problem of problems) console.log(k.dim(`   • ${problem}`));
            p.log.warn(
              brandBody(
                'Finish it with your coding agent of choice: open Codex CLI or Claude Code in this repo and run the /add-codex skill.',
              ),
            );
            throw new Error(`Codex provider is not fully installed: ${problems.join('; ')}`);
          }
          
          // Self-registration: the setup picker and the standalone `provider-auth` step
          // render from the registry — this call is codex's only reach-in to the setup
          // flow (guarded by the barrel-driven registration test).
          registerSetupProvider({
            value: 'codex',
            label: 'Codex',
            hint: 'OpenAI — ChatGPT subscription or API key',
            runAuth: runCodexAuthStep,
            runInstallCheck: runCodexInstallCheck,
            offerFailureAssist: offerCodexFailureAssist,
          });
          
    • src
      • provider-contracts
        • codex.ts 2.3 KB
          import { CODEX_PROJECT_DOC_MAX_BYTES } from '../providers/codex-agents-md.js';
          
          import { registerProviderHostContract } from './registry.js';
          
          const HOST_SEAM_VERSION = 1;
          
          registerProviderHostContract('codex', {
            modelEndpoints: {
              api: 'https://api.openai.com',
              subscription: 'https://chatgpt.com',
              token: 'https://auth.openai.com/oauth/token',
            },
            modelDomains: ['openai.com', 'chatgpt.com'],
            seamVersion: HOST_SEAM_VERSION,
            projectDocument: {
              fileName: 'AGENTS.md',
              maxBytes: CODEX_PROJECT_DOC_MAX_BYTES,
              containerPath: '/workspace/agent/AGENTS.md',
              mountClass: 'allowlisted-extra',
              // Instruction prose is core-owned canon; Codex declares only the facts
              // rendered into it.
              instructions: {
                nativeOverrideFiles: ['AGENTS.local.md', 'AGENTS.override.md'],
                nativeSkills: {
                  discoveryPath: '/workspace/agent/.agents/skills',
                  sharedSource: '/app/skills',
                  selfAuthoredHome: '~/.codex/skills',
                  persistentRoots: ['~/.codex', '~/.agents'],
                  ruleBearingInlined: true,
                },
              },
            },
            stateVolumes: [
              {
                id: 'codex-home',
                directory: '.codex-shared',
                containerPath: '/home/node/.codex',
                scope: 'group',
                mode: 'rw',
                mountClass: 'allowlisted-extra',
              },
            ],
            skillBackings: [
              {
                id: 'codex-skills',
                location: { kind: 'group-directory', directory: '.agents', subdirectory: '' },
                skillsSubdirectory: 'skills',
                conflictDiagnostics: 'silent',
                templateCopies: 'copy',
              },
            ],
            skillViews: [
              {
                backingId: 'codex-skills',
                containerPath: '/workspace/agent/.agents',
                mode: 'ro',
                mountClass: 'allowlisted-extra',
              },
              {
                backingId: 'codex-skills',
                containerPath: '/home/node/.agents',
                mode: 'ro',
                mountClass: 'allowlisted-extra',
              },
            ],
            files: [
              {
                id: 'codex-auth-stub',
                volumeId: 'codex-home',
                relativePath: 'auth.json',
                prepare: { operation: 'append-open-close', when: 'every-spawn' },
              },
            ],
            // Core validates `--speed` against these names; the runtime payload renders
            // only `fast` (as `service_tier = "fast"`), `standard` keeps Codex's default.
            inference: { speedTiers: ['standard', 'fast'] },
            legacyHostAdapter: 'required',
          });
          
  • REMOVE.md 3.5 KB
    # Remove the Codex agent provider
    
    Reverses every change `/add-codex` makes and returns every group to the default provider. Safe to run when partially installed — skip any step whose target is already absent.
    
    ## 1. Switch codex groups back to the default
    
    List groups still on codex and switch each one (each group's `memory/` tree stays on disk and readable; run `/migrate-memory` per group if its memory should carry back to Claude — see [docs/provider-migration.md](../../docs/provider-migration.md)):
    
    ```bash
    ncl groups list
    # for each group whose config shows provider=codex:
    ncl groups config update --id <group-id> --provider claude
    ncl groups restart --id <group-id>
    ```
    
    ## 2. Delete the barrel imports
    
    Delete (do not comment out) the `import './codex.js';` line from each of:
    
    - `src/providers/index.ts`
    - `src/provider-contracts/index.ts`
    - `container/agent-runner/src/providers/index.ts`
    - `container/agent-runner/src/provider-contracts/index.ts`
    - `setup/providers/index.ts`
    
    ## 3. Delete every copied file
    
    ```bash
    rm -f src/providers/codex.ts \
          src/providers/codex-agents-md.ts \
          src/providers/codex-registration.test.ts \
          src/providers/codex-host-contribution.test.ts \
          src/providers/codex-agents-md.test.ts \
          src/provider-contracts/codex.ts \
          container/agent-runner/src/providers/codex.ts \
          container/agent-runner/src/providers/codex-app-server.ts \
          container/agent-runner/src/providers/exchange-archive.ts \
          container/agent-runner/src/providers/exchange-archive.test.ts \
          container/agent-runner/src/providers/codex-registration.test.ts \
          container/agent-runner/src/providers/codex.factory.test.ts \
          container/agent-runner/src/providers/codex.turns.test.ts \
          container/agent-runner/src/providers/codex-app-server.test.ts \
          container/agent-runner/src/providers/codex-contract-parity.test.ts \
          container/agent-runner/src/providers/codex.conformance.test.ts \
          container/agent-runner/src/providers/codex-cli-tools.test.ts \
          container/agent-runner/src/provider-contracts/codex.ts \
          setup/providers/codex.ts \
          setup/providers/codex.test.ts \
          setup/providers/codex-registration.test.ts
    ```
    
    This skill itself (`.claude/skills/add-codex/`) stays — it ships with trunk so the provider can be re-added later.
    
    `container/AGENTS.md` stays only if another installed provider uses agent surfaces; otherwise remove it too.
    
    ## 4. Remove the CLI manifest entry
    
    Delete the `@openai/codex` entry from `container/cli-tools.json`:
    
    ```bash
    node -e '
      const fs = require("fs");
      const file = "container/cli-tools.json";
      const tools = JSON.parse(fs.readFileSync(file, "utf8")).filter((t) => t.name !== "@openai/codex");
      const fmt = (t) => "  { " + Object.entries(t).map(([k, v]) => JSON.stringify(k) + ": " + JSON.stringify(v)).join(", ") + " }";
      fs.writeFileSync(file, "[\n" + tools.map(fmt).join(",\n") + "\n]\n");
    '
    ```
    
    ## 5. Vault secret (optional)
    
    The ChatGPT/OpenAI secret in the OneCLI vault grants nothing once the provider is gone. To remove it: `onecli secrets list`, then `onecli secrets delete --id <id>` for the `chatgpt.com` / `api.openai.com` entry.
    
    ## 6. Rebuild and verify
    
    ```bash
    pnpm run build
    pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit
    ./container/build.sh
    pnpm test
    cd container/agent-runner && bun test
    ```
    
    All suites green and `ncl groups list` showing no codex groups means the removal is complete. Restart the service (`launchctl kickstart -k gui/$(id -u)/<label>` on macOS, `systemctl --user restart <unit>` on Linux).
    
  • SKILL.md 8.9 KB
    ---
    name: add-codex
    description: Use Codex (OpenAI's codex app-server) as a full agent provider — planning, tool orchestration, MCP tools, server-side history, session resume — alongside or instead of Claude. ChatGPT subscription or OpenAI API key, vault-only via the selected gateway. Per-group via `ncl groups config update --provider codex`. Distinct from using OpenAI as an MCP tool (where Claude remains the planner).
    metadata:
      nanoclaw-provider: codex
      nanoclaw-provider-label: Codex
      nanoclaw-provider-hint: OpenAI — ChatGPT subscription or API key
      nanoclaw-provider-offered: 'true'
      nanoclaw-provider-image: local-required
    ---
    
    # Codex agent provider
    
    > Shortcut: `pnpm exec tsx setup/index.ts --step provider-auth codex` performs this whole install (manifest-driven from the providers branch: files, barrels, CLI manifest entry, image rebuild) plus auth in one command. The steps below are the same operations, for agent-driven or manual application.
    
    NanoClaw selects each group's agent backend from `container_configs.provider` (default `claude`). This skill installs the Codex provider: copy the payload from the `providers` branch, append one import to each of the three provider barrels, add the pinned Codex CLI to the container manifest (`container/cli-tools.json`), rebuild, then run the vault auth walk-through.
    
    The provider runs `codex app-server` as a child process speaking JSON-RPC over stdio: native streaming, MCP tools, server-side conversation history (the continuation is a thread id, no on-disk transcript). Credentials are **vault-only**: The selected gateway serves a sentinel `auth.json` stub into the container and swaps the real ChatGPT token or API key on the wire — no key in `.env`, nothing readable in the container.
    
    The mechanical steps under **Install** carry `nc:` directive fences: an agent reads the prose and applies them, and a parser can apply them deterministically from the same document. Every directive is idempotent, so the whole skill is safe to re-run; anything a parser can't apply falls back to the prose beside it.
    
    ## Install
    
    ### Pre-flight
    
    Requires `src/project-doc-compose.ts` on trunk. If it is missing, stop and tell
    the operator to run `/update-nanoclaw` first.
    
    Check whether the payload is already wired (a prior apply, or a trunk that still carries it). All of these present means installed — skip to **Authenticate**:
    
    - `src/providers/codex.ts` and `src/providers/codex-agents-md.ts`
    - `container/agent-runner/src/providers/codex.ts` and `codex-app-server.ts`
    - `setup/providers/codex.ts` and both `provider-contracts/codex.ts` declarations (host and container)
    - `import './codex.js';` in the three provider barrels and both contract barrels
    - an `@openai/codex` entry in `container/cli-tools.json`
    
    ### 1. Fetch and copy the payload
    
    Fetch the `providers` branch and copy the Codex payload into all three trees (additive — overwrite each file, never merge the branch). The host files are the provider contribution + the AGENTS.md spec (composition itself lives in trunk's `src/project-doc-compose.ts`) + their guards; the container files are the provider runtime (turn loop, JSON-RPC wrapper, native memory SessionStart hook, per-exchange archiver) + their guards; the setup file is the picker entry + vault auth walk-through; `container/AGENTS.md` is the runtime-contract base the composed AGENTS.md embeds.
    
    ```nc:copy from-branch:providers
    src/providers/codex.ts
    src/providers/codex-agents-md.ts
    src/providers/codex-registration.test.ts
    src/providers/codex-host-contribution.test.ts
    src/providers/codex-agents-md.test.ts
    container/agent-runner/src/providers/codex.ts
    container/agent-runner/src/providers/codex-app-server.ts
    container/agent-runner/src/providers/exchange-archive.ts
    container/agent-runner/src/providers/exchange-archive.test.ts
    container/agent-runner/src/providers/codex-registration.test.ts
    container/agent-runner/src/providers/codex.factory.test.ts
    container/agent-runner/src/providers/codex.turns.test.ts
    container/agent-runner/src/providers/codex-app-server.test.ts
    container/agent-runner/src/providers/codex-contract-parity.test.ts
    container/agent-runner/src/providers/codex.conformance.test.ts
    container/agent-runner/src/providers/codex-cli-tools.test.ts
    container/agent-runner/src/provider-contracts/codex.ts
    setup/providers/codex-registration.test.ts
    container/AGENTS.md
    ```
    
    ### Use the selected gateway for authentication
    
    Install the bundled Codex authentication hook alongside the registry payload. This
    keeps the same login choices while delegating custody to the selected gateway,
    and preserves the hook when a provider refresh copies registry files again.
    These two files are omitted from the registry copy so refresh stays idempotent.
    The setup screens and step sequence do not change.
    
    ```nc:copy
    payload/src/provider-contracts/codex.ts -> src/provider-contracts/codex.ts
    payload/setup/providers/codex.ts -> setup/providers/codex.ts
    payload/setup/providers/codex.test.ts -> setup/providers/codex.test.ts
    ```
    
    ### 2. Wire the barrels
    
    Append the self-registration import to each provider and contract barrel (skipped if already present).
    
    ```nc:append to:src/providers/index.ts
    import './codex.js';
    ```
    
    ```nc:append to:src/provider-contracts/index.ts
    import './codex.js';
    ```
    
    ```nc:append to:container/agent-runner/src/provider-contracts/index.ts
    import './codex.js';
    ```
    
    ```nc:append to:container/agent-runner/src/providers/index.ts
    import './codex.js';
    ```
    
    ```nc:append to:setup/providers/index.ts
    import './codex.js';
    ```
    
    ### 3. CLI manifest
    
    The agent's global Node CLIs install from `container/cli-tools.json` (a json-merge seam), not hand-edited Dockerfile layers. Add Codex by appending one entry — idempotent on `name`, so a re-run is a no-op. `@openai/codex` has no native postinstall, so no `onlyBuilt`. The Dockerfile already installs every manifest entry via pinned `pnpm install -g`; no Dockerfile edit is needed.
    
    ```nc:json-merge into:container/cli-tools.json key:name
    { "name": "@openai/codex", "version": "0.155.1" }
    ```
    
    The version (`0.155.1`) is the canonical pin — this SKILL.md is the source of truth.
    
    ### 4. Build
    
    ```nc:run effect:build
    pnpm run build
    pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit
    ./container/build.sh
    ```
    
    ### 5. Validate
    
    ```nc:run effect:test
    pnpm exec tsx scripts/provider-contract-verifier.ts --required-declared codex
    ```
    
    The registration tests import only the real barrels — they go red if a barrel line is missing, a barrel fails to evaluate, or the payload is broken.
    
    ## Authenticate
    
    ```nc:run effect:external
    pnpm exec tsx setup/index.ts --step provider-auth codex
    ```
    
    The same walk-through fresh installs get from the setup picker: ChatGPT subscription (browser login or device pairing) or an OpenAI API key, landed in the selected gateway’s vault. Idempotent — it short-circuits when a matching secret already exists. It finishes with the install check.
    
    ## Use it
    
    Per group:
    
    ```bash
    ncl groups config update --id <group-id> --provider codex
    ncl groups restart --id <group-id>
    ```
    
    Switching is an operator action — run it from the host. Every provider uses the
    same `memory/` tree, so memory carries across automatically. Run
    `/migrate-memory` only when upgrading a group that still has legacy `.seed.md`,
    `CLAUDE.local.md`, or unindexed imported memory. See
    [docs/provider-migration.md](../../docs/provider-migration.md).
    
    ### Default new groups to codex (optional)
    
    New groups are created on the **instance default** (`DEFAULT_AGENT_PROVIDER` in `.env`, or `claude` when unset). Installing this skill wires codex in but does NOT change that default — "installed" is not "authenticated", so the default stays claude until you opt in explicitly.
    
    After install, ask the operator before flipping it:
    
    > "Codex is installed. Default new agent groups to codex? Existing groups keep their current provider."
    
    On yes — set it, then restart the host so it takes effect:
    
    ```bash
    pnpm exec tsx setup/index.ts --step set-env -- --key DEFAULT_AGENT_PROVIDER --value codex
    launchctl kickstart -k gui/$(id -u)/com.nanoclaw   # macOS; Linux: systemctl --user restart nanoclaw
    ```
    
    This affects only groups created afterward. Per-group `ncl groups config update --provider` still overrides the default in either direction. Creation itself stays provider-agnostic (no `--provider` flag — provider is a DB property stamped from the instance default at creation).
    
    ## Troubleshooting
    
    - **Container dies at boot, channel silent:** `grep 'Container exited non-zero' logs/nanoclaw.error.log` — the `stderrTail` carries the reason (e.g. `Unknown provider: codex. Registered: claude` means the barrels aren't wired in the running build).
    - **In-channel `Error: spawn codex ENOENT` on every message:** the image predates the manifest entry — re-run `./container/build.sh`.
    - **Auth errors mid-conversation:** the vault secret is missing or stale — re-run `pnpm exec tsx setup/index.ts --step provider-auth codex` (subscription re-login updates the vault copy).
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related