Merge remote-tracking branch 'origin/main' into fix/dockerfile-install-bun
This commit is contained in:
commit
9c13819f39
@ -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.
|
||||
|
||||
200
README.md
200
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:
|
||||
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="djangonavarro220" title="djangonavarro220"/></a>
|
||||
<a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a>
|
||||
<a href="https://github.com/adamgall"><img src="https://avatars.githubusercontent.com/u/706929?v=4&s=48" width="48" height="48" alt="adamgall" title="adamgall"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/regenrek"><img src="https://avatars.githubusercontent.com/u/5182020?v=4&s=48" width="48" height="48" alt="regenrek" title="regenrek"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="tobiasbischoff" title="tobiasbischoff"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a>
|
||||
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a>
|
||||
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a>
|
||||
</p>
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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`).
|
||||
|
||||
@ -19,7 +19,7 @@ read_when:
|
||||
<p align="center">
|
||||
<a href="https://github.com/clawdbot/clawdbot">GitHub</a> ·
|
||||
<a href="https://github.com/clawdbot/clawdbot/releases">Releases</a> ·
|
||||
<a href="https://docs.clawdbot.com/">Docs</a> ·
|
||||
<a href="https://github.com/clawdbot/clawdbot/blob/main/docs/index.md">Docs</a> ·
|
||||
<a href="./clawd.md">Clawd setup</a>
|
||||
</p>
|
||||
|
||||
|
||||
@ -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:<uuid>` 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
102
scripts/ui.js
Normal file
102
scripts/ui.js
Normal file
@ -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 <install|dev|build|test> [...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]);
|
||||
}
|
||||
@ -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", () => {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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] : []),
|
||||
];
|
||||
|
||||
@ -556,6 +556,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
browserControlUrl: sandbox?.browser?.controlUrl,
|
||||
agentSessionKey: options?.sessionKey,
|
||||
agentSurface: options?.surface,
|
||||
sandboxed: !!sandbox,
|
||||
config: options?.config,
|
||||
}),
|
||||
];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -17,7 +17,37 @@ const SessionsHistoryToolSchema = Type.Object({
|
||||
includeTools: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
export function createSessionsHistoryTool(): AnyAgentTool {
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
async function isSpawnedSessionAllowed(params: {
|
||||
requesterSessionKey: string;
|
||||
targetSessionKey: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const list = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
spawnedBy: params.requesterSessionKey,
|
||||
},
|
||||
})) as { sessions?: Array<Record<string, unknown>> };
|
||||
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))
|
||||
|
||||
@ -44,7 +44,16 @@ const SessionsListToolSchema = Type.Object({
|
||||
messageLimit: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
});
|
||||
|
||||
export function createSessionsListTool(): AnyAgentTool {
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
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<string, unknown>;
|
||||
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;
|
||||
|
||||
@ -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<Record<string, unknown>> };
|
||||
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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<string> | 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(
|
||||
|
||||
@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
43
src/commands/openai-codex-model-default.test.ts
Normal file
43
src/commands/openai-codex-model-default.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
46
src/commands/openai-codex-model-default.ts
Normal file
46
src/commands/openai-codex-model-default.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -77,6 +77,8 @@ export type AgentElevatedAllowFromConfig = {
|
||||
};
|
||||
|
||||
export type WhatsAppConfig = {
|
||||
/** Optional per-account WhatsApp configuration (multi-account). */
|
||||
accounts?: Record<string, WhatsAppAccountConfig>;
|
||||
/** 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. */
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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()]),
|
||||
),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user