diff --git a/CHANGELOG.md b/CHANGELOG.md
index 021462476..ff9b73b9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
- Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes.
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
+- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
- Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`).
- Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings.
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
@@ -51,12 +52,14 @@
- Auth: when `openai` has no API key but Codex OAuth exists, suggest `openai-codex/gpt-5.2` vs `OPENAI_API_KEY`.
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
+- Sandbox: enable session tools in sandboxed sessions with spawned-only visibility by default (opt-in `agent.sandbox.sessionToolsVisibility = "all"`).
- Control UI: show a reading indicator bubble while the assistant is responding.
- Control UI: animate reading indicator dots (honors reduced-motion).
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
- Control UI: add Chat focus mode toggle to collapse header + sidebar.
+- Control UI: standardize UI build instructions on `bun run ui:*` (fallback supported).
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
- Status: show model auth source (api-key/oauth).
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
@@ -72,6 +75,8 @@
- Docs: clarify Slack manifest scopes (current vs optional) with references. Thanks @jarvis-medmatic for PR #235.
- Control UI: avoid Slack config ReferenceError by reading slack config snapshots. Thanks @sreekaransrinath for PR #249.
- Auth: rotate across multiple OAuth profiles with cooldown tracking and email-based profile IDs. Thanks @mukhtharcm for PR #269.
+- Auth: fix multi-account OAuth rotation so round-robin alternates instead of pinning to lastGood. Thanks @mukhtharcm for PR #281.
+- Configure: stop auto-writing `auth.order` for newly added auth profiles (round-robin default unless explicitly pinned).
- Telegram: honor routing.groupChat.mentionPatterns for group mention gating. Thanks Kevin Kern (@regenrek) for PR #242.
- Telegram: gate groups via `telegram.groups` allowlist (align with WhatsApp/iMessage). Thanks @kitze for PR #241.
- Telegram: support media groups (multi-image messages). Thanks @obviyus for PR #220.
@@ -133,7 +138,7 @@
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
-- Onboarding: when running from source, auto-build missing Control UI assets (`pnpm ui:build`).
+- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`).
- Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).
- Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off.
- Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events.
diff --git a/README.md b/README.md
index 9bbc44217..2c8dccf6f 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ It answers you on the surfaces you already use (WhatsApp, Telegram, Slack, Disco
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
-Website: [https://clawdbot.com](https://clawdbot.com) · Docs: [https://docs.clawdbot.com](https://docs.clawdbot.com/) · Showcase: [https://docs.clawdbot.com/showcase](https://docs.clawdbot.com/showcase) · FAQ: [https://docs.clawdbot.com/faq](https://docs.clawdbot.com/faq) · Wizard: [https://docs.clawdbot.com/wizard](https://docs.clawdbot.com/wizard) · Nix: [https://github.com/clawdbot/nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [https://docs.clawdbot.com/docker](https://docs.clawdbot.com/docker) · Discord: [https://discord.gg/clawd](https://discord.gg/clawd)
+[Website](https://clawdbot.com) · [Docs](https://github.com/clawdbot/clawdbot/blob/main/docs/index.md) · Showcase: [https://github.com/clawdbot/clawdbot/blob/main/docs/showcase.md](https://github.com/clawdbot/clawdbot/blob/main/docs/showcase.md) · FAQ: [https://github.com/clawdbot/clawdbot/blob/main/docs/faq.md](https://github.com/clawdbot/clawdbot/blob/main/docs/faq.md) · Wizard: [https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md) · Nix: [https://github.com/clawdbot/nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md) · Discord: [https://discord.gg/clawd](https://discord.gg/clawd)
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Windows, and Linux**.
Works with npm, pnpm, or bun.
@@ -29,7 +29,7 @@ Works with npm, pnpm, or bun.
- **Anthropic** (Claude Pro/Max)
- **OpenAI** (ChatGPT/Codex)
-Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.clawdbot.com/onboarding).
+Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://github.com/clawdbot/clawdbot/blob/main/docs/onboarding.md).
## Recommended setup (from source)
@@ -40,10 +40,10 @@ Do **not** download prebuilt binaries. Build from source.
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
-pnpm install
-pnpm build
-pnpm ui:build
-pnpm clawdbot onboard
+bun install
+bun run build
+bun run ui:build
+bun run clawdbot onboard
```
## Quick start (from source)
@@ -88,45 +88,45 @@ If you run from source, prefer `bun run clawdbot …` or `pnpm clawdbot …` (no
## Highlights
-- **[Local-first Gateway](https://docs.clawdbot.com/gateway)** — single control plane for sessions, providers, tools, and events.
-- **[Multi-surface inbox](https://docs.clawdbot.com/surface)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat, macOS, iOS/Android.
-- **[Voice Wake](https://docs.clawdbot.com/voicewake) + [Talk Mode](https://docs.clawdbot.com/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
-- **[Live Canvas](https://docs.clawdbot.com/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui).
-- **[First-class tools](https://docs.clawdbot.com/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
-- **[Companion apps](https://docs.clawdbot.com/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.clawdbot.com/nodes).
-- **[Onboarding](https://docs.clawdbot.com/wizard) + [skills](https://docs.clawdbot.com/skills)** — wizard-driven setup with bundled/managed/workspace skills.
+- **[Local-first Gateway](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)** — single control plane for sessions, providers, tools, and events.
+- **[Multi-surface inbox](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat, macOS, iOS/Android.
+- **[Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md) + [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md)** — always-on speech for macOS/iOS/Android with ElevenLabs.
+- **[Live Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md)** — agent-driven visual workspace with [A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui).
+- **[First-class tools](https://github.com/clawdbot/clawdbot/blob/main/docs/tools.md)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
+- **[Companion apps](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md)** — macOS menu bar app + iOS/Android [nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md).
+- **[Onboarding](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md) + [skills](https://github.com/clawdbot/clawdbot/blob/main/docs/skills.md)** — wizard-driven setup with bundled/managed/workspace skills.
## Everything we built so far
### Core platform
-- [Gateway WS control plane](https://docs.clawdbot.com/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.clawdbot.com/web), and [Canvas host](https://docs.clawdbot.com/refactor/canvas-a2ui).
-- [CLI surface](https://docs.clawdbot.com/agent-send): gateway, agent, send, [wizard](https://docs.clawdbot.com/wizard), and [doctor](https://docs.clawdbot.com/doctor).
-- [Pi agent runtime](https://docs.clawdbot.com/agent) in RPC mode with tool streaming and block streaming.
-- [Session model](https://docs.clawdbot.com/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawdbot.com/groups).
-- [Media pipeline](https://docs.clawdbot.com/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawdbot.com/audio).
+- [Gateway WS control plane](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md) with sessions, presence, config, cron, webhooks, [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md), and [Canvas host](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui).
+- [CLI surface](https://github.com/clawdbot/clawdbot/blob/main/docs/agent-send.md): gateway, agent, send, [wizard](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md), and [doctor](https://github.com/clawdbot/clawdbot/blob/main/docs/doctor.md).
+- [Pi agent runtime](https://github.com/clawdbot/clawdbot/blob/main/docs/agent.md) in RPC mode with tool streaming and block streaming.
+- [Session model](https://github.com/clawdbot/clawdbot/blob/main/docs/session.md): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://github.com/clawdbot/clawdbot/blob/main/docs/groups.md).
+- [Media pipeline](https://github.com/clawdbot/clawdbot/blob/main/docs/images.md): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://github.com/clawdbot/clawdbot/blob/main/docs/audio.md).
### Surfaces + providers
-- [Providers](https://docs.clawdbot.com/surface): [WhatsApp](https://docs.clawdbot.com/whatsapp) (Baileys), [Telegram](https://docs.clawdbot.com/telegram) (grammY), [Slack](https://docs.clawdbot.com/slack) (Bolt), [Discord](https://docs.clawdbot.com/discord) (discord.js), [Signal](https://docs.clawdbot.com/signal) (signal-cli), [iMessage](https://docs.clawdbot.com/imessage) (imsg), [WebChat](https://docs.clawdbot.com/webchat).
-- [Group routing](https://docs.clawdbot.com/group-messages): mention gating, reply tags, per-surface chunking and routing. Surface rules: [Surface routing](https://docs.clawdbot.com/surface).
+- [Providers](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md): [WhatsApp](https://github.com/clawdbot/clawdbot/blob/main/docs/whatsapp.md) (Baileys), [Telegram](https://github.com/clawdbot/clawdbot/blob/main/docs/telegram.md) (grammY), [Slack](https://github.com/clawdbot/clawdbot/blob/main/docs/slack.md) (Bolt), [Discord](https://github.com/clawdbot/clawdbot/blob/main/docs/discord.md) (discord.js), [Signal](https://github.com/clawdbot/clawdbot/blob/main/docs/signal.md) (signal-cli), [iMessage](https://github.com/clawdbot/clawdbot/blob/main/docs/imessage.md) (imsg), [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md).
+- [Group routing](https://github.com/clawdbot/clawdbot/blob/main/docs/group-messages.md): mention gating, reply tags, per-surface chunking and routing. Surface rules: [Surface routing](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md).
### Apps + nodes
-- [macOS app](https://docs.clawdbot.com/macos): menu bar control plane, [Voice Wake](https://docs.clawdbot.com/voicewake)/PTT, [Talk Mode](https://docs.clawdbot.com/talk) overlay, [WebChat](https://docs.clawdbot.com/webchat), debug tools, [remote gateway](https://docs.clawdbot.com/remote) control.
-- [iOS node](https://docs.clawdbot.com/ios): [Canvas](https://docs.clawdbot.com/mac/canvas), [Voice Wake](https://docs.clawdbot.com/voicewake), [Talk Mode](https://docs.clawdbot.com/talk), camera, screen recording, Bonjour pairing.
-- [Android node](https://docs.clawdbot.com/android): [Canvas](https://docs.clawdbot.com/mac/canvas), [Talk Mode](https://docs.clawdbot.com/talk), camera, screen recording, optional SMS.
-- [macOS node mode](https://docs.clawdbot.com/nodes): system.run/notify + canvas/camera exposure.
+- [macOS app](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md): menu bar control plane, [Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md)/PTT, [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md) overlay, [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md), debug tools, [remote gateway](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) control.
+- [iOS node](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md): [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md), [Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md), [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md), camera, screen recording, Bonjour pairing.
+- [Android node](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md): [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md), [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md), camera, screen recording, optional SMS.
+- [macOS node mode](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md): system.run/notify + canvas/camera exposure.
### Tools + automation
-- [Browser control](https://docs.clawdbot.com/browser): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
-- [Canvas](https://docs.clawdbot.com/mac/canvas): [A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui) push/reset, eval, snapshot.
-- [Nodes](https://docs.clawdbot.com/nodes): camera snap/clip, screen record, [location.get](https://docs.clawdbot.com/location-command), notifications.
-- [Cron + wakeups](https://docs.clawdbot.com/cron); [webhooks](https://docs.clawdbot.com/webhook); [Gmail Pub/Sub](https://docs.clawdbot.com/gmail-pubsub).
-- [Skills platform](https://docs.clawdbot.com/skills): bundled, managed, and workspace skills with install gating + UI.
+- [Browser control](https://github.com/clawdbot/clawdbot/blob/main/docs/browser.md): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
+- [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md): [A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui) push/reset, eval, snapshot.
+- [Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md): camera snap/clip, screen record, [location.get](https://github.com/clawdbot/clawdbot/blob/main/docs/location-command.md), notifications.
+- [Cron + wakeups](https://github.com/clawdbot/clawdbot/blob/main/docs/cron.md); [webhooks](https://github.com/clawdbot/clawdbot/blob/main/docs/webhook.md); [Gmail Pub/Sub](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md).
+- [Skills platform](https://github.com/clawdbot/clawdbot/blob/main/docs/skills.md): bundled, managed, and workspace skills with install gating + UI.
### Ops + packaging
-- [Control UI](https://docs.clawdbot.com/web) + [WebChat](https://docs.clawdbot.com/webchat) served directly from the Gateway.
-- [Tailscale Serve/Funnel](https://docs.clawdbot.com/tailscale) or [SSH tunnels](https://docs.clawdbot.com/remote) with token/password auth.
-- [Nix mode](https://docs.clawdbot.com/nix) for declarative config; [Docker](https://docs.clawdbot.com/docker)-based installs.
-- [Doctor](https://docs.clawdbot.com/doctor) migrations, [logging](https://docs.clawdbot.com/logging).
+- [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md) + [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md) served directly from the Gateway.
+- [Tailscale Serve/Funnel](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md) or [SSH tunnels](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) with token/password auth.
+- [Nix mode](https://github.com/clawdbot/clawdbot/blob/main/docs/nix.md) for declarative config; [Docker](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md)-based installs.
+- [Doctor](https://github.com/clawdbot/clawdbot/blob/main/docs/doctor.md) migrations, [logging](https://github.com/clawdbot/clawdbot/blob/main/docs/logging.md).
## How it works (short)
@@ -148,12 +148,12 @@ WhatsApp / Telegram / Slack / Discord / Signal / iMessage / WebChat
## Key subsystems
-- **[Gateway WebSocket network](https://docs.clawdbot.com/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.clawdbot.com/gateway)).
-- **[Tailscale exposure](https://docs.clawdbot.com/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.clawdbot.com/remote)).
-- **[Browser control](https://docs.clawdbot.com/browser)** — clawd‑managed Chrome/Chromium with CDP control.
-- **[Canvas + A2UI](https://docs.clawdbot.com/mac/canvas)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui)).
-- **[Voice Wake](https://docs.clawdbot.com/voicewake) + [Talk Mode](https://docs.clawdbot.com/talk)** — always‑on speech and continuous conversation.
-- **[Nodes](https://docs.clawdbot.com/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`.
+- **[Gateway WebSocket network](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)).
+- **[Tailscale exposure](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md)).
+- **[Browser control](https://github.com/clawdbot/clawdbot/blob/main/docs/browser.md)** — clawd‑managed Chrome/Chromium with CDP control.
+- **[Canvas + A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui)).
+- **[Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md) + [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md)** — always‑on speech and continuous conversation.
+- **[Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`.
## Tailscale access (Gateway dashboard)
@@ -169,7 +169,7 @@ Notes:
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
- Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown.
-Details: [Tailscale guide](https://docs.clawdbot.com/tailscale) · [Web surfaces](https://docs.clawdbot.com/web)
+Details: [Tailscale guide](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md) · [Web surfaces](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md)
## Remote Gateway (Linux is great)
@@ -179,7 +179,7 @@ It’s perfectly fine to run the Gateway on a small Linux instance. Clients (mac
- **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: bash runs where the Gateway lives; device actions run where the device lives.
-Details: [Remote access](https://docs.clawdbot.com/remote) · [Nodes](https://docs.clawdbot.com/nodes) · [Security](https://docs.clawdbot.com/security)
+Details: [Remote access](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) · [Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md) · [Security](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md)
## macOS permissions via the Gateway protocol
@@ -194,7 +194,7 @@ Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle per‑session elevated access when enabled + allowlisted.
- Gateway persists the per‑session toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
-Details: [Nodes](https://docs.clawdbot.com/nodes) · [macOS app](https://docs.clawdbot.com/macos) · [Gateway protocol](https://docs.clawdbot.com/architecture)
+Details: [Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md) · [macOS app](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md) · [Gateway protocol](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)
## Agent to Agent (sessions_* tools)
@@ -203,7 +203,7 @@ Details: [Nodes](https://docs.clawdbot.com/nodes) · [macOS app](https://docs.cl
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional reply‑back ping‑pong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
-Details: [Session tools](https://docs.clawdbot.com/session-tool)
+Details: [Session tools](https://github.com/clawdbot/clawdbot/blob/main/docs/session-tool.md)
## Skills registry (ClawdHub)
@@ -249,13 +249,13 @@ Note: signed builds required for macOS permissions to stick across rebuilds (see
- Voice trigger forwarding + Canvas surface.
- Controlled via `clawdbot nodes …`.
-Runbook: [iOS connect](https://docs.clawdbot.com/ios).
+Runbook: [iOS connect](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md).
### Android node (optional)
- Pairs via the same Bridge + pairing flow as iOS.
- Exposes Canvas, Camera, and Screen capture commands.
-- Runbook: [Android connect](https://docs.clawdbot.com/android).
+- Runbook: [Android connect](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md).
## Agent workspace + skills
@@ -275,7 +275,7 @@ Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
}
```
-[Full configuration reference (all keys + examples).](https://docs.clawdbot.com/configuration)
+[Full configuration reference (all keys + examples).](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
## Security model (important)
@@ -283,15 +283,15 @@ Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
- **Group/channel safety:** set `agent.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
-Details: [Security guide](https://docs.clawdbot.com/security) · [Docker + sandboxing](https://docs.clawdbot.com/docker) · [Sandbox config](https://docs.clawdbot.com/configuration)
+Details: [Security guide](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md) · [Docker + sandboxing](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md) · [Sandbox config](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
-### [WhatsApp](https://docs.clawdbot.com/whatsapp)
+### [WhatsApp](https://github.com/clawdbot/clawdbot/blob/main/docs/whatsapp.md)
- Link the device: `pnpm clawdbot login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
- If `whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
-### [Telegram](https://docs.clawdbot.com/telegram)
+### [Telegram](https://github.com/clawdbot/clawdbot/blob/main/docs/telegram.md)
- Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins).
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `telegram.allowFrom` or `telegram.webhookUrl` as needed.
@@ -304,11 +304,11 @@ Details: [Security guide](https://docs.clawdbot.com/security) · [Docker + sandb
}
```
-### [Slack](https://docs.clawdbot.com/slack)
+### [Slack](https://github.com/clawdbot/clawdbot/blob/main/docs/slack.md)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
-### [Discord](https://docs.clawdbot.com/discord)
+### [Discord](https://github.com/clawdbot/clawdbot/blob/main/docs/discord.md)
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
- Optional: set `discord.slashCommand`, `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
@@ -321,16 +321,16 @@ Details: [Security guide](https://docs.clawdbot.com/security) · [Docker + sandb
}
```
-### [Signal](https://docs.clawdbot.com/signal)
+### [Signal](https://github.com/clawdbot/clawdbot/blob/main/docs/signal.md)
- Requires `signal-cli` and a `signal` config section.
-### [iMessage](https://docs.clawdbot.com/imessage)
+### [iMessage](https://github.com/clawdbot/clawdbot/blob/main/docs/imessage.md)
- macOS only; Messages must be signed in.
- If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
-### [WebChat](https://docs.clawdbot.com/webchat)
+### [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md)
- Uses the Gateway WebSocket; no separate WebChat port/config.
@@ -349,69 +349,69 @@ Browser control (optional):
## Docs
Use these when you’re past the onboarding flow and want the deeper reference.
-- [Start with the docs index for navigation and “what’s where.”](https://docs.clawdbot.com/)
-- [Read the architecture overview for the gateway + protocol model.](https://docs.clawdbot.com/architecture)
-- [Use the full configuration reference when you need every key and example.](https://docs.clawdbot.com/configuration)
-- [Run the Gateway by the book with the operational runbook.](https://docs.clawdbot.com/gateway)
-- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.clawdbot.com/web)
-- [Understand remote access over SSH tunnels or tailnets.](https://docs.clawdbot.com/remote)
-- [Follow the onboarding wizard flow for a guided setup.](https://docs.clawdbot.com/wizard)
-- [Wire external triggers via the webhook surface.](https://docs.clawdbot.com/webhook)
-- [Set up Gmail Pub/Sub triggers.](https://docs.clawdbot.com/gmail-pubsub)
-- [Learn the macOS menu bar companion details.](https://docs.clawdbot.com/mac/menu-bar)
-- [Platform guides: Windows](https://docs.clawdbot.com/windows), [Linux](https://docs.clawdbot.com/linux), [macOS](https://docs.clawdbot.com/macos), [iOS](https://docs.clawdbot.com/ios), [Android](https://docs.clawdbot.com/android)
-- [Debug common failures with the troubleshooting guide.](https://docs.clawdbot.com/troubleshooting)
-- [Review security guidance before exposing anything.](https://docs.clawdbot.com/security)
+- [Start with the docs index for navigation and “what’s where.”](https://github.com/clawdbot/clawdbot/blob/main/docs/index.md)
+- [Read the architecture overview for the gateway + protocol model.](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)
+- [Use the full configuration reference when you need every key and example.](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
+- [Run the Gateway by the book with the operational runbook.](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)
+- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md)
+- [Understand remote access over SSH tunnels or tailnets.](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md)
+- [Follow the onboarding wizard flow for a guided setup.](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md)
+- [Wire external triggers via the webhook surface.](https://github.com/clawdbot/clawdbot/blob/main/docs/webhook.md)
+- [Set up Gmail Pub/Sub triggers.](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md)
+- [Learn the macOS menu bar companion details.](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/menu-bar.md)
+- [Platform guides: Windows](https://github.com/clawdbot/clawdbot/blob/main/docs/windows.md), [Linux](https://github.com/clawdbot/clawdbot/blob/main/docs/linux.md), [macOS](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md), [iOS](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md), [Android](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md)
+- [Debug common failures with the troubleshooting guide.](https://github.com/clawdbot/clawdbot/blob/main/docs/troubleshooting.md)
+- [Review security guidance before exposing anything.](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md)
## Advanced docs (discovery + control)
-- [Discovery + transports](https://docs.clawdbot.com/discovery)
-- [Bonjour/mDNS](https://docs.clawdbot.com/bonjour)
-- [Gateway pairing](https://docs.clawdbot.com/gateway/pairing)
-- [Remote gateway README](https://docs.clawdbot.com/remote-gateway-readme)
-- [Control UI](https://docs.clawdbot.com/control-ui)
-- [Dashboard](https://docs.clawdbot.com/dashboard)
+- [Discovery + transports](https://github.com/clawdbot/clawdbot/blob/main/docs/discovery.md)
+- [Bonjour/mDNS](https://github.com/clawdbot/clawdbot/blob/main/docs/bonjour.md)
+- [Gateway pairing](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway/pairing.md)
+- [Remote gateway README](https://github.com/clawdbot/clawdbot/blob/main/docs/remote-gateway-readme.md)
+- [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/control-ui.md)
+- [Dashboard](https://github.com/clawdbot/clawdbot/blob/main/docs/dashboard.md)
## Operations & troubleshooting
-- [Health checks](https://docs.clawdbot.com/health)
-- [Gateway lock](https://docs.clawdbot.com/gateway-lock)
-- [Background process](https://docs.clawdbot.com/background-process)
-- [Browser troubleshooting (Linux)](https://docs.clawdbot.com/browser-linux-troubleshooting)
-- [Logging](https://docs.clawdbot.com/logging)
+- [Health checks](https://github.com/clawdbot/clawdbot/blob/main/docs/health.md)
+- [Gateway lock](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway-lock.md)
+- [Background process](https://github.com/clawdbot/clawdbot/blob/main/docs/background-process.md)
+- [Browser troubleshooting (Linux)](https://github.com/clawdbot/clawdbot/blob/main/docs/browser-linux-troubleshooting.md)
+- [Logging](https://github.com/clawdbot/clawdbot/blob/main/docs/logging.md)
## Deep dives
-- [Agent loop](https://docs.clawdbot.com/agent-loop)
-- [Presence](https://docs.clawdbot.com/presence)
-- [TypeBox schemas](https://docs.clawdbot.com/typebox)
-- [RPC adapters](https://docs.clawdbot.com/rpc)
-- [Queue](https://docs.clawdbot.com/queue)
+- [Agent loop](https://github.com/clawdbot/clawdbot/blob/main/docs/agent-loop.md)
+- [Presence](https://github.com/clawdbot/clawdbot/blob/main/docs/presence.md)
+- [TypeBox schemas](https://github.com/clawdbot/clawdbot/blob/main/docs/typebox.md)
+- [RPC adapters](https://github.com/clawdbot/clawdbot/blob/main/docs/rpc.md)
+- [Queue](https://github.com/clawdbot/clawdbot/blob/main/docs/queue.md)
## Workspace & skills
-- [Skills config](https://docs.clawdbot.com/skills-config)
-- [Default AGENTS](https://docs.clawdbot.com/AGENTS.default)
-- [Templates: AGENTS](https://docs.clawdbot.com/templates/AGENTS)
-- [Templates: BOOTSTRAP](https://docs.clawdbot.com/templates/BOOTSTRAP)
-- [Templates: IDENTITY](https://docs.clawdbot.com/templates/IDENTITY)
-- [Templates: SOUL](https://docs.clawdbot.com/templates/SOUL)
-- [Templates: TOOLS](https://docs.clawdbot.com/templates/TOOLS)
-- [Templates: USER](https://docs.clawdbot.com/templates/USER)
+- [Skills config](https://github.com/clawdbot/clawdbot/blob/main/docs/skills-config.md)
+- [Default AGENTS](https://github.com/clawdbot/clawdbot/blob/main/docs/AGENTS.default.md)
+- [Templates: AGENTS](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/AGENTS.md)
+- [Templates: BOOTSTRAP](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/BOOTSTRAP.md)
+- [Templates: IDENTITY](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/IDENTITY.md)
+- [Templates: SOUL](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/SOUL.md)
+- [Templates: TOOLS](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/TOOLS.md)
+- [Templates: USER](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/USER.md)
## Platform internals
-- [macOS dev setup](https://docs.clawdbot.com/mac/dev-setup)
-- [macOS menu bar](https://docs.clawdbot.com/mac/menu-bar)
-- [macOS voice wake](https://docs.clawdbot.com/mac/voicewake)
-- [iOS node](https://docs.clawdbot.com/ios)
-- [Android node](https://docs.clawdbot.com/android)
-- [Windows app](https://docs.clawdbot.com/windows)
-- [Linux app](https://docs.clawdbot.com/linux)
+- [macOS dev setup](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/dev-setup.md)
+- [macOS menu bar](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/menu-bar.md)
+- [macOS voice wake](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/voicewake.md)
+- [iOS node](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md)
+- [Android node](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md)
+- [Windows app](https://github.com/clawdbot/clawdbot/blob/main/docs/windows.md)
+- [Linux app](https://github.com/clawdbot/clawdbot/blob/main/docs/linux.md)
## Email hooks (Gmail)
-[Gmail Pub/Sub wiring (gcloud + gogcli), hook tokens, and auto-watch behavior are documented here.](https://docs.clawdbot.com/gmail-pubsub)
+[Gmail Pub/Sub wiring (gcloud + gogcli), hook tokens, and auto-watch behavior are documented here.](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md)
Gateway auto-starts the watcher when `hooks.enabled=true` and `hooks.gmail.account` is set; `clawdbot hooks gmail run` is the manual daemon wrapper if you don’t want auto-start.
@@ -442,5 +442,5 @@ Thanks to all clawtributors:
-
+
diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
index 85ee13fdb..77d74cf85 100644
--- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
+++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift
@@ -655,23 +655,27 @@ public struct SessionsListParams: Codable, Sendable {
public let activeminutes: Int?
public let includeglobal: Bool?
public let includeunknown: Bool?
+ public let spawnedby: String?
public init(
limit: Int?,
activeminutes: Int?,
includeglobal: Bool?,
- includeunknown: Bool?
+ includeunknown: Bool?,
+ spawnedby: String?
) {
self.limit = limit
self.activeminutes = activeminutes
self.includeglobal = includeglobal
self.includeunknown = includeunknown
+ self.spawnedby = spawnedby
}
private enum CodingKeys: String, CodingKey {
case limit
case activeminutes = "activeMinutes"
case includeglobal = "includeGlobal"
case includeunknown = "includeUnknown"
+ case spawnedby = "spawnedBy"
}
}
@@ -681,6 +685,7 @@ public struct SessionsPatchParams: Codable, Sendable {
public let verboselevel: AnyCodable?
public let elevatedlevel: AnyCodable?
public let model: AnyCodable?
+ public let spawnedby: AnyCodable?
public let sendpolicy: AnyCodable?
public let groupactivation: AnyCodable?
@@ -690,6 +695,7 @@ public struct SessionsPatchParams: Codable, Sendable {
verboselevel: AnyCodable?,
elevatedlevel: AnyCodable?,
model: AnyCodable?,
+ spawnedby: AnyCodable?,
sendpolicy: AnyCodable?,
groupactivation: AnyCodable?
) {
@@ -698,6 +704,7 @@ public struct SessionsPatchParams: Codable, Sendable {
self.verboselevel = verboselevel
self.elevatedlevel = elevatedlevel
self.model = model
+ self.spawnedby = spawnedby
self.sendpolicy = sendpolicy
self.groupactivation = groupactivation
}
@@ -707,6 +714,7 @@ public struct SessionsPatchParams: Codable, Sendable {
case verboselevel = "verboseLevel"
case elevatedlevel = "elevatedLevel"
case model
+ case spawnedby = "spawnedBy"
case sendpolicy = "sendPolicy"
case groupactivation = "groupActivation"
}
diff --git a/docs/control-ui.md b/docs/control-ui.md
index 315e1415c..0e4fe0c32 100644
--- a/docs/control-ui.md
+++ b/docs/control-ui.md
@@ -63,21 +63,21 @@ Paste the token into the UI settings (sent as `connect.params.auth.token`).
The Gateway serves static files from `dist/control-ui`. Build them with:
```bash
-pnpm ui:install
-pnpm ui:build
+bun run ui:install
+bun run ui:build
```
Optional absolute base (when you want fixed asset URLs):
```bash
-CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ pnpm ui:build
+CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ bun run ui:build
```
For local development (separate dev server):
```bash
-pnpm ui:install
-pnpm ui:dev
+bun run ui:install
+bun run ui:dev
```
Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`).
diff --git a/docs/index.md b/docs/index.md
index ef0abd887..88a2a33da 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -19,7 +19,7 @@ read_when:
GitHub ·
Releases ·
- Docs ·
+ Docs ·
Clawd setup
diff --git a/docs/session-tool.md b/docs/session-tool.md
index 253e0f7e4..272acac78 100644
--- a/docs/session-tool.md
+++ b/docs/session-tool.md
@@ -35,6 +35,7 @@ Parameters:
Behavior:
- `messageLimit > 0` fetches `chat.history` per session and includes the last N messages.
- Tool results are filtered out in list output; use `sessions_history` for tool messages.
+- When running in a **sandboxed** agent session, session tools default to **spawned-only visibility** (see below).
Row shape (JSON):
- `key`: session key (string)
@@ -131,5 +132,23 @@ Parameters:
Behavior:
- Starts a new `subagent:` session with `deliver: false`.
- Sub-agents default to the full tool surface **minus session tools** (configurable via `agent.subagents.tools`).
+- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
- After completion (or best-effort wait), Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat surface.
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
+
+## Sandbox Session Visibility
+
+Sandboxed sessions can use session tools, but by default they only see sessions they spawned via `sessions_spawn`.
+
+Config:
+
+```json5
+{
+ agent: {
+ sandbox: {
+ // default: "spawned"
+ sessionToolsVisibility: "spawned" // or "all"
+ }
+ }
+}
+```
diff --git a/docs/subagents.md b/docs/subagents.md
index 238fbf8be..0d66c85f4 100644
--- a/docs/subagents.md
+++ b/docs/subagents.md
@@ -13,6 +13,7 @@ Primary goals:
- Parallelize “research / long task / slow tool” work without blocking the main run.
- Keep sub-agents isolated by default (session separation + optional sandboxing).
- Keep the tool surface hard to misuse: sub-agents do **not** get session tools by default.
+- Avoid nested fan-out: sub-agents cannot spawn sub-agents.
## Tool
@@ -69,4 +70,3 @@ Sub-agents use a dedicated in-process queue lane:
- Sub-agent announce is **best-effort**. If the gateway restarts, pending “announce back” work is lost.
- Sub-agents still share the same gateway process resources; treat `maxConcurrent` as a safety valve.
-
diff --git a/docs/web.md b/docs/web.md
index aeb0967f1..6e1a7274a 100644
--- a/docs/web.md
+++ b/docs/web.md
@@ -110,6 +110,6 @@ Open:
The Gateway serves static files from `dist/control-ui`. Build them with:
```bash
-pnpm ui:install
-pnpm ui:build
+bun run ui:install
+bun run ui:build
```
diff --git a/package.json b/package.json
index 4e30de34a..1e2ba8f6d 100644
--- a/package.json
+++ b/package.json
@@ -51,9 +51,9 @@
"docs:build": "cd docs && pnpm dlx mint broken-links",
"build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts",
"release:check": "bun scripts/release-check.ts",
- "ui:install": "pnpm -C ui install",
- "ui:dev": "pnpm -C ui dev",
- "ui:build": "pnpm -C ui build",
+ "ui:install": "node scripts/ui.js install",
+ "ui:dev": "node scripts/ui.js dev",
+ "ui:build": "node scripts/ui.js build",
"start": "bun src/entry.ts",
"clawdbot": "bun src/entry.ts",
"gateway:watch": "bun --watch src/entry.ts gateway --force",
diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh
index 7c7fe1b1f..b60a6cb75 100755
--- a/scripts/package-mac-app.sh
+++ b/scripts/package-mac-app.sh
@@ -146,8 +146,8 @@ else
fi
if [[ "${SKIP_UI_BUILD:-0}" != "1" ]]; then
- echo "🖥 Building Control UI (pnpm ui:build)"
- (cd "$ROOT_DIR" && pnpm ui:build)
+ echo "🖥 Building Control UI (ui:build)"
+ (cd "$ROOT_DIR" && node scripts/ui.js build)
else
echo "🖥 Skipping Control UI build (SKIP_UI_BUILD=1)"
fi
diff --git a/scripts/ui.js b/scripts/ui.js
new file mode 100644
index 000000000..bb84ebbff
--- /dev/null
+++ b/scripts/ui.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+import { spawn } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(here, "..");
+const uiDir = path.join(repoRoot, "ui");
+
+function usage() {
+ // keep this tiny; it's invoked from npm scripts too
+ process.stderr.write(
+ "Usage: node scripts/ui.js [...args]\n",
+ );
+}
+
+function which(cmd) {
+ try {
+ const key = process.platform === "win32" ? "Path" : "PATH";
+ const paths = (process.env[key] ?? process.env.PATH ?? "")
+ .split(path.delimiter)
+ .filter(Boolean);
+ const extensions =
+ process.platform === "win32"
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
+ .split(";")
+ .filter(Boolean)
+ : [""];
+ for (const entry of paths) {
+ for (const ext of extensions) {
+ const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd);
+ try {
+ if (fs.existsSync(candidate)) return candidate;
+ } catch {
+ // ignore
+ }
+ }
+ }
+ } catch {
+ // ignore
+ }
+ return null;
+}
+
+function resolveRunner() {
+ const bun = which("bun");
+ if (bun) return { cmd: bun, kind: "bun" };
+ const pnpm = which("pnpm");
+ if (pnpm) return { cmd: pnpm, kind: "pnpm" };
+ return null;
+}
+
+function run(cmd, args) {
+ const child = spawn(cmd, args, {
+ cwd: uiDir,
+ stdio: "inherit",
+ env: process.env,
+ });
+ child.on("exit", (code, signal) => {
+ if (signal) process.exit(1);
+ process.exit(code ?? 1);
+ });
+}
+
+const [, , action, ...rest] = process.argv;
+if (!action) {
+ usage();
+ process.exit(2);
+}
+
+const runner = resolveRunner();
+if (!runner) {
+ process.stderr.write(
+ "Missing UI runner: install bun or pnpm, then retry.\n",
+ );
+ process.exit(1);
+}
+
+const script =
+ action === "install"
+ ? null
+ : action === "dev"
+ ? "dev"
+ : action === "build"
+ ? "build"
+ : action === "test"
+ ? "test"
+ : null;
+
+if (action !== "install" && !script) {
+ usage();
+ process.exit(2);
+}
+
+if (runner.kind === "bun") {
+ if (action === "install") run(runner.cmd, ["install", ...rest]);
+ else run(runner.cmd, ["run", script, ...rest]);
+} else {
+ if (action === "install") run(runner.cmd, ["install", ...rest]);
+ else run(runner.cmd, ["run", script, ...rest]);
+}
diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts
index ea5d5fdcb..3b19dd8d5 100644
--- a/src/agents/auth-profiles.test.ts
+++ b/src/agents/auth-profiles.test.ts
@@ -50,13 +50,20 @@ describe("resolveAuthProfileOrder", () => {
expect(order).toContain("anthropic:default");
});
- it("prioritizes last-good profile when no preferred override", () => {
+ it("does not prioritize lastGood over round-robin ordering", () => {
const order = resolveAuthProfileOrder({
cfg,
- store: { ...store, lastGood: { anthropic: "anthropic:work" } },
+ store: {
+ ...store,
+ lastGood: { anthropic: "anthropic:work" },
+ usageStats: {
+ "anthropic:default": { lastUsed: 100 },
+ "anthropic:work": { lastUsed: 200 },
+ },
+ },
provider: "anthropic",
});
- expect(order[0]).toBe("anthropic:work");
+ expect(order[0]).toBe("anthropic:default");
});
it("uses explicit profiles when order is missing", () => {
diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts
index 43308674c..1d96c94ac 100644
--- a/src/agents/auth-profiles.ts
+++ b/src/agents/auth-profiles.ts
@@ -433,19 +433,14 @@ export function resolveAuthProfileOrder(params: {
.filter(([, profile]) => profile.provider === provider)
.map(([profileId]) => profileId)
: [];
- const lastGood = store.lastGood?.[provider];
const baseOrder =
configuredOrder ??
(explicitProfiles.length > 0
? explicitProfiles
: listProfilesForProvider(store, provider));
if (baseOrder.length === 0) return [];
- const order =
- configuredOrder && configuredOrder.length > 0
- ? baseOrder
- : orderProfilesByMode(baseOrder, store);
- const filtered = order.filter((profileId) => {
+ const filtered = baseOrder.filter((profileId) => {
const cred = store.profiles[profileId];
return cred ? cred.provider === provider : true;
});
@@ -453,21 +448,29 @@ export function resolveAuthProfileOrder(params: {
for (const entry of filtered) {
if (!deduped.includes(entry)) deduped.push(entry);
}
- if (preferredProfile && deduped.includes(preferredProfile)) {
- const rest = deduped.filter((entry) => entry !== preferredProfile);
- if (lastGood && rest.includes(lastGood)) {
+
+ // If user specified explicit order in config, respect it exactly
+ if (configuredOrder && configuredOrder.length > 0) {
+ // Still put preferredProfile first if specified
+ if (preferredProfile && deduped.includes(preferredProfile)) {
return [
preferredProfile,
- lastGood,
- ...rest.filter((entry) => entry !== lastGood),
+ ...deduped.filter((e) => e !== preferredProfile),
];
}
- return [preferredProfile, ...rest];
+ return deduped;
}
- if (lastGood && deduped.includes(lastGood)) {
- return [lastGood, ...deduped.filter((entry) => entry !== lastGood)];
+
+ // Otherwise, use round-robin: sort by lastUsed (oldest first)
+ // preferredProfile goes first if specified (for explicit user choice)
+ // lastGood is NOT prioritized - that would defeat round-robin
+ const sorted = orderProfilesByMode(deduped, store);
+
+ if (preferredProfile && sorted.includes(preferredProfile)) {
+ return [preferredProfile, ...sorted.filter((e) => e !== preferredProfile)];
}
- return deduped;
+
+ return sorted;
}
function orderProfilesByMode(
diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts
index c5c35c1d0..b4400eba8 100644
--- a/src/agents/clawdbot-tools.ts
+++ b/src/agents/clawdbot-tools.ts
@@ -17,6 +17,7 @@ export function createClawdbotTools(options?: {
browserControlUrl?: string;
agentSessionKey?: string;
agentSurface?: string;
+ sandboxed?: boolean;
config?: ClawdbotConfig;
}): AnyAgentTool[] {
const imageTool = createImageTool({ config: options?.config });
@@ -28,15 +29,23 @@ export function createClawdbotTools(options?: {
createDiscordTool(),
createSlackTool(),
createGatewayTool(),
- createSessionsListTool(),
- createSessionsHistoryTool(),
+ createSessionsListTool({
+ agentSessionKey: options?.agentSessionKey,
+ sandboxed: options?.sandboxed,
+ }),
+ createSessionsHistoryTool({
+ agentSessionKey: options?.agentSessionKey,
+ sandboxed: options?.sandboxed,
+ }),
createSessionsSendTool({
agentSessionKey: options?.agentSessionKey,
agentSurface: options?.agentSurface,
+ sandboxed: options?.sandboxed,
}),
createSessionsSpawnTool({
agentSessionKey: options?.agentSessionKey,
agentSurface: options?.agentSurface,
+ sandboxed: options?.sandboxed,
}),
...(imageTool ? [imageTool] : []),
];
diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts
index 4c5fc5f4f..f438002ce 100644
--- a/src/agents/pi-tools.ts
+++ b/src/agents/pi-tools.ts
@@ -556,6 +556,7 @@ export function createClawdbotCodingTools(options?: {
browserControlUrl: sandbox?.browser?.controlUrl,
agentSessionKey: options?.sessionKey,
agentSurface: options?.surface,
+ sandboxed: !!sandbox,
config: options?.config,
}),
];
diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts
index 6166f3349..b788e25a5 100644
--- a/src/agents/sandbox.ts
+++ b/src/agents/sandbox.ts
@@ -114,7 +114,17 @@ const DEFAULT_SANDBOX_CONTAINER_PREFIX = "clawdbot-sbx-";
const DEFAULT_SANDBOX_WORKDIR = "/workspace";
const DEFAULT_SANDBOX_IDLE_HOURS = 24;
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
-const DEFAULT_TOOL_ALLOW = ["bash", "process", "read", "write", "edit"];
+const DEFAULT_TOOL_ALLOW = [
+ "bash",
+ "process",
+ "read",
+ "write",
+ "edit",
+ "sessions_list",
+ "sessions_history",
+ "sessions_send",
+ "sessions_spawn",
+];
const DEFAULT_TOOL_DENY = [
"browser",
"canvas",
diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts
index 38e7ee9e9..95f7a68ba 100644
--- a/src/agents/tools/cron-tool.ts
+++ b/src/agents/tools/cron-tool.ts
@@ -3,11 +3,23 @@ import {
normalizeCronJobCreate,
normalizeCronJobPatch,
} from "../../cron/normalize.js";
-import { CronAddParamsSchema } from "../../gateway/protocol/schema.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
-const CronJobPatchSchema = Type.Partial(CronAddParamsSchema);
+// NOTE: We use Type.Object({}, { additionalProperties: true }) for job/patch
+// instead of CronAddParamsSchema/CronJobPatchSchema because:
+//
+// 1. CronAddParamsSchema contains nested Type.Union (for schedule, payload, etc.)
+// 2. TypeBox compiles Type.Union to JSON Schema `anyOf`
+// 3. pi-ai's sanitizeSchemaForGoogle() strips `anyOf` from nested properties
+// 4. This leaves empty schemas `{}` which Claude rejects as invalid
+//
+// The actual validation happens at runtime via normalizeCronJobCreate/Patch
+// and the gateway's validateCronAddParams. This schema just needs to accept
+// any object so the AI can pass through the job definition.
+//
+// See: https://github.com/anthropics/anthropic-cookbook/blob/main/misc/tool_use_best_practices.md
+// Claude requires valid JSON Schema 2020-12 with explicit types.
const CronToolSchema = Type.Union([
Type.Object({
@@ -28,7 +40,7 @@ const CronToolSchema = Type.Union([
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
- job: CronAddParamsSchema,
+ job: Type.Object({}, { additionalProperties: true }),
}),
Type.Object({
action: Type.Literal("update"),
@@ -36,7 +48,7 @@ const CronToolSchema = Type.Union([
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
id: Type.String(),
- patch: CronJobPatchSchema,
+ patch: Type.Object({}, { additionalProperties: true }),
}),
Type.Object({
action: Type.Literal("remove"),
diff --git a/src/agents/tools/sessions-history-tool.ts b/src/agents/tools/sessions-history-tool.ts
index d3ddd5534..9ed9e1470 100644
--- a/src/agents/tools/sessions-history-tool.ts
+++ b/src/agents/tools/sessions-history-tool.ts
@@ -17,7 +17,37 @@ const SessionsHistoryToolSchema = Type.Object({
includeTools: Type.Optional(Type.Boolean()),
});
-export function createSessionsHistoryTool(): AnyAgentTool {
+function resolveSandboxSessionToolsVisibility(
+ cfg: ReturnType,
+) {
+ return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
+}
+
+async function isSpawnedSessionAllowed(params: {
+ requesterSessionKey: string;
+ targetSessionKey: string;
+}): Promise {
+ try {
+ const list = (await callGateway({
+ method: "sessions.list",
+ params: {
+ includeGlobal: false,
+ includeUnknown: false,
+ limit: 500,
+ spawnedBy: params.requesterSessionKey,
+ },
+ })) as { sessions?: Array> };
+ const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
+ return sessions.some((entry) => entry?.key === params.targetSessionKey);
+ } catch {
+ return false;
+ }
+}
+
+export function createSessionsHistoryTool(opts?: {
+ agentSessionKey?: string;
+ sandboxed?: boolean;
+}): AnyAgentTool {
return {
label: "Session History",
name: "sessions_history",
@@ -30,11 +60,37 @@ export function createSessionsHistoryTool(): AnyAgentTool {
});
const cfg = loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg);
+ const visibility = resolveSandboxSessionToolsVisibility(cfg);
+ const requesterInternalKey =
+ typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
+ ? resolveInternalSessionKey({
+ key: opts.agentSessionKey,
+ alias,
+ mainKey,
+ })
+ : undefined;
const resolvedKey = resolveInternalSessionKey({
key: sessionKey,
alias,
mainKey,
});
+ const restrictToSpawned =
+ opts?.sandboxed === true &&
+ visibility === "spawned" &&
+ requesterInternalKey &&
+ !requesterInternalKey.toLowerCase().startsWith("subagent:");
+ if (restrictToSpawned) {
+ const ok = await isSpawnedSessionAllowed({
+ requesterSessionKey: requesterInternalKey,
+ targetSessionKey: resolvedKey,
+ });
+ if (!ok) {
+ return jsonResult({
+ status: "forbidden",
+ error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
+ });
+ }
+ }
const limit =
typeof params.limit === "number" && Number.isFinite(params.limit)
? Math.max(1, Math.floor(params.limit))
diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts
index 0209813f2..dc2dd14aa 100644
--- a/src/agents/tools/sessions-list-tool.ts
+++ b/src/agents/tools/sessions-list-tool.ts
@@ -44,7 +44,16 @@ const SessionsListToolSchema = Type.Object({
messageLimit: Type.Optional(Type.Integer({ minimum: 0 })),
});
-export function createSessionsListTool(): AnyAgentTool {
+function resolveSandboxSessionToolsVisibility(
+ cfg: ReturnType,
+) {
+ return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
+}
+
+export function createSessionsListTool(opts?: {
+ agentSessionKey?: string;
+ sandboxed?: boolean;
+}): AnyAgentTool {
return {
label: "Sessions",
name: "sessions_list",
@@ -54,6 +63,20 @@ export function createSessionsListTool(): AnyAgentTool {
const params = args as Record;
const cfg = loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg);
+ const visibility = resolveSandboxSessionToolsVisibility(cfg);
+ const requesterInternalKey =
+ typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
+ ? resolveInternalSessionKey({
+ key: opts.agentSessionKey,
+ alias,
+ mainKey,
+ })
+ : undefined;
+ const restrictToSpawned =
+ opts?.sandboxed === true &&
+ visibility === "spawned" &&
+ requesterInternalKey &&
+ !requesterInternalKey.toLowerCase().startsWith("subagent:");
const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) =>
value.trim().toLowerCase(),
@@ -86,8 +109,9 @@ export function createSessionsListTool(): AnyAgentTool {
params: {
limit,
activeMinutes,
- includeGlobal: true,
- includeUnknown: true,
+ includeGlobal: !restrictToSpawned,
+ includeUnknown: !restrictToSpawned,
+ spawnedBy: restrictToSpawned ? requesterInternalKey : undefined,
},
})) as {
path?: string;
diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts
index bcc732486..72183c896 100644
--- a/src/agents/tools/sessions-send-tool.ts
+++ b/src/agents/tools/sessions-send-tool.ts
@@ -33,6 +33,7 @@ const SessionsSendToolSchema = Type.Object({
export function createSessionsSendTool(opts?: {
agentSessionKey?: string;
agentSurface?: string;
+ sandboxed?: boolean;
}): AnyAgentTool {
return {
label: "Session Send",
@@ -47,11 +48,64 @@ export function createSessionsSendTool(opts?: {
const message = readStringParam(params, "message", { required: true });
const cfg = loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg);
+ const visibility =
+ cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
+ const requesterInternalKey =
+ typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
+ ? resolveInternalSessionKey({
+ key: opts.agentSessionKey,
+ alias,
+ mainKey,
+ })
+ : undefined;
const resolvedKey = resolveInternalSessionKey({
key: sessionKey,
alias,
mainKey,
});
+ const restrictToSpawned =
+ opts?.sandboxed === true &&
+ visibility === "spawned" &&
+ requesterInternalKey &&
+ !requesterInternalKey.toLowerCase().startsWith("subagent:");
+ if (restrictToSpawned) {
+ try {
+ const list = (await callGateway({
+ method: "sessions.list",
+ params: {
+ includeGlobal: false,
+ includeUnknown: false,
+ limit: 500,
+ spawnedBy: requesterInternalKey,
+ },
+ })) as { sessions?: Array> };
+ const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
+ const ok = sessions.some((entry) => entry?.key === resolvedKey);
+ if (!ok) {
+ return jsonResult({
+ runId: crypto.randomUUID(),
+ status: "forbidden",
+ error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
+ sessionKey: resolveDisplaySessionKey({
+ key: sessionKey,
+ alias,
+ mainKey,
+ }),
+ });
+ }
+ } catch {
+ return jsonResult({
+ runId: crypto.randomUUID(),
+ status: "forbidden",
+ error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
+ sessionKey: resolveDisplaySessionKey({
+ key: sessionKey,
+ alias,
+ mainKey,
+ }),
+ });
+ }
+ }
const timeoutSeconds =
typeof params.timeoutSeconds === "number" &&
Number.isFinite(params.timeoutSeconds)
diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts
index 1464974c4..cd7a97f83 100644
--- a/src/agents/tools/sessions-spawn-tool.ts
+++ b/src/agents/tools/sessions-spawn-tool.ts
@@ -160,6 +160,7 @@ async function runSubagentAnnounceFlow(params: {
export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string;
agentSurface?: string;
+ sandboxed?: boolean;
}): AnyAgentTool {
return {
label: "Sessions",
@@ -185,6 +186,15 @@ export function createSessionsSpawnTool(opts?: {
const cfg = loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg);
const requesterSessionKey = opts?.agentSessionKey;
+ if (
+ typeof requesterSessionKey === "string" &&
+ requesterSessionKey.trim().toLowerCase().startsWith("subagent:")
+ ) {
+ return jsonResult({
+ status: "forbidden",
+ error: "sessions_spawn is not allowed from sub-agent sessions",
+ });
+ }
const requesterInternalKey = requesterSessionKey
? resolveInternalSessionKey({
key: requesterSessionKey,
@@ -199,6 +209,17 @@ export function createSessionsSpawnTool(opts?: {
});
const childSessionKey = `subagent:${crypto.randomUUID()}`;
+ if (opts?.sandboxed === true) {
+ try {
+ await callGateway({
+ method: "sessions.patch",
+ params: { key: childSessionKey, spawnedBy: requesterInternalKey },
+ timeoutMs: 10_000,
+ });
+ } catch {
+ // best-effort; scoping relies on this metadata but spawning still works without it
+ }
+ }
const childSystemPrompt = buildSubagentSystemPrompt({
requesterSessionKey,
requesterSurface: opts?.agentSurface,
diff --git a/src/commands/configure.ts b/src/commands/configure.ts
index d65908f5a..98eca3125 100644
--- a/src/commands/configure.ts
+++ b/src/commands/configure.ts
@@ -10,7 +10,12 @@ import {
spinner,
text,
} from "@clack/prompts";
-import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
+import {
+ loginAnthropic,
+ loginOpenAICodex,
+ type OAuthCredentials,
+ type OAuthProvider,
+} from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js";
import {
CONFIG_PATH_CLAWDBOT,
@@ -54,6 +59,10 @@ import {
import { setupProviders } from "./onboard-providers.js";
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
import { setupSkills } from "./onboard-skills.js";
+import {
+ applyOpenAICodexModelDefault,
+ OPENAI_CODEX_DEFAULT_MODEL,
+} from "./openai-codex-model-default.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
type WizardSection =
@@ -234,6 +243,7 @@ async function promptAuthConfig(
message: "Model/auth choice",
options: [
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
+ { value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
{
value: "antigravity",
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
@@ -244,7 +254,7 @@ async function promptAuthConfig(
],
}),
runtime,
- ) as "oauth" | "antigravity" | "apiKey" | "minimax" | "skip";
+ ) as "oauth" | "openai-codex" | "antigravity" | "apiKey" | "minimax" | "skip";
let next = cfg;
@@ -286,6 +296,79 @@ async function promptAuthConfig(
spin.stop("OAuth failed");
runtime.error(String(err));
}
+ } else if (authChoice === "openai-codex") {
+ const isRemote = isRemoteEnvironment();
+ note(
+ isRemote
+ ? [
+ "You are running in a remote/VPS environment.",
+ "A URL will be shown for you to open in your LOCAL browser.",
+ "After signing in, paste the redirect URL back here.",
+ ].join("\n")
+ : [
+ "Browser will open for OpenAI authentication.",
+ "If the callback doesn't auto-complete, paste the redirect URL.",
+ "OpenAI OAuth uses localhost:1455 for the callback.",
+ ].join("\n"),
+ "OpenAI Codex OAuth",
+ );
+ const spin = spinner();
+ spin.start("Starting OAuth flow…");
+ let manualCodePromise: Promise | undefined;
+ try {
+ const creds = await loginOpenAICodex({
+ onAuth: async ({ url }) => {
+ if (isRemote) {
+ spin.message("OAuth URL ready (see below)…");
+ runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
+ manualCodePromise = text({
+ message: "Paste the redirect URL (or authorization code)",
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }).then((value) => String(guardCancel(value, runtime)));
+ } else {
+ spin.message("Complete sign-in in browser…");
+ await openUrl(url);
+ runtime.log(`Open: ${url}`);
+ }
+ },
+ onPrompt: async (prompt) => {
+ if (manualCodePromise) return manualCodePromise;
+ const code = guardCancel(
+ await text({
+ message: prompt.message,
+ placeholder: prompt.placeholder,
+ validate: (value) => (value?.trim() ? undefined : "Required"),
+ }),
+ runtime,
+ );
+ return String(code);
+ },
+ onProgress: (msg) => spin.message(msg),
+ });
+ spin.stop("OpenAI OAuth complete");
+ if (creds) {
+ await writeOAuthCredentials(
+ "openai-codex" as unknown as OAuthProvider,
+ creds,
+ );
+ next = applyAuthProfileConfig(next, {
+ profileId: "openai-codex:default",
+ provider: "openai-codex",
+ mode: "oauth",
+ });
+ const applied = applyOpenAICodexModelDefault(next);
+ next = applied.next;
+ if (applied.changed) {
+ note(
+ `Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
+ "Model configured",
+ );
+ }
+ }
+ } catch (err) {
+ spin.stop("OpenAI OAuth failed");
+ runtime.error(String(err));
+ }
} else if (authChoice === "antigravity") {
const isRemote = isRemoteEnvironment();
note(
diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts
index 8151bfca3..f2032f8ad 100644
--- a/src/commands/onboard-auth.ts
+++ b/src/commands/onboard-auth.ts
@@ -44,16 +44,25 @@ export function applyAuthProfileConfig(
...(params.email ? { email: params.email } : {}),
},
};
- const order = { ...cfg.auth?.order };
- const list = order[params.provider] ? [...order[params.provider]] : [];
- if (!list.includes(params.profileId)) list.push(params.profileId);
- order[params.provider] = list;
+
+ // Only maintain `auth.order` when the user explicitly configured it.
+ // Default behavior: no explicit order -> resolveAuthProfileOrder can round-robin by lastUsed.
+ const existingProviderOrder = cfg.auth?.order?.[params.provider];
+ const order =
+ existingProviderOrder !== undefined
+ ? {
+ ...cfg.auth?.order,
+ [params.provider]: existingProviderOrder.includes(params.profileId)
+ ? existingProviderOrder
+ : [...existingProviderOrder, params.profileId],
+ }
+ : cfg.auth?.order;
return {
...cfg,
auth: {
...cfg.auth,
profiles,
- order,
+ ...(order ? { order } : {}),
},
};
}
diff --git a/src/commands/openai-codex-model-default.test.ts b/src/commands/openai-codex-model-default.test.ts
new file mode 100644
index 000000000..86497bb90
--- /dev/null
+++ b/src/commands/openai-codex-model-default.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from "vitest";
+
+import type { ClawdbotConfig } from "../config/config.js";
+import {
+ applyOpenAICodexModelDefault,
+ OPENAI_CODEX_DEFAULT_MODEL,
+} from "./openai-codex-model-default.js";
+
+describe("applyOpenAICodexModelDefault", () => {
+ it("sets openai-codex default when model is unset", () => {
+ const cfg: ClawdbotConfig = { agent: {} };
+ const applied = applyOpenAICodexModelDefault(cfg);
+ expect(applied.changed).toBe(true);
+ expect(applied.next.agent?.model).toEqual({
+ primary: OPENAI_CODEX_DEFAULT_MODEL,
+ });
+ });
+
+ it("sets openai-codex default when model is openai/*", () => {
+ const cfg: ClawdbotConfig = { agent: { model: "openai/gpt-5.2" } };
+ const applied = applyOpenAICodexModelDefault(cfg);
+ expect(applied.changed).toBe(true);
+ expect(applied.next.agent?.model).toEqual({
+ primary: OPENAI_CODEX_DEFAULT_MODEL,
+ });
+ });
+
+ it("does not override openai-codex/*", () => {
+ const cfg: ClawdbotConfig = { agent: { model: "openai-codex/gpt-5.2" } };
+ const applied = applyOpenAICodexModelDefault(cfg);
+ expect(applied.changed).toBe(false);
+ expect(applied.next).toEqual(cfg);
+ });
+
+ it("does not override non-openai models", () => {
+ const cfg: ClawdbotConfig = {
+ agent: { model: "anthropic/claude-opus-4-5" },
+ };
+ const applied = applyOpenAICodexModelDefault(cfg);
+ expect(applied.changed).toBe(false);
+ expect(applied.next).toEqual(cfg);
+ });
+});
diff --git a/src/commands/openai-codex-model-default.ts b/src/commands/openai-codex-model-default.ts
new file mode 100644
index 000000000..d1d5b0914
--- /dev/null
+++ b/src/commands/openai-codex-model-default.ts
@@ -0,0 +1,46 @@
+import type { ClawdbotConfig } from "../config/config.js";
+import type { AgentModelListConfig } from "../config/types.js";
+
+export const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";
+
+function shouldSetOpenAICodexModel(model?: string): boolean {
+ const trimmed = model?.trim();
+ if (!trimmed) return true;
+ const normalized = trimmed.toLowerCase();
+ if (normalized.startsWith("openai-codex/")) return false;
+ if (normalized.startsWith("openai/")) return true;
+ return normalized === "gpt" || normalized === "gpt-mini";
+}
+
+function resolvePrimaryModel(
+ model?: AgentModelListConfig | string,
+): string | undefined {
+ if (typeof model === "string") return model;
+ if (model && typeof model === "object" && typeof model.primary === "string") {
+ return model.primary;
+ }
+ return undefined;
+}
+
+export function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
+ next: ClawdbotConfig;
+ changed: boolean;
+} {
+ const current = resolvePrimaryModel(cfg.agent?.model);
+ if (!shouldSetOpenAICodexModel(current)) {
+ return { next: cfg, changed: false };
+ }
+ return {
+ next: {
+ ...cfg,
+ agent: {
+ ...cfg.agent,
+ model:
+ cfg.agent?.model && typeof cfg.agent.model === "object"
+ ? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
+ : { primary: OPENAI_CODEX_DEFAULT_MODEL },
+ },
+ },
+ changed: true,
+ };
+}
diff --git a/src/config/sessions.ts b/src/config/sessions.ts
index ff440eab8..a92219c40 100644
--- a/src/config/sessions.ts
+++ b/src/config/sessions.ts
@@ -26,6 +26,8 @@ export type SessionChatType = "direct" | "group" | "room";
export type SessionEntry = {
sessionId: string;
updatedAt: number;
+ /** Parent session key that spawned this session (used for sandbox session-tool scoping). */
+ spawnedBy?: string;
systemSent?: boolean;
abortedLastRun?: boolean;
chatType?: SessionChatType;
diff --git a/src/config/types.ts b/src/config/types.ts
index 9b03b78ef..7d7bb92b5 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -77,6 +77,8 @@ export type AgentElevatedAllowFromConfig = {
};
export type WhatsAppConfig = {
+ /** Optional per-account WhatsApp configuration (multi-account). */
+ accounts?: Record;
/** Optional allowlist for WhatsApp direct chats (E.164). */
allowFrom?: string[];
/** Optional allowlist for WhatsApp group senders (E.164). */
@@ -98,6 +100,23 @@ export type WhatsAppConfig = {
>;
};
+export type WhatsAppAccountConfig = {
+ /** If false, do not start this WhatsApp account provider. Default: true. */
+ enabled?: boolean;
+ /** Override auth directory (Baileys multi-file auth state). */
+ authDir?: string;
+ allowFrom?: string[];
+ groupAllowFrom?: string[];
+ groupPolicy?: GroupPolicy;
+ textChunkLimit?: number;
+ groups?: Record<
+ string,
+ {
+ requireMention?: boolean;
+ }
+ >;
+};
+
export type BrowserProfileConfig = {
/** CDP port for this profile. Allocated once at creation, persisted permanently. */
cdpPort?: number;
@@ -488,6 +507,37 @@ export type RoutingConfig = {
timeoutSeconds?: number;
};
groupChat?: GroupChatConfig;
+ /** Default agent id when no binding matches. Default: "main". */
+ defaultAgentId?: string;
+ agentToAgent?: {
+ /** Enable agent-to-agent messaging tools. Default: false. */
+ enabled?: boolean;
+ /** Allowlist of agent ids or patterns (implementation-defined). */
+ allow?: string[];
+ };
+ agents?: Record<
+ string,
+ {
+ workspace?: string;
+ agentDir?: string;
+ model?: string;
+ sandbox?: {
+ mode?: "off" | "non-main" | "all";
+ perSession?: boolean;
+ workspaceRoot?: string;
+ };
+ }
+ >;
+ bindings?: Array<{
+ agentId: string;
+ match: {
+ surface: string;
+ surfaceAccountId?: string;
+ peer?: { kind: "dm" | "group" | "channel"; id: string };
+ guildId?: string;
+ teamId?: string;
+ };
+ }>;
queue?: {
mode?: QueueMode;
bySurface?: QueueModeBySurface;
@@ -836,6 +886,12 @@ export type ClawdbotConfig = {
sandbox?: {
/** Enable sandboxing for sessions. */
mode?: "off" | "non-main" | "all";
+ /**
+ * Session tools visibility for sandboxed sessions.
+ * - "spawned": only allow session tools to target sessions spawned from this session (default)
+ * - "all": allow session tools to target any session
+ */
+ sessionToolsVisibility?: "spawned" | "all";
/** Use one container per session (recommended for hard isolation). */
perSession?: boolean;
/** Root directory for sandbox workspaces. */
diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts
index 15abff42e..6039afb70 100644
--- a/src/config/zod-schema.ts
+++ b/src/config/zod-schema.ts
@@ -201,6 +201,61 @@ const RoutingSchema = z
.object({
groupChat: GroupChatSchema,
transcribeAudio: TranscribeAudioSchema,
+ defaultAgentId: z.string().optional(),
+ agentToAgent: z
+ .object({
+ enabled: z.boolean().optional(),
+ allow: z.array(z.string()).optional(),
+ })
+ .optional(),
+ agents: z
+ .record(
+ z.string(),
+ z
+ .object({
+ workspace: z.string().optional(),
+ agentDir: z.string().optional(),
+ model: z.string().optional(),
+ sandbox: z
+ .object({
+ mode: z
+ .union([
+ z.literal("off"),
+ z.literal("non-main"),
+ z.literal("all"),
+ ])
+ .optional(),
+ perSession: z.boolean().optional(),
+ workspaceRoot: z.string().optional(),
+ })
+ .optional(),
+ })
+ .optional(),
+ )
+ .optional(),
+ bindings: z
+ .array(
+ z.object({
+ agentId: z.string(),
+ match: z.object({
+ surface: z.string(),
+ surfaceAccountId: z.string().optional(),
+ peer: z
+ .object({
+ kind: z.union([
+ z.literal("dm"),
+ z.literal("group"),
+ z.literal("channel"),
+ ]),
+ id: z.string(),
+ })
+ .optional(),
+ guildId: z.string().optional(),
+ teamId: z.string().optional(),
+ }),
+ }),
+ )
+ .optional(),
queue: z
.object({
mode: QueueModeSchema.optional(),
@@ -504,6 +559,9 @@ export const ClawdbotSchema = z.object({
mode: z
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
.optional(),
+ sessionToolsVisibility: z
+ .union([z.literal("spawned"), z.literal("all")])
+ .optional(),
perSession: z.boolean().optional(),
workspaceRoot: z.string().optional(),
docker: z
@@ -608,6 +666,32 @@ export const ClawdbotSchema = z.object({
.optional(),
whatsapp: z
.object({
+ accounts: z
+ .record(
+ z.string(),
+ z
+ .object({
+ enabled: z.boolean().optional(),
+ /** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
+ authDir: z.string().optional(),
+ allowFrom: z.array(z.string()).optional(),
+ groupAllowFrom: z.array(z.string()).optional(),
+ groupPolicy: GroupPolicySchema.optional().default("open"),
+ textChunkLimit: z.number().int().positive().optional(),
+ groups: z
+ .record(
+ z.string(),
+ z
+ .object({
+ requireMention: z.boolean().optional(),
+ })
+ .optional(),
+ )
+ .optional(),
+ })
+ .optional(),
+ )
+ .optional(),
allowFrom: z.array(z.string()).optional(),
groupAllowFrom: z.array(z.string()).optional(),
groupPolicy: GroupPolicySchema.optional().default("open"),
diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts
index e53f4e352..8f69e806a 100644
--- a/src/gateway/control-ui.ts
+++ b/src/gateway/control-ui.ts
@@ -157,7 +157,7 @@ export function handleControlUiHttpRequest(
res.statusCode = 503;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(
- "Control UI assets not found. Build them with `pnpm ui:build` (or run `pnpm ui:dev` during development).",
+ "Control UI assets not found. Build them with `bun run ui:build` (or run `bun run ui:dev` during development).",
);
return true;
}
diff --git a/src/gateway/protocol/schema.ts b/src/gateway/protocol/schema.ts
index c93645366..eec93fe79 100644
--- a/src/gateway/protocol/schema.ts
+++ b/src/gateway/protocol/schema.ts
@@ -311,6 +311,7 @@ export const SessionsListParamsSchema = Type.Object(
activeMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
includeGlobal: Type.Optional(Type.Boolean()),
includeUnknown: Type.Optional(Type.Boolean()),
+ spawnedBy: Type.Optional(NonEmptyString),
},
{ additionalProperties: false },
);
@@ -322,6 +323,7 @@ export const SessionsPatchParamsSchema = Type.Object(
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
+ spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
sendPolicy: Type.Optional(
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
),
diff --git a/src/gateway/server-bridge.ts b/src/gateway/server-bridge.ts
index 610c74946..a0be80f91 100644
--- a/src/gateway/server-bridge.ts
+++ b/src/gateway/server-bridge.ts
@@ -349,6 +349,52 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
}
: { sessionId: randomUUID(), updatedAt: now };
+ if ("spawnedBy" in p) {
+ const raw = p.spawnedBy;
+ if (raw === null) {
+ if (existing?.spawnedBy) {
+ return {
+ ok: false,
+ error: {
+ code: ErrorCodes.INVALID_REQUEST,
+ message: "spawnedBy cannot be cleared once set",
+ },
+ };
+ }
+ } else if (raw !== undefined) {
+ const trimmed = String(raw).trim();
+ if (!trimmed) {
+ return {
+ ok: false,
+ error: {
+ code: ErrorCodes.INVALID_REQUEST,
+ message: "invalid spawnedBy: empty",
+ },
+ };
+ }
+ if (!key.startsWith("subagent:")) {
+ return {
+ ok: false,
+ error: {
+ code: ErrorCodes.INVALID_REQUEST,
+ message:
+ "spawnedBy is only supported for subagent:* sessions",
+ },
+ };
+ }
+ if (existing?.spawnedBy && existing.spawnedBy !== trimmed) {
+ return {
+ ok: false,
+ error: {
+ code: ErrorCodes.INVALID_REQUEST,
+ message: "spawnedBy cannot be changed once set",
+ },
+ };
+ }
+ next.spawnedBy = trimmed;
+ }
+ }
+
if ("thinkingLevel" in p) {
const raw = p.thinkingLevel;
if (raw === null) {
diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts
index a0ec54352..4c45d22ea 100644
--- a/src/gateway/server-methods/sessions.ts
+++ b/src/gateway/server-methods/sessions.ts
@@ -110,6 +110,56 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
: { sessionId: randomUUID(), updatedAt: now };
+ if ("spawnedBy" in p) {
+ const raw = p.spawnedBy;
+ if (raw === null) {
+ if (existing?.spawnedBy) {
+ respond(
+ false,
+ undefined,
+ errorShape(
+ ErrorCodes.INVALID_REQUEST,
+ "spawnedBy cannot be cleared once set",
+ ),
+ );
+ return;
+ }
+ } else if (raw !== undefined) {
+ const trimmed = String(raw).trim();
+ if (!trimmed) {
+ respond(
+ false,
+ undefined,
+ errorShape(ErrorCodes.INVALID_REQUEST, "invalid spawnedBy: empty"),
+ );
+ return;
+ }
+ if (!key.startsWith("subagent:")) {
+ respond(
+ false,
+ undefined,
+ errorShape(
+ ErrorCodes.INVALID_REQUEST,
+ "spawnedBy is only supported for subagent:* sessions",
+ ),
+ );
+ return;
+ }
+ if (existing?.spawnedBy && existing.spawnedBy !== trimmed) {
+ respond(
+ false,
+ undefined,
+ errorShape(
+ ErrorCodes.INVALID_REQUEST,
+ "spawnedBy cannot be changed once set",
+ ),
+ );
+ return;
+ }
+ next.spawnedBy = trimmed;
+ }
+ }
+
if ("thinkingLevel" in p) {
const raw = p.thinkingLevel;
if (raw === null) {
diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts
index 6b387a4be..33f0f9112 100644
--- a/src/gateway/server.cron.test.ts
+++ b/src/gateway/server.cron.test.ts
@@ -327,7 +327,9 @@ describe("gateway server cron", () => {
: "";
expect(storePath).toContain("jobs.json");
- const atMs = Date.now() + 80;
+ // Avoid races: if we schedule too close to "now", the cron runner can
+ // finish before we start listening for the "finished" event.
+ const atMs = Date.now() + 1000;
const addRes = await rpcReq(ws, "cron.add", {
name: "auto run test",
enabled: true,
@@ -345,8 +347,12 @@ describe("gateway server cron", () => {
type: "event";
event: string;
payload?: { jobId?: string; action?: string; status?: string } | null;
- }>((resolve) => {
- const timeout = setTimeout(() => resolve(null as never), 8000);
+ }>((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(
+ new Error(`timeout waiting for cron finished event: ${jobId}`),
+ );
+ }, 8000);
ws.on("message", (data) => {
const obj = JSON.parse(decodeWsData(data));
if (
diff --git a/src/gateway/server.sessions.test.ts b/src/gateway/server.sessions.test.ts
index 1c8ef9176..590e1c774 100644
--- a/src/gateway/server.sessions.test.ts
+++ b/src/gateway/server.sessions.test.ts
@@ -53,6 +53,11 @@ describe("gateway server sessions", () => {
updatedAt: now - 120_000,
totalTokens: 50,
},
+ "subagent:one": {
+ sessionId: "sess-subagent",
+ updatedAt: now - 120_000,
+ spawnedBy: "main",
+ },
global: {
sessionId: "sess-global",
updatedAt: now - 10_000,
@@ -148,6 +153,31 @@ describe("gateway server sessions", () => {
expect(main2?.verboseLevel).toBeUndefined();
expect(main2?.sendPolicy).toBe("deny");
+ const spawnedOnly = await rpcReq<{
+ sessions: Array<{ key: string }>;
+ }>(ws, "sessions.list", {
+ includeGlobal: true,
+ includeUnknown: true,
+ spawnedBy: "main",
+ });
+ expect(spawnedOnly.ok).toBe(true);
+ expect(spawnedOnly.payload?.sessions.map((s) => s.key)).toEqual([
+ "subagent:one",
+ ]);
+
+ const spawnedPatched = await rpcReq<{
+ ok: true;
+ entry: { spawnedBy?: string };
+ }>(ws, "sessions.patch", { key: "subagent:two", spawnedBy: "main" });
+ expect(spawnedPatched.ok).toBe(true);
+ expect(spawnedPatched.payload?.entry.spawnedBy).toBe("main");
+
+ const spawnedPatchedInvalidKey = await rpcReq(ws, "sessions.patch", {
+ key: "main",
+ spawnedBy: "main",
+ });
+ expect(spawnedPatchedInvalidKey.ok).toBe(false);
+
piSdkMock.enabled = true;
piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }];
const modelPatched = await rpcReq<{
diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts
index 3deba73d9..46bb66fce 100644
--- a/src/gateway/session-utils.ts
+++ b/src/gateway/session-utils.ts
@@ -227,6 +227,7 @@ export function listSessionsFromStore(params: {
const includeGlobal = opts.includeGlobal === true;
const includeUnknown = opts.includeUnknown === true;
+ const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
const activeMinutes =
typeof opts.activeMinutes === "number" &&
Number.isFinite(opts.activeMinutes)
@@ -239,6 +240,11 @@ export function listSessionsFromStore(params: {
if (!includeUnknown && key === "unknown") return false;
return true;
})
+ .filter(([key, entry]) => {
+ if (!spawnedBy) return true;
+ if (key === "unknown" || key === "global") return false;
+ return entry?.spawnedBy === spawnedBy;
+ })
.map(([key, entry]) => {
const updatedAt = entry?.updatedAt ?? null;
const input = entry?.inputTokens ?? 0;
diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts
index e005789dc..0b995a0cd 100644
--- a/src/infra/control-ui-assets.ts
+++ b/src/infra/control-ui-assets.ts
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
-import { runCommandWithTimeout, runExec } from "../process/exec.js";
+import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
export function resolveControlUiRepoRoot(
@@ -76,7 +76,7 @@ export async function ensureControlUiAssetsBuilt(
return {
ok: false,
built: false,
- message: `${hint}. Build them with \`pnpm ui:build\`.`,
+ message: `${hint}. Build them with \`bun run ui:build\`.`,
};
}
@@ -85,35 +85,28 @@ export async function ensureControlUiAssetsBuilt(
return { ok: true, built: false };
}
- const pnpmWhich = process.platform === "win32" ? "where" : "which";
- const pnpm = await runExec(pnpmWhich, ["pnpm"])
- .then(
- (r) =>
- r.stdout
- .split(/\r?\n/g)
- .map((l) => l.trim())
- .find(Boolean) ?? "",
- )
- .catch(() => "");
- if (!pnpm) {
+ const uiScript = path.join(repoRoot, "scripts", "ui.js");
+ if (!fs.existsSync(uiScript)) {
return {
ok: false,
built: false,
- message:
- "Control UI assets not found and pnpm missing. Install pnpm, then run `pnpm ui:build`.",
+ message: `Control UI assets missing but ${uiScript} is unavailable.`,
};
}
- runtime.log("Control UI assets missing; building (pnpm ui:build)…");
+ runtime.log("Control UI assets missing; building (ui:build)…");
const ensureInstalled = !fs.existsSync(
path.join(repoRoot, "ui", "node_modules"),
);
if (ensureInstalled) {
- const install = await runCommandWithTimeout([pnpm, "ui:install"], {
- cwd: repoRoot,
- timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
- });
+ const install = await runCommandWithTimeout(
+ [process.execPath, uiScript, "install"],
+ {
+ cwd: repoRoot,
+ timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
+ },
+ );
if (install.code !== 0) {
return {
ok: false,
@@ -123,10 +116,13 @@ export async function ensureControlUiAssetsBuilt(
}
}
- const build = await runCommandWithTimeout([pnpm, "ui:build"], {
- cwd: repoRoot,
- timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
- });
+ const build = await runCommandWithTimeout(
+ [process.execPath, uiScript, "build"],
+ {
+ cwd: repoRoot,
+ timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
+ },
+ );
if (build.code !== 0) {
return {
ok: false,
diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts
index d157c7cf3..539e4497e 100644
--- a/src/wizard/onboarding.ts
+++ b/src/wizard/onboarding.ts
@@ -52,6 +52,10 @@ import type {
OnboardOptions,
ResetScope,
} from "../commands/onboard-types.js";
+import {
+ applyOpenAICodexModelDefault,
+ OPENAI_CODEX_DEFAULT_MODEL,
+} from "../commands/openai-codex-model-default.js";
import { ensureSystemdUserLingerInteractive } from "../commands/systemd-linger.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
@@ -60,7 +64,6 @@ import {
resolveGatewayPort,
writeConfigFile,
} from "../config/config.js";
-import type { AgentModelListConfig } from "../config/types.js";
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { resolveGatewayService } from "../daemon/service.js";
@@ -70,50 +73,6 @@ import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, sleep } from "../utils.js";
import type { WizardPrompter } from "./prompts.js";
-const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";
-
-function shouldSetOpenAICodexModel(model?: string): boolean {
- const trimmed = model?.trim();
- if (!trimmed) return true;
- const normalized = trimmed.toLowerCase();
- if (normalized.startsWith("openai-codex/")) return false;
- if (normalized.startsWith("openai/")) return true;
- return normalized === "gpt" || normalized === "gpt-mini";
-}
-
-function resolvePrimaryModel(
- model?: AgentModelListConfig | string,
-): string | undefined {
- if (typeof model === "string") return model;
- if (model && typeof model === "object" && typeof model.primary === "string") {
- return model.primary;
- }
- return undefined;
-}
-
-function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
- next: ClawdbotConfig;
- changed: boolean;
-} {
- const current = resolvePrimaryModel(cfg.agent?.model);
- if (!shouldSetOpenAICodexModel(current)) {
- return { next: cfg, changed: false };
- }
- return {
- next: {
- ...cfg,
- agent: {
- ...cfg.agent,
- model:
- cfg.agent?.model && typeof cfg.agent.model === "object"
- ? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
- : { primary: OPENAI_CODEX_DEFAULT_MODEL },
- },
- },
- changed: true,
- };
-}
-
async function warnIfModelConfigLooksOff(
config: ClawdbotConfig,
prompter: WizardPrompter,
diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css
index f47b34450..616f7f5e1 100644
--- a/ui/src/styles/components.css
+++ b/ui/src/styles/components.css
@@ -825,6 +825,13 @@
border-top: 1px solid var(--border);
}
+.shell--chat-focus .chat-compose {
+ bottom: var(--shell-pad);
+ padding-bottom: calc(var(--shell-pad) + env(safe-area-inset-bottom, 0px));
+ border-bottom-left-radius: 18px;
+ border-bottom-right-radius: 18px;
+}
+
.chat-compose__field {
gap: 4px;
}