Merge origin/main into pr-1432

This commit is contained in:
Peter Steinberger 2026-01-23 03:35:16 +00:00
commit 9a9afb389a
382 changed files with 36368 additions and 5138 deletions

View File

@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot
### Changes ### Changes
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster - Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.clawd.bot/tools/lobster
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. - Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. - Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting - Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
@ -21,26 +22,28 @@ Docs: https://docs.clawd.bot
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert. - **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert.
### Fixes ### Fixes
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
- Agents: surface concrete API error details instead of generic AI service errors.
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
- Agents: make tool summaries more readable and only show optional params when set.
- Agents: centralize transcript sanitization in the runner; keep <final> tags and error turns intact.
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik. - Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff. - Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
- Media: accept MEDIA paths with spaces/tilde and prefer the message tool hint for image replies. - Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.
- Google Antigravity: drop unsigned thinking blocks for Claude models to avoid signature errors. - Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
- Config: avoid stack traces for invalid configs and log the config path. - macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47. - Providers: improve GitHub Copilot integration (enterprise support, base URL, and auth flow alignment).
- Doctor: avoid recreating WhatsApp config when only legacy routing keys remain. (#900)
- Doctor: warn when gateway.mode is unset with configure/config guidance. ## 2026.1.21-2
- OpenCode Zen: route models to the Zen API shape per family so proxy endpoints are used. (#1416)
- Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat. ### Fixes
- Logs: align rolling log filenames with local time and fall back to latest file when today's log is missing. (#1343) - Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
- Models: inherit session model overrides in thread/topic sessions (Telegram topics, Slack/Discord threads). (#1376) - Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447)
- macOS: keep local auto bind loopback-first; only use tailnet when bind=tailnet.
- macOS: include Textual syntax highlighting resources in packaged app to prevent chat crashes. (#1362)
- macOS: keep chat pinned to bottom during streaming replies. (#1279)
- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
- Exec approvals: treat main as the default agent + migrate legacy default allowlists. (#1417) Thanks @czekaj.
- Exec: avoid defaulting to elevated mode when elevated is not allowed.
- Exec approvals: align node/gateway allowlist prechecks and approval gating; avoid null optional params in approval requests. (#1425) Thanks @czekaj.
- UI: refresh debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
## 2026.1.21 ## 2026.1.21

View File

@ -2,6 +2,80 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0"> <rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel> <channel>
<title>Clawdbot</title> <title>Clawdbot</title>
<item>
<title>2026.1.21</title>
<pubDate>Thu, 22 Jan 2026 12:22:35 +0000</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>7374</sparkle:version>
<sparkle:shortVersionString>2026.1.21</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.21</h2>
<h3>Highlights</h3>
<ul>
<li>Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster</li>
<li>Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui</li>
<li>Cache optimizations: cache-ttl pruning + defaults reduce token spend on cold requests. https://docs.clawd.bot/concepts/session-pruning</li>
<li>Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated</li>
<li>Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams</li>
<li><code>/models</code> UX refresh + <code>clawdbot update wizard</code>. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster (#1152) Thanks @vignesh07.</li>
<li>Agents/UI: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. https://docs.clawd.bot/gateway/configuration https://docs.clawd.bot/cli/agents</li>
<li>Control UI: add custom assistant identity support and per-session identity display. (#1420) Thanks @robbyczgw-cla. https://docs.clawd.bot/web/control-ui</li>
<li>CLI: add <code>clawdbot update wizard</code> with interactive channel selection + restart prompts, plus preflight checks before rebasing. https://docs.clawd.bot/cli/update</li>
<li>Models/Commands: add <code>/models</code>, improve <code>/model</code> listing UX, and expand <code>clawdbot models</code> paging. (#1398) Thanks @vignesh07. https://docs.clawd.bot/cli/models</li>
<li>CLI: move gateway service commands under <code>clawdbot gateway</code>, flatten node service commands under <code>clawdbot node</code>, and add <code>gateway probe</code> for reachability. https://docs.clawd.bot/cli/gateway https://docs.clawd.bot/cli/node</li>
<li>Exec: add elevated ask/full modes, tighten allowlist gating, and render approvals tables on write. https://docs.clawd.bot/tools/elevated https://docs.clawd.bot/tools/exec-approvals</li>
<li>Exec approvals: default to local host, add gateway/node targeting + target details, support wildcard agent allowlists, and tighten allowlist parsing/safe bins. https://docs.clawd.bot/cli/approvals https://docs.clawd.bot/tools/exec-approvals</li>
<li>Heartbeat: allow explicit session keys and active hours. (#1256) Thanks @zknicker. https://docs.clawd.bot/gateway/heartbeat</li>
<li>Sessions: add per-channel idle durations via <code>sessions.channelIdleMinutes</code>. (#1353) Thanks @cash-echo-bot.</li>
<li>Nodes: run exec-style, expose PATH in status/describe, and bootstrap PATH for node-host execution. https://docs.clawd.bot/cli/node</li>
<li>Cache: add <code>cache.ttlPrune</code> mode and auth-aware defaults for cache TTL behavior.</li>
<li>Queue: add per-channel debounce overrides for auto-reply. https://docs.clawd.bot/concepts/queue</li>
<li>Discord: add wildcard channel config support. (#1334) Thanks @pvoo. https://docs.clawd.bot/channels/discord</li>
<li>Signal: add typing indicators and DM read receipts via signal-cli. https://docs.clawd.bot/channels/signal</li>
<li>MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. https://docs.clawd.bot/channels/msteams</li>
<li>Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).</li>
<li>macOS: refresh Settings (location access in Permissions, connection mode in menu, remove CLI install UI).</li>
<li>Diagnostics: add cache trace config for debugging. (#1370) Thanks @parubets.</li>
<li>Docs: Lobster guides + org URL updates, /model allowlist troubleshooting, Gmail message search examples, gateway.mode troubleshooting, prompt injection guidance, npm prefix/node CLI notes, control UI dev gatewayUrl note, tool_use FAQ, showcase video, and sharp/node-gyp workaround. (#1427, #1220, #1405) Thanks @vignesh07, @mbelinky.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set <code>gateway.controlUi.allowInsecureAuth: true</code> to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http</li>
<li><strong>BREAKING:</strong> Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Streaming/Typing/Media: keep reply tags across streamed chunks, start typing indicators at run start, and accept MEDIA paths with spaces/tilde while preferring the message tool hint for image replies.</li>
<li>Agents/Providers: drop unsigned thinking blocks for Claude models (Google Antigravity) and enforce alphanumeric tool call ids for strict providers (Mistral/OpenRouter). (#1372) Thanks @zerone0x.</li>
<li>Exec approvals: treat main as the default agent, align node/gateway allowlist prechecks, validate resolved paths, avoid allowlist resolve races, and avoid null optional params. (#1417, #1414, #1425) Thanks @czekaj.</li>
<li>Exec/Windows: resolve Windows exec paths with extensions and handle safe-bin exe names.</li>
<li>Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.</li>
<li>Gateway: prevent multiple gateways from sharing the same config/state (singleton lock), keep auto bind loopback-first with explicit tailnet binding, and improve SSH auth handling. (#1380)</li>
<li>Control UI: remove the chat stop button, keep the composer aligned to the bottom edge, stabilize session previews, and refresh the debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.</li>
<li>UI/config: export <code>SECTION_META</code> for config form modules. (#1418) Thanks @MaudeBot.</li>
<li>macOS: keep chat pinned during streaming replies, include Textual resources, respect wildcard exec approvals, allow SSH agent auth, and default distribution builds to universal binaries. (#1279, #1362, #1384, #1396) Thanks @ameno-, @JustYannicc.</li>
<li>BlueBubbles: resolve short message IDs safely, expose full IDs in templates, and harden short-id fetch wrappers. (#1369, #1387) Thanks @tyler6204.</li>
<li>Models/Configure: inherit session model overrides in threads/topics, map OpenCode Zen models to the correct APIs, narrow Anthropic OAuth allowlist handling, seed allowlist fallbacks, list the full catalog when no allowlist is set, and limit <code>/model</code> list output. (#1376, #1416)</li>
<li>Memory: prevent CLI hangs by deferring vector probes, add sqlite-vec/embedding timeouts, and make session memory indexing async.</li>
<li>Cron: cap reminder context history to 10 messages and honor <code>contextMessages</code>. (#1103) Thanks @mkbehr.</li>
<li>Cache: restore the 1h cache TTL option and reset the pruning window.</li>
<li>Zalo Personal: tolerate ANSI/log-prefixed JSON output from <code>zca</code>. (#1379) Thanks @ptn1411.</li>
<li>Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.</li>
<li>Infra: preserve fetch helper methods/preconnect when wrapping abort signals and normalize Telegram fetch aborts.</li>
<li>Config/Doctor: avoid stack traces for invalid configs, log the config path, avoid WhatsApp config resurrection, and warn when <code>gateway.mode</code> is unset. (#900)</li>
<li>CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.</li>
<li>Logs/Status: align rolling log filenames with local time and report sandboxed runtime in <code>clawdbot status</code>. (#1343)</li>
<li>Embedded runner: persist injected history images so attachments arent reloaded each turn. (#1374) Thanks @Nicell.</li>
<li>Nodes/Subagents: include agent/node/gateway context in tool failure logs and ensure subagent list uses the command session.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="22284796" type="application/octet-stream" sparkle:edSignature="pXji4NMA/cu35iMxln385d6LnsT4yIZtFtFiR7sIimKeSC2CsyeWzzSD0EhJsN98PdSoy69iEFZt4I2ZtNCECg=="/>
</item>
<item> <item>
<title>2026.1.21</title> <title>2026.1.21</title>
<pubDate>Wed, 21 Jan 2026 08:18:22 +0000</pubDate> <pubDate>Wed, 21 Jan 2026 08:18:22 +0000</pubDate>
@ -208,86 +282,5 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
]]></description> ]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip" length="21399591" type="application/octet-stream" sparkle:edSignature="zelT+KzN32cXsihbFniPF5Heq0hkwFfL3Agrh/AaoKUkr7kJAFarkGSOZRTWZ9y+DvOluzn2wHHjVigRjMzrBA=="/> <enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip" length="21399591" type="application/octet-stream" sparkle:edSignature="zelT+KzN32cXsihbFniPF5Heq0hkwFfL3Agrh/AaoKUkr7kJAFarkGSOZRTWZ9y+DvOluzn2wHHjVigRjMzrBA=="/>
</item> </item>
<item>
<title>2026.1.15</title>
<pubDate>Fri, 16 Jan 2026 10:31:53 +0000</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>5998</sparkle:version>
<sparkle:shortVersionString>2026.1.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.15</h2>
<h3>Highlights</h3>
<ul>
<li>Plugins: add provider auth registry + <code>clawdbot models auth login</code> for plugin-driven OAuth/API key flows.</li>
<li>Browser: improve remote CDP/Browserless support (auth passthrough, <code>wss</code> upgrade, timeouts, clearer errors).</li>
<li>Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.</li>
<li>Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)</li>
<li><strong>BREAKING:</strong> Microsoft Teams is now a plugin; install <code>@clawdbot/msteams</code> via <code>clawdbot plugins install @clawdbot/msteams</code>.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>CLI: set process titles to <code>clawdbot-<command></code> for clearer process listings.</li>
<li>CLI/macOS: sync remote SSH target/identity to config and let <code>gateway status</code> auto-infer SSH targets (ssh-config aware).</li>
<li>Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.</li>
<li>Sessions/Security: add <code>session.dmScope</code> for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.</li>
<li>Plugins: add provider auth registry + <code>clawdbot models auth login</code> for plugin-driven OAuth/API key flows.</li>
<li>Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.</li>
<li>TUI: show provider/model labels for the active session and default model.</li>
<li>Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.</li>
<li>UI: show gateway auth guidance + doc link on unauthorized Control UI connections.</li>
<li>Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in <code>clawdbot security audit</code>.</li>
<li>Apps: store node auth tokens encrypted (Keychain/SecurePrefs).</li>
<li>Daemon: share profile/state-dir resolution across service helpers and honor <code>CLAWDBOT_STATE_DIR</code> for Windows task scripts.</li>
<li>Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.</li>
<li>Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).</li>
<li>Tools: normalize Slack/Discord message timestamps with <code>timestampMs</code>/<code>timestampUtc</code> while keeping raw provider fields.</li>
<li>macOS: add <code>system.which</code> for prompt-free remote skill discovery (with gateway fallback to <code>system.run</code>).</li>
<li>Docs: add Date & Time guide and update prompt/timezone configuration docs.</li>
<li>Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.</li>
<li>Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.</li>
<li>Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in <code>/status</code> and <code>clawdbot models status</code>, and update docs.</li>
<li>CLI: add <code>--json</code> output for <code>clawdbot daemon</code> lifecycle/install commands.</li>
<li>Memory: make <code>node-llama-cpp</code> an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.</li>
<li>Browser: add <code>snapshot refs=aria</code> (Playwright aria-ref ids) for self-resolving refs across <code>snapshot</code><code>act</code>.</li>
<li>Browser: <code>profile="chrome"</code> now defaults to host control and returns clearer “attach a tab” errors.</li>
<li>Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.</li>
<li>Browser: increase remote CDP reachability timeouts + add <code>remoteCdpTimeoutMs</code>/<code>remoteCdpHandshakeTimeoutMs</code>.</li>
<li>Browser: preserve auth/query tokens for remote CDP endpoints and pass Basic auth for CDP HTTP/WS. (#895) — thanks @mukhtharcm.</li>
<li>Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.</li>
<li>Telegram: allow custom commands in the bot menu (merged with native; conflicts ignored). (#860) — thanks @nachoiacovino.</li>
<li>Discord: allow allowlisted guilds without channel lists to receive messages when <code>groupPolicy="allowlist"</code>. — thanks @thewilloftheshadow.</li>
<li>Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.</li>
<li>Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.</li>
<li>Fix: persist <code>gateway.mode=local</code> after selecting Local run mode in <code>clawdbot configure</code>, even if no other sections are chosen.</li>
<li>Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.</li>
<li>Agents: avoid false positives when logging unsupported Google tool schema keywords.</li>
<li>Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm.</li>
<li>Status: restore usage summary line for current provider when no OAuth profiles exist.</li>
<li>Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.</li>
<li>Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.</li>
<li>Fix: clean up suspended CLI processes across backends. (#978) — thanks @Nachx639.</li>
<li>Fix: support MiniMax coding plan usage responses with <code>model_remains</code>/<code>current_interval_*</code> payloads.</li>
<li>Fix: suppress WhatsApp pairing replies for historical catch-up DMs on initial link. (#904)</li>
<li>Browser: extension mode recovers when only one tab is attached (stale targetId fallback).</li>
<li>Browser: fix <code>tab not found</code> for extension relay snapshots/actions when Playwright blocks <code>newCDPSession</code> (use the single available Page).</li>
<li>Browser: upgrade <code>ws</code><code>wss</code> when remote CDP uses <code>https</code> (fixes Browserless handshake).</li>
<li>Telegram: skip <code>message_thread_id=1</code> for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.</li>
<li>Fix: sanitize user-facing error text + strip <code><final></code> tags across reply pipelines. (#975) — thanks @ThomsenDrake.</li>
<li>Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.</li>
<li>Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.</li>
<li>Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.15/Clawdbot-2026.1.15.zip" length="12127276" type="application/octet-stream" sparkle:edSignature="o79vwTbtW/d91NQFRVfUDhsv6D4zIw7IkhY0N1iLImMu94BURgLcecA6z7Smy3bMobPwOyzN8yfm6mA/Rt8FCA=="/>
</item>
</channel> </channel>
</rss> </rss>

View File

@ -16,6 +16,8 @@ struct DebugSettings: View {
@State private var modelsError: String? @State private var modelsError: String?
private let gatewayManager = GatewayProcessManager.shared private let gatewayManager = GatewayProcessManager.shared
private let healthStore = HealthStore.shared private let healthStore = HealthStore.shared
@State private var launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
@State private var launchAgentWriteError: String?
@State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath() @State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath()
@State private var sessionStorePath: String = SessionLoader.defaultStorePath @State private var sessionStorePath: String = SessionLoader.defaultStorePath
@State private var sessionStoreSaveError: String? @State private var sessionStoreSaveError: String?
@ -47,6 +49,7 @@ struct DebugSettings: View {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
self.header self.header
self.launchdSection
self.appInfoSection self.appInfoSection
self.gatewaySection self.gatewaySection
self.logsSection self.logsSection
@ -79,6 +82,39 @@ struct DebugSettings: View {
} }
} }
private var launchdSection: some View {
GroupBox("Gateway startup") {
VStack(alignment: .leading, spacing: 8) {
Toggle("Attach only (skip launchd install)", isOn: self.$launchAgentWriteDisabled)
.onChange(of: self.launchAgentWriteDisabled) { _, newValue in
self.launchAgentWriteError = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(newValue)
if self.launchAgentWriteError != nil {
self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
return
}
if newValue {
Task {
_ = await GatewayLaunchAgentManager.set(
enabled: false,
bundlePath: Bundle.main.bundlePath,
port: GatewayEnvironment.gatewayPort())
}
}
}
Text("When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). It will only attach to an existing Gateway.")
.font(.caption)
.foregroundStyle(.secondary)
if let launchAgentWriteError {
Text(launchAgentWriteError)
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
private var header: some View { private var header: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text("Debug") Text("Debug")

View File

@ -475,8 +475,8 @@ enum ExecApprovalsStore {
private static func mergeAgents( private static func mergeAgents(
current: ExecApprovalsAgent, current: ExecApprovalsAgent,
legacy: ExecApprovalsAgent legacy: ExecApprovalsAgent) -> ExecApprovalsAgent
) -> ExecApprovalsAgent { {
var seen = Set<String>() var seen = Set<String>()
var allowlist: [ExecAllowlistEntry] = [] var allowlist: [ExecAllowlistEntry] = []
func append(_ entry: ExecAllowlistEntry) { func append(_ entry: ExecAllowlistEntry) {
@ -486,8 +486,12 @@ enum ExecApprovalsStore {
seen.insert(key) seen.insert(key)
allowlist.append(entry) allowlist.append(entry)
} }
for entry in current.allowlist ?? [] { append(entry) } for entry in current.allowlist ?? [] {
for entry in legacy.allowlist ?? [] { append(entry) } append(entry)
}
for entry in legacy.allowlist ?? [] {
append(entry)
}
return ExecApprovalsAgent( return ExecApprovalsAgent(
security: current.security ?? legacy.security, security: current.security ?? legacy.security,

View File

@ -1,5 +1,6 @@
import ClawdbotKit import ClawdbotKit
import ClawdbotProtocol import ClawdbotProtocol
import CoreGraphics
import Foundation import Foundation
import OSLog import OSLog
@ -44,6 +45,7 @@ final class ExecApprovalsGatewayPrompter {
do { do {
let data = try JSONEncoder().encode(payload) let data = try JSONEncoder().encode(payload)
let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data) let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data)
guard self.shouldPresent(request: request) else { return }
let decision = ExecApprovalsPromptPresenter.prompt(request.request) let decision = ExecApprovalsPromptPresenter.prompt(request.request)
try await GatewayConnection.shared.requestVoid( try await GatewayConnection.shared.requestVoid(
method: .execApprovalResolve, method: .execApprovalResolve,
@ -56,4 +58,66 @@ final class ExecApprovalsGatewayPrompter {
self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)") self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)")
} }
} }
private func shouldPresent(request: GatewayApprovalRequest) -> Bool {
let mode = AppStateStore.shared.connectionMode
let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
return Self.shouldPresent(
mode: mode,
activeSession: activeSession,
requestSession: requestSession,
lastInputSeconds: Self.lastInputSeconds(),
thresholdSeconds: 120)
}
private static func shouldPresent(
mode: AppState.ConnectionMode,
activeSession: String?,
requestSession: String?,
lastInputSeconds: Int?,
thresholdSeconds: Int) -> Bool
{
let active = activeSession?.trimmingCharacters(in: .whitespacesAndNewlines)
let requested = requestSession?.trimmingCharacters(in: .whitespacesAndNewlines)
let recentlyActive = lastInputSeconds.map { $0 <= thresholdSeconds } ?? (mode == .local)
if let session = requested, !session.isEmpty {
if let active, !active.isEmpty {
return active == session
}
return recentlyActive
}
if let active, !active.isEmpty {
return true
}
return mode == .local
}
private static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
return Int(seconds.rounded())
}
} }
#if DEBUG
extension ExecApprovalsGatewayPrompter {
static func _testShouldPresent(
mode: AppState.ConnectionMode,
activeSession: String?,
requestSession: String?,
lastInputSeconds: Int?,
thresholdSeconds: Int = 120) -> Bool
{
self.shouldPresent(
mode: mode,
activeSession: activeSession,
requestSession: requestSession,
lastInputSeconds: lastInputSeconds,
thresholdSeconds: thresholdSeconds)
}
}
#endif

View File

@ -13,6 +13,7 @@ struct ExecApprovalPromptRequest: Codable, Sendable {
var ask: String? var ask: String?
var agentId: String? var agentId: String?
var resolvedPath: String? var resolvedPath: String?
var sessionKey: String?
} }
private struct ExecApprovalSocketRequest: Codable { private struct ExecApprovalSocketRequest: Codable {
@ -412,7 +413,8 @@ private enum ExecHostExecutor {
security: context.security.rawValue, security: context.security.rawValue,
ask: context.ask.rawValue, ask: context.ask.rawValue,
agentId: context.trimmedAgent, agentId: context.trimmedAgent,
resolvedPath: context.resolution?.resolvedPath)) resolvedPath: context.resolution?.resolvedPath,
sessionKey: request.sessionKey))
switch decision { switch decision {
case .deny: case .deny:

View File

@ -69,6 +69,7 @@ actor GatewayConnection {
case channelsLogout = "channels.logout" case channelsLogout = "channels.logout"
case modelsList = "models.list" case modelsList = "models.list"
case chatHistory = "chat.history" case chatHistory = "chat.history"
case sessionsPreview = "sessions.preview"
case chatSend = "chat.send" case chatSend = "chat.send"
case chatAbort = "chat.abort" case chatAbort = "chat.abort"
case skillsStatus = "skills.status" case skillsStatus = "skills.status"
@ -540,6 +541,30 @@ extension GatewayConnection {
return try await self.requestDecoded(method: .skillsUpdate, params: params) return try await self.requestDecoded(method: .skillsUpdate, params: params)
} }
// MARK: - Sessions
func sessionsPreview(
keys: [String],
limit: Int? = nil,
maxChars: Int? = nil,
timeoutMs: Int? = nil) async throws -> ClawdbotSessionsPreviewPayload
{
let resolvedKeys = keys
.map { self.canonicalizeSessionKey($0) }
.filter { !$0.isEmpty }
if resolvedKeys.isEmpty {
return ClawdbotSessionsPreviewPayload(ts: 0, previews: [])
}
var params: [String: AnyCodable] = ["keys": AnyCodable(resolvedKeys)]
if let limit { params["limit"] = AnyCodable(limit) }
if let maxChars { params["maxChars"] = AnyCodable(maxChars) }
let timeout = timeoutMs.map { Double($0) }
return try await self.requestDecoded(
method: .sessionsPreview,
params: params,
timeoutMs: timeout)
}
// MARK: - Chat // MARK: - Chat
func chatHistory( func chatHistory(

View File

@ -560,13 +560,13 @@ actor GatewayEndpointStore {
{ {
switch bindMode { switch bindMode {
case "tailnet": case "tailnet":
return tailscaleIP ?? "127.0.0.1" tailscaleIP ?? "127.0.0.1"
case "auto": case "auto":
return "127.0.0.1" "127.0.0.1"
case "custom": case "custom":
return customBindHost ?? "127.0.0.1" customBindHost ?? "127.0.0.1"
default: default:
return "127.0.0.1" "127.0.0.1"
} }
} }
} }

View File

@ -4,11 +4,46 @@ enum GatewayLaunchAgentManager {
private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.launchd") private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.launchd")
private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent" private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent"
private static var disableLaunchAgentMarkerURL: URL {
FileManager().homeDirectoryForCurrentUser
.appendingPathComponent(self.disableLaunchAgentMarker)
}
private static var plistURL: URL { private static var plistURL: URL {
FileManager().homeDirectoryForCurrentUser FileManager().homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist") .appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
} }
static func isLaunchAgentWriteDisabled() -> Bool {
FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path)
}
static func setLaunchAgentWriteDisabled(_ disabled: Bool) -> String? {
let marker = self.disableLaunchAgentMarkerURL
if disabled {
do {
try FileManager().createDirectory(
at: marker.deletingLastPathComponent(),
withIntermediateDirectories: true)
if !FileManager().fileExists(atPath: marker.path) {
FileManager().createFile(atPath: marker.path, contents: nil)
}
} catch {
return error.localizedDescription
}
return nil
}
if FileManager().fileExists(atPath: marker.path) {
do {
try FileManager().removeItem(at: marker)
} catch {
return error.localizedDescription
}
}
return nil
}
static func isLoaded() async -> Bool { static func isLoaded() async -> Bool {
guard let loaded = await self.readDaemonLoaded() else { return false } guard let loaded = await self.readDaemonLoaded() else { return false }
return loaded return loaded
@ -66,12 +101,6 @@ enum GatewayLaunchAgentManager {
} }
extension GatewayLaunchAgentManager { extension GatewayLaunchAgentManager {
private static func isLaunchAgentWriteDisabled() -> Bool {
let marker = FileManager().homeDirectoryForCurrentUser
.appendingPathComponent(self.disableLaunchAgentMarker)
return FileManager().fileExists(atPath: marker.path)
}
private static func readDaemonLoaded() async -> Bool? { private static func readDaemonLoaded() async -> Bool? {
let result = await self.runDaemonCommandResult( let result = await self.runDaemonCommandResult(
["status", "--json", "--no-probe"], ["status", "--json", "--no-probe"],

View File

@ -79,6 +79,11 @@ final class GatewayProcessManager {
func ensureLaunchAgentEnabledIfNeeded() async { func ensureLaunchAgentEnabledIfNeeded() async {
guard !CommandResolver.connectionModeIsRemote() else { return } guard !CommandResolver.connectionModeIsRemote() else { return }
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
self.appendLog("[gateway] launchd auto-enable skipped (attach-only)\n")
self.logger.info("gateway launchd auto-enable skipped (disable marker set)")
return
}
let enabled = await GatewayLaunchAgentManager.isLoaded() let enabled = await GatewayLaunchAgentManager.isLoaded()
guard !enabled else { return } guard !enabled else { return }
let bundlePath = Bundle.main.bundleURL.path let bundlePath = Bundle.main.bundleURL.path
@ -237,13 +242,12 @@ final class GatewayProcessManager {
private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String { private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String {
let instanceText = instance ?? "pid unknown" let instanceText = instance ?? "pid unknown"
if let snap { if let snap {
let linkId = snap.channelOrder?.first(where: { let order = snap.channelOrder ?? Array(snap.channels.keys)
if let summary = snap.channels[$0] { return summary.linked != nil } let linkId = order.first(where: { snap.channels[$0]?.linked == true })
return false ?? order.first(where: { snap.channels[$0]?.linked != nil })
}) ?? snap.channels.keys.first(where: { guard let linkId else {
if let summary = snap.channels[$0] { return summary.linked != nil } return "port \(port), health probe succeeded, \(instanceText)"
return false }
})
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age" let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
let label = let label =
@ -308,6 +312,15 @@ final class GatewayProcessManager {
return return
} }
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
let message = "Launchd disabled; start the Gateway manually or disable attach-only."
self.status = .failed(message)
self.lastFailureReason = "launchd disabled"
self.appendLog("[gateway] launchd disabled; skipping auto-start\n")
self.logger.info("gateway launchd enable skipped (disable marker set)")
return
}
let bundlePath = Bundle.main.bundleURL.path let bundlePath = Bundle.main.bundleURL.path
let port = GatewayEnvironment.gatewayPort() let port = GatewayEnvironment.gatewayPort()
self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n") self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n")

View File

@ -166,6 +166,11 @@ final class HealthStore {
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)? _ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
{ {
let order = snap.channelOrder ?? Array(snap.channels.keys) let order = snap.channelOrder ?? Array(snap.channels.keys)
for id in order {
if let summary = snap.channels[id], summary.linked == true {
return (id: id, summary: summary)
}
}
for id in order { for id in order {
if let summary = snap.channels[id], summary.linked != nil { if let summary = snap.channels[id], summary.linked != nil {
return (id: id, summary: summary) return (id: id, summary: summary)

View File

@ -3,6 +3,7 @@ import Darwin
import Foundation import Foundation
import MenuBarExtraAccess import MenuBarExtraAccess
import Observation import Observation
import OSLog
import Security import Security
import SwiftUI import SwiftUI
@ -10,6 +11,7 @@ import SwiftUI
struct ClawdbotApp: App { struct ClawdbotApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
@State private var state: AppState @State private var state: AppState
private static let logger = Logger(subsystem: "com.clawdbot", category: "app")
private let gatewayManager = GatewayProcessManager.shared private let gatewayManager = GatewayProcessManager.shared
private let controlChannel = ControlChannel.shared private let controlChannel = ControlChannel.shared
private let activityStore = WorkActivityStore.shared private let activityStore = WorkActivityStore.shared
@ -31,6 +33,7 @@ struct ClawdbotApp: App {
init() { init() {
ClawdbotLogging.bootstrapIfNeeded() ClawdbotLogging.bootstrapIfNeeded()
Self.applyAttachOnlyOverrideIfNeeded()
_state = State(initialValue: AppStateStore.shared) _state = State(initialValue: AppStateStore.shared)
} }
@ -91,6 +94,22 @@ struct ClawdbotApp: App {
self.statusItem?.button?.appearsDisabled = paused || sleeping self.statusItem?.button?.appearsDisabled = paused || sleeping
} }
private static func applyAttachOnlyOverrideIfNeeded() {
let args = CommandLine.arguments
guard args.contains("--attach-only") || args.contains("--no-launchd") else { return }
if let error = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(true) {
Self.logger.error("attach-only flag failed: \(error, privacy: .public)")
return
}
Task {
_ = await GatewayLaunchAgentManager.set(
enabled: false,
bundlePath: Bundle.main.bundlePath,
port: GatewayEnvironment.gatewayPort())
}
Self.logger.info("attach-only flag enabled")
}
private var isGatewaySleeping: Bool { private var isGatewaySleeping: Bool {
if self.state.isPaused { return false } if self.state.isPaused { return false }
switch self.state.connectionMode { switch self.state.connectionMode {

View File

@ -1,4 +1,5 @@
import AppKit import AppKit
import Observation
import SwiftUI import SwiftUI
@MainActor @MainActor
@ -18,6 +19,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
private var isMenuOpen = false private var isMenuOpen = false
private var lastKnownMenuWidth: CGFloat? private var lastKnownMenuWidth: CGFloat?
private var menuOpenWidth: CGFloat? private var menuOpenWidth: CGFloat?
private var isObservingControlChannel = false
private var cachedSnapshot: SessionStoreSnapshot? private var cachedSnapshot: SessionStoreSnapshot?
private var cachedErrorText: String? private var cachedErrorText: String?
@ -50,6 +52,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.loadTask = Task { await self.refreshCache(force: true) } self.loadTask = Task { await self.refreshCache(force: true) }
} }
self.startControlChannelObservation()
self.nodesStore.start() self.nodesStore.start()
} }
@ -96,6 +99,50 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.cancelPreviewTasks() self.cancelPreviewTasks()
} }
private func startControlChannelObservation() {
guard !self.isObservingControlChannel else { return }
self.isObservingControlChannel = true
self.observeControlChannelState()
}
private func observeControlChannelState() {
withObservationTracking {
_ = ControlChannel.shared.state
} onChange: { [weak self] in
Task { @MainActor [weak self] in
guard let self else { return }
self.handleControlChannelStateChange()
self.observeControlChannelState()
}
}
}
private func handleControlChannelStateChange() {
guard self.isMenuOpen, let menu = self.statusItem?.menu else { return }
self.loadTask?.cancel()
self.loadTask = Task { [weak self, weak menu] in
guard let self, let menu else { return }
await self.refreshCache(force: true)
await self.refreshUsageCache(force: true)
await self.refreshCostUsageCache(force: true)
await MainActor.run {
guard self.isMenuOpen else { return }
self.inject(into: menu)
self.injectNodes(into: menu)
}
}
self.nodesLoadTask?.cancel()
self.nodesLoadTask = Task { [weak self, weak menu] in
guard let self, let menu else { return }
await self.nodesStore.refresh()
await MainActor.run {
guard self.isMenuOpen else { return }
self.injectNodes(into: menu)
}
}
}
func menuNeedsUpdate(_ menu: NSMenu) { func menuNeedsUpdate(_ menu: NSMenu) {
self.originalDelegate?.menuNeedsUpdate?(menu) self.originalDelegate?.menuNeedsUpdate?(menu)
} }
@ -141,11 +188,19 @@ extension MenuSessionsInjector {
if rhs.key == mainKey { return false } if rhs.key == mainKey { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast) return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
} }
if !rows.isEmpty {
let previewKeys = rows.prefix(20).map(\.key)
let task = Task {
await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)
}
self.previewTasks.append(task)
}
let headerItem = NSMenuItem() let headerItem = NSMenuItem()
headerItem.tag = self.tag headerItem.tag = self.tag
headerItem.isEnabled = false headerItem.isEnabled = false
let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState)) let statusText = self
.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
let hosted = self.makeHostedView( let hosted = self.makeHostedView(
rootView: AnyView(MenuSessionsHeaderView( rootView: AnyView(MenuSessionsHeaderView(
count: rows.count, count: rows.count,

View File

@ -679,7 +679,8 @@ actor MacNodeRuntime {
security: context.security.rawValue, security: context.security.rawValue,
ask: context.ask.rawValue, ask: context.ask.rawValue,
agentId: context.agentId, agentId: context.agentId,
resolvedPath: context.resolution?.resolvedPath)) resolvedPath: context.resolution?.resolvedPath,
sessionKey: context.sessionKey))
} }
switch decision { switch decision {
case .deny: case .deny:

View File

@ -1,5 +1,6 @@
import ClawdbotChatUI import ClawdbotChatUI
import ClawdbotKit import ClawdbotKit
import ClawdbotProtocol
import OSLog import OSLog
import SwiftUI import SwiftUI
@ -31,24 +32,24 @@ actor SessionPreviewCache {
static let shared = SessionPreviewCache() static let shared = SessionPreviewCache()
private struct CacheEntry { private struct CacheEntry {
let items: [SessionPreviewItem] let snapshot: SessionMenuPreviewSnapshot
let updatedAt: Date let updatedAt: Date
} }
private var entries: [String: CacheEntry] = [:] private var entries: [String: CacheEntry] = [:]
func cachedItems(for sessionKey: String, maxAge: TimeInterval) -> [SessionPreviewItem]? { func cachedSnapshot(for sessionKey: String, maxAge: TimeInterval) -> SessionMenuPreviewSnapshot? {
guard let entry = self.entries[sessionKey] else { return nil } guard let entry = self.entries[sessionKey] else { return nil }
guard Date().timeIntervalSince(entry.updatedAt) < maxAge else { return nil } guard Date().timeIntervalSince(entry.updatedAt) < maxAge else { return nil }
return entry.items return entry.snapshot
} }
func store(items: [SessionPreviewItem], for sessionKey: String) { func store(snapshot: SessionMenuPreviewSnapshot, for sessionKey: String) {
self.entries[sessionKey] = CacheEntry(items: items, updatedAt: Date()) self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: Date())
} }
func lastItems(for sessionKey: String) -> [SessionPreviewItem]? { func lastSnapshot(for sessionKey: String) -> SessionMenuPreviewSnapshot? {
self.entries[sessionKey]?.items self.entries[sessionKey]?.snapshot
} }
} }
@ -99,8 +100,12 @@ actor SessionPreviewLimiter {
#if DEBUG #if DEBUG
extension SessionPreviewCache { extension SessionPreviewCache {
func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) { func _testSet(
self.entries[sessionKey] = CacheEntry(items: items, updatedAt: updatedAt) snapshot: SessionMenuPreviewSnapshot,
for sessionKey: String,
updatedAt: Date = Date())
{
self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: updatedAt)
} }
func _testReset() { func _testReset() {
@ -219,50 +224,44 @@ enum SessionMenuPreviewLoader {
private static let logger = Logger(subsystem: "com.clawdbot", category: "SessionPreview") private static let logger = Logger(subsystem: "com.clawdbot", category: "SessionPreview")
private static let previewTimeoutSeconds: Double = 4 private static let previewTimeoutSeconds: Double = 4
private static let cacheMaxAgeSeconds: TimeInterval = 30 private static let cacheMaxAgeSeconds: TimeInterval = 30
private static let previewMaxChars = 240
private struct PreviewTimeoutError: LocalizedError { private struct PreviewTimeoutError: LocalizedError {
var errorDescription: String? { "preview timeout" } var errorDescription: String? { "preview timeout" }
} }
static func prewarm(sessionKeys: [String], maxItems: Int) async {
let keys = self.uniqueKeys(sessionKeys)
guard !keys.isEmpty else { return }
do {
let payload = try await self.requestPreview(keys: keys, maxItems: maxItems)
await self.cache(payload: payload, maxItems: maxItems)
} catch {
if self.isUnknownMethodError(error) { return }
let errorDescription = String(describing: error)
Self.logger.debug(
"Session preview prewarm failed count=\(keys.count, privacy: .public) " +
"error=\(errorDescription, privacy: .public)")
}
}
static func load(sessionKey: String, maxItems: Int) async -> SessionMenuPreviewSnapshot { static func load(sessionKey: String, maxItems: Int) async -> SessionMenuPreviewSnapshot {
if let cached = await SessionPreviewCache.shared.cachedItems(for: sessionKey, maxAge: cacheMaxAgeSeconds) { if let cached = await SessionPreviewCache.shared.cachedSnapshot(
return self.snapshot(from: cached) for: sessionKey,
} maxAge: cacheMaxAgeSeconds)
{
let isConnected = await MainActor.run { return cached
if case .connected = ControlChannel.shared.state { return true }
return false
}
guard isConnected else {
if let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey) {
return Self.snapshot(from: fallback)
}
return SessionMenuPreviewSnapshot(items: [], status: .error("Gateway disconnected"))
} }
do { do {
let timeoutMs = Int(self.previewTimeoutSeconds * 1000) let snapshot = try await self.fetchSnapshot(sessionKey: sessionKey, maxItems: maxItems)
let payload = try await SessionPreviewLimiter.shared.withPermit { await SessionPreviewCache.shared.store(snapshot: snapshot, for: sessionKey)
try await AsyncTimeout.withTimeout( return snapshot
seconds: self.previewTimeoutSeconds,
onTimeout: { PreviewTimeoutError() },
operation: {
try await GatewayConnection.shared.chatHistory(
sessionKey: sessionKey,
limit: self.previewLimit(for: maxItems),
timeoutMs: timeoutMs)
})
}
let built = Self.previewItems(from: payload, maxItems: maxItems)
await SessionPreviewCache.shared.store(items: built, for: sessionKey)
return Self.snapshot(from: built)
} catch is CancellationError { } catch is CancellationError {
return SessionMenuPreviewSnapshot(items: [], status: .loading) return SessionMenuPreviewSnapshot(items: [], status: .loading)
} catch { } catch {
let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey) if let fallback = await SessionPreviewCache.shared.lastSnapshot(for: sessionKey) {
if let fallback { return fallback
return Self.snapshot(from: fallback)
} }
let errorDescription = String(describing: error) let errorDescription = String(describing: error)
Self.logger.warning( Self.logger.warning(
@ -272,18 +271,120 @@ enum SessionMenuPreviewLoader {
} }
} }
private static func fetchSnapshot(sessionKey: String, maxItems: Int) async throws -> SessionMenuPreviewSnapshot {
do {
let payload = try await self.requestPreview(keys: [sessionKey], maxItems: maxItems)
if let entry = payload.previews.first(where: { $0.key == sessionKey }) ?? payload.previews.first {
return self.snapshot(from: entry, maxItems: maxItems)
}
return SessionMenuPreviewSnapshot(items: [], status: .error("Preview unavailable"))
} catch {
if self.isUnknownMethodError(error) {
return try await self.fetchHistorySnapshot(sessionKey: sessionKey, maxItems: maxItems)
}
throw error
}
}
private static func requestPreview(
keys: [String],
maxItems: Int) async throws -> ClawdbotSessionsPreviewPayload
{
let boundedItems = self.normalizeMaxItems(maxItems)
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
return try await SessionPreviewLimiter.shared.withPermit {
try await AsyncTimeout.withTimeout(
seconds: self.previewTimeoutSeconds,
onTimeout: { PreviewTimeoutError() },
operation: {
try await GatewayConnection.shared.sessionsPreview(
keys: keys,
limit: boundedItems,
maxChars: self.previewMaxChars,
timeoutMs: timeoutMs)
})
}
}
private static func fetchHistorySnapshot(
sessionKey: String,
maxItems: Int) async throws -> SessionMenuPreviewSnapshot
{
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
let payload = try await SessionPreviewLimiter.shared.withPermit {
try await AsyncTimeout.withTimeout(
seconds: self.previewTimeoutSeconds,
onTimeout: { PreviewTimeoutError() },
operation: {
try await GatewayConnection.shared.chatHistory(
sessionKey: sessionKey,
limit: self.previewLimit(for: maxItems),
timeoutMs: timeoutMs)
})
}
let built = Self.previewItems(from: payload, maxItems: maxItems)
return Self.snapshot(from: built)
}
private static func snapshot(from items: [SessionPreviewItem]) -> SessionMenuPreviewSnapshot { private static func snapshot(from items: [SessionPreviewItem]) -> SessionMenuPreviewSnapshot {
SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready) SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready)
} }
private static func snapshot(
from entry: ClawdbotSessionPreviewEntry,
maxItems: Int) -> SessionMenuPreviewSnapshot
{
let items = self.previewItems(from: entry, maxItems: maxItems)
let normalized = entry.status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
switch normalized {
case "ok":
return SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready)
case "empty":
return SessionMenuPreviewSnapshot(items: items, status: .empty)
case "missing":
return SessionMenuPreviewSnapshot(items: items, status: .error("Session missing"))
default:
return SessionMenuPreviewSnapshot(items: items, status: .error("Preview unavailable"))
}
}
private static func cache(payload: ClawdbotSessionsPreviewPayload, maxItems: Int) async {
for entry in payload.previews {
let snapshot = self.snapshot(from: entry, maxItems: maxItems)
await SessionPreviewCache.shared.store(snapshot: snapshot, for: entry.key)
}
}
private static func previewLimit(for maxItems: Int) -> Int { private static func previewLimit(for maxItems: Int) -> Int {
min(max(maxItems * 3, 20), 120) let boundedItems = self.normalizeMaxItems(maxItems)
return min(max(boundedItems * 3, 20), 120)
}
private static func normalizeMaxItems(_ maxItems: Int) -> Int {
max(1, min(maxItems, 50))
}
private static func previewItems(
from entry: ClawdbotSessionPreviewEntry,
maxItems: Int) -> [SessionPreviewItem]
{
let boundedItems = self.normalizeMaxItems(maxItems)
let built: [SessionPreviewItem] = entry.items.enumerated().compactMap { index, item in
let text = item.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return nil }
let role = self.previewRoleFromRaw(item.role)
return SessionPreviewItem(id: "\(entry.key)-\(index)", role: role, text: text)
}
let trimmed = built.suffix(boundedItems)
return Array(trimmed.reversed())
} }
private static func previewItems( private static func previewItems(
from payload: ClawdbotChatHistoryPayload, from payload: ClawdbotChatHistoryPayload,
maxItems: Int) -> [SessionPreviewItem] maxItems: Int) -> [SessionPreviewItem]
{ {
let boundedItems = self.normalizeMaxItems(maxItems)
let raw: [ClawdbotKit.AnyCodable] = payload.messages ?? [] let raw: [ClawdbotKit.AnyCodable] = payload.messages ?? []
let messages = self.decodeMessages(raw) let messages = self.decodeMessages(raw)
let built = messages.compactMap { message -> SessionPreviewItem? in let built = messages.compactMap { message -> SessionPreviewItem? in
@ -294,7 +395,7 @@ enum SessionMenuPreviewLoader {
return SessionPreviewItem(id: id, role: role, text: text) return SessionPreviewItem(id: id, role: role, text: text)
} }
let trimmed = built.suffix(maxItems) let trimmed = built.suffix(boundedItems)
return Array(trimmed.reversed()) return Array(trimmed.reversed())
} }
@ -307,12 +408,16 @@ enum SessionMenuPreviewLoader {
private static func previewRole(_ raw: String, isTool: Bool) -> PreviewRole { private static func previewRole(_ raw: String, isTool: Bool) -> PreviewRole {
if isTool { return .tool } if isTool { return .tool }
return self.previewRoleFromRaw(raw)
}
private static func previewRoleFromRaw(_ raw: String) -> PreviewRole {
switch raw.lowercased() { switch raw.lowercased() {
case "user": return .user case "user": .user
case "assistant": return .assistant case "assistant": .assistant
case "system": return .system case "system": .system
case "tool": return .tool case "tool": .tool
default: return .other default: .other
} }
} }
@ -375,4 +480,16 @@ enum SessionMenuPreviewLoader {
} }
return result return result
} }
private static func uniqueKeys(_ keys: [String]) -> [String] {
let trimmed = keys.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
return self.dedupePreservingOrder(trimmed.filter { !$0.isEmpty })
}
private static func isUnknownMethodError(_ error: Error) -> Bool {
guard let response = error as? GatewayResponseError else { return false }
guard response.code == ErrorCode.invalidRequest.rawValue else { return false }
let message = response.message.lowercased()
return message.contains("unknown method")
}
} }

View File

@ -925,6 +925,27 @@ public struct SessionsListParams: Codable, Sendable {
} }
} }
public struct SessionsPreviewParams: Codable, Sendable {
public let keys: [String]
public let limit: Int?
public let maxchars: Int?
public init(
keys: [String],
limit: Int?,
maxchars: Int?
) {
self.keys = keys
self.limit = limit
self.maxchars = maxchars
}
private enum CodingKeys: String, CodingKey {
case keys
case limit
case maxchars = "maxChars"
}
}
public struct SessionsResolveParams: Codable, Sendable { public struct SessionsResolveParams: Codable, Sendable {
public let key: String? public let key: String?
public let label: String? public let label: String?

View File

@ -0,0 +1,56 @@
import Testing
@testable import Clawdbot
@Suite
@MainActor
struct ExecApprovalsGatewayPrompterTests {
@Test func sessionMatchPrefersActiveSession() {
let matches = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .remote,
activeSession: " main ",
requestSession: "main",
lastInputSeconds: nil)
#expect(matches)
let mismatched = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .remote,
activeSession: "other",
requestSession: "main",
lastInputSeconds: 0)
#expect(!mismatched)
}
@Test func sessionFallbackUsesRecentActivity() {
let recent = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .remote,
activeSession: nil,
requestSession: "main",
lastInputSeconds: 10,
thresholdSeconds: 120)
#expect(recent)
let stale = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .remote,
activeSession: nil,
requestSession: "main",
lastInputSeconds: 200,
thresholdSeconds: 120)
#expect(!stale)
}
@Test func defaultBehaviorMatchesMode() {
let local = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .local,
activeSession: nil,
requestSession: nil,
lastInputSeconds: 400)
#expect(local)
let remote = ExecApprovalsGatewayPrompter._testShouldPresent(
mode: .remote,
activeSession: nil,
requestSession: nil,
lastInputSeconds: 400)
#expect(!remote)
}
}

View File

@ -1,3 +1,4 @@
import ClawdbotKit
import Foundation import Foundation
import os import os
import Testing import Testing

View File

@ -7,20 +7,22 @@ struct SessionMenuPreviewTests {
@Test func loaderReturnsCachedItems() async { @Test func loaderReturnsCachedItems() async {
await SessionPreviewCache.shared._testReset() await SessionPreviewCache.shared._testReset()
let items = [SessionPreviewItem(id: "1", role: .user, text: "Hi")] let items = [SessionPreviewItem(id: "1", role: .user, text: "Hi")]
await SessionPreviewCache.shared._testSet(items: items, for: "main") let snapshot = SessionMenuPreviewSnapshot(items: items, status: .ready)
await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main")
let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
#expect(snapshot.status == .ready) #expect(loaded.status == .ready)
#expect(snapshot.items.count == 1) #expect(loaded.items.count == 1)
#expect(snapshot.items.first?.text == "Hi") #expect(loaded.items.first?.text == "Hi")
} }
@Test func loaderReturnsEmptyWhenCachedEmpty() async { @Test func loaderReturnsEmptyWhenCachedEmpty() async {
await SessionPreviewCache.shared._testReset() await SessionPreviewCache.shared._testReset()
await SessionPreviewCache.shared._testSet(items: [], for: "main") let snapshot = SessionMenuPreviewSnapshot(items: [], status: .empty)
await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main")
let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10)
#expect(snapshot.status == .empty) #expect(loaded.status == .empty)
#expect(snapshot.items.isEmpty) #expect(loaded.items.isEmpty)
} }
} }

View File

@ -235,6 +235,27 @@ public struct ClawdbotChatHistoryPayload: Codable, Sendable {
public let thinkingLevel: String? public let thinkingLevel: String?
} }
public struct ClawdbotSessionPreviewItem: Codable, Hashable, Sendable {
public let role: String
public let text: String
}
public struct ClawdbotSessionPreviewEntry: Codable, Sendable {
public let key: String
public let status: String
public let items: [ClawdbotSessionPreviewItem]
}
public struct ClawdbotSessionsPreviewPayload: Codable, Sendable {
public let ts: Int
public let previews: [ClawdbotSessionPreviewEntry]
public init(ts: Int, previews: [ClawdbotSessionPreviewEntry]) {
self.ts = ts
self.previews = previews
}
}
public struct ClawdbotChatSendResponse: Codable, Sendable { public struct ClawdbotChatSendResponse: Codable, Sendable {
public let runId: String public let runId: String
public let status: String public let status: String

View File

@ -925,6 +925,27 @@ public struct SessionsListParams: Codable, Sendable {
} }
} }
public struct SessionsPreviewParams: Codable, Sendable {
public let keys: [String]
public let limit: Int?
public let maxchars: Int?
public init(
keys: [String],
limit: Int?,
maxchars: Int?
) {
self.keys = keys
self.limit = limit
self.maxchars = maxchars
}
private enum CodingKeys: String, CodingKey {
case keys
case limit
case maxchars = "maxChars"
}
}
public struct SessionsResolveParams: Codable, Sendable { public struct SessionsResolveParams: Codable, Sendable {
public let key: String? public let key: String?
public let label: String? public let label: String?

File diff suppressed because one or more lines are too long

3047
dist/control-ui/assets/index-bYQnHP3a.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15
dist/control-ui/index.html vendored Normal file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clawdbot Control</title>
<meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-bYQnHP3a.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BPDeGGxb.css">
</head>
<body>
<clawdbot-app></clawdbot-app>
</body>
</html>

View File

@ -121,7 +121,7 @@ Resolution priority:
### Delivery (channel + target) ### Delivery (channel + target)
Isolated jobs can deliver output to a channel. The job payload can specify: Isolated jobs can deliver output to a channel. The job payload can specify:
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage` / `last` - `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
- `to`: channel-specific recipient target - `to`: channel-specific recipient target
If `channel` or `to` is omitted, cron can fall back to the main sessions “last route” If `channel` or `to` is omitted, cron can fall back to the main sessions “last route”
@ -133,7 +133,7 @@ Delivery notes:
- Use `deliver: false` to keep output internal even if a `to` is present. - Use `deliver: false` to keep output internal even if a `to` is present.
Target format reminders: Target format reminders:
- Slack/Discord targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity. - Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity.
- Telegram topics should use the `:topic:` form (see below). - Telegram topics should use the `:topic:` form (see below).
#### Telegram delivery targets (topics / forum threads) #### Telegram delivery targets (topics / forum threads)

View File

@ -71,8 +71,8 @@ Payload:
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context. - `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check. - `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped. - `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`. - `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session. - `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted. - `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`). - `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds. - `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.

View File

@ -15,6 +15,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups. - [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs. - [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps. - [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
- [Signal](/channels/signal) — signal-cli; privacy-focused. - [Signal](/channels/signal) — signal-cli; privacy-focused.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe). - [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups). - [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).

123
docs/channels/mattermost.md Normal file
View File

@ -0,0 +1,123 @@
---
summary: "Mattermost bot setup and Clawdbot config"
read_when:
- Setting up Mattermost
- Debugging Mattermost routing
---
# Mattermost (plugin)
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
Mattermost is a self-hostable team messaging platform; see the official site at
[mattermost.com](https://mattermost.com) for product details and downloads.
## Plugin required
Mattermost ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
clawdbot plugins install @clawdbot/mattermost
```
Local checkout (when running from a git repo):
```bash
clawdbot plugins install ./extensions/mattermost
```
If you choose Mattermost during configure/onboarding and a git checkout is detected,
Clawdbot will offer the local install path automatically.
Details: [Plugins](/plugin)
## Quick setup
1) Install the Mattermost plugin.
2) Create a Mattermost bot account and copy the **bot token**.
3) Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
4) Configure Clawdbot and start the gateway.
Minimal config:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing"
}
}
}
```
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:
- `MATTERMOST_BOT_TOKEN=...`
- `MATTERMOST_URL=https://chat.example.com`
Env vars apply only to the **default** account (`default`). Other accounts must use config values.
## Chat modes
Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
- `oncall` (default): respond only when @mentioned in channels.
- `onmessage`: respond to every channel message.
- `onchar`: respond when a message starts with a trigger prefix.
Config example:
```json5
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"]
}
}
}
```
Notes:
- `onchar` still responds to explicit @mentions.
- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
## Access control (DMs)
- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code).
- Approve via:
- `clawdbot pairing list mattermost`
- `clawdbot pairing approve mattermost <CODE>`
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
## Channels (groups)
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
## Targets for outbound delivery
Use these target formats with `clawdbot message send` or cron/webhooks:
- `channel:<id>` for a channel
- `user:<id>` for a DM
- `@username` for a DM (resolved via the Mattermost API)
Bare IDs are treated as channels.
## Multi-account
Mattermost supports multiple accounts under `channels.mattermost.accounts`:
```json5
{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" }
}
}
}
}
```
## Troubleshooting
- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
- Auth errors: check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the `default` account.

View File

@ -1,7 +1,7 @@
--- ---
summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)" summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)"
read_when: read_when:
- You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage) - You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage)
- You want to check channel status or tail channel logs - You want to check channel status or tail channel logs
--- ---

View File

@ -122,7 +122,7 @@ clawdbot gateway probe --ssh user@gateway-host
Options: Options:
- `--ssh <target>`: `user@host` or `user@host:port` (port defaults to `22`). - `--ssh <target>`: `user@host` or `user@host:port` (port defaults to `22`).
- `--ssh-identity <path>`: identity file. - `--ssh-identity <path>`: identity file.
- `--ssh-auto`: pick the first discovered bridge host as SSH target (LAN/WAB only). - `--ssh-auto`: pick the first discovered gateway host as SSH target (LAN/WAB only).
Config (optional, used as defaults): Config (optional, used as defaults):
- `gateway.remote.sshTarget` - `gateway.remote.sshTarget`

View File

@ -293,7 +293,7 @@ Options:
- `--reset` (reset config + credentials + sessions + workspace before wizard) - `--reset` (reset config + credentials + sessions + workspace before wizard)
- `--non-interactive` - `--non-interactive`
- `--mode <local|remote>` - `--mode <local|remote>`
- `--flow <quickstart|advanced>` - `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>` - `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`) - `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`) - `--token <token>` (non-interactive; used with `--auth-choice token`)
@ -352,7 +352,7 @@ Options:
## Channel helpers ## Channel helpers
### `channels` ### `channels`
Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams). Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
Subcommands: Subcommands:
- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included). - `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
@ -365,7 +365,7 @@ Subcommands:
- `channels logout`: log out of a channel session (if supported). - `channels logout`: log out of a channel session (if supported).
Common options: Common options:
- `--channel <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams` - `--channel <name>`: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams`
- `--account <id>`: channel account id (default `default`) - `--account <id>`: channel account id (default `default`)
- `--name <label>`: display name for the account - `--name <label>`: display name for the account
@ -472,7 +472,7 @@ Options:
- `--session-id <id>` - `--session-id <id>`
- `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only) - `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `--verbose <on|full|off>` - `--verbose <on|full|off>`
- `--channel <whatsapp|telegram|discord|slack|signal|imessage>` - `--channel <whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams>`
- `--local` - `--local`
- `--deliver` - `--deliver`
- `--json` - `--json`
@ -791,11 +791,10 @@ All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`.
[`clawdbot node`](/cli/node). [`clawdbot node`](/cli/node).
Subcommands: Subcommands:
- `node run --host <gateway-host> --port 18790` - `node run --host <gateway-host> --port 18789`
- `node status` - `node status`
- `node install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]` - `node install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
- `node uninstall` - `node uninstall`
- `node run`
- `node stop` - `node stop`
- `node restart` - `node restart`

View File

@ -8,7 +8,7 @@ read_when:
# `clawdbot message` # `clawdbot message`
Single outbound command for sending messages and channel actions Single outbound command for sending messages and channel actions
(Discord/Slack/Telegram/WhatsApp/Signal/iMessage/MS Teams). (Discord/Slack/Mattermost (plugin)/Telegram/WhatsApp/Signal/iMessage/MS Teams).
## Usage ## Usage
@ -19,13 +19,14 @@ clawdbot message <subcommand> [flags]
Channel selection: Channel selection:
- `--channel` required if more than one channel is configured. - `--channel` required if more than one channel is configured.
- If exactly one channel is configured, it becomes the default. - If exactly one channel is configured, it becomes the default.
- Values: `whatsapp|telegram|discord|slack|signal|imessage|msteams` - Values: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams` (Mattermost requires plugin)
Target formats (`--target`): Target formats (`--target`):
- WhatsApp: E.164 or group JID - WhatsApp: E.164 or group JID
- Telegram: chat id or `@username` - Telegram: chat id or `@username`
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels) - Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
- Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted) - Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted)
- Mattermost (plugin): `channel:<id>`, `user:<id>`, or `@username` (bare ids are treated as channels)
- Signal: `+E.164`, `group:<id>`, `signal:+E.164`, `signal:group:<id>`, or `username:<name>`/`u:<name>` - Signal: `+E.164`, `group:<id>`, `signal:+E.164`, `signal:group:<id>`, or `username:<name>`/`u:<name>`
- iMessage: handle, `chat_id:<id>`, `chat_guid:<guid>`, or `chat_identifier:<id>` - iMessage: handle, `chat_id:<id>`, `chat_guid:<guid>`, or `chat_identifier:<id>`
- MS Teams: conversation id (`19:...@thread.tacv2`) or `conversation:<id>` or `user:<aad-object-id>` - MS Teams: conversation id (`19:...@thread.tacv2`) or `conversation:<id>` or `user:<aad-object-id>`
@ -49,7 +50,7 @@ Name lookup:
### Core ### Core
- `send` - `send`
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams - Channels: WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams
- Required: `--target`, plus `--message` or `--media` - Required: `--target`, plus `--message` or `--media`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback` - Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it) - Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)

View File

@ -7,7 +7,7 @@ read_when:
# `clawdbot node` # `clawdbot node`
Run a **headless node host** that connects to the Gateway bridge and exposes Run a **headless node host** that connects to the Gateway WebSocket and exposes
`system.run` / `system.which` on this machine. `system.run` / `system.which` on this machine.
## Why use a node host? ## Why use a node host?
@ -26,14 +26,14 @@ node host, so you can keep command access scoped and explicit.
## Run (foreground) ## Run (foreground)
```bash ```bash
clawdbot node run --host <gateway-host> --port 18790 clawdbot node run --host <gateway-host> --port 18789
``` ```
Options: Options:
- `--host <host>`: Gateway bridge host (default: `127.0.0.1`) - `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
- `--port <port>`: Gateway bridge port (default: `18790`) - `--port <port>`: Gateway WebSocket port (default: `18789`)
- `--tls`: Use TLS for the bridge connection - `--tls`: Use TLS for the gateway connection
- `--tls-fingerprint <sha256>`: Pin the bridge certificate fingerprint - `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
- `--node-id <id>`: Override node id (clears pairing token) - `--node-id <id>`: Override node id (clears pairing token)
- `--display-name <name>`: Override the node display name - `--display-name <name>`: Override the node display name
@ -42,14 +42,14 @@ Options:
Install a headless node host as a user service. Install a headless node host as a user service.
```bash ```bash
clawdbot node install --host <gateway-host> --port 18790 clawdbot node install --host <gateway-host> --port 18789
``` ```
Options: Options:
- `--host <host>`: Gateway bridge host (default: `127.0.0.1`) - `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
- `--port <port>`: Gateway bridge port (default: `18790`) - `--port <port>`: Gateway WebSocket port (default: `18789`)
- `--tls`: Use TLS for the bridge connection - `--tls`: Use TLS for the gateway connection
- `--tls-fingerprint <sha256>`: Pin the bridge certificate fingerprint - `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
- `--node-id <id>`: Override node id (clears pairing token) - `--node-id <id>`: Override node id (clears pairing token)
- `--display-name <name>`: Override the node display name - `--display-name <name>`: Override the node display name
- `--runtime <runtime>`: Service runtime (`node` or `bun`) - `--runtime <runtime>`: Service runtime (`node` or `bun`)
@ -59,12 +59,15 @@ Manage the service:
```bash ```bash
clawdbot node status clawdbot node status
clawdbot node run
clawdbot node stop clawdbot node stop
clawdbot node restart clawdbot node restart
clawdbot node uninstall clawdbot node uninstall
``` ```
Use `clawdbot node run` for a foreground node host (no service).
Service commands accept `--json` for machine-readable output.
## Pairing ## Pairing
The first connection creates a pending node pair request on the Gateway. The first connection creates a pending node pair request on the Gateway.
@ -75,7 +78,8 @@ clawdbot nodes pending
clawdbot nodes approve <requestId> clawdbot nodes approve <requestId>
``` ```
The node host stores its node id + token in `~/.clawdbot/node.json`. The node host stores its node id, token, display name, and gateway connection info in
`~/.clawdbot/node.json`.
## Exec approvals ## Exec approvals

View File

@ -14,6 +14,9 @@ Related:
- Camera: [Camera nodes](/nodes/camera) - Camera: [Camera nodes](/nodes/camera)
- Images: [Image nodes](/nodes/images) - Images: [Image nodes](/nodes/images)
Common options:
- `--url`, `--token`, `--timeout`, `--json`
## Common commands ## Common commands
```bash ```bash
@ -40,6 +43,11 @@ clawdbot nodes run --raw "git status"
clawdbot nodes run --agent main --node <id|name|ip> --raw "git status" clawdbot nodes run --agent main --node <id|name|ip> --raw "git status"
``` ```
Invoke flags:
- `--params <json>`: JSON object string (default `{}`).
- `--invoke-timeout <ms>`: node invoke timeout (default `15000`).
- `--idempotency-key <key>`: optional idempotency key.
### Exec-style defaults ### Exec-style defaults
`nodes run` mirrors the models exec behavior (defaults + approvals): `nodes run` mirrors the models exec behavior (defaults + approvals):
@ -47,8 +55,14 @@ clawdbot nodes run --agent main --node <id|name|ip> --raw "git status"
- Reads `tools.exec.*` (plus `agents.list[].tools.exec.*` overrides). - Reads `tools.exec.*` (plus `agents.list[].tools.exec.*` overrides).
- Uses exec approvals (`exec.approval.request`) before invoking `system.run`. - Uses exec approvals (`exec.approval.request`) before invoking `system.run`.
- `--node` can be omitted when `tools.exec.node` is set. - `--node` can be omitted when `tools.exec.node` is set.
- Requires a node that advertises `system.run` (macOS companion app or headless node host).
Flags: Flags:
- `--cwd <path>`: working directory.
- `--env <key=val>`: env override (repeatable).
- `--command-timeout <ms>`: command timeout.
- `--invoke-timeout <ms>`: node invoke timeout (default `30000`).
- `--needs-screen-recording`: require screen recording permission.
- `--raw <command>`: run a shell string (`/bin/sh -lc` or `cmd.exe /c`). - `--raw <command>`: run a shell string (`/bin/sh -lc` or `cmd.exe /c`).
- `--agent <id>`: agent-scoped approvals/allowlists (defaults to configured agent). - `--agent <id>`: agent-scoped approvals/allowlists (defaults to configured agent).
- `--ask <off|on-miss|always>`, `--security <deny|allowlist|full>`: overrides. - `--ask <off|on-miss|always>`, `--security <deny|allowlist|full>`: overrides.

View File

@ -16,6 +16,10 @@ Related:
```bash ```bash
clawdbot onboard clawdbot onboard
clawdbot onboard --flow quickstart clawdbot onboard --flow quickstart
clawdbot onboard --flow manual
clawdbot onboard --mode remote --remote-url ws://gateway-host:18789 clawdbot onboard --mode remote --remote-url ws://gateway-host:18789
``` ```
Flow notes:
- `quickstart`: minimal prompts, auto-generates a gateway token.
- `manual`: full prompts for port/bind/auth (alias of `advanced`).

View File

@ -5,7 +5,7 @@ read_when:
--- ---
# Gateway architecture # Gateway architecture
Last updated: 2026-01-19 Last updated: 2026-01-22
## Overview ## Overview
@ -34,7 +34,8 @@ Last updated: 2026-01-19
### Nodes (macOS / iOS / Android / headless) ### Nodes (macOS / iOS / Android / headless)
- Connect to the **same WS server** with `role: node`. - Connect to the **same WS server** with `role: node`.
- Pair with the Gateway to receive a token. - Provide a device identity in `connect`; pairing is **devicebased** (role `node`) and
approval lives in the device pairing store.
- Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`. - Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`.
Protocol details: Protocol details:

View File

@ -52,10 +52,9 @@ Instances list, `client.mode === "cli"` is **not** turned into a presence entry.
Clients can send richer periodic beacons via the `system-event` method. The mac Clients can send richer periodic beacons via the `system-event` method. The mac
app uses this to report host name, IP, and `lastInputSeconds`. app uses this to report host name, IP, and `lastInputSeconds`.
### 4) Node bridge beacons ### 4) Node connects (role: node)
When a node connects over the Gateway WebSocket with `role: node`, the Gateway
When a node bridge connection authenticates, the Gateway emits a presence entry upserts a presence entry for that node (same flow as other WS clients).
for that node and refreshes it periodically so it doesnt expire.
## Merge + dedupe rules (why `instanceId` matters) ## Merge + dedupe rules (why `instanceId` matters)

View File

@ -19,7 +19,7 @@ Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history,
- Group chats use `agent:<agentId>:<channel>:group:<id>` or `agent:<agentId>:<channel>:channel:<id>` (pass the full key). - Group chats use `agent:<agentId>:<channel>:group:<id>` or `agent:<agentId>:<channel>:channel:<id>` (pass the full key).
- Cron jobs use `cron:<job.id>`. - Cron jobs use `cron:<job.id>`.
- Hooks use `hook:<uuid>` unless explicitly set. - Hooks use `hook:<uuid>` unless explicitly set.
- Node bridge uses `node-<nodeId>` unless explicitly set. - Node sessions use `node-<nodeId>` unless explicitly set.
`global` and `unknown` are reserved values and are never listed. If `session.scope = "global"`, we alias it to `main` for all tools so callers never see `global`. `global` and `unknown` are reserved values and are never listed. If `session.scope = "global"`, we alias it to `main` for all tools so callers never see `global`.

View File

@ -52,7 +52,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- Other sources: - Other sources:
- Cron jobs: `cron:<job.id>` - Cron jobs: `cron:<job.id>`
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook) - Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
- Node bridge runs: `node-<nodeId>` - Node runs: `node-<nodeId>`
## Lifecycle ## Lifecycle
- Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message. - Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message.

View File

@ -46,7 +46,7 @@ Common methods + events:
| Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` | | Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` |
| Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these | | Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these |
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin | | Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | bridge + node actions | | Nodes | `node.list`, `node.invoke`, `node.pair.*` | Gateway WS + node actions |
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push | | Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
Authoritative list lives in `src/gateway/server.ts` (`METHODS`, `EVENTS`). Authoritative list lives in `src/gateway/server.ts` (`METHODS`, `EVENTS`).

View File

@ -70,7 +70,7 @@ What this does:
- `CLAWDBOT_PROFILE=dev` - `CLAWDBOT_PROFILE=dev`
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev` - `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json` - `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
- `CLAWDBOT_GATEWAY_PORT=19001` (bridge/canvas/browser shift accordingly) - `CLAWDBOT_GATEWAY_PORT=19001` (browser/canvas shift accordingly)
2) **Dev bootstrap** (`gateway --dev`) 2) **Dev bootstrap** (`gateway --dev`)
- Writes a minimal config if missing (`gateway.mode=local`, bind loopback). - Writes a minimal config if missing (`gateway.mode=local`, bind loopback).

View File

@ -109,6 +109,14 @@
"source": "/opencode/", "source": "/opencode/",
"destination": "/providers/opencode" "destination": "/providers/opencode"
}, },
{
"source": "/mattermost",
"destination": "/channels/mattermost"
},
{
"source": "/mattermost/",
"destination": "/channels/mattermost"
},
{ {
"source": "/glm", "source": "/glm",
"destination": "/providers/glm" "destination": "/providers/glm"
@ -165,6 +173,14 @@
"source": "/providers/location/", "source": "/providers/location/",
"destination": "/channels/location" "destination": "/channels/location"
}, },
{
"source": "/providers/mattermost",
"destination": "/channels/mattermost"
},
{
"source": "/providers/mattermost/",
"destination": "/channels/mattermost"
},
{ {
"source": "/providers/msteams", "source": "/providers/msteams",
"destination": "/channels/msteams" "destination": "/channels/msteams"
@ -930,6 +946,7 @@
"channels/grammy", "channels/grammy",
"channels/discord", "channels/discord",
"channels/slack", "channels/slack",
"channels/mattermost",
"channels/signal", "channels/signal",
"channels/imessage", "channels/imessage",
"channels/msteams", "channels/msteams",

View File

@ -7,7 +7,7 @@ read_when:
# Bonjour / mDNS discovery # Bonjour / mDNS discovery
Clawdbot uses Bonjour (mDNS / DNSSD) as a **LANonly convenience** to discover Clawdbot uses Bonjour (mDNS / DNSSD) as a **LANonly convenience** to discover
an active Gateway bridge. It is besteffort and does **not** replace SSH or an active Gateway (WebSocket endpoint). It is besteffort and does **not** replace SSH or
Tailnet-based connectivity. Tailnet-based connectivity.
## Widearea Bonjour (Unicast DNSSD) over Tailscale ## Widearea Bonjour (Unicast DNSSD) over Tailscale
@ -31,7 +31,7 @@ browse both `local.` and `clawdbot.internal.` automatically.
```json5 ```json5
{ {
bridge: { bind: "tailnet" }, // tailnet-only (recommended) gateway: { bind: "tailnet" }, // tailnet-only (recommended)
discovery: { wideArea: { enabled: true } } // enables clawdbot.internal DNS-SD publishing discovery: { wideArea: { enabled: true } } // enables clawdbot.internal DNS-SD publishing
} }
``` ```
@ -63,13 +63,13 @@ In the Tailscale admin console:
Once clients accept tailnet DNS, iOS nodes can browse Once clients accept tailnet DNS, iOS nodes can browse
`_clawdbot-gw._tcp` in `clawdbot.internal.` without multicast. `_clawdbot-gw._tcp` in `clawdbot.internal.` without multicast.
### Bridge listener security (recommended) ### Gateway listener security (recommended)
The bridge port (default `18790`) is a plain TCP service. By default it binds to The Gateway WS port (default `18789`) binds to loopback by default. For LAN/tailnet
`0.0.0.0`, which makes it reachable from any interface on the gateway host. access, bind explicitly and keep auth enabled.
For tailnetonly setups: For tailnetonly setups:
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json`. - Set `gateway.bind: "tailnet"` in `~/.clawdbot/clawdbot.json`.
- Restart the Gateway (or restart the macOS menubar app). - Restart the Gateway (or restart the macOS menubar app).
## What advertises ## What advertises
@ -87,11 +87,12 @@ The Gateway advertises small nonsecret hints to make UI flows convenient:
- `role=gateway` - `role=gateway`
- `displayName=<friendly name>` - `displayName=<friendly name>`
- `lanHost=<hostname>.local` - `lanHost=<hostname>.local`
- `gatewayPort=<port>` (informational; Gateway WS is usually loopbackonly) - `gatewayPort=<port>` (Gateway WS + HTTP)
- `bridgePort=<port>` (only when bridge is enabled) - `gatewayTls=1` (only when TLS is enabled)
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
- `canvasPort=<port>` (only when the canvas host is enabled; default `18793`) - `canvasPort=<port>` (only when the canvas host is enabled; default `18793`)
- `sshPort=<port>` (defaults to 22 when not overridden) - `sshPort=<port>` (defaults to 22 when not overridden)
- `transport=bridge` - `transport=gateway`
- `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint) - `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint)
- `tailnetDns=<magicdns>` (optional hint when Tailnet is available) - `tailnetDns=<magicdns>` (optional hint when Tailnet is available)
@ -125,8 +126,8 @@ The Gateway writes a rolling log file (printed on startup as
The iOS node uses `NWBrowser` to discover `_clawdbot-gw._tcp`. The iOS node uses `NWBrowser` to discover `_clawdbot-gw._tcp`.
To capture logs: To capture logs:
- Settings → Bridge → Advanced → **Discovery Debug Logs** - Settings → Gateway → Advanced → **Discovery Debug Logs**
- Settings → Bridge → Advanced → **Discovery Logs** → reproduce → **Copy** - Settings → Gateway → Advanced → **Discovery Logs** → reproduce → **Copy**
The log includes browser state transitions and resultset changes. The log includes browser state transitions and resultset changes.
@ -136,7 +137,7 @@ The log includes browser state transitions and resultset changes.
- **Multicast blocked**: some WiFi networks disable mDNS. - **Multicast blocked**: some WiFi networks disable mDNS.
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry. - **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or - **Browse works but resolve fails**: keep machine names simple (avoid emojis or
punctuation), then restart the Gateway. The bridge instance name derives from punctuation), then restart the Gateway. The service instance name derives from
the host name, so overly complex names can confuse some resolvers. the host name, so overly complex names can confuse some resolvers.
## Escaped instance names (`\032`) ## Escaped instance names (`\032`)
@ -150,9 +151,7 @@ sequences (e.g. spaces become `\032`).
## Disabling / configuration ## Disabling / configuration
- `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising. - `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising.
- `CLAWDBOT_BRIDGE_ENABLED=0` disables the bridge listener (and the bridge beacon). - `gateway.bind` in `~/.clawdbot/clawdbot.json` controls the Gateway bind mode.
- `bridge.bind` / `bridge.port` in `~/.clawdbot/clawdbot.json` control bridge bind/port.
- `CLAWDBOT_BRIDGE_HOST` / `CLAWDBOT_BRIDGE_PORT` still work as backcompat overrides.
- `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in TXT. - `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in TXT.
- `CLAWDBOT_TAILNET_DNS` publishes a MagicDNS hint in TXT. - `CLAWDBOT_TAILNET_DNS` publishes a MagicDNS hint in TXT.
- `CLAWDBOT_CLI_PATH` overrides the advertised CLI path. - `CLAWDBOT_CLI_PATH` overrides the advertised CLI path.

View File

@ -14,6 +14,9 @@ should use the unified Gateway WebSocket protocol instead.
If you are building an operator or node client, use the If you are building an operator or node client, use the
[Gateway protocol](/gateway/protocol). [Gateway protocol](/gateway/protocol).
**Note:** Current Clawdbot builds no longer ship the TCP bridge listener; this document is kept for historical reference.
Legacy `bridge.*` config keys are no longer part of the config schema.
## Why we have both ## Why we have both
- **Security boundary**: the bridge exposes a small allowlist instead of the - **Security boundary**: the bridge exposes a small allowlist instead of the
@ -28,7 +31,7 @@ If you are building an operator or node client, use the
- TCP, one JSON object per line (JSONL). - TCP, one JSON object per line (JSONL).
- Optional TLS (when `bridge.tls.enabled` is true). - Optional TLS (when `bridge.tls.enabled` is true).
- Gateway owns the listener (default `18790`). - Legacy default listener port was `18790` (current builds do not start a TCP bridge).
When TLS is enabled, discovery TXT records include `bridgeTls=1` plus When TLS is enabled, discovery TXT records include `bridgeTls=1` plus
`bridgeTlsSha256` so nodes can pin the certificate. `bridgeTlsSha256` so nodes can pin the certificate.
@ -54,7 +57,7 @@ Gateway → Client:
- `event`: chat updates for subscribed sessions - `event`: chat updates for subscribed sessions
- `ping` / `pong`: keepalive - `ping` / `pong`: keepalive
Exact allowlist is enforced in `src/gateway/server-bridge.ts`. Legacy allowlist enforcement lived in `src/gateway/server-bridge.ts` (removed).
## Exec lifecycle events ## Exec lifecycle events

View File

@ -568,5 +568,5 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`. - If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format. - Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
- Optional sections to add later: `web`, `browser`, `ui`, `bridge`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`. - Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
- See [Providers](/channels/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes. - See [Providers](/channels/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.

View File

@ -543,7 +543,7 @@ Notes:
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted). - Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`. - The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.signal.accounts` / `channels.imessage.accounts` ### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.mattermost.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
Run multiple accounts per channel (each account has its own `accountId` and optional `name`): Run multiple accounts per channel (each account has its own `accountId` and optional `name`):
@ -1204,6 +1204,44 @@ Slack action groups (gate `slack` tool actions):
| memberInfo | enabled | Member info | | memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list | | emojiList | enabled | Custom emoji list |
### `channels.mattermost` (bot token)
Mattermost ships as a plugin and is not bundled with the core install.
Install it first: `clawdbot plugins install @clawdbot/mattermost` (or `./extensions/mattermost` from a git checkout).
Mattermost requires a bot token plus the base URL for your server:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
textChunkLimit: 4000
}
}
}
```
Clawdbot starts Mattermost when the account is configured (bot token + base URL) and enabled. The token + base URL are resolved from `channels.mattermost.botToken` + `channels.mattermost.baseUrl` or `MATTERMOST_BOT_TOKEN` + `MATTERMOST_URL` for the default account (unless `channels.mattermost.enabled` is `false`).
Chat modes:
- `oncall` (default): respond to channel messages only when @mentioned.
- `onmessage`: respond to every channel message.
- `onchar`: respond when a message starts with a trigger prefix (`channels.mattermost.oncharPrefixes`, default `[">", "!"]`).
Access control:
- Default DMs: `channels.mattermost.dmPolicy="pairing"` (unknown senders get a pairing code).
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
- Groups: `channels.mattermost.groupPolicy="allowlist"` by default (mention-gated). Use `channels.mattermost.groupAllowFrom` to restrict senders.
Multi-account support lives under `channels.mattermost.accounts` (see the multi-account section above). Env vars only apply to the default account.
Use `channel:<id>` or `user:<id>` (or `@username`) when specifying delivery targets; bare ids are treated as channel ids.
### `channels.signal` (signal-cli) ### `channels.signal` (signal-cli)
Signal reactions can emit system events (shared reaction tooling): Signal reactions can emit system events (shared reaction tooling):
@ -1690,7 +1728,7 @@ auto-compaction, instructing the model to store durable memories on disk (e.g.
`memory/YYYY-MM-DD.md`). It triggers when the session token estimate crosses a `memory/YYYY-MM-DD.md`). It triggers when the session token estimate crosses a
soft threshold below the compaction limit. soft threshold below the compaction limit.
Defaults: Legacy defaults:
- `memoryFlush.enabled`: `true` - `memoryFlush.enabled`: `true`
- `memoryFlush.softThresholdTokens`: `4000` - `memoryFlush.softThresholdTokens`: `4000`
- `memoryFlush.prompt` / `memoryFlush.systemPrompt`: built-in defaults with `NO_REPLY` - `memoryFlush.prompt` / `memoryFlush.systemPrompt`: built-in defaults with `NO_REPLY`
@ -1735,8 +1773,9 @@ Block streaming:
with `maxChars` capped to the channel text limit. Signal/Slack/Discord default with `maxChars` capped to the channel text limit. Signal/Slack/Discord default
to `minChars: 1500` unless overridden. to `minChars: 1500` unless overridden.
Channel overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`, Channel overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`,
`channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.signal.blockStreamingCoalesce`, `channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.mattermost.blockStreamingCoalesce`,
`channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce` (and per-account variants). `channels.signal.blockStreamingCoalesce`, `channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce`
(and per-account variants).
- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first. - `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
Modes: `off` (default), `natural` (8002500ms), `custom` (use `minMs`/`maxMs`). Modes: `off` (default), `natural` (8002500ms), `custom` (use `minMs`/`maxMs`).
Per-agent override: `agents.list[].humanDelay`. Per-agent override: `agents.list[].humanDelay`.
@ -2784,7 +2823,7 @@ Hot-applied (no full gateway restart):
Requires full Gateway restart: Requires full Gateway restart:
- `gateway` (port/bind/auth/control UI/tailscale) - `gateway` (port/bind/auth/control UI/tailscale)
- `bridge` - `bridge` (legacy)
- `discovery` - `discovery`
- `canvasHost` - `canvasHost`
- `plugins` - `plugins`
@ -2802,7 +2841,7 @@ Convenience flags (CLI):
- `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001` - `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001`
- `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags) - `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags)
See [Gateway runbook](/gateway) for the derived port mapping (gateway/bridge/browser/canvas). See [Gateway runbook](/gateway) for the derived port mapping (gateway/browser/canvas).
See [Multiple gateways](/gateway/multiple-gateways) for browser/CDP port isolation details. See [Multiple gateways](/gateway/multiple-gateways) for browser/CDP port isolation details.
Example: Example:
@ -2921,7 +2960,7 @@ The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can
Default root: `~/clawd/canvas` Default root: `~/clawd/canvas`
Default port: `18793` (chosen to avoid the clawd browser CDP port `18792`) Default port: `18793` (chosen to avoid the clawd browser CDP port `18792`)
The server listens on the **bridge bind host** (LAN or Tailnet) so nodes can reach it. The server listens on the **gateway bind host** (LAN or Tailnet) so nodes can reach it.
The server: The server:
- serves files under `canvasHost.root` - serves files under `canvasHost.root`
@ -2950,9 +2989,13 @@ Disable with:
- config: `canvasHost: { enabled: false }` - config: `canvasHost: { enabled: false }`
- env: `CLAWDBOT_SKIP_CANVAS_HOST=1` - env: `CLAWDBOT_SKIP_CANVAS_HOST=1`
### `bridge` (node bridge server) ### `bridge` (legacy TCP bridge, removed)
The Gateway can expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`. Current builds no longer include the TCP bridge listener; `bridge.*` config keys are ignored.
Nodes connect over the Gateway WebSocket. This section is kept for historical reference.
Legacy behavior:
- The Gateway could expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`.
Defaults: Defaults:
- enabled: `true` - enabled: `true`

View File

@ -3,7 +3,7 @@ summary: "Node discovery and transports (Bonjour, Tailscale, SSH) for finding th
read_when: read_when:
- Implementing or changing Bonjour discovery/advertising - Implementing or changing Bonjour discovery/advertising
- Adjusting remote connection modes (direct vs SSH) - Adjusting remote connection modes (direct vs SSH)
- Designing bridge + pairing for remote nodes - Designing node discovery + pairing for remote nodes
--- ---
# Discovery & transports # Discovery & transports
@ -17,17 +17,18 @@ The design goal is to keep all network discovery/advertising in the **Node Gatew
## Terms ## Terms
- **Gateway**: a single long-running gateway process that owns state (sessions, pairing, node registry) and runs channels. Most setups use one per host; isolated multi-gateway setups are possible. - **Gateway**: a single long-running gateway process that owns state (sessions, pairing, node registry) and runs channels. Most setups use one per host; isolated multi-gateway setups are possible.
- **Gateway WS (loopback)**: the existing gateway WebSocket control endpoint on `127.0.0.1:18789`. - **Gateway WS (control plane)**: the WebSocket endpoint on `127.0.0.1:18789` by default; can be bound to LAN/tailnet via `gateway.bind`.
- **Bridge (direct transport)**: a LAN/tailnet-facing endpoint owned by the gateway that allows authenticated clients/nodes to call a scoped subset of gateway methods. The bridge exists so the gateway can remain loopback-only. - **Direct WS transport**: a LAN/tailnet-facing Gateway WS endpoint (no SSH).
- **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH. - **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH.
- **Legacy TCP bridge (deprecated/removed)**: older node transport (see [Bridge protocol](/gateway/bridge-protocol)); no longer advertised for discovery.
Protocol details: Protocol details:
- [Gateway protocol](/gateway/protocol) - [Gateway protocol](/gateway/protocol)
- [Bridge protocol](/gateway/bridge-protocol) - [Bridge protocol (legacy)](/gateway/bridge-protocol)
## Why we keep both “direct” and SSH ## Why we keep both “direct” and SSH
- **Direct bridge** is the best UX on the same network and within a tailnet: - **Direct WS** is the best UX on the same network and within a tailnet:
- auto-discovery on LAN via Bonjour - auto-discovery on LAN via Bonjour
- pairing tokens + ACLs owned by the gateway - pairing tokens + ACLs owned by the gateway
- no shell access required; protocol surface can stay tight and auditable - no shell access required; protocol surface can stay tight and auditable
@ -43,7 +44,7 @@ Protocol details:
Bonjour is best-effort and does not cross networks. It is only used for “same LAN” convenience. Bonjour is best-effort and does not cross networks. It is only used for “same LAN” convenience.
Target direction: Target direction:
- The **gateway** advertises its bridge via Bonjour. - The **gateway** advertises its WS endpoint via Bonjour.
- Clients browse and show a “pick a gateway” list, then store the chosen endpoint. - Clients browse and show a “pick a gateway” list, then store the chosen endpoint.
Troubleshooting and beacon details: [Bonjour](/gateway/bonjour). Troubleshooting and beacon details: [Bonjour](/gateway/bonjour).
@ -56,19 +57,19 @@ Troubleshooting and beacon details: [Bonjour](/gateway/bonjour).
- `role=gateway` - `role=gateway`
- `lanHost=<hostname>.local` - `lanHost=<hostname>.local`
- `sshPort=22` (or whatever is advertised) - `sshPort=22` (or whatever is advertised)
- `gatewayPort=18789` (loopback WS port; informational) - `gatewayPort=18789` (Gateway WS + HTTP)
- `bridgePort=18790` (when bridge is enabled) - `gatewayTls=1` (only when TLS is enabled)
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
- `canvasPort=18793` (default canvas host port; serves `/__clawdbot__/canvas/`) - `canvasPort=18793` (default canvas host port; serves `/__clawdbot__/canvas/`)
- `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint or binary) - `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint or binary)
- `tailnetDns=<magicdns>` (optional hint; auto-detected when Tailscale is available) - `tailnetDns=<magicdns>` (optional hint; auto-detected when Tailscale is available)
Disable/override: Disable/override:
- `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising. - `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising.
- `CLAWDBOT_BRIDGE_ENABLED=0` disables the bridge listener. - `gateway.bind` in `~/.clawdbot/clawdbot.json` controls the Gateway bind mode.
- `bridge.bind` / `bridge.port` in `~/.clawdbot/clawdbot.json` control bridge bind/port (preferred). - `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in TXT (defaults to 22).
- `CLAWDBOT_BRIDGE_HOST` / `CLAWDBOT_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set. - `CLAWDBOT_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS).
- `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in the bridge beacon (defaults to 22). - `CLAWDBOT_CLI_PATH` overrides the advertised CLI path.
- `CLAWDBOT_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in the bridge beacon (auto-detected if unset).
### 2) Tailnet (cross-network) ### 2) Tailnet (cross-network)
@ -97,13 +98,13 @@ Recommended client behavior:
The gateway is the source of truth for node/client admission. The gateway is the source of truth for node/client admission.
- Pairing requests are created/approved/rejected in the gateway (see [Gateway pairing](/gateway/pairing)). - Pairing requests are created/approved/rejected in the gateway (see [Gateway pairing](/gateway/pairing)).
- The bridge enforces: - The gateway enforces:
- auth (token / keypair) - auth (token / keypair)
- scopes/ACLs (bridge is not a raw proxy to every gateway method) - scopes/ACLs (the gateway is not a raw proxy to every method)
- rate limits - rate limits
## Responsibilities by component ## Responsibilities by component
- **Gateway**: advertises discovery beacons, owns pairing decisions, runs the bridge listener. - **Gateway**: advertises discovery beacons, owns pairing decisions, and hosts the WS endpoint.
- **macOS app**: helps you pick a gateway, shows pairing prompts, and uses SSH only as a fallback. - **macOS app**: helps you pick a gateway, shows pairing prompts, and uses SSH only as a fallback.
- **iOS/Android nodes**: browse Bonjour as a convenience and connect via the paired bridge. - **iOS/Android nodes**: browse Bonjour as a convenience and connect to the paired Gateway WS.

View File

@ -82,14 +82,12 @@ Defaults (can be overridden via env/flags/config):
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev` - `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json` - `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
- `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP) - `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP)
- `bridge.port=19002` (derived: `gateway.port+1`)
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`) - `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
- `canvasHost.port=19005` (derived: `gateway.port+4`) - `canvasHost.port=19005` (derived: `gateway.port+4`)
- `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`. - `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
Derived ports (rules of thumb): Derived ports (rules of thumb):
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`) - Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
- `bridge.port = base + 1` (or `CLAWDBOT_BRIDGE_PORT` / config override)
- `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override) - `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override)
- `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override) - `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override)
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile). - Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
@ -114,7 +112,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
``` ```
## Protocol (operator view) ## Protocol (operator view)
- Full docs: [Gateway protocol](/gateway/protocol) and [Bridge protocol](/gateway/bridge-protocol). - Full docs: [Gateway protocol](/gateway/protocol) and [Bridge protocol (legacy)](/gateway/bridge-protocol).
- Mandatory first frame from client: `req {type:"req", id, method:"connect", params:{minProtocol,maxProtocol,client:{id,displayName?,version,platform,deviceFamily?,modelIdentifier?,mode,instanceId?}, caps, auth?, locale?, userAgent? } }`. - Mandatory first frame from client: `req {type:"req", id, method:"connect", params:{minProtocol,maxProtocol,client:{id,displayName?,version,platform,deviceFamily?,modelIdentifier?,mode,instanceId?}, caps, auth?, locale?, userAgent? } }`.
- Gateway replies `res {type:"res", id, ok:true, payload:hello-ok }` (or `ok:false` with an error, then closes). - Gateway replies `res {type:"res", id, ok:true, payload:hello-ok }` (or `ok:false` with an error, then closes).
- After handshake: - After handshake:
@ -130,7 +128,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
- `system-event` — post a presence/system note (structured). - `system-event` — post a presence/system note (structured).
- `send` — send a message via the active channel(s). - `send` — send a message via the active channel(s).
- `agent` — run an agent turn (streams events back on same connection). - `agent` — run an agent turn (streams events back on same connection).
- `node.list` — list paired + currently-connected bridge nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`). - `node.list` — list paired + currently-connected nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`).
- `node.describe` — describe a node (capabilities + supported `node.invoke` commands; works for paired nodes and for currently-connected unpaired nodes). - `node.describe` — describe a node (capabilities + supported `node.invoke` commands; works for paired nodes and for currently-connected unpaired nodes).
- `node.invoke` — invoke a command on a node (e.g. `canvas.*`, `camera.*`). - `node.invoke` — invoke a command on a node (e.g. `canvas.*`, `camera.*`).
- `node.pair.*` — pairing lifecycle (`request`, `list`, `approve`, `reject`, `verify`). - `node.pair.*` — pairing lifecycle (`request`, `list`, `approve`, `reject`, `verify`).

View File

@ -51,7 +51,7 @@ You can tune console verbosity independently via:
## Tool summary redaction ## Tool summary redaction
Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs. console stream. This is **tools-only** and does not alter file logs.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`) - `logging.redactSensitive`: `off` | `tools` (default: `tools`)

View File

@ -13,7 +13,7 @@ Most setups should use one Gateway because a single Gateway can handle multiple
- `CLAWDBOT_STATE_DIR` — per-instance sessions, creds, caches - `CLAWDBOT_STATE_DIR` — per-instance sessions, creds, caches
- `agents.defaults.workspace` — per-instance workspace root - `agents.defaults.workspace` — per-instance workspace root
- `gateway.port` (or `--port`) — unique per instance - `gateway.port` (or `--port`) — unique per instance
- Derived ports (bridge/browser/canvas) must not overlap - Derived ports (browser/canvas) must not overlap
If these are shared, you will hit config races and port conflicts. If these are shared, you will hit config races and port conflicts.
@ -47,7 +47,7 @@ Run a second Gateway on the same host with its own:
This keeps the rescue bot isolated from the main bot so it can debug or apply config changes if the primary bot is down. This keeps the rescue bot isolated from the main bot so it can debug or apply config changes if the primary bot is down.
Port spacing: leave at least 20 ports between base ports so the derived bridge/browser/canvas/CDP ports never collide. Port spacing: leave at least 20 ports between base ports so the derived browser/canvas/CDP ports never collide.
### How to install (rescue bot) ### How to install (rescue bot)
@ -73,7 +73,6 @@ clawdbot --profile rescue gateway install
Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`). Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`).
- `bridge.port = base + 1`
- `browser.controlUrl port = base + 2` - `browser.controlUrl port = base + 2`
- `canvasHost.port = base + 4` - `canvasHost.port = base + 4`
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` - Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108`

View File

@ -11,16 +11,20 @@ In Gateway-owned pairing, the **Gateway** is the source of truth for which nodes
are allowed to join. UIs (macOS app, future clients) are just frontends that are allowed to join. UIs (macOS app, future clients) are just frontends that
approve or reject pending requests. approve or reject pending requests.
**Important:** WS nodes use **device pairing** (role `node`) during `connect`.
`node.pair.*` is a separate pairing store and does **not** gate the WS handshake.
Only clients that explicitly call `node.pair.*` use this flow.
## Concepts ## Concepts
- **Pending request**: a node asked to join; requires approval. - **Pending request**: a node asked to join; requires approval.
- **Paired node**: approved node with an issued auth token. - **Paired node**: approved node with an issued auth token.
- **Bridge**: transport endpoint only; it forwards requests but does not decide - **Transport**: the Gateway WS endpoint forwards requests but does not decide
membership. membership. (Legacy TCP bridge support is deprecated/removed.)
## How pairing works ## How pairing works
1. A node connects to the bridge and requests pairing. 1. A node connects to the Gateway WS and requests pairing.
2. The Gateway stores a **pending request** and emits `node.pair.requested`. 2. The Gateway stores a **pending request** and emits `node.pair.requested`.
3. You approve or reject the request (CLI or UI). 3. You approve or reject the request (CLI or UI).
4. On approval, the Gateway issues a **new token** (tokens are rotated on repair). 4. On approval, the Gateway issues a **new token** (tokens are rotated on repair).
@ -81,9 +85,8 @@ Security notes:
- Tokens are secrets; treat `paired.json` as sensitive. - Tokens are secrets; treat `paired.json` as sensitive.
- Rotating a token requires re-approval (or deleting the node entry). - Rotating a token requires re-approval (or deleting the node entry).
## Bridge behavior ## Transport behavior
- The bridge is **transport only**; it does not store membership. - The transport is **stateless**; it does not store membership.
- If the Gateway is offline or pairing is disabled, nodes cannot pair. - If the Gateway is offline or pairing is disabled, nodes cannot pair.
- If the bridge is running but the Gateway is in remote mode, pairing still - If the Gateway is in remote mode, pairing still happens against the remote Gateways store.
happens against the remote Gateways store.

View File

@ -8,7 +8,7 @@ read_when:
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it. This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it.
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback. - For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
- For **nodes (iOS/Android and future devices)**: prefer the Gateway **Bridge** when on the same LAN/tailnet (see [Discovery](/gateway/discovery)). - For **nodes (iOS/Android and future devices)**: connect to the Gateway **WebSocket** (LAN/tailnet or SSH tunnel as needed).
## The core idea ## The core idea
@ -55,12 +55,12 @@ One gateway service owns state + channels. Nodes are peripherals.
Flow example (Telegram → node): Flow example (Telegram → node):
- Telegram message arrives at the **Gateway**. - Telegram message arrives at the **Gateway**.
- Gateway runs the **agent** and decides whether to call a node tool. - Gateway runs the **agent** and decides whether to call a node tool.
- Gateway calls the **node** over the Bridge (`node.*` RPC). - Gateway calls the **node** over the Gateway WebSocket (`node.*` RPC).
- Node returns the result; Gateway replies back out to Telegram. - Node returns the result; Gateway replies back out to Telegram.
Notes: Notes:
- **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). - **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
- macOS app “node mode” is just a node client over the Bridge. - macOS app “node mode” is just a node client over the Gateway WebSocket.
## SSH tunnel (CLI + tools) ## SSH tunnel (CLI + tools)

View File

@ -94,8 +94,8 @@ clawdbot gateway --tailscale funnel --auth password
or `tailscale funnel` configuration on shutdown. or `tailscale funnel` configuration on shutdown.
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel). - `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only. - `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic - Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
uses the separate bridge port (default `18790`) and is **not** proxied by Serve. the same Gateway WS endpoint, so Serve can work for node access.
## Browser control server (remote Gateway + local browser) ## Browser control server (remote Gateway + local browser)

View File

@ -13,6 +13,7 @@ read_when:
<p align="center"> <p align="center">
<strong>Any OS + WhatsApp/Telegram/Discord/iMessage gateway for AI agents (Pi).</strong><br /> <strong>Any OS + WhatsApp/Telegram/Discord/iMessage gateway for AI agents (Pi).</strong><br />
Plugins add Mattermost and more.
Send a message, get an agent response — from your pocket. Send a message, get an agent response — from your pocket.
</p> </p>
@ -23,7 +24,7 @@ read_when:
<a href="/start/clawd">Clawdbot assistant setup</a> <a href="/start/clawd">Clawdbot assistant setup</a>
</p> </p>
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono). Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono). Plugins add Mattermost (Bot API + WebSocket) and more.
Clawdbot also powers [Clawd](https://clawd.me), the spacelobster assistant. Clawdbot also powers [Clawd](https://clawd.me), the spacelobster assistant.
## Start here ## Start here
@ -44,12 +45,12 @@ Remote access: [Web surfaces](/web) and [Tailscale](/gateway/tailscale)
## How it works ## How it works
``` ```
WhatsApp / Telegram / Discord WhatsApp / Telegram / Discord / iMessage (+ plugins)
┌───────────────────────────┐ ┌───────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789 (loopback-only) │ Gateway │ ws://127.0.0.1:18789 (loopback-only)
│ (single source) │ tcp://0.0.0.0:18790 (Bridge) │ (single source) │
│ │ http://<gateway-host>:18793 │ │ http://<gateway-host>:18793
│ │ /__clawdbot__/canvas/ (Canvas host) │ │ /__clawdbot__/canvas/ (Canvas host)
└───────────┬───────────────┘ └───────────┬───────────────┘
@ -58,8 +59,8 @@ WhatsApp / Telegram / Discord
├─ CLI (clawdbot …) ├─ CLI (clawdbot …)
├─ Chat UI (SwiftUI) ├─ Chat UI (SwiftUI)
├─ macOS app (Clawdbot.app) ├─ macOS app (Clawdbot.app)
├─ iOS node via Bridge + pairing ├─ iOS node via Gateway WS + pairing
└─ Android node via Bridge + pairing └─ Android node via Gateway WS + pairing
``` ```
Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns channel connections and the WebSocket control plane. Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns channel connections and the WebSocket control plane.
@ -70,7 +71,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- **Loopback-first**: Gateway WS defaults to `ws://127.0.0.1:18789`. - **Loopback-first**: Gateway WS defaults to `ws://127.0.0.1:18789`.
- The wizard now generates a gateway token by default (even for loopback). - The wizard now generates a gateway token by default (even for loopback).
- For Tailnet access, run `clawdbot gateway --bind tailnet --token ...` (token is required for non-loopback binds). - For Tailnet access, run `clawdbot gateway --bind tailnet --token ...` (token is required for non-loopback binds).
- **Bridge for nodes**: optional LAN/tailnet-facing bridge on `tcp://0.0.0.0:18790` for paired nodes (Bonjour-discoverable). - **Nodes**: connect to the Gateway WebSocket (LAN/tailnet/SSH as needed); legacy TCP bridge is deprecated/removed.
- **Canvas host**: HTTP file server on `canvasHost.port` (default `18793`), serving `/__clawdbot__/canvas/` for node WebViews; see [Gateway configuration](/gateway/configuration) (`canvasHost`). - **Canvas host**: HTTP file server on `canvasHost.port` (default `18793`), serving `/__clawdbot__/canvas/` for node WebViews; see [Gateway configuration](/gateway/configuration) (`canvasHost`).
- **Remote use**: SSH tunnel or tailnet/VPN; see [Remote access](/gateway/remote) and [Discovery](/gateway/discovery). - **Remote use**: SSH tunnel or tailnet/VPN; see [Remote access](/gateway/remote) and [Discovery](/gateway/discovery).
@ -79,6 +80,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol - 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol
- ✈️ **Telegram Bot** — DMs + groups via grammY - ✈️ **Telegram Bot** — DMs + groups via grammY
- 🎮 **Discord Bot** — DMs + guild channels via channels.discord.js - 🎮 **Discord Bot** — DMs + guild channels via channels.discord.js
- 🧩 **Mattermost Bot (plugin)** — Bot token + WebSocket events
- 💬 **iMessage** — Local imsg CLI integration (macOS) - 💬 **iMessage** — Local imsg CLI integration (macOS)
- 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming - 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming
- ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming)) - ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming))
@ -190,6 +192,7 @@ Example:
- [Control UI (browser)](/web/control-ui) - [Control UI (browser)](/web/control-ui)
- [Telegram](/channels/telegram) - [Telegram](/channels/telegram)
- [Discord](/channels/discord) - [Discord](/channels/discord)
- [Mattermost (plugin)](/channels/mattermost)
- [iMessage](/channels/imessage) - [iMessage](/channels/imessage)
- [Groups](/concepts/groups) - [Groups](/concepts/groups)
- [WhatsApp group messages](/concepts/group-messages) - [WhatsApp group messages](/concepts/group-messages)

View File

@ -36,7 +36,7 @@ Local trust:
- [Remote access (SSH)](/gateway/remote) - [Remote access (SSH)](/gateway/remote)
- [Tailscale](/gateway/tailscale) - [Tailscale](/gateway/tailscale)
## Nodes + bridge ## Nodes + transports
- [Nodes overview](/nodes) - [Nodes overview](/nodes)
- [Bridge protocol (legacy nodes)](/gateway/bridge-protocol) - [Bridge protocol (legacy nodes)](/gateway/bridge-protocol)

View File

@ -138,7 +138,7 @@ Notes:
## Safety + practical limits ## Safety + practical limits
- Camera and microphone access trigger the usual OS permission prompts (and require usage strings in Info.plist). - Camera and microphone access trigger the usual OS permission prompts (and require usage strings in Info.plist).
- Video clips are capped (currently `<= 60s`) to avoid oversized bridge payloads (base64 overhead + message limits). - Video clips are capped (currently `<= 60s`) to avoid oversized node payloads (base64 overhead + message limits).
## macOS screen video (OS-level) ## macOS screen video (OS-level)

View File

@ -8,9 +8,11 @@ read_when:
# Nodes # Nodes
A **node** is a companion device (iOS/Android today) that connects to the Gateway over the **Bridge** and exposes a command surface (e.g. `canvas.*`, `camera.*`, `system.*`) via `node.invoke`. Bridge protocol details: [Bridge protocol](/gateway/bridge-protocol). A **node** is a companion device (macOS/iOS/Android/headless) that connects to the Gateway **WebSocket** (same port as operators) with `role: "node"` and exposes a command surface (e.g. `canvas.*`, `camera.*`, `system.*`) via `node.invoke`. Protocol details: [Gateway protocol](/gateway/protocol).
macOS can also run in **node mode**: the menubar app connects to the Gateways bridge and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac). Legacy transport: [Bridge protocol](/gateway/bridge-protocol) (TCP JSONL; deprecated/removed for current nodes).
macOS can also run in **node mode**: the menubar app connects to the Gateways WS server and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac).
Notes: Notes:
- Nodes are **peripherals**, not gateways. They dont run the gateway service. - Nodes are **peripherals**, not gateways. They dont run the gateway service.
@ -18,21 +20,23 @@ Notes:
## Pairing + status ## Pairing + status
Pairing is gateway-owned and approval-based. See [Gateway pairing](/gateway/pairing) for the full flow. **WS nodes use device pairing.** Nodes present a device identity during `connect`; the Gateway
creates a device pairing request for `role: node`. Approve via the devices CLI (or UI).
Quick CLI: Quick CLI:
```bash ```bash
clawdbot nodes pending clawdbot devices list
clawdbot nodes approve <requestId> clawdbot devices approve <requestId>
clawdbot nodes reject <requestId> clawdbot devices reject <requestId>
clawdbot nodes status clawdbot nodes status
clawdbot nodes describe --node <idOrNameOrIp> clawdbot nodes describe --node <idOrNameOrIp>
clawdbot nodes rename --node <idOrNameOrIp> --name "Kitchen iPad"
``` ```
Notes: Notes:
- `nodes rename` stores a display name override in the gateway pairing store. - `nodes status` marks a node as **paired** when its device pairing role includes `node`.
- `node.pair.*` (CLI: `clawdbot nodes pending/approve/reject`) is a separate gateway-owned
node pairing store; it does **not** gate the WS `connect` handshake.
## Remote node host (system.run) ## Remote node host (system.run)
@ -57,7 +61,7 @@ clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
```bash ```bash
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node" clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
clawdbot node start clawdbot node restart
``` ```
### Pair + name ### Pair + name
@ -239,6 +243,7 @@ Notes:
- `system.notify` respects notification permission state on the macOS app. - `system.notify` respects notification permission state on the macOS app.
- `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`. - `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`.
- `system.notify` supports `--priority <passive|active|timeSensitive>` and `--delivery <system|overlay|auto>`. - `system.notify` supports `--priority <passive|active|timeSensitive>` and `--delivery <system|overlay|auto>`.
- macOS nodes drop `PATH` overrides; headless node hosts only accept `PATH` when it prepends the node host PATH.
- On macOS node mode, `system.run` is gated by exec approvals in the macOS app (Settings → Exec approvals). - On macOS node mode, `system.run` is gated by exec approvals in the macOS app (Settings → Exec approvals).
Ask/allowlist/full behave the same as the headless node host; denied prompts return `SYSTEM_RUN_DENIED`. Ask/allowlist/full behave the same as the headless node host; denied prompts return `SYSTEM_RUN_DENIED`.
- On headless node host, `system.run` is gated by exec approvals (`~/.clawdbot/exec-approvals.json`). - On headless node host, `system.run` is gated by exec approvals (`~/.clawdbot/exec-approvals.json`).
@ -275,26 +280,26 @@ Nodes may include a `permissions` map in `node.list` / `node.describe`, keyed by
## Headless node host (cross-platform) ## Headless node host (cross-platform)
Clawdbot can run a **headless node host** (no UI) that connects to the Gateway Clawdbot can run a **headless node host** (no UI) that connects to the Gateway
bridge and exposes `system.run` / `system.which`. This is useful on Linux/Windows WebSocket and exposes `system.run` / `system.which`. This is useful on Linux/Windows
or for running a minimal node alongside a server. or for running a minimal node alongside a server.
Start it: Start it:
```bash ```bash
clawdbot node run --host <gateway-host> --port 18790 clawdbot node run --host <gateway-host> --port 18789
``` ```
Notes: Notes:
- Pairing is still required (the Gateway will show a node approval prompt). - Pairing is still required (the Gateway will show a node approval prompt).
- The node host stores its node id + pairing token in `~/.clawdbot/node.json`. - The node host stores its node id, token, display name, and gateway connection info in `~/.clawdbot/node.json`.
- Exec approvals are enforced locally via `~/.clawdbot/exec-approvals.json` - Exec approvals are enforced locally via `~/.clawdbot/exec-approvals.json`
(see [Exec approvals](/tools/exec-approvals)). (see [Exec approvals](/tools/exec-approvals)).
- On macOS, the headless node host prefers the companion app exec host when reachable and falls - On macOS, the headless node host prefers the companion app exec host when reachable and falls
back to local execution if the app is unavailable. Set `CLAWDBOT_NODE_EXEC_HOST=app` to require back to local execution if the app is unavailable. Set `CLAWDBOT_NODE_EXEC_HOST=app` to require
the app, or `CLAWDBOT_NODE_EXEC_FALLBACK=0` to disable fallback. the app, or `CLAWDBOT_NODE_EXEC_FALLBACK=0` to disable fallback.
- Add `--tls` / `--tls-fingerprint` when the bridge requires TLS. - Add `--tls` / `--tls-fingerprint` when the Gateway WS uses TLS.
## Mac node mode ## Mac node mode
- The macOS menubar app connects to the Gateway bridge as a node (so `clawdbot nodes …` works against this Mac). - The macOS menubar app connects to the Gateway WS server as a node (so `clawdbot nodes …` works against this Mac).
- In remote mode, the app opens an SSH tunnel for the bridge port and connects to `localhost`. - In remote mode, the app opens an SSH tunnel for the Gateway port and connects to `localhost`.

View File

@ -41,7 +41,7 @@ Notes:
Who receives it: Who receives it:
- All WebSocket clients (macOS app, WebChat, etc.) - All WebSocket clients (macOS app, WebChat, etc.)
- All connected bridge nodes (iOS/Android), and also on node connect as an initial “current state” push. - All connected nodes (iOS/Android), and also on node connect as an initial “current state” push.
## Client behavior ## Client behavior
@ -53,9 +53,9 @@ Who receives it:
### iOS node ### iOS node
- Uses the global list for `VoiceWakeManager` trigger detection. - Uses the global list for `VoiceWakeManager` trigger detection.
- Editing Wake Words in Settings calls `voicewake.set` (over the bridge) and also keeps local wake-word detection responsive. - Editing Wake Words in Settings calls `voicewake.set` (over the Gateway WS) and also keeps local wake-word detection responsive.
### Android node ### Android node
- Exposes a Wake Words editor in Settings. - Exposes a Wake Words editor in Settings.
- Calls `voicewake.set` over the bridge so edits sync everywhere. - Calls `voicewake.set` over the Gateway WS so edits sync everywhere.

View File

@ -129,7 +129,6 @@ CLAWDBOT_IMAGE=clawdbot:latest
CLAWDBOT_GATEWAY_TOKEN=change-me-now CLAWDBOT_GATEWAY_TOKEN=change-me-now
CLAWDBOT_GATEWAY_BIND=lan CLAWDBOT_GATEWAY_BIND=lan
CLAWDBOT_GATEWAY_PORT=18789 CLAWDBOT_GATEWAY_PORT=18789
CLAWDBOT_BRIDGE_PORT=18790
CLAWDBOT_CONFIG_DIR=/root/.clawdbot CLAWDBOT_CONFIG_DIR=/root/.clawdbot
CLAWDBOT_WORKSPACE_DIR=/root/clawd CLAWDBOT_WORKSPACE_DIR=/root/clawd
@ -166,7 +165,6 @@ services:
- TERM=xterm-256color - TERM=xterm-256color
- CLAWDBOT_GATEWAY_BIND=${CLAWDBOT_GATEWAY_BIND} - CLAWDBOT_GATEWAY_BIND=${CLAWDBOT_GATEWAY_BIND}
- CLAWDBOT_GATEWAY_PORT=${CLAWDBOT_GATEWAY_PORT} - CLAWDBOT_GATEWAY_PORT=${CLAWDBOT_GATEWAY_PORT}
- CLAWDBOT_BRIDGE_PORT=${CLAWDBOT_BRIDGE_PORT}
- CLAWDBOT_GATEWAY_TOKEN=${CLAWDBOT_GATEWAY_TOKEN} - CLAWDBOT_GATEWAY_TOKEN=${CLAWDBOT_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD} - GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME} - XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
@ -179,9 +177,8 @@ services:
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly. # To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${CLAWDBOT_GATEWAY_PORT}:18789" - "127.0.0.1:${CLAWDBOT_GATEWAY_PORT}:18789"
# Optional: only if you run iOS/Android nodes against this VPS. # Optional: only if you run iOS/Android nodes against this VPS and need Canvas host.
# If you expose these publicly, read /gateway/security and firewall accordingly. # If you expose this publicly, read /gateway/security and firewall accordingly.
# - "${CLAWDBOT_BRIDGE_PORT}:18790"
# - "18793:18793" # - "18793:18793"
command: command:
[ [

View File

@ -40,7 +40,7 @@ node commands return `CANVAS_DISABLED`.
## Agent API surface ## Agent API surface
Canvas is exposed via the **node bridge**, so the agent can: Canvas is exposed via the **Gateway WebSocket**, so the agent can:
- show/hide the panel - show/hide the panel
- navigate to a path or URL - navigate to a path or URL

View File

@ -45,6 +45,13 @@ present. To reset manually:
rm ~/.clawdbot/disable-launchagent rm ~/.clawdbot/disable-launchagent
``` ```
## Attach-only mode
To force the macOS app to **never install or manage launchd**, launch it with
`--attach-only` (or `--no-launchd`). This sets `~/.clawdbot/disable-launchagent`,
so the app only attaches to an already running Gateway. You can toggle the same
behavior in Debug Settings.
## Remote mode ## Remote mode
Remote mode never starts a local Gateway. The app uses an SSH tunnel to the Remote mode never starts a local Gateway. The app uses an SSH tunnel to the

View File

@ -8,7 +8,7 @@ read_when:
## What is shown ## What is shown
- We surface the current agent work state in the menu bar icon and in the first status row of the menu. - We surface the current agent work state in the menu bar icon and in the first status row of the menu.
- Health status is hidden while work is active; it returns when all sessions are idle. - Health status is hidden while work is active; it returns when all sessions are idle.
- The “Nodes” block in the menu lists **devices** only (gateway bridge nodes via `node.list`), not client/presence entries. - The “Nodes” block in the menu lists **devices** only (paired nodes via `node.list`), not client/presence entries.
- A “Usage” section appears under Context when provider usage snapshots are available. - A “Usage” section appears under Context when provider usage snapshots are available.
## State model ## State model

View File

@ -1,5 +1,5 @@
--- ---
summary: "macOS IPC architecture for Clawdbot app, gateway node bridge, and PeekabooBridge" summary: "macOS IPC architecture for Clawdbot app, gateway node transport, and PeekabooBridge"
read_when: read_when:
- Editing IPC contracts or menu bar app IPC - Editing IPC contracts or menu bar app IPC
--- ---
@ -13,21 +13,21 @@ read_when:
- Predictable permissions: always the same signed bundle ID, launched by launchd, so TCC grants stick. - Predictable permissions: always the same signed bundle ID, launched by launchd, so TCC grants stick.
## How it works ## How it works
### Gateway + node bridge ### Gateway + node transport
- The app runs the Gateway (local mode) and connects to it as a node. - The app runs the Gateway (local mode) and connects to it as a node.
- Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`). - Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`).
### Node service + app IPC ### Node service + app IPC
- A headless node host service connects to the Gateway bridge. - A headless node host service connects to the Gateway WebSocket.
- `system.run` requests are forwarded to the macOS app over a local Unix socket. - `system.run` requests are forwarded to the macOS app over a local Unix socket.
- The app performs the exec in UI context, prompts if needed, and returns output. - The app performs the exec in UI context, prompts if needed, and returns output.
Diagram (SCI): Diagram (SCI):
``` ```
Agent -> Gateway -> Bridge -> Node Service (TS) Agent -> Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL) | IPC (UDS + token + HMAC + TTL)
v v
Mac App (UI + TCC + system.run) Mac App (UI + TCC + system.run)
``` ```
### PeekabooBridge (UI automation) ### PeekabooBridge (UI automation)

View File

@ -62,7 +62,7 @@ Node service + app IPC:
Diagram (SCI): Diagram (SCI):
``` ```
Gateway -> Bridge -> Node Service (TS) Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL) | IPC (UDS + token + HMAC + TTL)
v v
Mac App (UI + TCC + system.run) Mac App (UI + TCC + system.run)
@ -99,7 +99,7 @@ Example:
``` ```
Notes: Notes:
- `allowlist` entries are JSON-encoded argv arrays. - `allowlist` entries are glob patterns for resolved binary paths.
- Choosing “Always Allow” in the prompt adds that command to the allowlist. - Choosing “Always Allow” in the prompt adds that command to the allowlist.
- `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `NODE_OPTIONS`, `PYTHON*`, `PERL*`, `RUBYOPT`) and then merged with the apps environment. - `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `NODE_OPTIONS`, `PYTHON*`, `PERL*`, `RUBYOPT`) and then merged with the apps environment.

View File

@ -61,6 +61,7 @@ Plugins can register:
- CLI commands - CLI commands
- Background services - Background services
- Optional config validation - Optional config validation
- **Skills** (by listing `skills` directories in the plugin manifest)
Plugins run **inprocess** with the Gateway, so treat them as trusted code. Plugins run **inprocess** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools). Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).

View File

@ -34,6 +34,7 @@ Optional keys:
- `kind` (string): plugin kind (example: `"memory"`). - `kind` (string): plugin kind (example: `"memory"`).
- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`). - `channels` (array): channel ids registered by this plugin (example: `["matrix"]`).
- `providers` (array): provider ids registered by this plugin. - `providers` (array): provider ids registered by this plugin.
- `skills` (array): skill directories to load (relative to the plugin root).
- `name` (string): display name for the plugin. - `name` (string): display name for the plugin.
- `description` (string): short plugin summary. - `description` (string): short plugin summary.
- `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering. - `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering.

131
docs/prose.md Normal file
View File

@ -0,0 +1,131 @@
---
summary: "OpenProse: .prose workflows, slash commands, and state in Clawdbot"
read_when:
- You want to run or write .prose workflows
- You want to enable the OpenProse plugin
- You need to understand state storage
---
# OpenProse
OpenProse is a portable, markdown-first workflow format for orchestrating AI sessions. In Clawdbot it ships as a plugin that installs an OpenProse skill pack plus a `/prose` slash command. Programs live in `.prose` files and can spawn multiple sub-agents with explicit control flow.
Official site: https://www.prose.md
## What it can do
- Multi-agent research + synthesis with explicit parallelism.
- Repeatable approval-safe workflows (code review, incident triage, content pipelines).
- Reusable `.prose` programs you can run across supported agent runtimes.
## Install + enable
Bundled plugins are disabled by default. Enable OpenProse:
```bash
clawdbot plugins enable open-prose
```
Restart the Gateway after enabling the plugin.
Dev/local checkout: `clawdbot plugins install ./extensions/open-prose`
Related docs: [Plugins](/plugin), [Plugin manifest](/plugins/manifest), [Skills](/tools/skills).
## Slash command
OpenProse registers `/prose` as a user-invocable skill command. It routes to the OpenProse VM instructions and uses Clawdbot tools under the hood.
Common commands:
```
/prose help
/prose run <file.prose>
/prose run <handle/slug>
/prose run <https://example.com/file.prose>
/prose compile <file.prose>
/prose examples
/prose update
```
## Example: a simple `.prose` file
```prose
# Research + synthesis with two agents running in parallel.
input topic: "What should we research?"
agent researcher:
model: sonnet
prompt: "You research thoroughly and cite sources."
agent writer:
model: opus
prompt: "You write a concise summary."
parallel:
findings = session: researcher
prompt: "Research {topic}."
draft = session: writer
prompt: "Summarize {topic}."
session "Merge the findings + draft into a final answer."
context: { findings, draft }
```
## File locations
OpenProse keeps state under `.prose/` in your workspace:
```
.prose/
├── .env
├── runs/
│ └── {YYYYMMDD}-{HHMMSS}-{random}/
│ ├── program.prose
│ ├── state.md
│ ├── bindings/
│ └── agents/
└── agents/
```
User-level persistent agents live at:
```
~/.prose/agents/
```
## State modes
OpenProse supports multiple state backends:
- **filesystem** (default): `.prose/runs/...`
- **in-context**: transient, for small programs
- **sqlite** (experimental): requires `sqlite3` binary
- **postgres** (experimental): requires `psql` and a connection string
Notes:
- sqlite/postgres are opt-in and experimental.
- postgres credentials flow into subagent logs; use a dedicated, least-privileged DB.
## Remote programs
`/prose run <handle/slug>` resolves to `https://p.prose.md/<handle>/<slug>`.
Direct URLs are fetched as-is. This uses the `web_fetch` tool (or `exec` for POST).
## Clawdbot runtime mapping
OpenProse programs map to Clawdbot primitives:
| OpenProse concept | Clawdbot tool |
| --- | --- |
| Spawn session / Task tool | `sessions_spawn` |
| File read/write | `read` / `write` |
| Web fetch | `web_fetch` |
If your tool allowlist blocks these tools, OpenProse programs will fail. See [Skills config](/tools/skills-config).
## Security + approvals
Treat `.prose` files like code. Review before running. Use Clawdbot tool allowlists and approval gates to control side effects.
For deterministic, approval-gated workflows, compare with [Lobster](/tools/lobster).

View File

@ -16,9 +16,9 @@ provider in two different ways.
### 1) Built-in GitHub Copilot provider (`github-copilot`) ### 1) Built-in GitHub Copilot provider (`github-copilot`)
Use the native device-login flow to obtain a GitHub token, then exchange it for Use the native device-login flow to obtain a GitHub token and use it directly
Copilot API tokens when Clawdbot runs. This is the **default** and simplest path against the Copilot API. This is the **default** and simplest path because it
because it does not require VS Code. does not require VS Code. Enterprise domains are supported.
### 2) Copilot Proxy plugin (`copilot-proxy`) ### 2) Copilot Proxy plugin (`copilot-proxy`)
@ -39,6 +39,8 @@ clawdbot models auth login-github-copilot
You'll be prompted to visit a URL and enter a one-time code. Keep the terminal You'll be prompted to visit a URL and enter a one-time code. Keep the terminal
open until it completes. open until it completes.
If you're on GitHub Enterprise, the login will ask for your enterprise URL or
domain (for example `company.ghe.com`).
### Optional flags ### Optional flags
@ -66,5 +68,7 @@ clawdbot models set github-copilot/gpt-4o
- Requires an interactive TTY; run it directly in a terminal. - Requires an interactive TTY; run it directly in a terminal.
- Copilot model availability depends on your plan; if a model is rejected, try - Copilot model availability depends on your plan; if a model is rejected, try
another ID (for example `github-copilot/gpt-4.1`). another ID (for example `github-copilot/gpt-4.1`).
- The login stores a GitHub token in the auth profile store and exchanges it for a - The login stores a GitHub token in the auth profile store and uses it directly
Copilot API token when Clawdbot runs. for Copilot API calls.
- Base URL: `https://api.githubcopilot.com` (public) or `https://copilot-api.<domain>`
for GitHub Enterprise.

View File

@ -9,7 +9,7 @@ read_when:
Clawdbot can use many LLM providers. Pick a provider, authenticate, then set the Clawdbot can use many LLM providers. Pick a provider, authenticate, then set the
default model as `provider/model`. default model as `provider/model`.
Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Channels](/channels). Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/etc.)? See [Channels](/channels).
## Quick start ## Quick start

View File

@ -12,6 +12,7 @@ This document explains how Clawdbot manages sessions end-to-end:
- **Session routing** (how inbound messages map to a `sessionKey`) - **Session routing** (how inbound messages map to a `sessionKey`)
- **Session store** (`sessions.json`) and what it tracks - **Session store** (`sessions.json`) and what it tracks
- **Transcript persistence** (`*.jsonl`) and its structure - **Transcript persistence** (`*.jsonl`) and its structure
- **Transcript hygiene** (provider-specific fixups before runs)
- **Context limits** (context window vs tracked tokens) - **Context limits** (context window vs tracked tokens)
- **Compaction** (manual + auto-compaction) and where to hook pre-compaction work - **Compaction** (manual + auto-compaction) and where to hook pre-compaction work
- **Silent housekeeping** (e.g. memory writes that shouldnt produce user-visible output) - **Silent housekeeping** (e.g. memory writes that shouldnt produce user-visible output)
@ -20,6 +21,7 @@ If you want a higher-level overview first, start with:
- [/concepts/session](/concepts/session) - [/concepts/session](/concepts/session)
- [/concepts/compaction](/concepts/compaction) - [/concepts/compaction](/concepts/compaction)
- [/concepts/session-pruning](/concepts/session-pruning) - [/concepts/session-pruning](/concepts/session-pruning)
- [/reference/transcript-hygiene](/reference/transcript-hygiene)
--- ---

View File

@ -7,6 +7,8 @@ read_when:
*You just woke up. Time to figure out who you are.* *You just woke up. Time to figure out who you are.*
There is no memory yet. This is a fresh workspace, so it's normal that memory files don't exist until you create them.
## The Conversation ## The Conversation
Don't interrogate. Don't be robotic. Just... talk. Don't interrogate. Don't be robotic. Just... talk.

View File

@ -0,0 +1,94 @@
---
summary: "Reference: provider-specific transcript sanitization and repair rules"
read_when:
- You are debugging provider request rejections tied to transcript shape
- You are changing transcript sanitization or tool-call repair logic
- You are investigating tool-call id mismatches across providers
---
# Transcript Hygiene (Provider Fixups)
This document describes **provider-specific fixes** applied to transcripts before a run
(building model context). These are **in-memory** adjustments used to satisfy strict
provider requirements. They do **not** rewrite the stored JSONL transcript on disk.
Scope includes:
- Tool call id sanitization
- Tool result pairing repair
- Turn validation / ordering
- Thought signature cleanup
- Image payload sanitization
If you need transcript storage details, see:
- [/reference/session-management-compaction](/reference/session-management-compaction)
---
## Where this runs
All transcript hygiene is centralized in the embedded runner:
- Policy selection: `src/agents/transcript-policy.ts`
- Sanitization/repair application: `sanitizeSessionHistory` in `src/agents/pi-embedded-runner/google.ts`
The policy uses `provider`, `modelApi`, and `modelId` to decide what to apply.
---
## Global rule: image sanitization
Image payloads are always sanitized to prevent provider-side rejection due to size
limits (downscale/recompress oversized base64 images).
Implementation:
- `sanitizeSessionMessagesImages` in `src/agents/pi-embedded-helpers/images.ts`
- `sanitizeContentBlocksImages` in `src/agents/tool-images.ts`
---
## Provider matrix (current behavior)
**OpenAI / OpenAI Codex**
- Image sanitization only.
- No tool call id sanitization.
- No tool result pairing repair.
- No turn validation or reordering.
- No synthetic tool results.
- No thought signature stripping.
**Google (Generative AI / Gemini CLI / Antigravity)**
- Tool call id sanitization: strict alphanumeric.
- Tool result pairing repair and synthetic tool results.
- Turn validation (Gemini-style turn alternation).
- Google turn ordering fixup (prepend a tiny user bootstrap if history starts with assistant).
- Antigravity Claude: normalize thinking signatures; drop unsigned thinking blocks.
**Anthropic / Minimax (Anthropic-compatible)**
- Tool result pairing repair and synthetic tool results.
- Turn validation (merge consecutive user turns to satisfy strict alternation).
**Mistral (including model-id based detection)**
- Tool call id sanitization: strict9 (alphanumeric length 9).
**OpenRouter Gemini**
- Thought signature cleanup: strip non-base64 `thought_signature` values (keep base64).
**Everything else**
- Image sanitization only.
---
## Historical behavior (pre-2026.1.22)
Before the 2026.1.22 release, Clawdbot applied multiple layers of transcript hygiene:
- A **transcript-sanitize extension** ran on every context build and could:
- Repair tool use/result pairing.
- Sanitize tool call ids (including a non-strict mode that preserved `_`/`-`).
- The runner also performed provider-specific sanitization, which duplicated work.
- Additional mutations occurred outside the provider policy, including:
- Stripping `<final>` tags from assistant text before persistence.
- Dropping empty assistant error turns.
- Trimming assistant content after tool calls.
This complexity caused cross-provider regressions (notably `openai-responses`
`call_id|fc_id` pairing). The 2026.1.22 cleanup removed the extension, centralized
logic in the runner, and made OpenAI **no-touch** beyond image sanitization.

View File

@ -6,14 +6,14 @@ read_when:
--- ---
# Building a personal assistant with Clawdbot (Clawd-style) # Building a personal assistant with Clawdbot (Clawd-style)
Clawdbot is a WhatsApp + Telegram + Discord gateway for **Pi** agents. This guide is the “personal assistant” setup: one dedicated WhatsApp number that behaves like your always-on agent. Clawdbot is a WhatsApp + Telegram + Discord + iMessage gateway for **Pi** agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent.
## ⚠️ Safety first ## ⚠️ Safety first
Youre putting an agent in a position to: Youre putting an agent in a position to:
- run commands on your machine (depending on your Pi tool setup) - run commands on your machine (depending on your Pi tool setup)
- read/write files in your workspace - read/write files in your workspace
- send messages back out via WhatsApp/Telegram/Discord - send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin)
Start conservative: Start conservative:
- Always set `channels.whatsapp.allowFrom` (never run open-to-the-world on your personal Mac). - Always set `channels.whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).

View File

@ -178,7 +178,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
### What is Clawdbot, in one paragraph? ### What is Clawdbot, in one paragraph?
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the alwayson control plane; the assistant is the product. Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Mattermost (plugin), Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always-on control plane; the assistant is the product.
## Quick start and first-run setup ## Quick start and first-run setup
@ -235,7 +235,7 @@ Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** fo
- **Model/auth setup** (Anthropic **setup-token** recommended for Claude subscriptions, OpenAI Codex OAuth supported, API keys optional, LM Studio local models supported) - **Model/auth setup** (Anthropic **setup-token** recommended for Claude subscriptions, OpenAI Codex OAuth supported, API keys optional, LM Studio local models supported)
- **Workspace** location + bootstrap files - **Workspace** location + bootstrap files
- **Gateway settings** (bind/port/auth/tailscale) - **Gateway settings** (bind/port/auth/tailscale)
- **Providers** (WhatsApp, Telegram, Discord, Signal, iMessage) - **Providers** (WhatsApp, Telegram, Discord, Mattermost (plugin), Signal, iMessage)
- **Daemon install** (LaunchAgent on macOS; systemd user unit on Linux/WSL2) - **Daemon install** (LaunchAgent on macOS; systemd user unit on Linux/WSL2)
- **Health checks** and **skills** selection - **Health checks** and **skills** selection
@ -363,7 +363,7 @@ lowest friction and youre okay with sleep/restarts, run it locally.
- **Pros:** alwayson, stable network, no laptop sleep issues, easier to keep running. - **Pros:** alwayson, stable network, no laptop sleep issues, easier to keep running.
- **Cons:** often run headless (use screenshots), remote file access only, you must SSH for updates. - **Cons:** often run headless (use screenshots), remote file access only, you must SSH for updates.
**Clawdbotspecific note:** WhatsApp/Telegram/Slack/Discord all work fine from a VPS. The only real tradeoff is **headless browser** vs a visible window. See [Browser](/tools/browser). **Clawdbot-specific note:** WhatsApp/Telegram/Slack/Mattermost (plugin)/Discord all work fine from a VPS. The only real trade-off is **headless browser** vs a visible window. See [Browser](/tools/browser).
**Recommended default:** VPS if you had gateway disconnects before. Local is great when youre actively using the Mac and want local file access or UI automation with a visible browser. **Recommended default:** VPS if you had gateway disconnects before. Local is great when youre actively using the Mac and want local file access or UI automation with a visible browser.
@ -719,43 +719,42 @@ See the full config examples in [Browser](/tools/browser#use-brave-or-another-ch
### How do commands propagate between Telegram, the gateway, and nodes? ### How do commands propagate between Telegram, the gateway, and nodes?
Telegram messages are handled by the **gateway**. The gateway runs the agent and Telegram messages are handled by the **gateway**. The gateway runs the agent and
only then calls nodes over the **Bridge** when a node tool is needed: only then calls nodes over the **Gateway WebSocket** when a node tool is needed:
Telegram → Gateway → Agent → `node.*` → Node → Gateway → Telegram Telegram → Gateway → Agent → `node.*` → Node → Gateway → Telegram
Nodes dont see inbound provider traffic; they only receive bridge RPC calls. Nodes dont see inbound provider traffic; they only receive node RPC calls.
### How can my agent access my computer if the Gateway is hosted remotely? ### How can my agent access my computer if the Gateway is hosted remotely?
Short answer: **pair your computer as a node**. The Gateway runs elsewhere, but it can Short answer: **pair your computer as a node**. The Gateway runs elsewhere, but it can
call `node.*` tools (screen, camera, system) on your local machine over the Bridge. call `node.*` tools (screen, camera, system) on your local machine over the Gateway WebSocket.
Typical setup: Typical setup:
1) Run the Gateway on the alwayson host (VPS/home server). 1) Run the Gateway on the alwayson host (VPS/home server).
2) Put the Gateway host + your computer on the same tailnet. 2) Put the Gateway host + your computer on the same tailnet.
3) Enable the bridge on the Gateway host: 3) Ensure the Gateway WS is reachable (tailnet bind or SSH tunnel).
```json5 4) Open the macOS app locally and connect in **Remote over SSH** mode (or direct tailnet)
{ bridge: { enabled: true, bind: "auto" } } so it can register as a node.
```
4) Open the macOS app locally and connect in **Remote over SSH** mode so it can tunnel
the bridge port and register as a node.
5) Approve the node on the Gateway: 5) Approve the node on the Gateway:
```bash ```bash
clawdbot nodes pending clawdbot nodes pending
clawdbot nodes approve <requestId> clawdbot nodes approve <requestId>
``` ```
No separate TCP bridge is required; nodes connect over the Gateway WebSocket.
Security reminder: pairing a macOS node allows `system.run` on that machine. Only Security reminder: pairing a macOS node allows `system.run` on that machine. Only
pair devices you trust, and review [Security](/gateway/security). pair devices you trust, and review [Security](/gateway/security).
Docs: [Nodes](/nodes), [Bridge protocol](/gateway/bridge-protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security). Docs: [Nodes](/nodes), [Gateway protocol](/gateway/protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security).
### Do nodes run a gateway service? ### Do nodes run a gateway service?
No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect
to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app). to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app).
A full restart is required for `gateway`, `bridge`, `discovery`, and `canvasHost` changes. A full restart is required for `gateway`, `discovery`, and `canvasHost` changes.
### Is there an API / RPC way to apply config? ### Is there an API / RPC way to apply config?
@ -797,26 +796,19 @@ This keeps the gateway bound to loopback and exposes HTTPS via Tailscale. See [T
### How do I connect a Mac node to a remote Gateway (Tailscale Serve)? ### How do I connect a Mac node to a remote Gateway (Tailscale Serve)?
Serve only exposes the **Gateway Control UI**. Nodes use the **bridge port**. Serve exposes the **Gateway Control UI + WS**. Nodes connect over the same Gateway WS endpoint.
Recommended setup: Recommended setup:
1) **Enable the bridge on the gateway host**: 1) **Make sure the VPS + Mac are on the same tailnet**.
```json5 2) **Use the macOS app in Remote mode** (SSH target can be the tailnet hostname).
{ The app will tunnel the Gateway port and connect as a node.
bridge: { enabled: true, bind: "auto" } 3) **Approve the node** on the gateway:
}
```
`auto` prefers a tailnet IP when Tailscale is present.
2) **Make sure the VPS + Mac are on the same tailnet**.
3) **Use the macOS app in Remote mode** (SSH target can be the tailnet hostname).
The app will tunnel the bridge port and connect as a node.
4) **Approve the node** on the gateway:
```bash ```bash
clawdbot nodes pending clawdbot nodes pending
clawdbot nodes approve <requestId> clawdbot nodes approve <requestId>
``` ```
Docs: [Bridge protocol](/gateway/bridge-protocol), [Discovery](/gateway/discovery), [macOS remote mode](/platforms/mac/remote). Docs: [Gateway protocol](/gateway/protocol), [Discovery](/gateway/discovery), [macOS remote mode](/platforms/mac/remote).
## Env vars and .env loading ## Env vars and .env loading
@ -1067,6 +1059,17 @@ You can also force a specific auth profile for the provider (per session):
Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next. Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next.
It also shows the configured provider endpoint (`baseUrl`) and API mode (`api`) when available. It also shows the configured provider endpoint (`baseUrl`) and API mode (`api`) when available.
### How do I unpin a profile I set with `@profile`?
Re-run `/model` **without** the `@profile` suffix:
```
/model anthropic/claude-opus-4-5
```
If you want to return to the default, pick it from `/model` (or send `/model <default provider/model>`).
Use `/model status` to confirm which auth profile is active.
### Why do I see “Model … is not allowed” and then no reply? ### Why do I see “Model … is not allowed” and then no reply?
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any

View File

@ -12,7 +12,7 @@ Goal: go from **zero** → **first working chat** (with sane defaults) as quickl
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up: Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
- model/auth (OAuth recommended) - model/auth (OAuth recommended)
- gateway settings - gateway settings
- channels (WhatsApp/Telegram/Discord/) - channels (WhatsApp/Telegram/Discord/Mattermost (plugin)/...)
- pairing defaults (secure DMs) - pairing defaults (secure DMs)
- workspace bootstrap + skills - workspace bootstrap + skills
- optional background service - optional background service
@ -80,7 +80,7 @@ clawdbot onboard --install-daemon
What youll choose: What youll choose:
- **Local vs Remote** gateway - **Local vs Remote** gateway
- **Auth**: OpenAI Code (Codex) subscription (OAuth) or API keys. For Anthropic we recommend an API key; `claude setup-token` is also supported. - **Auth**: OpenAI Code (Codex) subscription (OAuth) or API keys. For Anthropic we recommend an API key; `claude setup-token` is also supported.
- **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, etc. - **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, Mattermost plugin tokens, etc.
- **Daemon**: background install (launchd/systemd; WSL2 uses systemd) - **Daemon**: background install (launchd/systemd; WSL2 uses systemd)
- **Runtime**: Node (recommended; required for WhatsApp/Telegram). Bun is **not recommended**. - **Runtime**: Node (recommended; required for WhatsApp/Telegram). Bun is **not recommended**.
- **Gateway token**: the wizard generates one by default (even on loopback) and stores it in `gateway.auth.token`. - **Gateway token**: the wizard generates one by default (even on loopback) and stores it in `gateway.auth.token`.
@ -140,6 +140,7 @@ WhatsApp doc: [WhatsApp](/channels/whatsapp)
The wizard can write tokens/config for you. If you prefer manual config, start with: The wizard can write tokens/config for you. If you prefer manual config, start with:
- Telegram: [Telegram](/channels/telegram) - Telegram: [Telegram](/channels/telegram)
- Discord: [Discord](/channels/discord) - Discord: [Discord](/channels/discord)
- Mattermost (plugin): [Mattermost](/channels/mattermost)
**Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot wont respond. **Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot wont respond.

View File

@ -67,6 +67,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Telegram (grammY notes)](/channels/grammy) - [Telegram (grammY notes)](/channels/grammy)
- [Slack](/channels/slack) - [Slack](/channels/slack)
- [Discord](/channels/discord) - [Discord](/channels/discord)
- [Mattermost](/channels/mattermost) (plugin)
- [Signal](/channels/signal) - [Signal](/channels/signal)
- [iMessage](/channels/imessage) - [iMessage](/channels/imessage)
- [Location parsing](/channels/location) - [Location parsing](/channels/location)
@ -96,6 +97,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
## Tools + automation ## Tools + automation
- [Tools surface](/tools) - [Tools surface](/tools)
- [OpenProse](/prose)
- [CLI reference](/cli) - [CLI reference](/cli)
- [Exec tool](/tools/exec) - [Exec tool](/tools/exec)
- [Elevated mode](/tools/elevated) - [Elevated mode](/tools/elevated)

View File

@ -45,27 +45,29 @@ Stored under `~/.clawdbot/credentials/`:
Treat these as sensitive (they gate access to your assistant). Treat these as sensitive (they gate access to your assistant).
## 2) Node pairing (iOS/Android nodes joining the gateway) ## 2) Node device pairing (iOS/Android/macOS/headless nodes)
Nodes (iOS/Android, future hardware, etc.) connect to the Gateway and request to join. Nodes connect to the Gateway as **devices** with `role: node`. The Gateway
The Gateway keeps an authoritative allowlist; new nodes require explicit approve/reject. creates a device pairing request that must be approved.
### Approve a node ### Approve a node device
```bash ```bash
clawdbot nodes pending clawdbot devices list
clawdbot nodes approve <requestId> clawdbot devices approve <requestId>
clawdbot devices reject <requestId>
``` ```
### Where the state lives ### Where the state lives
Stored under `~/.clawdbot/nodes/`: Stored under `~/.clawdbot/devices/`:
- `pending.json` (short-lived; pending requests expire) - `pending.json` (short-lived; pending requests expire)
- `paired.json` (paired nodes + tokens) - `paired.json` (paired devices + tokens)
### Details ### Notes
Full protocol + design notes: [Gateway pairing](/gateway/pairing) - The legacy `node.pair.*` API (CLI: `clawdbot nodes pending/approve`) is a
separate gateway-owned pairing store. WS nodes still require device pairing.
## Related docs ## Related docs

View File

@ -48,7 +48,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control).
- Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or setup-token (paste), plus MiniMax/GLM/Moonshot/AI Gateway options) - Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or setup-token (paste), plus MiniMax/GLM/Moonshot/AI Gateway options)
- Workspace location + bootstrap files - Workspace location + bootstrap files
- Gateway settings (port/bind/auth/tailscale) - Gateway settings (port/bind/auth/tailscale)
- Providers (Telegram, WhatsApp, Discord, Signal) - Providers (Telegram, WhatsApp, Discord, Mattermost (plugin), Signal)
- Daemon install (LaunchAgent / systemd user unit) - Daemon install (LaunchAgent / systemd user unit)
- Health check - Health check
- Skills (recommended) - Skills (recommended)
@ -117,6 +117,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
- WhatsApp: optional QR login. - WhatsApp: optional QR login.
- Telegram: bot token. - Telegram: bot token.
- Discord: bot token. - Discord: bot token.
- Mattermost (plugin): bot token + base URL.
- Signal: optional `signal-cli` install + account config. - Signal: optional `signal-cli` install + account config.
- iMessage: local `imsg` CLI path + DB access. - iMessage: local `imsg` CLI path + DB access.
- DM security: default is pairing. First DM sends a code; approve via `clawdbot pairing approve <channel> <code>` or use allowlists. - DM security: default is pairing. First DM sends a code; approve via `clawdbot pairing approve <channel> <code>` or use allowlists.

View File

@ -22,7 +22,7 @@ Exec approvals are enforced locally on the execution host:
- **gateway host**`clawdbot` process on the gateway machine - **gateway host**`clawdbot` process on the gateway machine
- **node host** → node runner (macOS companion app or headless node host) - **node host** → node runner (macOS companion app or headless node host)
Planned macOS split: macOS split:
- **node host service** forwards `system.run` to the **macOS app** over local IPC. - **node host service** forwards `system.run` to the **macOS app** over local IPC.
- **macOS app** enforces approvals + executes the command in UI context. - **macOS app** enforces approvals + executes the command in UI context.
@ -103,8 +103,8 @@ Each allowlist entry tracks:
## Auto-allow skill CLIs ## Auto-allow skill CLIs
When **Auto-allow skill CLIs** is enabled, executables referenced by known skills When **Auto-allow skill CLIs** is enabled, executables referenced by known skills
are treated as allowlisted on nodes (macOS node or headless node host). This uses the Bridge RPC to ask the are treated as allowlisted on nodes (macOS node or headless node host). This uses
gateway for the skill bin list. Disable this if you want strict manual allowlists. `skills.bins` over the Gateway RPC to fetch the skill bin list. Disable this if you want strict manual allowlists.
## Safe bins (stdin-only) ## Safe bins (stdin-only)
@ -113,6 +113,9 @@ that can run in allowlist mode **without** explicit allowlist entries. Safe bins
positional file args and path-like tokens, so they can only operate on the incoming stream. positional file args and path-like tokens, so they can only operate on the incoming stream.
Shell chaining and redirections are not auto-allowed in allowlist mode. Shell chaining and redirections are not auto-allowed in allowlist mode.
Shell chaining (`&&`, `||`, `;`) is allowed when every top-level segment satisfies the allowlist
(including safe bins or skill auto-allow). Redirections remain unsupported in allowlist mode.
Default safe bins: `jq`, `grep`, `cut`, `sort`, `uniq`, `head`, `tail`, `tr`, `wc`. Default safe bins: `jq`, `grep`, `cut`, `sort`, `uniq`, `head`, `tail`, `tr`, `wc`.
## Control UI editing ## Control UI editing
@ -151,12 +154,12 @@ Actions:
- **Always allow** → add to allowlist + run - **Always allow** → add to allowlist + run
- **Deny** → block - **Deny** → block
### macOS IPC flow (planned) ### macOS IPC flow
``` ```
Gateway -> Bridge -> Node Service (TS) Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL) | IPC (UDS + token + HMAC + TTL)
v v
Mac App (UI + approvals + system.run) Mac App (UI + approvals + system.run)
``` ```
Security notes: Security notes:

View File

@ -66,8 +66,8 @@ Example:
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`. - `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too. Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies - `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
if the exec call already sets `env.PATH`. Node PATH overrides are accepted only when they prepend if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
the node host PATH (no replacement). the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.
Per-agent node binding (use the agent list index in config): Per-agent node binding (use the agent list index in config):

View File

@ -11,6 +11,10 @@ read_when:
Lobster is a workflow shell that lets Clawdbot run multi-step tool sequences as a single, deterministic operation with explicit approval checkpoints. Lobster is a workflow shell that lets Clawdbot run multi-step tool sequences as a single, deterministic operation with explicit approval checkpoints.
## Hook
Your assistant can build the tools that manage itself. Ask for a workflow, and 30 minutes later you have a CLI plus pipelines that run as one call. Lobster is the missing piece: deterministic pipelines, explicit approvals, and resumable state.
## Why ## Why
Today, complex workflows require many back-and-forth tool calls. Each call costs tokens, and the LLM has to orchestrate every step. Lobster moves that orchestration into a typed runtime: Today, complex workflows require many back-and-forth tool calls. Each call costs tokens, and the LLM has to orchestrate every step. Lobster moves that orchestration into a typed runtime:
@ -24,6 +28,73 @@ Today, complex workflows require many back-and-forth tool calls. Each call costs
Clawdbot launches the local `lobster` CLI in **tool mode** and parses a JSON envelope from stdout. Clawdbot launches the local `lobster` CLI in **tool mode** and parses a JSON envelope from stdout.
If the pipeline pauses for approval, the tool returns a `resumeToken` so you can continue later. If the pipeline pauses for approval, the tool returns a `resumeToken` so you can continue later.
## Pattern: small CLI + JSON pipes + approvals
Build tiny commands that speak JSON, then chain them into a single Lobster call. (Example command names below — swap in your own.)
```bash
inbox list --json
inbox categorize --json
inbox apply --json
```
```json
{
"action": "run",
"pipeline": "exec --json --shell 'inbox list --json' | exec --stdin json --shell 'inbox categorize --json' | exec --stdin json --shell 'inbox apply --json' | approve --preview-from-stdin --limit 5 --prompt 'Apply changes?'",
"timeoutMs": 30000
}
```
If the pipeline requests approval, resume with the token:
```json
{
"action": "resume",
"token": "<resumeToken>",
"approve": true
}
```
AI triggers the workflow; Lobster executes the steps. Approval gates keep side effects explicit and auditable.
Example: map input items into tool calls:
```bash
gog.gmail.search --query 'newer_than:1d' \
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
```
## Workflow files (.lobster)
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
```yaml
name: inbox-triage
args:
tag:
default: "family"
steps:
- id: collect
command: inbox list --json
- id: categorize
command: inbox categorize --json
stdin: $collect.stdout
- id: approve
command: inbox apply --approve
stdin: $categorize.stdout
approval: required
- id: execute
command: inbox apply --execute
stdin: $categorize.stdout
condition: $approve.approved
```
Notes:
- `stdin: $step.stdout` and `stdin: $step.json` pass a prior steps output.
- `condition` (or `when`) can gate steps on `$step.approved`.
## Install Lobster ## Install Lobster
Install the Lobster CLI on the **same host** that runs the Clawdbot Gateway (see the [Lobster repo](https://github.com/clawdbot/lobster)), and ensure `lobster` is on `PATH`. Install the Lobster CLI on the **same host** that runs the Clawdbot Gateway (see the [Lobster repo](https://github.com/clawdbot/lobster)), and ensure `lobster` is on `PATH`.
@ -115,6 +186,16 @@ Run a pipeline in tool mode.
} }
``` ```
Run a workflow file with args:
```json
{
"action": "run",
"pipeline": "/path/to/inbox-triage.lobster",
"argsJson": "{\"tag\":\"family\"}"
}
```
### `resume` ### `resume`
Continue a halted workflow after approval. Continue a halted workflow after approval.
@ -133,6 +214,7 @@ Continue a halted workflow after approval.
- `cwd`: Working directory for the pipeline (defaults to the current process working directory). - `cwd`: Working directory for the pipeline (defaults to the current process working directory).
- `timeoutMs`: Kill the subprocess if it exceeds this duration (default: 20000). - `timeoutMs`: Kill the subprocess if it exceeds this duration (default: 20000).
- `maxStdoutBytes`: Kill the subprocess if stdout exceeds this size (default: 512000). - `maxStdoutBytes`: Kill the subprocess if stdout exceeds this size (default: 512000).
- `argsJson`: JSON string passed to `lobster run --args-json` (workflow files only).
## Output envelope ## Output envelope
@ -151,6 +233,12 @@ If `requiresApproval` is present, inspect the prompt and decide:
- `approve: true` → resume and continue side effects - `approve: true` → resume and continue side effects
- `approve: false` → cancel and finalize the workflow - `approve: false` → cancel and finalize the workflow
Use `approve --preview-from-stdin --limit N` to attach a JSON preview to approval requests without custom jq/heredoc glue. Resume tokens are now compact: Lobster stores workflow resume state under its state dir and hands back a small token key.
## OpenProse
OpenProse pairs well with Lobster: use `/prose` to orchestrate multi-agent prep, then run a Lobster pipeline for deterministic approvals. If a Prose program needs Lobster, allow the `lobster` tool for sub-agents via `tools.subagents.tools`. See [OpenProse](/prose).
## Safety ## Safety
- **Local subprocess only** — no network calls from the plugin itself. - **Local subprocess only** — no network calls from the plugin itself.
@ -169,3 +257,10 @@ If `requiresApproval` is present, inspect the prompt and decide:
- [Plugins](/plugin) - [Plugins](/plugin)
- [Plugin tool authoring](/plugins/agent-tools) - [Plugin tool authoring](/plugins/agent-tools)
## Case study: community workflows
One public example: a “second brain” CLI + Lobster pipelines that manage three Markdown vaults (personal, partner, shared). The CLI emits JSON for stats, inbox listings, and stale scans; Lobster chains those commands into workflows like `weekly-review`, `inbox-triage`, `memory-consolidation`, and `shared-task-sync`, each with approval gates. AI handles judgment (categorization) when available and falls back to deterministic rules when not.
- Thread: https://x.com/plattenschieber/status/2014508656335770033
- Repo: https://github.com/bloomedai/brain-cli

View File

@ -38,10 +38,12 @@ applies: workspace wins, then managed/local, then bundled.
## Plugins + skills ## Plugins + skills
Plugins can ship their own skills (for example, `voice-call`) and gate them via Plugins can ship their own skills by listing `skills` directories in
`metadata.clawdbot.requires.config` on the plugins config entry. See `clawdbot.plugin.json` (paths relative to the plugin root). Plugin skills load
[Plugins](/plugin) for plugin discovery/config and [Tools](/tools) for the tool when the plugin is enabled and participate in the normal skill precedence rules.
surface those skills teach. You can gate them via `metadata.clawdbot.requires.config` on the plugins config
entry. See [Plugins](/plugin) for discovery/config and [Tools](/tools) for the
tool surface those skills teach.
## ClawdHub (install + sync) ## ClawdHub (install + sync)

View File

@ -109,6 +109,7 @@ Notes:
- `/skill <name> [input]` runs a skill by name (useful when native command limits prevent per-skill commands). - `/skill <name> [input]` runs a skill by name (useful when native command limits prevent per-skill commands).
- By default, skill commands are forwarded to the model as a normal request. - By default, skill commands are forwarded to the model as a normal request.
- Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model). - Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model).
- Example: `/prose` (OpenProse plugin) — see [OpenProse](/prose).
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg. - **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg.
## Usage surfaces (what shows where) ## Usage surfaces (what shows where)

View File

@ -88,6 +88,12 @@ Session lifecycle:
- `/settings` - `/settings`
- `/exit` - `/exit`
## Local shell commands
- Prefix a line with `!` to run a local shell command on the TUI host.
- The TUI prompts once per session to allow local execution; declining keeps `!` disabled for the session.
- Commands run in a fresh, non-interactive shell in the TUI working directory (no persistent `cd`/env).
- A lone `!` is sent as a normal message; leading spaces do not trigger local exec.
## Tool output ## Tool output
- Tool calls show as cards with args + results. - Tool calls show as cards with args + results.
- Ctrl+O toggles between collapsed/expanded views. - Ctrl+O toggles between collapsed/expanded views.

View File

@ -30,7 +30,7 @@ The onboarding wizard generates a gateway token by default, so paste it here on
## What it can do (today) ## What it can do (today)
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`, `chat.inject`) - Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`, `chat.inject`)
- Stream tool calls + live tool output cards in Chat (agent events) - Stream tool calls + live tool output cards in Chat (agent events)
- Channels: WhatsApp/Telegram status + QR login + per-channel config (`channels.status`, `web.login.*`, `config.patch`) - Channels: WhatsApp/Telegram/Discord/Slack + plugin channels (Mattermost, etc.) status + QR login + per-channel config (`channels.status`, `web.login.*`, `config.patch`)
- Instances: presence list + refresh (`system-presence`) - Instances: presence list + refresh (`system-presence`)
- Sessions: list + per-session thinking/verbose overrides (`sessions.list`, `sessions.patch`) - Sessions: list + per-session thinking/verbose overrides (`sessions.list`, `sessions.patch`)
- Cron jobs: list/add/run/enable/disable + run history (`cron.*`) - Cron jobs: list/add/run/enable/disable + run history (`cron.*`)

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/bluebubbles", "name": "@clawdbot/bluebubbles",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot BlueBubbles channel plugin", "description": "Clawdbot BlueBubbles channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1563,6 +1563,100 @@ describe("BlueBubbles webhook monitor", () => {
expect.any(Object), expect.any(Object),
); );
}); });
it("stops typing on idle", async () => {
const { sendBlueBubblesTyping } = await import("./chat.js");
vi.mocked(sendBlueBubblesTyping).mockClear();
const account = createMockAccount();
const config: ClawdbotConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
chatGuid: "iMessage;-;+15551234567",
date: Date.now(),
},
};
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
await params.dispatcherOptions.onReplyStart?.();
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
await params.dispatcherOptions.onIdle?.();
});
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
expect.any(String),
false,
expect.any(Object),
);
});
it("stops typing when no reply is sent", async () => {
const { sendBlueBubblesTyping } = await import("./chat.js");
vi.mocked(sendBlueBubblesTyping).mockClear();
const account = createMockAccount();
const config: ClawdbotConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
chatGuid: "iMessage;-;+15551234567",
date: Date.now(),
},
};
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async () => undefined);
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
expect.any(String),
false,
expect.any(Object),
);
});
}); });
describe("outbound message ids", () => { describe("outbound message ids", () => {

View File

@ -1713,8 +1713,17 @@ async function processMessage(
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`); runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
} }
}, },
onIdle: () => { onIdle: async () => {
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout. if (!chatGuidForActions) return;
if (!baseUrl || !password) return;
try {
await sendBlueBubblesTyping(chatGuidForActions, false, {
cfg: config,
accountId: account.accountId,
});
} catch (err) {
logVerbose(core, runtime, `typing stop failed: ${String(err)}`);
}
}, },
onError: (err, info) => { onError: (err, info) => {
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`); runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
@ -1754,7 +1763,13 @@ async function processMessage(
}); });
} }
if (chatGuidForActions && baseUrl && password && !sentMessage) { if (chatGuidForActions && baseUrl && password && !sentMessage) {
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout. // Stop typing indicator when no message was sent (e.g., NO_REPLY)
sendBlueBubblesTyping(chatGuidForActions, false, {
cfg: config,
accountId: account.accountId,
}).catch((err) => {
logVerbose(core, runtime, `typing stop (no reply) failed: ${String(err)}`);
});
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/copilot-proxy", "name": "@clawdbot/copilot-proxy",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot Copilot Proxy provider plugin", "description": "Clawdbot Copilot Proxy provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/diagnostics-otel", "name": "@clawdbot/diagnostics-otel",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot diagnostics OpenTelemetry exporter", "description": "Clawdbot diagnostics OpenTelemetry exporter",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/discord", "name": "@clawdbot/discord",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot Discord channel plugin", "description": "Clawdbot Discord channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/google-antigravity-auth", "name": "@clawdbot/google-antigravity-auth",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot Google Antigravity OAuth provider plugin", "description": "Clawdbot Google Antigravity OAuth provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/google-gemini-cli-auth", "name": "@clawdbot/google-gemini-cli-auth",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot Gemini CLI OAuth provider plugin", "description": "Clawdbot Gemini CLI OAuth provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/imessage", "name": "@clawdbot/imessage",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot iMessage channel plugin", "description": "Clawdbot iMessage channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,9 +1,11 @@
{ {
"name": "@clawdbot/lobster", "name": "@clawdbot/lobster",
"version": "2026.1.17-1", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"clawdbot": { "clawdbot": {
"extensions": ["./index.ts"] "extensions": [
"./index.ts"
]
} }
} }

View File

@ -159,6 +159,7 @@ export function createLobsterTool(api: ClawdbotPluginApi) {
// NOTE: Prefer string enums in tool schemas; some providers reject unions/anyOf. // NOTE: Prefer string enums in tool schemas; some providers reject unions/anyOf.
action: Type.Unsafe<"run" | "resume">({ type: "string", enum: ["run", "resume"] }), action: Type.Unsafe<"run" | "resume">({ type: "string", enum: ["run", "resume"] }),
pipeline: Type.Optional(Type.String()), pipeline: Type.Optional(Type.String()),
argsJson: Type.Optional(Type.String()),
token: Type.Optional(Type.String()), token: Type.Optional(Type.String()),
approve: Type.Optional(Type.Boolean()), approve: Type.Optional(Type.Boolean()),
lobsterPath: Type.Optional(Type.String()), lobsterPath: Type.Optional(Type.String()),
@ -181,7 +182,12 @@ export function createLobsterTool(api: ClawdbotPluginApi) {
if (action === "run") { if (action === "run") {
const pipeline = typeof params.pipeline === "string" ? params.pipeline : ""; const pipeline = typeof params.pipeline === "string" ? params.pipeline : "";
if (!pipeline.trim()) throw new Error("pipeline required"); if (!pipeline.trim()) throw new Error("pipeline required");
return ["run", "--mode", "tool", pipeline]; const argv = ["run", "--mode", "tool", pipeline];
const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
if (argsJson.trim()) {
argv.push("--args-json", argsJson);
}
return argv;
} }
if (action === "resume") { if (action === "resume") {
const token = typeof params.token === "string" ? params.token : ""; const token = typeof params.token === "string" ? params.token : "";

View File

@ -1,5 +1,10 @@
# Changelog # Changelog
## 2026.1.22
### Changes
- Version alignment with core Clawdbot release numbers.
## 2026.1.21 ## 2026.1.21
### Changes ### Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/matrix", "name": "@clawdbot/matrix",
"version": "2026.1.21", "version": "2026.1.22",
"type": "module", "type": "module",
"description": "Clawdbot Matrix channel plugin", "description": "Clawdbot Matrix channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -0,0 +1,11 @@
{
"id": "mattermost",
"channels": [
"mattermost"
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

Some files were not shown because too many files have changed in this diff Show More