From fe8da265a5e79947d9926e8ec4c4d1308931f780 Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:09:48 -0300 Subject: [PATCH 1/2] docs(spec): what codex app-server does in the real tool (30.4) --- docs/spec.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/spec.md b/docs/spec.md index bad67687..ba2a4398 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -2700,4 +2700,10 @@ Um agente só roda bots quando o Botloft impõe o que a seção 7.5 impõe no Cl | `agy models` | Lista `gemini-3.8-flash-{high,medium,low}`, `gemini-3.7-...`, `gemini-3.1-pro-{high,low}` e `claude-opus-5-5-{low,medium,high}`, entre outros: o nível de esforço faz parte do id do modelo | feito | | `codex` 0.148.0-alpha.9: `exec --json` | Um turno por processo: `thread.started`, `turn.started`, `item.completed` (`agent_message`), `turn.completed` com `usage` (`input_tokens`, `cached_input_tokens`, `output_tokens`, `reasoning_output_tokens`). Não serve para um bot que fica ligado | feito: um turno, `read-only` | | `codex app-server` | Experimental, JSON-RPC. `app-server generate-json-schema` gera o esquema, que traz pedidos de aprovação (`CommandExecutionRequestApprovalParams`, `FileChangeRequestApprovalParams`, `PermissionsRequestApprovalParams`, `McpServerElicitationRequestParams`) e `DynamicToolCallParams` | esquema gerado e lido; conversa **não vista** (A3) | -| `codex`: pasta, sandbox e MCP | `-C`, `--add-dir`, `-s read-only\|workspace-write\|danger-full-access`, `codex mcp`, `-c chave=valor`, `AGENTS.md` | só o `--help` lido | +| `codex` 0.148.0-alpha.9: `app-server` por stdio | **Testado**: `initialize` (`clientInfo`) e a notificação `initialized`; `thread/start { cwd, approvalPolicy, sandbox, config, model }` devolve `thread.id`, `model`, `sandbox`, `approvalPolicy`; `turn/start { threadId, input: [{type:"text",text}, {type:"localImage",path}] }`. Notificações: `turn/started`, `item/started` e `item/completed` (`userMessage`, `reasoning`, `agentMessage` com `text` e `phase`, `commandExecution` com `command`, `cwd` e `status`, `fileChange` com `changes[{path,kind,diff}]`, `mcpToolCall` com `server`, `tool`, `arguments`, `result`), `item/agentMessage/delta` (texto ao vivo), `turn/completed` (`status`, `durationMs`, `error`), `thread/tokenUsage/updated` (`total` e `last` com `inputTokens`, `cachedInputTokens`, `outputTokens`, `reasoningOutputTokens` e **`modelContextWindow`**: 258400 com `gpt-5.5`) e `account/rateLimits/updated` (`usedPercent`, `windowDurationMins`, `resetsAt`, `planType`) | feito: uma conversa de vários itens | +| `codex`: aprovações | Com `sandbox: "read-only"` e `approvalPolicy: "on-request"`, uma alteração de arquivo chega como pedido do servidor `item/fileChange/requestApproval` (`itemId`; o caminho está no `item/started` do mesmo `itemId`); responder `{decision:"accept"}` aplica e `"decline"` recusa. `item/commandExecution/requestApproval` traz `command`, `cwd`, `reason`. Comandos só de leitura (`echo hello`) rodaram sem perguntar. Uma chamada a uma ferramenta MCP chega como `mcpServer/elicitation/request` com `_meta.codex_approval_kind = "mcp_tool_call"`; responder `{action:"accept", content:null}` deixa a chamada seguir | feito | +| `codex`: sandbox do Windows | `sandbox: "workspace-write"` **travou** o comando de escrever um arquivo (`commandExecution` ficou `inProgress` por mais de 3 minutos, sem pedido de aprovação), mesmo com `windowsSandbox/readiness` em `ready` e depois de `windowsSandbox/setupStart`. `codex sandbox -- cmd.exe /c echo hi` funciona fora do app-server. `danger-full-access` roda sem perguntar. Por isso o Botloft usa `read-only` com aprovações | feito: neste computador; **não** visto fora do ambiente de teste | +| `codex`: MCP por conversa | `thread/start { config: { mcp_servers: { botloft: { url, http_headers: { Authorization: "Bearer " } } } } } }`: o servidor viu `initialize`, `tools/list` e `tools/call` com o cabeçalho; a ferramenta voltou na resposta. O MCP do dono fica de fora com `-c mcp_servers..enabled=false` e `-c features.apps=false` (o `mcp_servers={}` **não** remove) | feito | +| `codex`: instruções e imagem | `AGENTS.md` na pasta de trabalho foi lido (o bot disse o codinome); `localImage` deixou o bot ver uma imagem (respondeu "Blue") | feito | +| `codex`: configuração pessoal | O app-server carrega `~/.codex/config.toml` do dono: o MCP `node_repl`, os plugins e a configuração de `notify`. `-c features.apps=false -c features.hooks=false -c features.plugins=false -c mcp_servers..enabled=false -c notify=[]` tirou todos os MCP e plugins; ficaram as skills da pasta pessoal (44 para 36). Cada pasta de trabalho nova foi **gravada em `~/.codex/config.toml`** como `[projects.'']`. `CODEX_HOME` numa pasta do bot sem o `config.toml` do dono tira tudo isso, mas leva o login junto: um `auth.json` ligado por hard link funcionou, e uma cópia arriscaria invalidar o token do dono ao renovar (não testado) | feito | +| `codex`: retomar conversa, erros de login e de limite, `turn/steer`, compactação | **Não visto** | a fazer (A3) | From 93d126d0c22aa2612e414727cfe308367d48e480 Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:44:49 -0300 Subject: [PATCH 2/2] feat(agents): Codex bots, experimental (A3a of spec 30) One codex app-server per bot, spoken to in JSON-RPC: a wrapper (CodexControl) turns Botloft's turn line into turn/start once the thread exists and no turn runs; the decoder maps item/turn/tokenUsage notifications onto the chat, the exact context window and the turn's tokens. Botloft's MCP server goes in the thread config with the bot's token; the owner's MCP servers, plugins, hooks and notifier are switched off with -c. Read-only sandbox, nothing asked, so the bot reads and answers; approvals are A3b. Agent gains attach (wrap the process). Checked with the real codex 0.148.0-alpha.9 in a dev daemon. --- CLAUDE.md | 2 +- README.md | 25 +- app/src/features/bots/BotDialog.tsx | 6 +- app/src/features/bots/agentPicker.test.tsx | 16 + app/src/features/chat/ChatComposer.tsx | 2 +- app/src/i18n/en/bots.ts | 2 + app/src/i18n/es/bots.ts | 2 + app/src/i18n/pt-BR/bots.ts | 2 + crates/botloftd/src/agent/agy.rs | 12 +- crates/botloftd/src/agent/codex.rs | 394 ++++++++++++++++++++ crates/botloftd/src/agent/codex_wire.rs | 382 +++++++++++++++++++ crates/botloftd/src/agent/mod.rs | 60 ++- crates/botloftd/src/chat/mod.rs | 6 +- crates/botloftd/src/config.rs | 3 + crates/botloftd/src/service/mod.rs | 14 +- crates/botloftd/src/supervisor/mod.rs | 23 +- crates/botloftd/src/supervisor/settings.rs | 3 + crates/botloftd/src/supervisor/spawn.rs | 14 +- crates/botloftd/src/supervisor/start.rs | 26 +- crates/botloftd/tests/codex_bot.rs | 404 +++++++++++++++++++++ crates/botloftd/tests/common/mod.rs | 1 + docs/spec.md | 16 +- 22 files changed, 1362 insertions(+), 53 deletions(-) create mode 100644 crates/botloftd/src/agent/codex.rs create mode 100644 crates/botloftd/src/agent/codex_wire.rs create mode 100644 crates/botloftd/tests/codex_bot.rs diff --git a/CLAUDE.md b/CLAUDE.md index 992a67fd..d5407a8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Botloft -Workspace desktop Windows-first, com código público sob a FSL (vira Apache 2.0 após dois anos), para rodar bots de agentes de código sempre ligados que colaboram entre si: Claude Code, Antigravity (experimental) e, em seguida, Codex. A seção 30 da spec e o ADR 0003 dizem como cada agente entra. Daemon Rust (`botloftd`) + app Tauri v2 + React. +Workspace desktop Windows-first, com código público sob a FSL (vira Apache 2.0 após dois anos), para rodar bots de agentes de código sempre ligados que colaboram entre si: Claude Code, Antigravity e Codex (os dois últimos experimentais). A seção 30 da spec e o ADR 0003 dizem como cada agente entra. Daemon Rust (`botloftd`) + app Tauri v2 + React. ## Fonte de verdade diff --git a/README.md b/README.md index f0720801..5a2e1fab 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ Botloft keeps a crew of bots running on your computer, each on the AI coding agent you choose: -[Claude Code](https://code.claude.com), or [Antigravity](https://antigravity.google) as an experimental -option, with Codex next. Each bot has its own folder, memory and conversation. They pass work to each other, keep going after you close the +[Claude Code](https://code.claude.com), or, as experimental options, +[Antigravity](https://antigravity.google) and [Codex](https://developers.openai.com/codex). Each bot has its own folder, memory and conversation. They pass work to each other, keep going after you close the window and pick up where they left off after a restart. You talk to each one like in a chat app, and you decide what they may do. @@ -88,15 +88,18 @@ itself. | Usage meter and "Compact" | yes | an estimate of the conversation size; no compaction | | Isolation between crews | yes | folders are fenced for its file tools, not for commands you allow | -Claude Code is the full experience. Antigravity is off until you add `experimental_agents = ["agy"]` -to Botloft's `config.toml`; then a new bot can be made on it, and Settings has "Agent for new -bots". Codex is the next agent. See [spec section 30](docs/spec.md) for what was checked. +Claude Code is the full experience. Antigravity and Codex are off until you list them in Botloft's +`config.toml` (`experimental_agents = ["agy"]`, `["codex"]` or both); then a new bot can be made on +them, and Settings has "Agent for new bots". A Codex bot reads files, uses the crew's tools and +answers, with the exact size of its conversation; it cannot change files or run commands yet, and +asking you before it acts is the next step. See [spec section 30](docs/spec.md) for what was +checked. ## Get started You need at least one agent installed and signed in once, with your own account: -[Claude Code](https://code.claude.com/docs/en/setup), or Antigravity (experimental, see -[Agents](#agents)). Bots run on your own plan with that agent. +[Claude Code](https://code.claude.com/docs/en/setup), Antigravity or Codex +(experimental, see [Agents](#agents)). Bots run on your own plan with that agent. 1. Download Botloft for your computer from the [latest release](https://github.com/httpsphl/botloft/releases/latest): @@ -137,7 +140,7 @@ flowchart LR scheduled task of your Windows user, a systemd user service on Linux or a launch agent on macOS: it starts when you sign in and comes back soon if it dies. - **Your agent is the runtime.** Each bot is a real session of the agent you chose (`claude -p` for - Claude Code, `agy -p` for Antigravity) with its own workspace, memory and conversation. There is + Claude Code, `agy -p` for Antigravity, `codex app-server` for Codex) with its own workspace, memory and conversation. There is no custom agent SDK and no API key to manage. - **One conversation per bot.** Your messages, messages from other bots and notices from the daemon reach the bot in order. What it does comes back as a chat: replies, tool use and requests you @@ -230,8 +233,8 @@ Windows came first and has had the most use; if something does not work elsewher
Is Botloft made by Anthropic? -No. Botloft is an independent project. It uses Claude Code and Antigravity as installed on your -computer and is not affiliated with or endorsed by Anthropic or Google. +No. Botloft is an independent project. It uses Claude Code, Antigravity and Codex as installed on your +computer and is not affiliated with or endorsed by Anthropic, Google or OpenAI.
## Status @@ -247,7 +250,7 @@ and a code-signed installer. `main` can be ahead of the latest release. Requirements: Rust stable (the MSVC toolchain on Windows), Node.js 24 and pnpm. On Linux, the app also needs the desktop libraries Tauri uses (on Ubuntu: `libwebkit2gtk-4.1-dev`, `libayatana-appindicator3-dev`, `librsvg2-dev`, `libxdo-dev`). Running real bots also needs an agent -installed and signed in: Claude Code (the native `claude` executable) or Antigravity (`agy`). +installed and signed in: Claude Code (the native `claude` executable), Antigravity (`agy`) or Codex. ```powershell # Daemon and libraries diff --git a/app/src/features/bots/BotDialog.tsx b/app/src/features/bots/BotDialog.tsx index 0bc8646d..6c9f3bdc 100644 --- a/app/src/features/bots/BotDialog.tsx +++ b/app/src/features/bots/BotDialog.tsx @@ -148,12 +148,14 @@ export function BotDialog(props: Props) { /> {agent !== "claude" && (

- {t.bots.dialog.agent.experimental} + {agent === "codex" + ? t.bots.dialog.agent.experimentalCodex + : t.bots.dialog.agent.experimental}

)} )} - {editing && editing.agent !== "claude" && ( + {editing && editing.agent === "agy" && ( <> { expect(dialog.queryByLabelText("Model")).toBeNull(); }); }); + +describe("Codex in the new-bot dialog", () => { + test("says what it cannot do yet", async () => { + const fake = new FakeBotloft(); + fake.system = { ...fake.system, enabledAgents: ["claude", "codex"] }; + fake.addCrew("Ops"); + renderApp(fake); + await crewOpened("Ops"); + const [button] = await screen.findAllByRole("button", { name: "New bot" }); + fireEvent.click(button as HTMLElement); + const dialog = within(await screen.findByRole("dialog", { name: "New bot" })); + fireEvent.change(dialog.getByLabelText("Agent"), { target: { value: "codex" } }); + expect(dialog.getByRole("note").textContent).toMatch(/cannot change files or run commands yet/); + expect(dialog.queryByLabelText("Model")).toBeNull(); + }); +}); diff --git a/app/src/features/chat/ChatComposer.tsx b/app/src/features/chat/ChatComposer.tsx index c0ca0818..d4f1580b 100644 --- a/app/src/features/chat/ChatComposer.tsx +++ b/app/src/features/chat/ChatComposer.tsx @@ -254,7 +254,7 @@ export const ChatComposer = memo(function ChatComposer({ ) : ( - + bot.agent === "agy" && )}