diff --git a/AGENTS.md b/AGENTS.md index 1b1a5b209..285630310 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ - Prefer Bun for TypeScript execution (scripts, dev, tests): `bun ` / `bunx `. - Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`. - Node remains supported for running built output (`dist/*`) and production installs. +- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`. - Type-check/build: `pnpm build` (tsc) - Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt) - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` diff --git a/CHANGELOG.md b/CHANGELOG.md index a9585be95..4a6a80d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,21 +2,76 @@ Docs: https://docs.clawd.bot +## 2026.1.22 + +### Changes +- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster +- 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. +- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting +- Docs: add /model allowlist troubleshooting note. (#1405) +- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. +- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla. +- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead). +- Signal: add typing indicators and DM read receipts via signal-cli. +- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. +- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update + +### Breaking +- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. + +### Fixes +- Config: avoid stack traces for invalid configs and log the config path. +- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47. +- Doctor: avoid recreating WhatsApp config when only legacy routing keys remain. (#900) +- Doctor: warn when gateway.mode is unset with configure/config guidance. +- 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. +- Logs: align rolling log filenames with local time and fall back to latest file when today's log is missing. (#1343) +- Models: inherit session model overrides in thread/topic sessions (Telegram topics, Slack/Discord threads). (#1376) +- 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 ### Changes +- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker. - CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output. - CLI: exec approvals mutations render tables instead of raw JSON. - Exec approvals: support wildcard agent allowlists (`*`) across all agents. +- Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing. - Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution. +- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs. +- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability. +- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot. + +### Breaking +- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http ### Fixes +- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman. +- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380) +- Agents: enforce 9-char alphanumeric tool call ids for Mistral providers. (#1372) Thanks @zerone0x. +- Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell. - Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging. - macOS: exec approvals now respect wildcard agent allowlists (`*`). +- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-. +- Gateway: prevent multiple gateways from sharing the same config/state at once (singleton lock). - UI: remove the chat stop button and keep the composer aligned to the bottom edge. +- Typing: start instant typing indicators at run start so DMs and mentions show immediately. - Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5. - Configure: seed model fallbacks from the allowlist selection when multiple models are chosen. - Model picker: list the full catalog when no model allowlist is configured. +- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo. +- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204. +- Infra: preserve fetch helper methods when wrapping abort signals. (#1387) +- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc. ## 2026.1.20 @@ -51,6 +106,7 @@ Docs: https://docs.clawd.bot - Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.clawd.bot/plugins/manifest - Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.clawd.bot/plugins/manifest - Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.clawd.bot/web/control-ui +- Agents/UI: add agent avatar support in identity config, IDENTITY.md, and the Control UI. (#1329) https://docs.clawd.bot/gateway/configuration - Plugins: add plugin slots with a dedicated memory slot selector. https://docs.clawd.bot/plugins/agent-tools - Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.clawd.bot/channels/bluebubbles - Plugins: migrate bundled messaging extensions to the plugin SDK and resolve plugin-sdk imports in the loader. @@ -60,6 +116,7 @@ Docs: https://docs.clawd.bot - Plugins: auto-enable bundled channel/provider plugins when configuration is present. - Plugins: sync plugin sources on channel switches and update npm-installed plugins during `clawdbot update`. - Plugins: share npm plugin update logic between `clawdbot update` and `clawdbot plugins update`. + - Gateway/API: add `/v1/responses` (OpenResponses) with item-based input + semantic streaming events. (#1229) - Gateway/API: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229) - Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.clawd.bot/reference/api-usage-costs diff --git a/README.md b/README.md index f24c37382..2a58b9155 100644 --- a/README.md +++ b/README.md @@ -471,30 +471,34 @@ by Peter Steinberger and the community. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs. AI/vibe-coded PRs welcome! 🤖 +Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for +[pi-mono](https://github.com/badlogic/pi-mono). + Thanks to all clawtributors:

steipete bohdanpodvirnyi joaohlisboa mneves75 MatthieuBizien MaudeBot rahthakor vrknetha radek-paclt joshp123 - mukhtharcm maxsumrall xadenryan Tobias Bischoff juanpablodlc hsrvc magimetal meaningfool NicholasSpisak abhisekbasu1 - sebslight claude jamesgroat Hyaxia dantelex daveonkels mteam88 Eng. Juan Combetto dbhurley Mariano Belinky - TSavo julianengel benithors bradleypriest timolins nachx639 sreekaransrinath gupsammy cristip73 nachoiacovino - Vasanth Rao Naik Sabavat cpojer lc0rp scald gumadeiras andranik-sahakyan davidguttman sleontenko sircrumpet peschee - rafaelreis-r thewilloftheshadow ratulsarna lutr0 danielz1z emanuelst KristijanJovanovski CashWilliams rdev osolmaz - joshrad-dev kiranjd adityashaw2 sheeek artuskg onutc tyler6204 manuelhettich minghinmatthewlam myfunc - buddyh connorshea mcinteerj John-Rood timkrase zerone0x gerardward2007 obviyus tosh-hamburg azade-c - roshanasingh4 bjesuiter cheeeee Josh Phillips Whoaa512 YuriNachos chriseidhof vignesh07 ysqander superman32432432 - Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic - kkarimi mahmoudashraf93 petter-b pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 Ghost jonasjancarik - Keith the Silly Goose L36 Server Marc mitschabaude-bot neist ngutman chrisrodz Friederike Seiler gabriel-trigo iamadig - Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff sibbl suminhthanh VACInc - wes-davis zats 24601 Chris Taylor Django Navarro evalexpr henrino3 humanwritten larlyssa mkbehr - oswalpalash pcty-nextgen-service-account Syhids Aaron Konyer aaronveklabs adam91holt ClawdFx erik-agens fcatuhe ivanrvpereira - jayhickey jeffersonwarrior jeffersonwarrior Jonathan D. Rhyne (DJ-D) jverdi longmaba mickahouan mjrussell p6l-richard philipp-spiess - robaxelsen Sash Catanzarite T5-AndyML VAC zknicker alejandro maza ameno- andrewting19 anpoirier Asleep123 - bolismauro cash-echo-bot Clawd conhecendocontato Dimitrios Ploutarchos Drake Thomsen Felix Krause gtsifrikas HazAT hrdwdmrbl - hugobarauna Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin kitze levifig Lloyd loukotal martinpucik - Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 odysseus0 prathamdby reeltimeapps RLTCmpe - rodrigouroz Rolf Fredheim Rony Kelner Samrat Jha siraht snopoke The Admiral thesash Ubuntu voidserf - wstock Zach Knickerbocker Alphonse-arianee Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani - odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock + mukhtharcm maxsumrall xadenryan Tobias Bischoff juanpablodlc hsrvc magimetal meaningfool NicholasSpisak sebslight + abhisekbasu1 jamesgroat zerone0x claude SocialNerd42069 Hyaxia dantelex daveonkels mteam88 Eng. Juan Combetto + Mariano Belinky dbhurley TSavo julianengel benithors bradleypriest timolins nachx639 sreekaransrinath gupsammy + cristip73 nachoiacovino Vasanth Rao Naik Sabavat cpojer lc0rp scald gumadeiras andranik-sahakyan davidguttman sleontenko + sircrumpet peschee rafaelreis-r thewilloftheshadow ratulsarna lutr0 danielz1z emanuelst KristijanJovanovski CashWilliams + rdev osolmaz joshrad-dev kiranjd adityashaw2 sheeek artuskg onutc tyler6204 manuelhettich + minghinmatthewlam myfunc buddyh connorshea vignesh07 mcinteerj dependabot[bot] John-Rood timkrase gerardward2007 + obviyus tosh-hamburg azade-c roshanasingh4 bjesuiter cheeeee Josh Phillips Whoaa512 YuriNachos chriseidhof + ysqander superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] dan-dr dlauer HeimdallStrategy + imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 ngutman petter-b pkrmf RandyVentures Ryan Lisse + dougvk erikpr1994 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist + chrisrodz Friederike Seiler gabriel-trigo iamadig Kit koala73 manmal ogulcancelik pasogott petradonka + rubyrunsstuff sibbl siddhantjain suminhthanh VACInc wes-davis zats 24601 Chris Taylor czekaj + Django Navarro evalexpr henrino3 humanwritten larlyssa oswalpalash pcty-nextgen-service-account Syhids Aaron Konyer aaronveklabs + adam91holt ameno- cash-echo-bot Clawd ClawdFx erik-agens fcatuhe ivanrvpereira jayhickey jeffersonwarrior + jeffersonwarrior Jonathan D. Rhyne (DJ-D) jverdi longmaba mickahouan mjrussell p6l-richard philipp-spiess robaxelsen Sash Catanzarite + T5-AndyML VAC zknicker aj47 alejandro maza andrewting19 Andrii anpoirier Asleep123 bolismauro + conhecendoia Dimitrios Ploutarchos Drake Thomsen Felix Krause gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jarvis + Jefferson Nunn Kevin Lin kitze levifig Lloyd loukotal martinpucik Miles mrdbstn MSch + Mustafa Tag Eldeen ndraiman nexty5870 odysseus0 prathamdby ptn1411 reeltimeapps RLTCmpe robbyczgw-cla rodrigouroz + Rolf Fredheim Rony Kelner Samrat Jha siraht snopoke The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin + Wimmie wstock yazinsai Zach Knickerbocker Alphonse-arianee Azade carlulsoe ddyo Erik latitudeki5223 + Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock

diff --git a/appcast.xml b/appcast.xml index a84493f32..3e2e01c6c 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,13 +3,13 @@ Clawdbot - 2026.1.20 + 2026.1.21 Wed, 21 Jan 2026 08:18:22 +0000 https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml 7116 - 2026.1.20 + 2026.1.21 15.0 - Clawdbot 2026.1.20 + Clawdbot 2026.1.21

Changes

  • Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui
  • @@ -190,7 +190,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x.

    View full changelog

    ]]> - + 2026.1.16-2 @@ -290,4 +290,4 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - \ No newline at end of file + diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index ef7a1b522..7a99b672a 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.clawdbot.android" minSdk = 31 targetSdk = 36 - versionCode = 202601200 - versionName = "2026.1.20" + versionCode = 202601210 + versionName = "2026.1.21" } buildTypes { diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 5a57ede0c..1b7b5b3d5 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.20 + 2026.1.21 CFBundleVersion - 20260120 + 20260121 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 2e154887e..e0351f399 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2026.1.20 + 2026.1.21 CFBundleVersion - 20260120 + 20260121 diff --git a/apps/ios/project.yml b/apps/ios/project.yml index eeea03bbf..ea6519001 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -81,8 +81,8 @@ targets: properties: CFBundleDisplayName: Clawdbot CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.1.20" - CFBundleVersion: "20260120" + CFBundleShortVersionString: "2026.1.21" + CFBundleVersion: "20260121" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false @@ -130,5 +130,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: ClawdbotTests - CFBundleShortVersionString: "2026.1.20" - CFBundleVersion: "20260120" + CFBundleShortVersionString: "2026.1.21" + CFBundleVersion: "20260121" diff --git a/apps/macos/Package.resolved b/apps/macos/Package.resolved index 1a357dbe6..ffc524d1c 100644 --- a/apps/macos/Package.resolved +++ b/apps/macos/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "550d4ea41d4bb2546b99a7bfa1c5cba7e28a13862bc226727ea7426c61555a33", + "originHash" : "f847d54db16b371dbb1a79271d50436cdec572179b0f0cf14cfe1b75df8dfbc2", "pins" : [ { "identity" : "axorcist", @@ -24,7 +24,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/ElevenLabsKit", "state" : { - "revision" : "7e3c948d8340abe3977014f3de020edf221e9269", + "revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d", "version" : "0.1.0" } }, diff --git a/apps/macos/Sources/Clawdbot/CommandResolver.swift b/apps/macos/Sources/Clawdbot/CommandResolver.swift index 117930710..7661c48f1 100644 --- a/apps/macos/Sources/Clawdbot/CommandResolver.swift +++ b/apps/macos/Sources/Clawdbot/CommandResolver.swift @@ -284,13 +284,16 @@ enum CommandResolver { var args: [String] = [ "-o", "BatchMode=yes", - "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", ] if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } - if !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - args.append(contentsOf: ["-i", settings.identity]) + let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) + if !identity.isEmpty { + // Only use IdentitiesOnly when an explicit identity file is provided. + // This allows 1Password SSH agent and other SSH agents to provide keys. + args.append(contentsOf: ["-o", "IdentitiesOnly=yes"]) + args.append(contentsOf: ["-i", identity]) } let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host args.append(userHost) diff --git a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift index b1f893b1e..00f93bd85 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift @@ -6,15 +6,20 @@ final class ConnectionModeCoordinator { static let shared = ConnectionModeCoordinator() private let logger = Logger(subsystem: "com.clawdbot", category: "connection") + private var lastMode: AppState.ConnectionMode? /// Apply the requested connection mode by starting/stopping local gateway, /// managing the control-channel SSH tunnel, and cleaning up chat windows/panels. func apply(mode: AppState.ConnectionMode, paused: Bool) async { + if let lastMode = self.lastMode, lastMode != mode { + GatewayProcessManager.shared.clearLastFailure() + NodesStore.shared.lastError = nil + } + self.lastMode = mode switch mode { case .unconfigured: - if let error = await NodeServiceManager.stop() { - NodesStore.shared.lastError = "Node service stop failed: \(error)" - } + _ = await NodeServiceManager.stop() + NodesStore.shared.lastError = nil await RemoteTunnelManager.shared.stopAll() WebChatManager.shared.resetTunnels() GatewayProcessManager.shared.stop() @@ -23,9 +28,8 @@ final class ConnectionModeCoordinator { Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) } case .local: - if let error = await NodeServiceManager.stop() { - NodesStore.shared.lastError = "Node service stop failed: \(error)" - } + _ = await NodeServiceManager.stop() + NodesStore.shared.lastError = nil await RemoteTunnelManager.shared.stopAll() WebChatManager.shared.resetTunnels() let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused) @@ -56,6 +60,7 @@ final class ConnectionModeCoordinator { WebChatManager.shared.resetTunnels() do { + NodesStore.shared.lastError = nil if let error = await NodeServiceManager.start() { NodesStore.shared.lastError = "Node service start failed: \(error)" } diff --git a/apps/macos/Sources/Clawdbot/ControlChannel.swift b/apps/macos/Sources/Clawdbot/ControlChannel.swift index e72ccbbde..4c47ee26e 100644 --- a/apps/macos/Sources/Clawdbot/ControlChannel.swift +++ b/apps/macos/Sources/Clawdbot/ControlChannel.swift @@ -74,6 +74,7 @@ final class ControlChannel { } private(set) var lastPingMs: Double? + private(set) var authSourceLabel: String? private let logger = Logger(subsystem: "com.clawdbot", category: "control") @@ -128,6 +129,7 @@ final class ControlChannel { await GatewayConnection.shared.shutdown() self.state = .disconnected self.lastPingMs = nil + self.authSourceLabel = nil } func health(timeout: TimeInterval? = nil) async throws -> Data { @@ -188,8 +190,11 @@ final class ControlChannel { urlErr.code == .dataNotAllowed // used for WS close 1008 auth failures { let reason = urlErr.failureURLString ?? urlErr.localizedDescription + let tokenKey = CommandResolver.connectionModeIsRemote() + ? "gateway.remote.token" + : "gateway.auth.token" return - "Gateway rejected token; set gateway.auth.token (or CLAWDBOT_GATEWAY_TOKEN) " + + "Gateway rejected token; set \(tokenKey) (or CLAWDBOT_GATEWAY_TOKEN) " + "or clear it on the gateway. " + "Reason: \(reason)" } @@ -300,6 +305,27 @@ final class ControlChannel { code: 0, userInfo: [NSLocalizedDescriptionKey: "gateway health not ok"]) } + await self.refreshAuthSourceLabel() + } + + private func refreshAuthSourceLabel() async { + let isRemote = CommandResolver.connectionModeIsRemote() + let authSource = await GatewayConnection.shared.authSource() + self.authSourceLabel = Self.formatAuthSource(authSource, isRemote: isRemote) + } + + private static func formatAuthSource(_ source: GatewayAuthSource?, isRemote: Bool) -> String? { + guard let source else { return nil } + switch source { + case .deviceToken: + return "Auth: device token (paired device)" + case .sharedToken: + return "Auth: shared token (\(isRemote ? "gateway.remote.token" : "gateway.auth.token"))" + case .password: + return "Auth: password (\(isRemote ? "gateway.remote.password" : "gateway.auth.password"))" + case .none: + return "Auth: none" + } } func sendSystemEvent(_ text: String, params: [String: AnyHashable] = [:]) async throws { diff --git a/apps/macos/Sources/Clawdbot/DebugSettings.swift b/apps/macos/Sources/Clawdbot/DebugSettings.swift index 26a1f9830..542c69112 100644 --- a/apps/macos/Sources/Clawdbot/DebugSettings.swift +++ b/apps/macos/Sources/Clawdbot/DebugSettings.swift @@ -484,6 +484,22 @@ struct DebugSettings: View { } } + VStack(alignment: .leading, spacing: 6) { + Text( + "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + Button { + LaunchdManager.startClawdbot() + } label: { + Label("Restart Clawdbot", systemImage: "arrow.counterclockwise") + } + .buttonStyle(.bordered) + .controlSize(.small) + } + HStack(spacing: 8) { Button("Restart app") { DebugActions.restartApp() } Button("Restart onboarding") { DebugActions.restartOnboarding() } diff --git a/apps/macos/Sources/Clawdbot/ExecApprovals.swift b/apps/macos/Sources/Clawdbot/ExecApprovals.swift index 15a0eeb71..53e0b10a8 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovals.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovals.swift @@ -149,6 +149,7 @@ struct ExecApprovalsResolvedDefaults { enum ExecApprovalsStore { private static let logger = Logger(subsystem: "com.clawdbot", category: "exec-approvals") + private static let defaultAgentId = "main" private static let defaultSecurity: ExecSecurity = .deny private static let defaultAsk: ExecAsk = .onMiss private static let defaultAskFallback: ExecSecurity = .deny @@ -165,13 +166,22 @@ enum ExecApprovalsStore { static func normalizeIncoming(_ file: ExecApprovalsFile) -> ExecApprovalsFile { let socketPath = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let token = file.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + var agents = file.agents ?? [:] + if let legacyDefault = agents["default"] { + if let main = agents[self.defaultAgentId] { + agents[self.defaultAgentId] = self.mergeAgents(current: main, legacy: legacyDefault) + } else { + agents[self.defaultAgentId] = legacyDefault + } + agents.removeValue(forKey: "default") + } return ExecApprovalsFile( version: 1, socket: ExecApprovalsSocketConfig( path: socketPath.isEmpty ? nil : socketPath, token: token.isEmpty ? nil : token), defaults: file.defaults, - agents: file.agents) + agents: agents) } static func readSnapshot() -> ExecApprovalsSnapshot { @@ -272,16 +282,16 @@ enum ExecApprovalsStore { ask: defaults.ask ?? self.defaultAsk, askFallback: defaults.askFallback ?? self.defaultAskFallback, autoAllowSkills: defaults.autoAllowSkills ?? self.defaultAutoAllowSkills) - let key = (agentId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) - ? agentId!.trimmingCharacters(in: .whitespacesAndNewlines) - : "default" + let key = self.agentKey(agentId) let agentEntry = file.agents?[key] ?? ExecApprovalsAgent() let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent() let resolvedAgent = ExecApprovalsResolvedDefaults( security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security, ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask, - askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback ?? resolvedDefaults.askFallback, - autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills ?? resolvedDefaults.autoAllowSkills) + askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback + ?? resolvedDefaults.askFallback, + autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills + ?? resolvedDefaults.autoAllowSkills) let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? [])) .map { entry in ExecAllowlistEntry( @@ -455,7 +465,36 @@ enum ExecApprovalsStore { private static func agentKey(_ agentId: String?) -> String { let trimmed = agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return trimmed.isEmpty ? "default" : trimmed + return trimmed.isEmpty ? self.defaultAgentId : trimmed + } + + private static func normalizedPattern(_ pattern: String?) -> String? { + let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return trimmed.isEmpty ? nil : trimmed.lowercased() + } + + private static func mergeAgents( + current: ExecApprovalsAgent, + legacy: ExecApprovalsAgent + ) -> ExecApprovalsAgent { + var seen = Set() + var allowlist: [ExecAllowlistEntry] = [] + func append(_ entry: ExecAllowlistEntry) { + guard let key = self.normalizedPattern(entry.pattern), !seen.contains(key) else { + return + } + seen.insert(key) + allowlist.append(entry) + } + for entry in current.allowlist ?? [] { append(entry) } + for entry in legacy.allowlist ?? [] { append(entry) } + + return ExecApprovalsAgent( + security: current.security ?? legacy.security, + ask: current.ask ?? legacy.ask, + askFallback: current.askFallback ?? legacy.askFallback, + autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills, + allowlist: allowlist.isEmpty ? nil : allowlist) } } @@ -554,6 +593,30 @@ enum ExecCommandFormatter { } } +enum ExecApprovalHelpers { + static func parseDecision(_ raw: String?) -> ExecApprovalDecision? { + let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmed.isEmpty else { return nil } + return ExecApprovalDecision(rawValue: trimmed) + } + + static func requiresAsk( + ask: ExecAsk, + security: ExecSecurity, + allowlistMatch: ExecAllowlistEntry?, + skillAllow: Bool) -> Bool + { + if ask == .always { return true } + if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true } + return false + } + + static func allowlistPattern(command: [String], resolution: ExecCommandResolution?) -> String? { + let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? "" + return pattern.isEmpty ? nil : pattern + } +} + enum ExecAllowlistMatcher { static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? { guard let resolution, !entries.isEmpty else { return nil } diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift index 268d155a0..dcbc0a9bb 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift @@ -314,7 +314,7 @@ private enum ExecHostExecutor { } var approvedByAsk = approvalDecision != nil - if self.requiresAsk( + if ExecApprovalHelpers.requiresAsk( ask: context.ask, security: context.security, allowlistMatch: context.allowlistMatch, @@ -417,36 +417,20 @@ private enum ExecHostExecutor { skillAllow: skillAllow) } - private static func requiresAsk( - ask: ExecAsk, - security: ExecSecurity, - allowlistMatch: ExecAllowlistEntry?, - skillAllow: Bool) -> Bool - { - if ask == .always { return true } - if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true } - return false - } - private static func persistAllowlistEntry( decision: ExecApprovalDecision?, context: ExecApprovalContext) { guard decision == .allowAlways, context.security == .allowlist else { return } - guard let pattern = self.allowlistPattern(command: context.command, resolution: context.resolution) else { + guard let pattern = ExecApprovalHelpers.allowlistPattern( + command: context.command, + resolution: context.resolution) + else { return } ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern) } - private static func allowlistPattern( - command: [String], - resolution: ExecCommandResolution?) -> String? - { - let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? "" - return pattern.isEmpty ? nil : pattern - } - private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? { guard needsScreenRecording == true else { return nil } let authorized = await PermissionManager diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift index 6a69d7860..5a6c89a92 100644 --- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift @@ -250,6 +250,11 @@ actor GatewayConnection { await self.configure(url: cfg.url, token: cfg.token, password: cfg.password) } + func authSource() async -> GatewayAuthSource? { + guard let client else { return nil } + return await client.authSource() + } + func shutdown() async { if let client { await client.shutdown() diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift index 6a5f02ca0..418d0b810 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift @@ -482,7 +482,7 @@ actor GatewayEndpointStore { let bind = GatewayEndpointStore.resolveGatewayBindMode( root: root, env: ProcessInfo.processInfo.environment) - guard bind == "auto" else { return nil } + guard bind == "tailnet" else { return nil } let currentHost = currentURL.host?.lowercased() ?? "" guard currentHost == "127.0.0.1" || currentHost == "localhost" else { return nil } @@ -562,9 +562,6 @@ actor GatewayEndpointStore { case "tailnet": return tailscaleIP ?? "127.0.0.1" case "auto": - if let tailscaleIP, !tailscaleIP.isEmpty { - return tailscaleIP - } return "127.0.0.1" case "custom": return customBindHost ?? "127.0.0.1" diff --git a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift index 6031677ea..154932c64 100644 --- a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift @@ -115,7 +115,7 @@ extension GatewayLaunchAgentManager { quiet: Bool) async -> CommandResult { let command = CommandResolver.clawdbotCommand( - subcommand: "daemon", + subcommand: "gateway", extraArgs: self.withJsonFlag(args), // Launchd management must always run locally, even if remote mode is configured. configRoot: ["gateway": ["mode": "local"]]) diff --git a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift index 9c1761544..33156f58f 100644 --- a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift @@ -42,10 +42,20 @@ final class GatewayProcessManager { private var environmentRefreshTask: Task? private var lastEnvironmentRefresh: Date? private var logRefreshTask: Task? + #if DEBUG + private var testingConnection: GatewayConnection? + #endif private let logger = Logger(subsystem: "com.clawdbot", category: "gateway.process") private let logLimit = 20000 // characters to keep in-memory private let environmentRefreshMinInterval: TimeInterval = 30 + private var connection: GatewayConnection { + #if DEBUG + return self.testingConnection ?? .shared + #else + return .shared + #endif + } func setActive(_ active: Bool) { // Remote mode should never spawn a local gateway; treat as stopped. @@ -126,6 +136,10 @@ final class GatewayProcessManager { } } + func clearLastFailure() { + self.lastFailureReason = nil + } + func refreshEnvironmentStatus(force: Bool = false) { let now = Date() if !force { @@ -178,7 +192,7 @@ final class GatewayProcessManager { let hasListener = instance != nil let attemptAttach = { - try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 2000) + try await self.connection.requestRaw(method: .health, timeoutMs: 2000) } for attempt in 0..<(hasListener ? 3 : 1) { @@ -187,6 +201,7 @@ final class GatewayProcessManager { let snap = decodeHealthSnapshot(from: data) let details = self.describe(details: instanceText, port: port, snap: snap) self.existingGatewayDetails = details + self.clearLastFailure() self.status = .attachedExisting(details: details) self.appendLog("[gateway] using existing instance: \(details)\n") self.logger.info("gateway using existing instance details=\(details)") @@ -310,9 +325,10 @@ final class GatewayProcessManager { while Date() < deadline { if !self.desiredActive { return } do { - _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500) + _ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500) let instance = await PortGuardian.shared.describe(port: port) let details = instance.map { "pid \($0.pid)" } + self.clearLastFailure() self.status = .running(details: details) self.logger.info("gateway started details=\(details ?? "ok")") self.refreshControlChannelIfNeeded(reason: "gateway started") @@ -352,7 +368,8 @@ final class GatewayProcessManager { while Date() < deadline { if !self.desiredActive { return false } do { - _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500) + _ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500) + self.clearLastFailure() return true } catch { try? await Task.sleep(nanoseconds: 300_000_000) @@ -385,3 +402,19 @@ final class GatewayProcessManager { return String(text.suffix(limit)) } } + +#if DEBUG +extension GatewayProcessManager { + func setTestingConnection(_ connection: GatewayConnection?) { + self.testingConnection = connection + } + + func setTestingDesiredActive(_ active: Bool) { + self.desiredActive = active + } + + func setTestingLastFailureReason(_ reason: String?) { + self.lastFailureReason = reason + } +} +#endif diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 8210cacd1..bffd75b3c 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -2,52 +2,25 @@ import AppKit import ClawdbotDiscovery import ClawdbotIPC import ClawdbotKit -import CoreLocation import Observation import SwiftUI struct GeneralSettings: View { @Bindable var state: AppState @AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false - @AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue - @AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true private let healthStore = HealthStore.shared private let gatewayManager = GatewayProcessManager.shared @State private var gatewayDiscovery = GatewayDiscoveryModel( localDisplayName: InstanceIdentity.displayName) - @State private var isInstallingCLI = false - @State private var cliStatus: String? - @State private var cliInstalled = false - @State private var cliInstallLocation: String? @State private var gatewayStatus: GatewayEnvironmentStatus = .checking @State private var remoteStatus: RemoteStatus = .idle @State private var showRemoteAdvanced = false private let isPreview = ProcessInfo.processInfo.isPreview private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode } - @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue var body: some View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 18) { - if !self.state.onboardingSeen { - Button { - DebugActions.restartOnboarding() - } label: { - HStack(spacing: 8) { - Label("Complete onboarding to finish setup", systemImage: "arrow.counterclockwise") - .font(.callout.weight(.semibold)) - .foregroundStyle(Color.accentColor) - Spacer(minLength: 0) - Image(systemName: "chevron.right") - .font(.caption.weight(.semibold)) - .foregroundStyle(.tertiary) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .padding(.bottom, 2) - } - VStack(alignment: .leading, spacing: 12) { SettingsToggleRow( title: "Clawdbot active", @@ -83,29 +56,6 @@ struct GeneralSettings: View { subtitle: "Allow the agent to capture a photo or short video via the built-in camera.", binding: self.$cameraEnabled) - SystemRunSettingsView() - - VStack(alignment: .leading, spacing: 6) { - Text("Location Access") - .font(.body) - - Picker("", selection: self.$locationModeRaw) { - Text("Off").tag(ClawdbotLocationMode.off.rawValue) - Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue) - Text("Always").tag(ClawdbotLocationMode.always.rawValue) - } - .labelsHidden() - .pickerStyle(.menu) - - Toggle("Precise Location", isOn: self.$locationPreciseEnabled) - .disabled(self.locationMode == .off) - - Text("Always may require System Settings to approve background location.") - .font(.footnote) - .foregroundStyle(.tertiary) - .fixedSize(horizontal: false, vertical: true) - } - SettingsToggleRow( title: "Enable Peekaboo Bridge", subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.", @@ -130,29 +80,13 @@ struct GeneralSettings: View { } .onAppear { guard !self.isPreview else { return } - self.refreshCLIStatus() self.refreshGatewayStatus() - self.lastLocationModeRaw = self.locationModeRaw } .onChange(of: self.state.canvasEnabled) { _, enabled in if !enabled { CanvasManager.shared.hideAll() } } - .onChange(of: self.locationModeRaw) { _, newValue in - let previous = self.lastLocationModeRaw - self.lastLocationModeRaw = newValue - guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return } - Task { - let granted = await self.requestLocationAuthorization(mode: mode) - if !granted { - await MainActor.run { - self.locationModeRaw = previous - self.lastLocationModeRaw = previous - } - } - } - } } private var activeBinding: Binding { @@ -161,39 +95,20 @@ struct GeneralSettings: View { set: { self.state.isPaused = !$0 }) } - private var locationMode: ClawdbotLocationMode { - ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off - } - - private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { - guard mode != .off else { return true } - guard CLLocationManager.locationServicesEnabled() else { - await MainActor.run { LocationPermissionHelper.openSettings() } - return false - } - - let status = CLLocationManager().authorizationStatus - let requireAlways = mode == .always - if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) { - return true - } - let updated = await LocationPermissionRequester.shared.request(always: requireAlways) - return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways) - } - private var connectionSection: some View { VStack(alignment: .leading, spacing: 10) { Text("Clawdbot runs") .font(.title3.weight(.semibold)) .frame(maxWidth: .infinity, alignment: .leading) - Picker("", selection: self.$state.connectionMode) { + Picker("Mode", selection: self.$state.connectionMode) { Text("Not configured").tag(AppState.ConnectionMode.unconfigured) Text("Local (this Mac)").tag(AppState.ConnectionMode.local) Text("Remote over SSH").tag(AppState.ConnectionMode.remote) } - .pickerStyle(.segmented) - .frame(width: 380, alignment: .leading) + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 260, alignment: .leading) if self.state.connectionMode == .unconfigured { Text("Pick Local or Remote to start the Gateway.") @@ -216,8 +131,6 @@ struct GeneralSettings: View { if self.state.connectionMode == .remote { self.remoteCard } - - self.cliInstaller } } @@ -299,6 +212,11 @@ struct GeneralSettings: View { .font(.caption) .foregroundStyle(.secondary) } + if let authLabel = ControlChannel.shared.authSourceLabel { + Text(authLabel) + .font(.caption) + .foregroundStyle(.secondary) + } } Text("Tip: enable Tailscale for stable remote access.") @@ -346,59 +264,6 @@ struct GeneralSettings: View { return message == self.controlStatusLine } - private var cliInstaller: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 10) { - Button { - Task { await self.installCLI() } - } label: { - let title = self.cliInstalled ? "Reinstall CLI" : "Install CLI" - ZStack { - Text(title) - .opacity(self.isInstallingCLI ? 0 : 1) - if self.isInstallingCLI { - ProgressView() - .controlSize(.mini) - } - } - .frame(minWidth: 150) - } - .disabled(self.isInstallingCLI) - - if self.isInstallingCLI { - Text("Working...") - .font(.callout) - .foregroundStyle(.secondary) - } else if self.cliInstalled { - Label("Installed", systemImage: "checkmark.circle.fill") - .font(.callout) - .foregroundStyle(.secondary) - } else { - Text("Not installed") - .font(.callout) - .foregroundStyle(.secondary) - } - } - - if let status = cliStatus { - Text(status) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } else if let installLocation = self.cliInstallLocation { - Text("Found at \(installLocation)") - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } else { - Text("Installs a user-space Node 22+ runtime and the CLI (no Homebrew).") - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } - } - } - private var gatewayInstallerCard: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { @@ -454,22 +319,6 @@ struct GeneralSettings: View { .cornerRadius(10) } - private func installCLI() async { - guard !self.isInstallingCLI else { return } - self.isInstallingCLI = true - defer { isInstallingCLI = false } - await CLIInstaller.install { status in - self.cliStatus = status - self.refreshCLIStatus() - } - } - - private func refreshCLIStatus() { - let installLocation = CLIInstaller.installedLocation() - self.cliInstallLocation = installLocation - self.cliInstalled = installLocation != nil - } - private func refreshGatewayStatus() { Task { let status = await Task.detached(priority: .utility) { @@ -763,9 +612,6 @@ extension GeneralSettings { message: "Gateway ready") view.remoteStatus = .failed("SSH failed") view.showRemoteAdvanced = true - view.cliInstalled = true - view.cliInstallLocation = "/usr/local/bin/clawdbot" - view.cliStatus = "Installed" _ = view.body state.connectionMode = .unconfigured diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift index 7bbe907ba..ad0487893 100644 --- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift +++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift @@ -145,10 +145,11 @@ extension MenuSessionsInjector { let headerItem = NSMenuItem() headerItem.tag = self.tag headerItem.isEnabled = false + let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState)) let hosted = self.makeHostedView( rootView: AnyView(MenuSessionsHeaderView( count: rows.count, - statusText: isConnected ? nil : self.controlChannelStatusText(for: channelState))), + statusText: statusText)), width: width, highlighted: false) headerItem.view = hosted @@ -598,8 +599,11 @@ extension MenuSessionsInjector { } guard self.isControlChannelConnected else { - self.cachedSnapshot = nil - self.cachedErrorText = nil + if self.cachedSnapshot != nil { + self.cachedErrorText = "Gateway disconnected (showing cached)" + } else { + self.cachedErrorText = nil + } self.cacheUpdatedAt = Date() return } @@ -624,8 +628,6 @@ extension MenuSessionsInjector { } guard self.isControlChannelConnected else { - self.cachedUsageSummary = nil - self.cachedUsageErrorText = nil self.usageCacheUpdatedAt = Date() return } @@ -648,8 +650,6 @@ extension MenuSessionsInjector { } guard self.isControlChannelConnected else { - self.cachedCostSummary = nil - self.cachedCostErrorText = nil self.costCacheUpdatedAt = Date() return } diff --git a/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift b/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift index 48001b4e9..2f1c75fe6 100644 --- a/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift +++ b/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift @@ -2,14 +2,28 @@ import Foundation import JavaScriptCore enum ModelCatalogLoader { - static let defaultPath: String = FileManager().homeDirectoryForCurrentUser - .appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path + static var defaultPath: String { self.resolveDefaultPath() } private static let logger = Logger(subsystem: "com.clawdbot", category: "models") + private nonisolated static let appSupportDir: URL = { + let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + return base.appendingPathComponent("Clawdbot", isDirectory: true) + }() + + private static var cachePath: URL { + self.appSupportDir.appendingPathComponent("model-catalog/models.generated.js", isDirectory: false) + } static func load(from path: String) async throws -> [ModelChoice] { let expanded = (path as NSString).expandingTildeInPath - self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)") - let source = try String(contentsOfFile: expanded, encoding: .utf8) + guard let resolved = self.resolvePath(preferred: expanded) else { + self.logger.error("model catalog load failed: file not found") + throw NSError( + domain: "ModelCatalogLoader", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Model catalog file not found"]) + } + self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: resolved.path).lastPathComponent)") + let source = try String(contentsOfFile: resolved.path, encoding: .utf8) let sanitized = self.sanitize(source: source) let ctx = JSContext() @@ -45,9 +59,82 @@ enum ModelCatalogLoader { return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending } self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)") + if resolved.shouldCache { + self.cacheCatalog(sourcePath: resolved.path) + } return sorted } + private static func resolveDefaultPath() -> String { + let cache = self.cachePath.path + if FileManager().isReadableFile(atPath: cache) { return cache } + if let bundlePath = self.bundleCatalogPath() { return bundlePath } + if let nodePath = self.nodeModulesCatalogPath() { return nodePath } + return cache + } + + private static func resolvePath(preferred: String) -> (path: String, shouldCache: Bool)? { + if FileManager().isReadableFile(atPath: preferred) { + return (preferred, preferred != self.cachePath.path) + } + + if let bundlePath = self.bundleCatalogPath(), bundlePath != preferred { + self.logger.warning("model catalog path missing; falling back to bundled catalog") + return (bundlePath, true) + } + + let cache = self.cachePath.path + if cache != preferred, FileManager().isReadableFile(atPath: cache) { + self.logger.warning("model catalog path missing; falling back to cached catalog") + return (cache, false) + } + + if let nodePath = self.nodeModulesCatalogPath(), nodePath != preferred { + self.logger.warning("model catalog path missing; falling back to node_modules catalog") + return (nodePath, true) + } + + return nil + } + + private static func bundleCatalogPath() -> String? { + guard let url = Bundle.main.url(forResource: "models.generated", withExtension: "js") else { + return nil + } + return url.path + } + + private static func nodeModulesCatalogPath() -> String? { + let roots = [ + URL(fileURLWithPath: CommandResolver.projectRootPath()), + URL(fileURLWithPath: FileManager().currentDirectoryPath), + ] + for root in roots { + let candidate = root + .appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js") + if FileManager().isReadableFile(atPath: candidate.path) { + return candidate.path + } + } + return nil + } + + private static func cacheCatalog(sourcePath: String) { + let destination = self.cachePath + do { + try FileManager().createDirectory( + at: destination.deletingLastPathComponent(), + withIntermediateDirectories: true) + if FileManager().fileExists(atPath: destination.path) { + try FileManager().removeItem(at: destination) + } + try FileManager().copyItem(atPath: sourcePath, toPath: destination.path) + self.logger.debug("model catalog cached file=\(destination.lastPathComponent)") + } catch { + self.logger.warning("model catalog cache failed: \(error.localizedDescription)") + } + } + private static func sanitize(source: String) -> String { guard let exportRange = source.range(of: "export const MODELS"), let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"), diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift index 16f340415..184164262 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift @@ -480,26 +480,26 @@ actor MacNodeRuntime { message: "SYSTEM_RUN_DISABLED: security=deny") } - let requiresAsk: Bool = { - if ask == .always { return true } - if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true } - return false - }() - - let approvedByAsk = params.approved == true - if requiresAsk, !approvedByAsk { - await self.emitExecEvent( - "exec.denied", - payload: ExecEventPayload( - sessionKey: sessionKey, - runId: runId, - host: "node", - command: displayCommand, - reason: "approval-required")) - return Self.errorResponse( - req, - code: .unavailable, - message: "SYSTEM_RUN_DENIED: approval required") + let approval = await self.resolveSystemRunApproval( + req: req, + params: params, + context: ExecRunContext( + displayCommand: displayCommand, + security: security, + ask: ask, + agentId: agentId, + resolution: resolution, + allowlistMatch: allowlistMatch, + skillAllow: skillAllow, + sessionKey: sessionKey, + runId: runId)) + if let response = approval.response { return response } + let approvedByAsk = approval.approvedByAsk + let persistAllowlist = approval.persistAllowlist + if persistAllowlist, security == .allowlist, + let pattern = ExecApprovalHelpers.allowlistPattern(command: command, resolution: resolution) + { + ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern) } if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk { @@ -619,6 +619,99 @@ actor MacNodeRuntime { return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) } + private struct ExecApprovalOutcome { + var approvedByAsk: Bool + var persistAllowlist: Bool + var response: BridgeInvokeResponse? + } + + private struct ExecRunContext { + var displayCommand: String + var security: ExecSecurity + var ask: ExecAsk + var agentId: String? + var resolution: ExecCommandResolution? + var allowlistMatch: ExecAllowlistEntry? + var skillAllow: Bool + var sessionKey: String + var runId: String + } + + private func resolveSystemRunApproval( + req: BridgeInvokeRequest, + params: ClawdbotSystemRunParams, + context: ExecRunContext) async -> ExecApprovalOutcome + { + let requiresAsk = ExecApprovalHelpers.requiresAsk( + ask: context.ask, + security: context.security, + allowlistMatch: context.allowlistMatch, + skillAllow: context.skillAllow) + + let decisionFromParams = ExecApprovalHelpers.parseDecision(params.approvalDecision) + var approvedByAsk = params.approved == true || decisionFromParams != nil + var persistAllowlist = decisionFromParams == .allowAlways + if decisionFromParams == .deny { + await self.emitExecEvent( + "exec.denied", + payload: ExecEventPayload( + sessionKey: context.sessionKey, + runId: context.runId, + host: "node", + command: context.displayCommand, + reason: "user-denied")) + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: Self.errorResponse( + req, + code: .unavailable, + message: "SYSTEM_RUN_DENIED: user denied")) + } + + if requiresAsk, !approvedByAsk { + let decision = await MainActor.run { + ExecApprovalsPromptPresenter.prompt( + ExecApprovalPromptRequest( + command: context.displayCommand, + cwd: params.cwd, + host: "node", + security: context.security.rawValue, + ask: context.ask.rawValue, + agentId: context.agentId, + resolvedPath: context.resolution?.resolvedPath)) + } + switch decision { + case .deny: + await self.emitExecEvent( + "exec.denied", + payload: ExecEventPayload( + sessionKey: context.sessionKey, + runId: context.runId, + host: "node", + command: context.displayCommand, + reason: "user-denied")) + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: Self.errorResponse( + req, + code: .unavailable, + message: "SYSTEM_RUN_DENIED: user denied")) + case .allowAlways: + approvedByAsk = true + persistAllowlist = true + case .allowOnce: + approvedByAsk = true + } + } + + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: nil) + } + private func handleSystemExecApprovalsGet(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { _ = ExecApprovalsStore.ensureFile() let snapshot = ExecApprovalsStore.readSnapshot() diff --git a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift index d7ee5339c..f5a926032 100644 --- a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift +++ b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift @@ -1,4 +1,6 @@ import ClawdbotIPC +import ClawdbotKit +import CoreLocation import SwiftUI struct PermissionsSettings: View { @@ -8,6 +10,8 @@ struct PermissionsSettings: View { var body: some View { VStack(alignment: .leading, spacing: 14) { + SystemRunSettingsView() + Text("Allow these so Clawdbot can notify and capture when needed.") .padding(.top, 4) @@ -15,6 +19,8 @@ struct PermissionsSettings: View { .padding(.horizontal, 2) .padding(.vertical, 6) + LocationAccessSettings() + Button("Restart onboarding") { self.showOnboarding() } .buttonStyle(.bordered) Spacer() @@ -24,6 +30,72 @@ struct PermissionsSettings: View { } } +private struct LocationAccessSettings: View { + @AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue + @AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true + @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Location Access") + .font(.body) + + Picker("", selection: self.$locationModeRaw) { + Text("Off").tag(ClawdbotLocationMode.off.rawValue) + Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue) + Text("Always").tag(ClawdbotLocationMode.always.rawValue) + } + .labelsHidden() + .pickerStyle(.menu) + + Toggle("Precise Location", isOn: self.$locationPreciseEnabled) + .disabled(self.locationMode == .off) + + Text("Always may require System Settings to approve background location.") + .font(.footnote) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + } + .onAppear { + self.lastLocationModeRaw = self.locationModeRaw + } + .onChange(of: self.locationModeRaw) { _, newValue in + let previous = self.lastLocationModeRaw + self.lastLocationModeRaw = newValue + guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return } + Task { + let granted = await self.requestLocationAuthorization(mode: mode) + if !granted { + await MainActor.run { + self.locationModeRaw = previous + self.lastLocationModeRaw = previous + } + } + } + } + } + + private var locationMode: ClawdbotLocationMode { + ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off + } + + private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { + guard mode != .off else { return true } + guard CLLocationManager.locationServicesEnabled() else { + await MainActor.run { LocationPermissionHelper.openSettings() } + return false + } + + let status = CLLocationManager().authorizationStatus + let requireAlways = mode == .always + if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) { + return true + } + let updated = await LocationPermissionRequester.shared.request(always: requireAlways) + return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways) + } +} + struct PermissionStatusList: View { let status: [Capability: Bool] let refresh: () async -> Void @@ -45,25 +117,6 @@ struct PermissionStatusList: View { .font(.footnote) .padding(.top, 2) .help("Refresh status") - - if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false { - VStack(alignment: .leading, spacing: 8) { - Text( - "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - - Button { - LaunchdManager.startClawdbot() - } label: { - Label("Restart Clawdbot", systemImage: "arrow.counterclockwise") - } - .buttonStyle(.bordered) - .controlSize(.small) - } - .padding(.top, 4) - } } } diff --git a/apps/macos/Sources/Clawdbot/PortGuardian.swift b/apps/macos/Sources/Clawdbot/PortGuardian.swift index 50d4e0e9f..422300d59 100644 --- a/apps/macos/Sources/Clawdbot/PortGuardian.swift +++ b/apps/macos/Sources/Clawdbot/PortGuardian.swift @@ -184,6 +184,14 @@ actor PortGuardian { } } + func isListening(port: Int, pid: Int32? = nil) async -> Bool { + let listeners = await self.listeners(on: port) + if let pid { + return listeners.contains(where: { $0.pid == pid }) + } + return !listeners.isEmpty + } + private func listeners(on port: Int) async -> [Listener] { let res = await ShellExecutor.run( command: ["lsof", "-nP", "-iTCP:\(port)", "-sTCP:LISTEN", "-Fpcn"], diff --git a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift index ccbeb6e8d..8eaee1c05 100644 --- a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift +++ b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift @@ -72,7 +72,6 @@ final class RemotePortTunnel { } var args: [String] = [ "-o", "BatchMode=yes", - "-o", "IdentitiesOnly=yes", "-o", "ExitOnForwardFailure=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", @@ -84,7 +83,12 @@ final class RemotePortTunnel { ] if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) - if !identity.isEmpty { args.append(contentsOf: ["-i", identity]) } + if !identity.isEmpty { + // Only use IdentitiesOnly when an explicit identity file is provided. + // This allows 1Password SSH agent and other SSH agents to provide keys. + args.append(contentsOf: ["-o", "IdentitiesOnly=yes"]) + args.append(contentsOf: ["-i", identity]) + } let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host args.append(userHost) diff --git a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift index 5e42fbd05..898b1b482 100644 --- a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift +++ b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift @@ -20,11 +20,13 @@ actor RemoteTunnelManager { tunnel.process.isRunning, let local = tunnel.localPort { - if await self.isTunnelHealthy(port: local) { + let pid = tunnel.process.processIdentifier + if await PortGuardian.shared.isListening(port: Int(local), pid: pid) { self.logger.info("reusing active SSH tunnel localPort=\(local, privacy: .public)") return local } - self.logger.error("active SSH tunnel on port \(local, privacy: .public) is unhealthy; restarting") + self.logger.error( + "active SSH tunnel on port \(local, privacy: .public) is not listening; restarting") await self.beginRestart() tunnel.terminate() self.controlTunnel = nil @@ -35,19 +37,11 @@ actor RemoteTunnelManager { if let desc = await PortGuardian.shared.describe(port: Int(desiredPort)), self.isSshProcess(desc) { - if await self.isTunnelHealthy(port: desiredPort) { - self.logger.info( - "reusing existing SSH tunnel listener " + - "localPort=\(desiredPort, privacy: .public) " + - "pid=\(desc.pid, privacy: .public)") - return desiredPort - } - if self.restartInFlight { - self.logger.info("control tunnel restart in flight; skip stale tunnel cleanup") - return nil - } - await self.beginRestart() - await self.cleanupStaleTunnel(desc: desc, port: desiredPort) + self.logger.info( + "reusing existing SSH tunnel listener " + + "localPort=\(desiredPort, privacy: .public) " + + "pid=\(desc.pid, privacy: .public)") + return desiredPort } return nil } @@ -88,10 +82,6 @@ actor RemoteTunnelManager { self.controlTunnel = nil } - private func isTunnelHealthy(port: UInt16) async -> Bool { - await PortGuardian.shared.probeGatewayHealth(port: Int(port)) - } - private func isSshProcess(_ desc: PortGuardian.Descriptor) -> Bool { let cmd = desc.command.lowercased() if cmd.contains("ssh") { return true } @@ -128,21 +118,5 @@ actor RemoteTunnelManager { try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000)) } - private func cleanupStaleTunnel(desc: PortGuardian.Descriptor, port: UInt16) async { - let pid = desc.pid - self.logger.error( - "stale SSH tunnel detected on port \(port, privacy: .public) pid \(pid, privacy: .public)") - let killed = await self.kill(pid: pid) - if !killed { - self.logger.error("failed to terminate stale SSH tunnel pid \(pid, privacy: .public)") - } - await PortGuardian.shared.removeRecord(pid: pid) - } - - private func kill(pid: Int32) async -> Bool { - let term = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2) - if term.ok { return true } - let sigkill = await ShellExecutor.run(command: ["kill", "-KILL", "\(pid)"], cwd: nil, env: nil, timeout: 2) - return sigkill.ok - } + // Keep tunnel reuse lightweight; restart only when the listener disappears. } diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist index a3e616ffc..283901c0f 100644 --- a/apps/macos/Sources/Clawdbot/Resources/Info.plist +++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.20 + 2026.1.21 CFBundleVersion - 202601200 + 202601210 CFBundleIconFile Clawdbot CFBundleURLTypes diff --git a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift index f48840385..88c75160e 100644 --- a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift +++ b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift @@ -52,6 +52,51 @@ actor SessionPreviewCache { } } +actor SessionPreviewLimiter { + static let shared = SessionPreviewLimiter(maxConcurrent: 2) + + private let maxConcurrent: Int + private var available: Int + private var waitQueue: [UUID] = [] + private var waiters: [UUID: CheckedContinuation] = [:] + + init(maxConcurrent: Int) { + let normalized = max(1, maxConcurrent) + self.maxConcurrent = normalized + self.available = normalized + } + + func withPermit(_ operation: () async throws -> T) async throws -> T { + await self.acquire() + defer { self.release() } + if Task.isCancelled { throw CancellationError() } + return try await operation() + } + + private func acquire() async { + if self.available > 0 { + self.available -= 1 + return + } + let id = UUID() + await withCheckedContinuation { cont in + self.waitQueue.append(id) + self.waiters[id] = cont + } + } + + private func release() { + if let id = self.waitQueue.first { + self.waitQueue.removeFirst() + if let cont = self.waiters.removeValue(forKey: id) { + cont.resume() + } + return + } + self.available = min(self.available + 1, self.maxConcurrent) + } +} + #if DEBUG extension SessionPreviewCache { func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) { @@ -184,17 +229,31 @@ enum SessionMenuPreviewLoader { return self.snapshot(from: cached) } + let isConnected = await MainActor.run { + 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 { let timeoutMs = Int(self.previewTimeoutSeconds * 1000) - let payload = 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 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) await SessionPreviewCache.shared.store(items: built, for: sessionKey) return Self.snapshot(from: built) diff --git a/apps/macos/Sources/Clawdbot/TailscaleService.swift b/apps/macos/Sources/Clawdbot/TailscaleService.swift index 57424f6a0..413e8d0c8 100644 --- a/apps/macos/Sources/Clawdbot/TailscaleService.swift +++ b/apps/macos/Sources/Clawdbot/TailscaleService.swift @@ -221,6 +221,6 @@ final class TailscaleService { } nonisolated static func fallbackTailnetIPv4() -> String? { - Self.detectTailnetIPv4() + self.detectTailnetIPv4() } } diff --git a/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift b/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift index 91bac16cf..4df1c96a1 100644 --- a/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift +++ b/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift @@ -309,7 +309,7 @@ private func resolveLocalHost(bind: String?) -> String { let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let tailnetIP = detectTailnetIPv4() switch normalized { - case "tailnet", "auto": + case "tailnet": return tailnetIP ?? "127.0.0.1" default: return "127.0.0.1" diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift index 04c3bab09..aefbdb572 100644 --- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift @@ -18,6 +18,7 @@ public struct ConnectParams: Codable, Sendable { public let caps: [String]? public let commands: [String]? public let permissions: [String: AnyCodable]? + public let pathenv: String? public let role: String? public let scopes: [String]? public let device: [String: AnyCodable]? @@ -32,6 +33,7 @@ public struct ConnectParams: Codable, Sendable { caps: [String]?, commands: [String]?, permissions: [String: AnyCodable]?, + pathenv: String?, role: String?, scopes: [String]?, device: [String: AnyCodable]?, @@ -45,6 +47,7 @@ public struct ConnectParams: Codable, Sendable { self.caps = caps self.commands = commands self.permissions = permissions + self.pathenv = pathenv self.role = role self.scopes = scopes self.device = device @@ -59,6 +62,7 @@ public struct ConnectParams: Codable, Sendable { case caps case commands case permissions + case pathenv = "pathEnv" case role case scopes case device @@ -548,6 +552,44 @@ public struct AgentParams: Codable, Sendable { } } +public struct AgentIdentityParams: Codable, Sendable { + public let agentid: String? + public let sessionkey: String? + + public init( + agentid: String?, + sessionkey: String? + ) { + self.agentid = agentid + self.sessionkey = sessionkey + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case sessionkey = "sessionKey" + } +} + +public struct AgentIdentityResult: Codable, Sendable { + public let agentid: String + public let name: String? + public let avatar: String? + + public init( + agentid: String, + name: String?, + avatar: String? + ) { + self.agentid = agentid + self.name = name + self.avatar = avatar + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case name + case avatar + } +} + public struct AgentWaitParams: Codable, Sendable { public let runid: String public let timeoutms: Int? @@ -1443,17 +1485,21 @@ public struct WebLoginWaitParams: Codable, Sendable { public struct AgentSummary: Codable, Sendable { public let id: String public let name: String? + public let identity: [String: AnyCodable]? public init( id: String, - name: String? + name: String?, + identity: [String: AnyCodable]? ) { self.id = id self.name = name + self.identity = identity } private enum CodingKeys: String, CodingKey { case id case name + case identity } } @@ -1904,6 +1950,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable { } public struct ExecApprovalRequestParams: Codable, Sendable { + public let id: String? public let command: String public let cwd: String? public let host: String? @@ -1915,6 +1962,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { public let timeoutms: Int? public init( + id: String?, command: String, cwd: String?, host: String?, @@ -1925,6 +1973,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { sessionkey: String?, timeoutms: Int? ) { + self.id = id self.command = command self.cwd = cwd self.host = host @@ -1936,6 +1985,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { self.timeoutms = timeoutms } private enum CodingKeys: String, CodingKey { + case id case command case cwd case host diff --git a/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift new file mode 100644 index 000000000..a1774d49f --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift @@ -0,0 +1,60 @@ +import Foundation +import Testing +@testable import Clawdbot + +@Suite struct ExecApprovalHelpersTests { + @Test func parseDecisionTrimsAndRejectsInvalid() { + #expect(ExecApprovalHelpers.parseDecision("allow-once") == .allowOnce) + #expect(ExecApprovalHelpers.parseDecision(" allow-always ") == .allowAlways) + #expect(ExecApprovalHelpers.parseDecision("deny") == .deny) + #expect(ExecApprovalHelpers.parseDecision("") == nil) + #expect(ExecApprovalHelpers.parseDecision("nope") == nil) + } + + @Test func allowlistPatternPrefersResolution() { + let resolved = ExecCommandResolution( + rawExecutable: "rg", + resolvedPath: "/opt/homebrew/bin/rg", + executableName: "rg", + cwd: nil) + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: resolved) == resolved.resolvedPath) + + let rawOnly = ExecCommandResolution( + rawExecutable: "rg", + resolvedPath: nil, + executableName: "rg", + cwd: nil) + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: rawOnly) == "rg") + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: nil) == "rg") + #expect(ExecApprovalHelpers.allowlistPattern(command: [], resolution: nil) == nil) + } + + @Test func requiresAskMatchesPolicy() { + let entry = ExecAllowlistEntry(pattern: "/bin/ls", lastUsedAt: nil, lastUsedCommand: nil, lastResolvedPath: nil) + #expect(ExecApprovalHelpers.requiresAsk( + ask: .always, + security: .deny, + allowlistMatch: nil, + skillAllow: false)) + #expect(ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: nil, + skillAllow: false)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: entry, + skillAllow: false)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: nil, + skillAllow: true)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .off, + security: .allowlist, + allowlistMatch: nil, + skillAllow: false)) + } +} diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift index 11c9ec158..3513388a2 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift @@ -140,14 +140,14 @@ import Testing #expect(resolved.mode == .remote) } - @Test func resolveLocalGatewayHostPrefersTailnetForAuto() { + @Test func resolveLocalGatewayHostUsesLoopbackForAutoEvenWithTailnet() { let host = GatewayEndpointStore._testResolveLocalGatewayHost( bindMode: "auto", tailscaleIP: "100.64.1.2") - #expect(host == "100.64.1.2") + #expect(host == "127.0.0.1") } - @Test func resolveLocalGatewayHostFallsBackToLoopbackForAuto() { + @Test func resolveLocalGatewayHostUsesLoopbackForAutoWithoutTailnet() { let host = GatewayEndpointStore._testResolveLocalGatewayHost( bindMode: "auto", tailscaleIP: nil) diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift index 36bed4aa6..9446919e7 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift @@ -48,7 +48,10 @@ import Testing @Test func expectedGatewayVersionFromStringUsesParser() { #expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2)) - #expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11)) + #expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver( + major: 2026, + minor: 1, + patch: 11)) #expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil) } } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift new file mode 100644 index 000000000..18e529389 --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift @@ -0,0 +1,146 @@ +import Foundation +import os +import Testing +@testable import Clawdbot + +@Suite(.serialized) +@MainActor +struct GatewayProcessManagerTests { + private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable { + private let connectRequestID = OSAllocatedUnfairLock(initialState: nil) + private let pendingReceiveHandler = + OSAllocatedUnfairLock<(@Sendable (Result) + -> Void)?>(initialState: nil) + private let cancelCount = OSAllocatedUnfairLock(initialState: 0) + private let sendCount = OSAllocatedUnfairLock(initialState: 0) + + var state: URLSessionTask.State = .suspended + + func resume() { + self.state = .running + } + + func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + _ = (closeCode, reason) + self.state = .canceling + self.cancelCount.withLock { $0 += 1 } + let handler = self.pendingReceiveHandler.withLock { handler in + defer { handler = nil } + return handler + } + handler?(Result.failure(URLError(.cancelled))) + } + + func send(_ message: URLSessionWebSocketTask.Message) async throws { + let currentSendCount = self.sendCount.withLock { count in + defer { count += 1 } + return count + } + + if currentSendCount == 0 { + guard case let .data(data) = message else { return } + if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + (obj["type"] as? String) == "req", + (obj["method"] as? String) == "connect", + let id = obj["id"] as? String + { + self.connectRequestID.withLock { $0 = id } + } + return + } + + guard case let .data(data) = message else { return } + guard + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + (obj["type"] as? String) == "req", + let id = obj["id"] as? String + else { + return + } + + let response = Self.responseData(id: id) + let handler = self.pendingReceiveHandler.withLock { $0 } + handler?(Result.success(.data(response))) + } + + func receive() async throws -> URLSessionWebSocketTask.Message { + let id = self.connectRequestID.withLock { $0 } ?? "connect" + return .data(Self.connectOkData(id: id)) + } + + func receive( + completionHandler: @escaping @Sendable (Result) -> Void) + { + self.pendingReceiveHandler.withLock { $0 = completionHandler } + } + + private static func connectOkData(id: String) -> Data { + let json = """ + { + "type": "res", + "id": "\(id)", + "ok": true, + "payload": { + "type": "hello-ok", + "protocol": 2, + "server": { "version": "test", "connId": "test" }, + "features": { "methods": [], "events": [] }, + "snapshot": { + "presence": [ { "ts": 1 } ], + "health": {}, + "stateVersion": { "presence": 0, "health": 0 }, + "uptimeMs": 0 + }, + "policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 } + } + } + """ + return Data(json.utf8) + } + + private static func responseData(id: String) -> Data { + let json = """ + { + "type": "res", + "id": "\(id)", + "ok": true, + "payload": { "ok": true } + } + """ + return Data(json.utf8) + } + } + + private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable { + private let tasks = OSAllocatedUnfairLock(initialState: [FakeWebSocketTask]()) + + func makeWebSocketTask(url: URL) -> WebSocketTaskBox { + _ = url + let task = FakeWebSocketTask() + self.tasks.withLock { $0.append(task) } + return WebSocketTaskBox(task: task) + } + } + + @Test func clearsLastFailureWhenHealthSucceeds() async { + let session = FakeWebSocketSession() + let url = URL(string: "ws://example.invalid")! + let connection = GatewayConnection( + configProvider: { (url: url, token: nil, password: nil) }, + sessionBox: WebSocketSessionBox(session: session)) + + let manager = GatewayProcessManager.shared + manager.setTestingConnection(connection) + manager.setTestingDesiredActive(true) + manager.setTestingLastFailureReason("health failed") + defer { + manager.setTestingConnection(nil) + manager.setTestingDesiredActive(false) + manager.setTestingLastFailureReason(nil) + } + + let ready = await manager.waitForGatewayReady(timeout: 0.5) + #expect(ready) + #expect(manager.lastFailureReason == nil) + } +} diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift index d243f6a96..44399a3e6 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift @@ -12,6 +12,7 @@ public struct ClawdbotChatView: View { @State private var scrollPosition: UUID? @State private var showSessions = false @State private var hasPerformedInitialScroll = false + @State private var isPinnedToBottom = true private let showsSessionSwitcher: Bool private let style: Style private let markdownVariant: ChatMarkdownVariant @@ -87,36 +88,28 @@ public struct ClawdbotChatView: View { private var messageList: some View { ZStack { ScrollView { - #if os(macOS) - VStack(spacing: 0) { - LazyVStack(spacing: Layout.messageSpacing) { - self.messageListRows - } - - Color.clear - .frame(height: Layout.messageListPaddingBottom) - .id(self.scrollerBottomID) - } - // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. - .scrollTargetLayout() - .padding(.top, Layout.messageListPaddingTop) - .padding(.horizontal, Layout.messageListPaddingHorizontal) - #else LazyVStack(spacing: Layout.messageSpacing) { self.messageListRows Color.clear + #if os(macOS) + .frame(height: Layout.messageListPaddingBottom) + #else .frame(height: Layout.messageListPaddingBottom + 1) + #endif .id(self.scrollerBottomID) } // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. .scrollTargetLayout() .padding(.top, Layout.messageListPaddingTop) .padding(.horizontal, Layout.messageListPaddingHorizontal) - #endif } // Keep the scroll pinned to the bottom for new messages. .scrollPosition(id: self.$scrollPosition, anchor: .bottom) + .onChange(of: self.scrollPosition) { _, position in + guard let position else { return } + self.isPinnedToBottom = position == self.scrollerBottomID + } if self.viewModel.isLoading { ProgressView() @@ -133,18 +126,26 @@ public struct ClawdbotChatView: View { guard !isLoading, !self.hasPerformedInitialScroll else { return } self.scrollPosition = self.scrollerBottomID self.hasPerformedInitialScroll = true + self.isPinnedToBottom = true } .onChange(of: self.viewModel.sessionKey) { _, _ in self.hasPerformedInitialScroll = false + self.isPinnedToBottom = true } .onChange(of: self.viewModel.messages.count) { _, _ in - guard self.hasPerformedInitialScroll else { return } + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } withAnimation(.snappy(duration: 0.22)) { self.scrollPosition = self.scrollerBottomID } } .onChange(of: self.viewModel.pendingRunCount) { _, _ in - guard self.hasPerformedInitialScroll else { return } + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } + withAnimation(.snappy(duration: 0.22)) { + self.scrollPosition = self.scrollerBottomID + } + } + .onChange(of: self.viewModel.streamingAssistantText) { _, _ in + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } withAnimation(.snappy(duration: 0.22)) { self.scrollPosition = self.scrollerBottomID } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift index 813af5c0a..db2ffa36d 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift @@ -94,6 +94,13 @@ public struct GatewayConnectOptions: Sendable { } } +public enum GatewayAuthSource: String, Sendable { + case deviceToken = "device-token" + case sharedToken = "shared-token" + case password = "password" + case none = "none" +} + // Avoid ambiguity with the app's own AnyCodable type. private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable @@ -117,6 +124,7 @@ public actor GatewayChannelActor { private var lastSeq: Int? private var lastTick: Date? private var tickIntervalMs: Double = 30000 + private var lastAuthSource: GatewayAuthSource = .none private let decoder = JSONDecoder() private let encoder = JSONEncoder() private let connectTimeoutSeconds: Double = 6 @@ -149,6 +157,8 @@ public actor GatewayChannelActor { } } + public func authSource() -> GatewayAuthSource { self.lastAuthSource } + public func shutdown() async { self.shouldReconnect = false self.connected = false @@ -300,6 +310,18 @@ public actor GatewayChannelActor { let identity = DeviceIdentityStore.loadOrCreate() let storedToken = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role)?.token let authToken = storedToken ?? self.token + let authSource: GatewayAuthSource + if storedToken != nil { + authSource = .deviceToken + } else if authToken != nil { + authSource = .sharedToken + } else if self.password != nil { + authSource = .password + } else { + authSource = .none + } + self.lastAuthSource = authSource + self.logger.info("gateway connect auth=\(authSource.rawValue, privacy: .public)") let canFallbackToShared = storedToken != nil && self.token != nil if let authToken { params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(authToken)]) @@ -571,7 +593,14 @@ public actor GatewayChannelActor { id: id, method: method, params: paramsObject) - let data = try self.encoder.encode(frame) + let data: Data + do { + data = try self.encoder.encode(frame) + } catch { + self.logger.error( + "gateway request encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + throw error + } let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in self.pending[id] = cont Task { [weak self] in diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift index 2cc26a51d..a2ac2ad6d 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift @@ -219,8 +219,8 @@ public actor GatewayNodeSession { } if let error = response.error { params["error"] = AnyCodable([ - "code": AnyCodable(error.code.rawValue), - "message": AnyCodable(error.message), + "code": error.code.rawValue, + "message": error.message, ]) } do { diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift index 89857da9d..bfe980f41 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift @@ -30,6 +30,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { public var agentId: String? public var sessionKey: String? public var approved: Bool? + public var approvalDecision: String? public init( command: [String], @@ -40,7 +41,8 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { needsScreenRecording: Bool? = nil, agentId: String? = nil, sessionKey: String? = nil, - approved: Bool? = nil) + approved: Bool? = nil, + approvalDecision: String? = nil) { self.command = command self.rawCommand = rawCommand @@ -51,6 +53,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { self.agentId = agentId self.sessionKey = sessionKey self.approved = approved + self.approvalDecision = approvalDecision } } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift index 04c3bab09..aefbdb572 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift @@ -18,6 +18,7 @@ public struct ConnectParams: Codable, Sendable { public let caps: [String]? public let commands: [String]? public let permissions: [String: AnyCodable]? + public let pathenv: String? public let role: String? public let scopes: [String]? public let device: [String: AnyCodable]? @@ -32,6 +33,7 @@ public struct ConnectParams: Codable, Sendable { caps: [String]?, commands: [String]?, permissions: [String: AnyCodable]?, + pathenv: String?, role: String?, scopes: [String]?, device: [String: AnyCodable]?, @@ -45,6 +47,7 @@ public struct ConnectParams: Codable, Sendable { self.caps = caps self.commands = commands self.permissions = permissions + self.pathenv = pathenv self.role = role self.scopes = scopes self.device = device @@ -59,6 +62,7 @@ public struct ConnectParams: Codable, Sendable { case caps case commands case permissions + case pathenv = "pathEnv" case role case scopes case device @@ -548,6 +552,44 @@ public struct AgentParams: Codable, Sendable { } } +public struct AgentIdentityParams: Codable, Sendable { + public let agentid: String? + public let sessionkey: String? + + public init( + agentid: String?, + sessionkey: String? + ) { + self.agentid = agentid + self.sessionkey = sessionkey + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case sessionkey = "sessionKey" + } +} + +public struct AgentIdentityResult: Codable, Sendable { + public let agentid: String + public let name: String? + public let avatar: String? + + public init( + agentid: String, + name: String?, + avatar: String? + ) { + self.agentid = agentid + self.name = name + self.avatar = avatar + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case name + case avatar + } +} + public struct AgentWaitParams: Codable, Sendable { public let runid: String public let timeoutms: Int? @@ -1443,17 +1485,21 @@ public struct WebLoginWaitParams: Codable, Sendable { public struct AgentSummary: Codable, Sendable { public let id: String public let name: String? + public let identity: [String: AnyCodable]? public init( id: String, - name: String? + name: String?, + identity: [String: AnyCodable]? ) { self.id = id self.name = name + self.identity = identity } private enum CodingKeys: String, CodingKey { case id case name + case identity } } @@ -1904,6 +1950,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable { } public struct ExecApprovalRequestParams: Codable, Sendable { + public let id: String? public let command: String public let cwd: String? public let host: String? @@ -1915,6 +1962,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { public let timeoutms: Int? public init( + id: String?, command: String, cwd: String?, host: String?, @@ -1925,6 +1973,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { sessionkey: String?, timeoutms: Int? ) { + self.id = id self.command = command self.cwd = cwd self.host = host @@ -1936,6 +1985,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable { self.timeoutms = timeoutms } private enum CodingKeys: String, CodingKey { + case id case command case cwd case host diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index b566fc795..2336f3609 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -149,6 +149,19 @@ Available actions: - **leaveGroup**: Leave a group chat (`chatGuid`) - **sendAttachment**: Send media/files (`to`, `buffer`, `filename`) +### Message IDs (short vs full) +Clawdbot may surface *short* message IDs (e.g., `1`, `2`) to save tokens. +- `MessageSid` / `ReplyToId` can be short IDs. +- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs. +- Short IDs are in-memory; they can expire on restart or cache eviction. +- Actions accept short or full `messageId`, but short IDs will error if no longer available. + +Use full IDs for durable automations and storage: +- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}` +- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads + +See [Configuration](/gateway/configuration) for template variables. + ## Block streaming Control whether responses are sent as a single message or streamed in blocks: ```json5 diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 28ecfdfa0..ca6ff6c9c 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -175,6 +175,7 @@ Notes: - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - If `channels` is present, any channel not listed is denied by default. +- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard. - Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly. - Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered). - Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index 4c9279941..3315153e6 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -8,9 +8,9 @@ read_when: > "Abandon all hope, ye who enter here." -Updated: 2026-01-16 +Updated: 2026-01-21 -Status: text + DM attachments are supported; channel/group attachments require Microsoft Graph permissions. Polls are sent via Adaptive Cards. +Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. ## Plugin required Microsoft Teams ships as a plugin and is not bundled with the core install. @@ -403,7 +403,7 @@ Clawdbot handles this by returning quickly and sending replies proactively, but Teams markdown is more limited than Slack or Discord: - Basic formatting works: **bold**, *italic*, `code`, links - Complex markdown (tables, nested lists) may not render correctly -- Adaptive Cards are used for polls; other card types are not yet supported +- Adaptive Cards are supported for polls and arbitrary card sends (see below) ## Configuration Key settings (see `/gateway/configuration` for shared channel patterns): @@ -422,6 +422,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns): - `channels.msteams.teams..requireMention`: per-team override. - `channels.msteams.teams..channels..replyStyle`: per-channel override. - `channels.msteams.teams..channels..requireMention`: per-channel override. +- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)). ## Routing & Sessions - Session keys follow the standard agent format (see [/concepts/session](/concepts/session)): @@ -471,6 +472,75 @@ Teams recently introduced two channel UI styles over the same underlying data mo Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot). By default, Clawdbot only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host). +## Sending files in group chats + +Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup: + +| Context | How files are sent | Setup needed | +|---------|-------------------|--------------| +| **DMs** | FileConsentCard → user accepts → bot uploads | Works out of the box | +| **Group chats/channels** | Upload to SharePoint → share link | Requires `sharePointSiteId` + Graph permissions | +| **Images (any context)** | Base64-encoded inline | Works out of the box | + +### Why group chats need SharePoint + +Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. + +### Setup + +1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration: + - `Sites.ReadWrite.All` (Application) - upload files to SharePoint + - `Chat.Read.All` (Application) - optional, enables per-user sharing links + +2. **Grant admin consent** for the tenant. + +3. **Get your SharePoint site ID:** + ```bash + # Via Graph Explorer or curl with a valid token: + curl -H "Authorization: Bearer $TOKEN" \ + "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}" + + # Example: for a site at "contoso.sharepoint.com/sites/BotFiles" + curl -H "Authorization: Bearer $TOKEN" \ + "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles" + + # Response includes: "id": "contoso.sharepoint.com,guid1,guid2" + ``` + +4. **Configure Clawdbot:** + ```json5 + { + channels: { + msteams: { + // ... other config ... + sharePointSiteId: "contoso.sharepoint.com,guid1,guid2" + } + } + } + ``` + +### Sharing behavior + +| Permission | Sharing behavior | +|------------|------------------| +| `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) | +| `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) | + +Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing. + +### Fallback behavior + +| Scenario | Result | +|----------|--------| +| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link | +| Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only | +| Personal chat + file | FileConsentCard flow (works without SharePoint) | +| Any context + image | Base64-encoded inline (works without SharePoint) | + +### Files stored location + +Uploaded files are stored in a `/ClawdbotShared/` folder in the configured SharePoint site's default document library. + ## Polls (Adaptive Cards) Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API). @@ -479,6 +549,82 @@ Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API) - The gateway must stay online to record votes. - Polls do not auto-post result summaries yet (inspect the store file if needed). +## Adaptive Cards (arbitrary) +Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI. + +The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional. + +**Agent tool:** +```json +{ + "action": "send", + "channel": "msteams", + "target": "user:", + "card": { + "type": "AdaptiveCard", + "version": "1.5", + "body": [{"type": "TextBlock", "text": "Hello!"}] + } +} +``` + +**CLI:** +```bash +clawdbot message send --channel msteams \ + --target "conversation:19:abc...@thread.tacv2" \ + --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}' +``` + +See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below. + +## Target formats + +MSTeams targets use prefixes to distinguish between users and conversations: + +| Target type | Format | Example | +|-------------|--------|---------| +| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | +| User (by name) | `user:` | `user:John Smith` (requires Graph API) | +| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | +| Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) | + +**CLI examples:** +```bash +# Send to a user by ID +clawdbot message send --channel msteams --target "user:40a1a0ed-..." --message "Hello" + +# Send to a user by display name (triggers Graph API lookup) +clawdbot message send --channel msteams --target "user:John Smith" --message "Hello" + +# Send to a group chat or channel +clawdbot message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello" + +# Send an Adaptive Card to a conversation +clawdbot message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \ + --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}' +``` + +**Agent tool examples:** +```json +{ + "action": "send", + "channel": "msteams", + "target": "user:John Smith", + "message": "Hello!" +} +``` + +```json +{ + "action": "send", + "channel": "msteams", + "target": "conversation:19:abc...@thread.tacv2", + "card": {"type": "AdaptiveCard", "version": "1.5", "body": [{"type": "TextBlock", "text": "Hello"}]} +} +``` + +Note: Without the `user:` prefix, names default to group/team resolution. Always use `user:` when targeting people by display name. + ## Proactive messaging - Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point. - See `/gateway/configuration` for `dmPolicy` and allowlist gating. diff --git a/docs/channels/signal.md b/docs/channels/signal.md index 5ed20e82a..b015d02bf 100644 --- a/docs/channels/signal.md +++ b/docs/channels/signal.md @@ -100,6 +100,11 @@ Groups: - Use `channels.signal.ignoreAttachments` to skip downloading media. - Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). +## Typing + read receipts +- **Typing indicators**: Clawdbot sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running. +- **Read receipts**: when `channels.signal.sendReadReceipts` is true, Clawdbot forwards read receipts for allowed DMs. +- Signal-cli does not expose read receipts for groups. + ## Delivery targets (CLI/cron) - DMs: `signal:+15551234567` (or plain E.164). - Groups: `signal:group:`. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 3c894303e..da29b3c90 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -484,6 +484,10 @@ The agent sees reactions as **system notifications** in the conversation history - Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`) - Commands require authorization even in groups with `groupPolicy: "open"` +**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):** +- Node 22+ is stricter about `AbortSignal` instances; foreign signals can abort `fetch` calls right away. +- Upgrade to a Clawdbot build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade. + **Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):** - Some hosts resolve `api.telegram.org` to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests. - Fix by enabling IPv6 egress **or** forcing IPv4 resolution for `api.telegram.org` (for example, add an `/etc/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway. diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 45a4798a2..a496d1654 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -334,6 +334,7 @@ WhatsApp sends audio as **voice notes** (PTT bubble). - `agents.defaults.heartbeat.model` (optional override) - `agents.defaults.heartbeat.target` - `agents.defaults.heartbeat.to` +- `agents.defaults.heartbeat.session` - `agents.list[].heartbeat.*` (per-agent overrides) - `session.*` (scope, idle, store, mainKey) - `web.enabled` (disable channel startup when false) diff --git a/docs/cli/agents.md b/docs/cli/agents.md index fd8b81d2c..e7df32e52 100644 --- a/docs/cli/agents.md +++ b/docs/cli/agents.md @@ -18,5 +18,54 @@ Related: clawdbot agents list clawdbot agents add work --workspace ~/clawd-work clawdbot agents set-identity --workspace ~/clawd --from-identity +clawdbot agents set-identity --agent main --avatar avatars/clawd.png clawdbot agents delete work ``` + +## Identity files + +Each agent workspace can include an `IDENTITY.md` at the workspace root: +- Example path: `~/clawd/IDENTITY.md` +- `set-identity --from-identity` reads from the workspace root (or an explicit `--identity-file`) + +Avatar paths resolve relative to the workspace root. + +## Set identity + +`set-identity` writes fields into `agents.list[].identity`: +- `name` +- `theme` +- `emoji` +- `avatar` (workspace-relative path, http(s) URL, or data URI) + +Load from `IDENTITY.md`: + +```bash +clawdbot agents set-identity --workspace ~/clawd --from-identity +``` + +Override fields explicitly: + +```bash +clawdbot agents set-identity --agent main --name "Clawd" --emoji "🦞" --avatar avatars/clawd.png +``` + +Config sample: + +```json5 +{ + agents: { + list: [ + { + id: "main", + identity: { + name: "Clawd", + theme: "space lobster", + emoji: "🦞", + avatar: "avatars/clawd.png" + } + } + ] + } +} +``` diff --git a/docs/cli/daemon.md b/docs/cli/daemon.md deleted file mode 100644 index 71c43d1f8..000000000 --- a/docs/cli/daemon.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -summary: "CLI reference for `clawdbot daemon` (install/uninstall/status for the Gateway service)" -read_when: - - You want to run the Gateway as a background service - - You’re debugging daemon install, status, or logs ---- - -# `clawdbot daemon` - -Manage the Gateway daemon (background service). - -Note: `clawdbot service gateway …` is the preferred surface; `daemon` remains -as a legacy alias for compatibility. - -Related: -- Gateway CLI: [Gateway](/cli/gateway) -- macOS platform notes: [macOS](/platforms/macos) - -Tip: run `clawdbot daemon --help` for platform-specific flags. - -Notes: -- `daemon status` supports `--json` for scripting. -- `daemon install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly). diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index 21538deef..253130780 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -25,6 +25,12 @@ Run a local Gateway process: clawdbot gateway ``` +Foreground alias: + +```bash +clawdbot gateway run +``` + Notes: - By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.clawdbot/clawdbot.json`. Use `--allow-unconfigured` for ad-hoc/dev runs. - Binding beyond loopback without auth is blocked (safety guardrail). @@ -34,7 +40,7 @@ Notes: ### Options - `--port `: WebSocket port (default comes from config/env; usually `18789`). -- `--bind `: listener bind mode. +- `--bind `: listener bind mode. - `--auth `: auth mode override. - `--token `: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process). - `--password `: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process). @@ -75,15 +81,32 @@ clawdbot gateway health --url ws://127.0.0.1:18789 ### `gateway status` -`gateway status` is the “debug everything” command. It always probes: +`gateway status` shows the Gateway service (launchd/systemd/schtasks) plus an optional RPC probe. + +```bash +clawdbot gateway status +clawdbot gateway status --json +``` + +Options: +- `--url `: override the probe URL. +- `--token `: token auth for the probe. +- `--password `: password auth for the probe. +- `--timeout `: probe timeout (default `10000`). +- `--no-probe`: skip the RPC probe (service-only view). +- `--deep`: scan system-level services too. + +### `gateway probe` + +`gateway probe` is the “debug everything” command. It always probes: - your configured remote gateway (if set), and - localhost (loopback) **even if remote is configured**. If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use isolated profiles/ports (e.g., a rescue bot), but most installs still run a single gateway. ```bash -clawdbot gateway status -clawdbot gateway status --json +clawdbot gateway probe +clawdbot gateway probe --json ``` #### Remote over SSH (Mac app parity) @@ -93,7 +116,7 @@ The macOS app “Remote over SSH” mode uses a local port-forward so the remote CLI equivalent: ```bash -clawdbot gateway status --ssh user@gateway-host +clawdbot gateway probe --ssh user@gateway-host ``` Options: @@ -114,6 +137,20 @@ clawdbot gateway call status clawdbot gateway call logs.tail --params '{"sinceMs": 60000}' ``` +## Manage the Gateway service + +```bash +clawdbot gateway install +clawdbot gateway start +clawdbot gateway stop +clawdbot gateway restart +clawdbot gateway uninstall +``` + +Notes: +- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`. +- Lifecycle commands accept `--json` for scripting. + ## Discover gateways (Bonjour) `gateway discover` scans for Gateway beacons (`_clawdbot-gw._tcp`). diff --git a/docs/cli/index.md b/docs/cli/index.md index 3b6d50f2b..94e575462 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -28,8 +28,6 @@ This page describes the current CLI behavior. If commands change, update this do - [`health`](/cli/health) - [`sessions`](/cli/sessions) - [`gateway`](/cli/gateway) -- [`daemon`](/cli/daemon) -- [`service`](/cli/service) - [`logs`](/cli/logs) - [`models`](/cli/models) - [`memory`](/cli/memory) @@ -138,29 +136,14 @@ clawdbot [--dev] [--profile ] call health status + probe discover - daemon - status install uninstall start stop restart - service - gateway - status - install - uninstall - start - stop - restart - node - status - install - uninstall - start - stop - restart + run logs models list @@ -191,14 +174,13 @@ clawdbot [--dev] [--profile ] nodes devices node + run + status + install + uninstall start - daemon - status - install - uninstall - start - stop - restart + stop + restart approvals get set @@ -328,7 +310,7 @@ Options: - `--minimax-api-key ` - `--opencode-zen-api-key ` - `--gateway-port ` -- `--gateway-bind ` +- `--gateway-bind ` - `--gateway-auth ` - `--gateway-token ` - `--gateway-password ` @@ -544,7 +526,7 @@ Options: - `--debug` (alias for `--verbose`) Notes: -- Overview includes Gateway + Node service status when available. +- Overview includes Gateway + node host service status when available. ### Usage tracking Clawdbot can surface provider usage/quota when OAuth/API creds are available. @@ -614,7 +596,7 @@ Run the WebSocket Gateway. Options: - `--port ` -- `--bind ` +- `--bind ` - `--token ` - `--auth ` - `--password ` @@ -631,25 +613,25 @@ Options: - `--raw-stream` - `--raw-stream-path ` -### `daemon` +### `gateway service` Manage the Gateway service (launchd/systemd/schtasks). Subcommands: -- `daemon status` (probes the Gateway RPC by default) -- `daemon install` (service install) -- `daemon uninstall` -- `daemon start` -- `daemon stop` -- `daemon restart` +- `gateway status` (probes the Gateway RPC by default) +- `gateway install` (service install) +- `gateway uninstall` +- `gateway start` +- `gateway stop` +- `gateway restart` Notes: -- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url/--token/--password`). -- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting. -- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra". -- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL. -- `daemon install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly). -- `daemon install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs). -- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`. +- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url/--token/--password`). +- `gateway status` supports `--no-probe`, `--deep`, and `--json` for scripting. +- `gateway status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra". +- `gateway status` prints which config path the CLI uses vs which config the service likely uses (service env), plus the resolved probe target URL. +- `gateway install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly). +- `gateway install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs). +- `gateway install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`. ### `logs` Tail Gateway file logs via RPC. @@ -668,13 +650,16 @@ clawdbot logs --no-color ``` ### `gateway ` -Gateway RPC helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for each). +Gateway CLI helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for RPC subcommands). Subcommands: - `gateway call [--params ]` - `gateway health` - `gateway status` +- `gateway probe` - `gateway discover` +- `gateway install|uninstall|start|stop|restart` +- `gateway run` Common RPCs: - `config.apply` (validate + write config + restart + wake) @@ -806,16 +791,13 @@ All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`. [`clawdbot node`](/cli/node). Subcommands: -- `node start --host --port 18790` -- `node service status` -- `node service install [--host ] [--port ] [--tls] [--tls-fingerprint ] [--node-id ] [--display-name ] [--runtime ] [--force]` -- `node service uninstall` -- `node service start` -- `node service stop` -- `node service restart` - -Legacy alias: -- `node daemon …` (same as `node service …`) +- `node run --host --port 18790` +- `node status` +- `node install [--host ] [--port ] [--tls] [--tls-fingerprint ] [--node-id ] [--display-name ] [--runtime ] [--force]` +- `node uninstall` +- `node run` +- `node stop` +- `node restart` ## Nodes diff --git a/docs/cli/node.md b/docs/cli/node.md index c6a070376..ee9893f87 100644 --- a/docs/cli/node.md +++ b/docs/cli/node.md @@ -23,10 +23,10 @@ Common use cases: Execution is still guarded by **exec approvals** and per‑agent allowlists on the node host, so you can keep command access scoped and explicit. -## Start (foreground) +## Run (foreground) ```bash -clawdbot node start --host --port 18790 +clawdbot node run --host --port 18790 ``` Options: @@ -42,9 +42,7 @@ Options: Install a headless node host as a user service. ```bash -clawdbot node service install --host --port 18790 -# or -clawdbot service node install --host --port 18790 +clawdbot node install --host --port 18790 ``` Options: @@ -61,18 +59,10 @@ Manage the service: ```bash clawdbot node status -clawdbot service node status -clawdbot node service status -clawdbot node service start -clawdbot node service stop -clawdbot node service restart -clawdbot node service uninstall -``` - -Legacy alias: - -```bash -clawdbot node daemon status +clawdbot node run +clawdbot node stop +clawdbot node restart +clawdbot node uninstall ``` ## Pairing diff --git a/docs/cli/nodes.md b/docs/cli/nodes.md index b76bf4d36..4064006ee 100644 --- a/docs/cli/nodes.md +++ b/docs/cli/nodes.md @@ -36,4 +36,19 @@ filter to nodes that connected within a duration (e.g. `24h`, `7d`). ```bash clawdbot nodes invoke --node --command --params clawdbot nodes run --node +clawdbot nodes run --raw "git status" +clawdbot nodes run --agent main --node --raw "git status" ``` + +### Exec-style defaults + +`nodes run` mirrors the model’s exec behavior (defaults + approvals): + +- Reads `tools.exec.*` (plus `agents.list[].tools.exec.*` overrides). +- Uses exec approvals (`exec.approval.request`) before invoking `system.run`. +- `--node` can be omitted when `tools.exec.node` is set. + +Flags: +- `--raw `: run a shell string (`/bin/sh -lc` or `cmd.exe /c`). +- `--agent `: agent-scoped approvals/allowlists (defaults to configured agent). +- `--ask `, `--security `: overrides. diff --git a/docs/cli/service.md b/docs/cli/service.md deleted file mode 100644 index a3cf536a2..000000000 --- a/docs/cli/service.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -summary: "CLI reference for `clawdbot service` (manage gateway + node services)" -read_when: - - You want to manage Gateway or node services cross-platform - - You want a single surface for start/stop/install/uninstall ---- - -# `clawdbot service` - -Manage the **Gateway** service and **node host** services. - -Related: -- Gateway daemon (legacy alias): [Daemon](/cli/daemon) -- Node host: [Node](/cli/node) - -## Gateway service - -```bash -clawdbot service gateway status -clawdbot service gateway install --port 18789 -clawdbot service gateway start -clawdbot service gateway stop -clawdbot service gateway restart -clawdbot service gateway uninstall -``` - -Notes: -- `service gateway status` supports `--json` and `--deep` for system checks. -- `service gateway install` supports `--runtime node|bun` and `--token`. - -## Node host service - -```bash -clawdbot service node status -clawdbot service node install --host --port 18790 -clawdbot service node start -clawdbot service node stop -clawdbot service node restart -clawdbot service node uninstall -``` - -Notes: -- `service node install` supports `--runtime node|bun`, `--node-id`, `--display-name`, - and TLS options (`--tls`, `--tls-fingerprint`). - -## Aliases - -- `clawdbot daemon …` → `clawdbot service gateway …` -- `clawdbot node service …` → `clawdbot service node …` -- `clawdbot node status` → `clawdbot service node status` -- `clawdbot node daemon …` → `clawdbot service node …` (legacy) diff --git a/docs/cli/status.md b/docs/cli/status.md index a66dd958a..f2131cbb8 100644 --- a/docs/cli/status.md +++ b/docs/cli/status.md @@ -19,6 +19,6 @@ clawdbot status --usage Notes: - `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal). - Output includes per-agent session stores when multiple agents are configured. -- Overview includes Gateway + Node service install/runtime status when available. +- Overview includes Gateway + node host service install/runtime status when available. - Overview includes update channel + git SHA (for source checkouts). - Update info surfaces in the Overview; if an update is available, status prints a hint to run `clawdbot update` (see [Updating](/install/updating)). diff --git a/docs/cli/update.md b/docs/cli/update.md index 12fa90e57..8dafbbd87 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -1,5 +1,5 @@ --- -summary: "CLI reference for `clawdbot update` (safe-ish source update + optional daemon restart)" +summary: "CLI reference for `clawdbot update` (safe-ish source update + optional gateway restart)" read_when: - You want to update a source checkout safely - You need to understand `--update` shorthand behavior @@ -16,6 +16,7 @@ If you installed via **npm/pnpm** (global install, no git metadata), updates hap ```bash clawdbot update clawdbot update status +clawdbot update wizard clawdbot update --channel beta clawdbot update --channel dev clawdbot update --tag beta @@ -26,7 +27,7 @@ clawdbot --update ## Options -- `--restart`: restart the Gateway daemon after a successful update. +- `--restart`: restart the Gateway service after a successful update. - `--channel `: set the update channel (git + npm; persisted in config). - `--tag `: override the npm dist-tag or version for this update only. - `--json`: print machine-readable `UpdateRunResult` JSON. @@ -48,6 +49,11 @@ Options: - `--json`: print machine-readable status JSON. - `--timeout `: timeout for checks (default is 3s). +## `update wizard` + +Interactive flow to pick an update channel and confirm whether to restart the Gateway +after updating. If you select `dev` without a git checkout, it offers to create one. + ## What it does When you switch channels explicitly (`--channel ...`), Clawdbot also keeps the @@ -69,11 +75,13 @@ High-level: 1. Requires a clean worktree (no uncommitted changes). 2. Switches to the selected channel (tag or branch). -3. Fetches and rebases against `@{upstream}` (dev only). -4. Installs deps (pnpm preferred; npm fallback). -5. Builds + builds the Control UI. -6. Runs `clawdbot doctor` as the final “safe update” check. -7. Syncs plugins to the active channel (dev uses bundled extensions; stable/beta uses npm) and updates npm-installed plugins. +3. Fetches upstream (dev only). +4. Dev only: preflight lint + TypeScript build in a temp worktree; if the tip fails, walks back up to 10 commits to find the newest clean build. +5. Rebases onto the selected commit (dev only). +6. Installs deps (pnpm preferred; npm fallback). +7. Builds + builds the Control UI. +8. Runs `clawdbot doctor` as the final “safe update” check. +9. Syncs plugins to the active channel (dev uses bundled extensions; stable/beta uses npm) and updates npm-installed plugins. ## `--update` shorthand diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 12c5afe5b..fd21e4a57 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -38,7 +38,7 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no** - Provider: `anthropic` - Auth: `ANTHROPIC_API_KEY` or `claude setup-token` - Example model: `anthropic/claude-opus-4-5` -- CLI: `clawdbot onboard --auth-choice setup-token` +- CLI: `clawdbot onboard --auth-choice token` (paste setup-token) or `clawdbot models auth paste-token --provider anthropic` ```json5 { diff --git a/docs/concepts/session-pruning.md b/docs/concepts/session-pruning.md index 4e76f9bb4..c49fe5370 100644 --- a/docs/concepts/session-pruning.md +++ b/docs/concepts/session-pruning.md @@ -9,8 +9,22 @@ read_when: Session pruning trims **old tool results** from the in-memory context right before each LLM call. It does **not** rewrite the on-disk session history (`*.jsonl`). ## When it runs -- Before each LLM request (context hook). +- When `mode: "cache-ttl"` is enabled and the last Anthropic call for the session is older than `ttl`. - Only affects the messages sent to the model for that request. + - Only active for Anthropic API calls (and OpenRouter Anthropic models). + - For best results, match `ttl` to your model `cacheControlTtl`. + - After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again. + +## Smart defaults (Anthropic) +- **OAuth or setup-token** profiles: enable `cache-ttl` pruning and set heartbeat to `1h`. +- **API key** profiles: enable `cache-ttl` pruning, set heartbeat to `30m`, and default `cacheControlTtl` to `1h` on Anthropic models. +- If you set any of these values explicitly, Clawdbot does **not** override them. + +## What this improves (cost + cache behavior) +- **Why prune:** Anthropic prompt caching only applies within the TTL. If a session goes idle past the TTL, the next request re-caches the full prompt unless you trim it first. +- **What gets cheaper:** pruning reduces the **cacheWrite** size for that first request after the TTL expires. +- **Why the TTL reset matters:** once pruning runs, the cache window resets, so follow‑up requests can reuse the freshly cached prompt instead of re-caching the full history again. +- **What it does not do:** pruning doesn’t add tokens or “double” costs; it only changes what gets cached on that first post‑TTL request. ## What can be pruned - Only `toolResult` messages. @@ -26,14 +40,10 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz 3) `agents.defaults.contextTokens`. 4) Default `200000` tokens. -## Modes -### adaptive -- If estimated context ratio ≥ `softTrimRatio`: soft-trim oversized tool results. -- If still ≥ `hardClearRatio` **and** prunable tool text ≥ `minPrunableToolChars`: hard-clear oldest eligible tool results. - -### aggressive -- Always hard-clears eligible tool results before the cutoff. -- Ignores `hardClear.enabled` (always clears when eligible). +## Mode +### cache-ttl +- Pruning only runs if the last Anthropic call is older than `ttl` (default `5m`). +- When it runs: same soft-trim + hard-clear behavior as before. ## Soft vs hard pruning - **Soft-trim**: only for oversized tool results. @@ -52,6 +62,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz - Compaction is separate: compaction summarizes and persists, pruning is transient per request. See [/concepts/compaction](/concepts/compaction). ## Defaults (when enabled) +- `ttl`: `"5m"` - `keepLastAssistants`: `3` - `softTrimRatio`: `0.3` - `hardClearRatio`: `0.5` @@ -60,16 +71,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz - `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }` ## Examples -Default (adaptive): -```json5 -{ - agent: { - contextPruning: { mode: "adaptive" } - } -} -``` - -To disable: +Default (off): ```json5 { agent: { @@ -78,11 +80,11 @@ To disable: } ``` -Aggressive: +Enable TTL-aware pruning: ```json5 { agent: { - contextPruning: { mode: "aggressive" } + contextPruning: { mode: "cache-ttl", ttl: "5m" } } } ``` @@ -92,7 +94,7 @@ Restrict pruning to specific tools: { agent: { contextPruning: { - mode: "adaptive", + mode: "cache-ttl", tools: { allow: ["exec", "read"], deny: ["*image*"] } } } diff --git a/docs/concepts/session.md b/docs/concepts/session.md index c88e2ddfe..48c85af09 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -60,6 +60,7 @@ the workspace is writable. See [Memory](/concepts/memory) and - Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session. - Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, Clawdbot stays in idle-only mode for backward compatibility. - Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector). +- Per-channel overrides (optional): `resetByChannel` overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over `reset`/`resetByType`). - Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new ` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset. - Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them. - Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse). @@ -109,6 +110,9 @@ Send these as standalone messages so they register. dm: { mode: "idle", idleMinutes: 240 }, group: { mode: "idle", idleMinutes: 120 } }, + resetByChannel: { + discord: { mode: "idle", idleMinutes: 10080 } + }, resetTriggers: ["/new", "/reset"], store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json", mainKey: "main", diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index b46f11578..1a52fc501 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -24,7 +24,7 @@ The prompt is intentionally compact and uses fixed sections: - **Current Date & Time**: user-local time, timezone, and time format. - **Reply Tags**: optional reply tag syntax for supported providers. - **Heartbeats**: heartbeat prompt and ack behavior. -- **Runtime**: host, OS, node, model, thinking level (one line). +- **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line). - **Reasoning**: current visibility level + /reasoning toggle hint. ## Prompt modes diff --git a/docs/concepts/timezone.md b/docs/concepts/timezone.md index 3a6d3a4dd..b8b168cc9 100644 --- a/docs/concepts/timezone.md +++ b/docs/concepts/timezone.md @@ -9,15 +9,15 @@ read_when: Clawdbot standardizes timestamps so the model sees a **single reference time**. -## Message envelopes (UTC by default) +## Message envelopes (local by default) Inbound messages are wrapped in an envelope like: ``` -[Provider ... 2026-01-05T21:26Z] message text +[Provider ... 2026-01-05 16:26 PST] message text ``` -The timestamp in the envelope is **UTC by default**, with minutes precision. +The timestamp in the envelope is **host-local by default**, with minutes precision. You can override this with: @@ -25,7 +25,7 @@ You can override this with: { agents: { defaults: { - envelopeTimezone: "user", // "utc" | "local" | "user" | IANA timezone + envelopeTimezone: "local", // "utc" | "local" | "user" | IANA timezone envelopeTimestamp: "on", // "on" | "off" envelopeElapsed: "on" // "on" | "off" } @@ -33,6 +33,7 @@ You can override this with: } ``` +- `envelopeTimezone: "utc"` uses UTC. - `envelopeTimezone: "user"` uses `agents.defaults.userTimezone` (falls back to host timezone). - Use an explicit IANA timezone (e.g., `"Europe/Vienna"`) for a fixed offset. - `envelopeTimestamp: "off"` removes absolute timestamps from envelope headers. @@ -40,10 +41,10 @@ You can override this with: ### Examples -**UTC (default):** +**Local (default):** ``` -[Signal Alice +1555 2026-01-18T05:19Z] hello +[Signal Alice +1555 2026-01-18 00:19 PST] hello ``` **Fixed timezone:** diff --git a/docs/date-time.md b/docs/date-time.md index 99da67630..8b711350d 100644 --- a/docs/date-time.md +++ b/docs/date-time.md @@ -7,18 +7,18 @@ read_when: # Date & Time -Clawdbot defaults to **UTC for transport timestamps** and **user-local time only in the system prompt**. +Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**. Provider timestamps are preserved so tools keep their native semantics. -## Message envelopes (UTC by default) +## Message envelopes (local by default) -Inbound messages are wrapped with a UTC timestamp (minute precision): +Inbound messages are wrapped with a timestamp (minute precision): ``` -[Provider ... 2026-01-05T21:26Z] message text +[Provider ... 2026-01-05 16:26 PST] message text ``` -This envelope timestamp is **UTC by default**, regardless of the host timezone. +This envelope timestamp is **host-local by default**, regardless of the provider timezone. You can override this behavior: @@ -26,7 +26,7 @@ You can override this behavior: { agents: { defaults: { - envelopeTimezone: "utc", // "utc" | "local" | "user" | IANA timezone + envelopeTimezone: "local", // "utc" | "local" | "user" | IANA timezone envelopeTimestamp: "on", // "on" | "off" envelopeElapsed: "on" // "on" | "off" } @@ -34,6 +34,7 @@ You can override this behavior: } ``` +- `envelopeTimezone: "utc"` uses UTC. - `envelopeTimezone: "local"` uses the host timezone. - `envelopeTimezone: "user"` uses `agents.defaults.userTimezone` (falls back to host timezone). - Use an explicit IANA timezone (e.g., `"America/Chicago"`) for a fixed zone. @@ -42,10 +43,10 @@ You can override this behavior: ### Examples -**UTC (default):** +**Local (default):** ``` -[WhatsApp +1555 2026-01-18T05:19Z] hello +[WhatsApp +1555 2026-01-18 00:19 PST] hello ``` **User timezone:** @@ -73,12 +74,13 @@ Time format: 12-hour If only the timezone is known, we still include the section and instruct the model to assume UTC for unknown time references. -## System event lines (UTC) +## System event lines (local by default) -Queued system events inserted into agent context are prefixed with a UTC timestamp: +Queued system events inserted into agent context are prefixed with a timestamp using the +same timezone selection as message envelopes (default: host-local). ``` -System: [2026-01-12T20:19:17Z] Model switched. +System: [2026-01-12 12:19:17 PST] Model switched. ``` ### Configure user timezone + format diff --git a/docs/debugging.md b/docs/debugging.md index a7f8a85ff..b7f94d276 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -100,7 +100,7 @@ CLAWDBOT_PROFILE=dev clawdbot gateway --dev --reset Tip: if a non‑dev gateway is already running (launchd/systemd), stop it first: ```bash -clawdbot daemon stop +clawdbot gateway stop ``` ## Raw stream logging (Clawdbot) diff --git a/docs/docs.json b/docs/docs.json index 88983fc47..b9c1dc5fd 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -845,8 +845,6 @@ "cli/nodes", "cli/approvals", "cli/gateway", - "cli/daemon", - "cli/service", "cli/tui", "cli/voicecall", "cli/wake", diff --git a/docs/gateway/bridge-protocol.md b/docs/gateway/bridge-protocol.md index 1e43ccc75..47ff09b4d 100644 --- a/docs/gateway/bridge-protocol.md +++ b/docs/gateway/bridge-protocol.md @@ -58,8 +58,8 @@ Exact allowlist is enforced in `src/gateway/server-bridge.ts`. ## Exec lifecycle events -Nodes can emit `exec.started`, `exec.finished`, or `exec.denied` events to surface -system.run activity. These are mapped to system events in the gateway. +Nodes can emit `exec.finished` or `exec.denied` events to surface system.run activity. +These are mapped to system events in the gateway. (Legacy nodes may still emit `exec.started`.) Payload fields (all optional unless noted): - `sessionKey` (required): agent session to receive the system event. diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 793ece412..91b40beac 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -151,7 +151,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number. atHour: 4, idleMinutes: 60 }, - heartbeatIdleMinutes: 120, + resetByChannel: { + discord: { mode: "idle", idleMinutes: 10080 } + }, resetTriggers: ["/new", "/reset"], store: "~/.clawdbot/agents/default/sessions/sessions.json", typingIntervalSeconds: 5, diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index f83c4ab58..bd157af7f 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -400,12 +400,26 @@ Optional per-agent identity used for defaults and UX. This is written by the mac If set, Clawdbot derives defaults (only when you haven’t set them explicitly): - `messages.ackReaction` from the **active agent**’s `identity.emoji` (falls back to 👀) - `agents.list[].groupChat.mentionPatterns` from the agent’s `identity.name`/`identity.emoji` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp) +- `identity.avatar` accepts a workspace-relative image path or a remote URL/data URL. Local files must live inside the agent workspace. + +`identity.avatar` accepts: +- Workspace-relative path (must stay within the agent workspace) +- `http(s)` URL +- `data:` URI ```json5 { agents: { list: [ - { id: "main", identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" } } + { + id: "main", + identity: { + name: "Samantha", + theme: "helpful sloth", + emoji: "🦥", + avatar: "avatars/samantha.png" + } + } ] } } @@ -1295,6 +1309,18 @@ Default: `~/clawd`. If `agents.defaults.sandbox` is enabled, non-main sessions can override this with their own per-scope workspaces under `agents.defaults.sandbox.workspaceRoot`. +### `agents.defaults.repoRoot` + +Optional repository root to show in the system prompt’s Runtime line. If unset, Clawdbot +tries to detect a `.git` directory by walking upward from the workspace (and current +working directory). The path must exist to be used. + +```json5 +{ + agents: { defaults: { repoRoot: "~/Projects/clawdbot" } } +} +``` + ### `agents.defaults.skipBootstrap` Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`). @@ -1443,7 +1469,7 @@ Each `agents.defaults.models` entry can include: - `alias` (optional model shortcut, e.g. `/opus`). - `params` (optional provider-specific API params passed through to the model request). -`params` is also applied to streaming runs (embedded agent + compaction). Supported keys today: `temperature`, `maxTokens`, `cacheControlTtl` (`"5m"` or `"1h"`, Anthropic API + OpenRouter Anthropic models only; ignored for Anthropic OAuth/Claude Code tokens). These merge with call-time options; caller-supplied values win. `temperature` is an advanced knob—leave unset unless you know the model’s defaults and need a change. Anthropic API defaults to `"1h"` unless you override (`cacheControlTtl: "5m"`). Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API; keep it if you override provider headers. +`params` is also applied to streaming runs (embedded agent + compaction). Supported keys today: `temperature`, `maxTokens`. These merge with call-time options; caller-supplied values win. `temperature` is an advanced knob—leave unset unless you know the model’s defaults and need a change. Example: @@ -1772,8 +1798,9 @@ Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require `30m`. Set `0m` to disable. - `model`: optional override model for heartbeat runs (`provider/model`). - `includeReasoning`: when `true`, heartbeats will also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). Default: `false`. -- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`. +- `session`: optional session key to control which session the heartbeat runs in. Default: `main`. - `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp, chat id for Telegram). +- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `msteams`, `signal`, `imessage`, `none`). Default: `last`. - `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md` line if you still want the file read. - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 300). @@ -1804,7 +1831,6 @@ Note: `applyPatch` is only under `tools.exec`. - `tools.web.fetch.maxChars` (default 50000) - `tools.web.fetch.timeoutSeconds` (default 30) - `tools.web.fetch.cacheTtlMinutes` (default 15) -- `tools.web.fetch.maxRedirects` (default 3) - `tools.web.fetch.userAgent` (optional override) - `tools.web.fetch.readability` (default true; disable to use basic HTML cleanup only) - `tools.web.fetch.firecrawl.enabled` (default true when an API key is set) @@ -1871,7 +1897,7 @@ Example: `agents.defaults.subagents` configures sub-agent defaults: - `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call. -- `maxConcurrent`: max concurrent sub-agent runs (default 8) +- `maxConcurrent`: max concurrent sub-agent runs (default 1) - `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable) - Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins) @@ -1999,13 +2025,13 @@ Per-agent override (further restrict): Notes: - `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow). -- `/elevated on|off` stores state per session key; inline directives apply to a single message. +- `/elevated on|off|ask|full` stores state per session key; inline directives apply to a single message. - Elevated `exec` runs on the host and bypasses sandboxing. - Tool policy still applies; if `exec` is denied, elevated cannot be used. `agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can execute in parallel across sessions. Each session is still serialized (one run -per session key at a time). Default: 4. +per session key at a time). Default: 1. ### `agents.defaults.sandbox` @@ -2645,13 +2671,10 @@ Defaults: // noSandbox: false, // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", // attachOnly: false, // set true when tunneling a remote CDP to localhost - // snapshotDefaults: { mode: "efficient" }, // tool/CLI default snapshot preset } } ``` -Note: `browser.snapshotDefaults` only affects Clawdbot's browser tool + CLI. Direct HTTP clients must pass `mode` explicitly. - ### `ui` (Appearance) Optional accent color used by the native apps for UI chrome (e.g. Talk Mode bubble tint). @@ -2661,7 +2684,13 @@ If unset, clients fall back to a muted light-blue. ```json5 { ui: { - seamColor: "#FF4500" // hex (RRGGBB or #RRGGBB) + seamColor: "#FF4500", // hex (RRGGBB or #RRGGBB) + // Optional: Control UI assistant identity override. + // If unset, the Control UI uses the active agent identity (config or IDENTITY.md). + assistant: { + name: "Clawdbot", + avatar: "CB" // emoji, short text, or image URL/data URI + } } } ``` @@ -2692,6 +2721,8 @@ Control UI base path: - `gateway.controlUi.basePath` sets the URL prefix where the Control UI is served. - Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`. - Default: root (`/`) (unchanged). +- `gateway.controlUi.allowInsecureAuth` allows token-only auth over **HTTP** (no device identity). + Default: `false`. Prefer HTTPS (Tailscale Serve) or `127.0.0.1`. Related docs: - [Control UI](/web/control-ui) @@ -2703,7 +2734,6 @@ Notes: - `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag). - `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI). - OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`. -- OpenResponses endpoint: **disabled by default**; enable with `gateway.http.endpoints.responses.enabled: true`. - Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`. - Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`). - The onboarding wizard generates a gateway token by default (even on loopback). @@ -2711,7 +2741,7 @@ Notes: Auth and Tailscale: - `gateway.auth.mode` sets the handshake requirements (`token` or `password`). -- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine and as the bootstrap credential for device pairing). +- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine). - When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers). - `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended). - `gateway.auth.allowTailscale` allows Tailscale Serve identity headers @@ -2720,9 +2750,6 @@ Auth and Tailscale: `true`, Serve requests do not need a token/password; set `false` to require explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and auth mode is not `password`. -- After pairing, the Gateway issues **device tokens** scoped to the device role + scopes. - These are returned in `hello-ok.auth.deviceToken`; clients should persist and reuse them - instead of the shared token. Rotate/revoke via `device.token.rotate`/`device.token.revoke`. - `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind). - `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth. - `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown. @@ -2731,7 +2758,6 @@ Remote client defaults (CLI): - `gateway.remote.url` sets the default Gateway WebSocket URL for CLI calls when `gateway.mode = "remote"`. - `gateway.remote.token` supplies the token for remote calls (leave unset for no auth). - `gateway.remote.password` supplies the password for remote calls (leave unset for no auth). -- `gateway.remote.tlsFingerprint` pins the gateway TLS cert fingerprint (sha256). macOS app behavior: - Clawdbot.app watches `~/.clawdbot/clawdbot.json` and switches modes live when `gateway.mode` or `gateway.remote.url` changes. @@ -2745,36 +2771,12 @@ macOS app behavior: remote: { url: "ws://gateway.tailnet:18789", token: "your-token", - password: "your-password", - tlsFingerprint: "sha256:ab12cd34..." + password: "your-password" } } } ``` -### `gateway.nodes` (Node command allowlist) - -The Gateway enforces a per-platform command allowlist for `node.invoke`. Nodes must both -**declare** a command and have it **allowed** by the Gateway to run it. - -Use this section to extend or deny commands: - -```json5 -{ - gateway: { - nodes: { - allowCommands: ["custom.vendor.command"], // extra commands beyond defaults - denyCommands: ["sms.send"] // block a command even if declared - } - } -} -``` - -Notes: -- `allowCommands` extends the built-in per-platform defaults. -- `denyCommands` always wins (even if the node claims the command). -- `node.invoke` rejects commands that are not declared by the node. - ### `gateway.reload` (Config hot reload) The Gateway watches `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`) and applies changes automatically. @@ -3022,7 +3024,7 @@ Auto-generated certs require `openssl` on PATH; if generation fails, the bridge ### `discovery.wideArea` (Wide-Area Bonjour / unicast DNS‑SD) -When enabled, the Gateway writes a unicast DNS-SD zone for `_clawdbot-gw._tcp` under `~/.clawdbot/dns/` using the standard discovery domain `clawdbot.internal.` +When enabled, the Gateway writes a unicast DNS-SD zone for `_clawdbot-bridge._tcp` under `~/.clawdbot/dns/` using the standard discovery domain `clawdbot.internal.` To make iOS/Android discover across networks (Vienna ⇄ London), pair this with: - a DNS server on the gateway host serving `clawdbot.internal.` (CoreDNS is recommended) diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index b3c77e848..50e7ffdcc 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -225,10 +225,10 @@ Notes: - `clawdbot doctor --yes` accepts the default repair prompts. - `clawdbot doctor --repair` applies recommended fixes without prompts. - `clawdbot doctor --repair --force` overwrites custom supervisor configs. -- You can always force a full rewrite via `clawdbot daemon install --force`. +- You can always force a full rewrite via `clawdbot gateway install --force`. ### 16) Gateway runtime + port diagnostics -Doctor inspects the daemon runtime (PID, last exit status) and warns when the +Doctor inspects the service runtime (PID, last exit status) and warns when the service is installed but not actually running. It also checks for port collisions on the gateway port (default `18789`) and reports likely causes (gateway already running, SSH tunnel). @@ -236,7 +236,7 @@ running, SSH tunnel). ### 17) Gateway runtime best practices Doctor warns when the gateway service runs on Bun or a version-managed Node path (`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node, -and version-manager paths can break after upgrades because the daemon does not +and version-manager paths can break after upgrades because the service does not load your shell init. Doctor offers to migrate to a system Node install when available (Homebrew/apt/choco). diff --git a/docs/gateway/heartbeat.md b/docs/gateway/heartbeat.md index 639ee23cb..cc6c2a7d5 100644 --- a/docs/gateway/heartbeat.md +++ b/docs/gateway/heartbeat.md @@ -10,10 +10,11 @@ surface anything that needs attention without spamming you. ## Quick start (beginner) -1. Leave heartbeats enabled (default is `30m`) or set your own cadence. +1. Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/setup-token) or set your own cadence. 2. Create a tiny `HEARTBEAT.md` checklist in the agent workspace (optional but recommended). 3. Decide where heartbeat messages should go (`target: "last"` is the default). 4. Optional: enable heartbeat reasoning delivery for transparency. +5. Optional: restrict heartbeats to active hours (local time). Example config: @@ -24,6 +25,7 @@ Example config: heartbeat: { every: "30m", target: "last", + // activeHours: { start: "08:00", end: "24:00" }, // includeReasoning: true, // optional: send separate `Reasoning:` message too } } @@ -33,11 +35,13 @@ Example config: ## Defaults -- Interval: `30m` (set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable). +- Interval: `30m` (or `1h` when Anthropic OAuth/setup-token is the detected auth mode). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable. - Prompt body (configurable via `agents.defaults.heartbeat.prompt`): `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.` - The heartbeat prompt is sent **verbatim** as the user message. The system prompt includes a “Heartbeat” section and the run is flagged internally. +- Active hours (`heartbeat.activeHours`) are checked in the configured timezone. + Outside the window, heartbeats are skipped until the next tick inside the window. ## What the heartbeat prompt is for @@ -123,18 +127,26 @@ Example: two agents, only the second agent runs heartbeats. - `every`: heartbeat interval (duration string; default unit = minutes). - `model`: optional model override for heartbeat runs (`provider/model`). - `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). +- `session`: optional session key for heartbeat runs. + - `main` (default): agent main session. + - Explicit session key (copy from `clawdbot sessions --json` or the [sessions CLI](/cli/sessions)). + - Session key formats: see [Sessions](/concepts/session) and [Groups](/concepts/groups). - `target`: - `last` (default): deliver to the last used external channel. - - explicit channel: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage`. + - explicit channel: `whatsapp` / `telegram` / `discord` / `slack` / `msteams` / `signal` / `imessage`. - `none`: run the heartbeat but **do not deliver** externally. -- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram, etc.). +- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). - `prompt`: overrides the default prompt body (not merged). - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery. ## Delivery behavior -- Heartbeats run in each agent’s **main session** (`agent::`), or `global` - when `session.scope = "global"`. +- Heartbeats run in the agent’s main session by default (`agent::`), + or `global` when `session.scope = "global"`. Set `session` to override to a + specific channel session (Discord/WhatsApp/etc.). +- `session` only affects the run context; delivery is controlled by `target` and `to`. +- To deliver to a specific channel/recipient, set `target` + `to`. With + `target: "last"`, delivery uses the last external channel for that session. - If the main queue is busy, the heartbeat is skipped and retried later. - If `target` resolves to no external destination, the run still happens but no outbound message is sent. diff --git a/docs/gateway/index.md b/docs/gateway/index.md index 83ed93952..50552bcc5 100644 --- a/docs/gateway/index.md +++ b/docs/gateway/index.md @@ -1,9 +1,9 @@ --- -summary: "Runbook for the Gateway daemon, lifecycle, and operations" +summary: "Runbook for the Gateway service, lifecycle, and operations" read_when: - Running or debugging the gateway process --- -# Gateway (daemon) runbook +# Gateway service runbook Last updated: 2025-12-09 @@ -101,10 +101,10 @@ Checklist per instance: - unique `agents.defaults.workspace` - separate WhatsApp numbers (if using WA) -Daemon install per profile: +Service install per profile: ```bash -clawdbot --profile main daemon install -clawdbot --profile rescue daemon install +clawdbot --profile main gateway install +clawdbot --profile rescue gateway install ``` Example: @@ -175,49 +175,49 @@ See also: [Presence](/concepts/presence) for how presence is produced/deduped an - Events are not replayed. Clients detect seq gaps and should refresh (`health` + `system-presence`) before continuing. WebChat and macOS clients now auto-refresh on gap. ## Supervision (macOS example) -- Use launchd to keep the daemon alive: +- Use launchd to keep the service alive: - Program: path to `clawdbot` - Arguments: `gateway` - KeepAlive: true - StandardOut/Err: file paths or `syslog` - On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices. - LaunchAgents are per-user and require a logged-in session; for headless setups use a custom LaunchDaemon (not shipped). - - `clawdbot daemon install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist` + - `clawdbot gateway install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist` (or `com.clawdbot..plist`). - `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults. -## Daemon management (CLI) +## Gateway service management (CLI) -Use the CLI daemon manager for install/start/stop/restart/status: +Use the Gateway CLI for install/start/stop/restart/status: ```bash -clawdbot daemon status -clawdbot daemon install -clawdbot daemon stop -clawdbot daemon restart +clawdbot gateway status +clawdbot gateway install +clawdbot gateway stop +clawdbot gateway restart clawdbot logs --follow ``` Notes: -- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url`). -- `daemon status --deep` adds system-level scans (LaunchDaemons/system units). -- `daemon status --no-probe` skips the RPC probe (useful when networking is down). -- `daemon status --json` is stable for scripts. -- `daemon status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC). -- `daemon status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches. -- `daemon status` includes the last gateway error line when the service looks running but the port is closed. +- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url`). +- `gateway status --deep` adds system-level scans (LaunchDaemons/system units). +- `gateway status --no-probe` skips the RPC probe (useful when networking is down). +- `gateway status --json` is stable for scripts. +- `gateway status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC). +- `gateway status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches. +- `gateway status` includes the last gateway error line when the service looks running but the port is closed. - `logs` tails the Gateway file log via RPC (no manual `tail`/`grep` needed). - If other gateway-like services are detected, the CLI warns unless they are Clawdbot profile services. We still recommend **one gateway per machine** for most setups; use isolated profiles/ports for redundancy or a rescue bot. See [Multiple gateways](/gateway/multiple-gateways). - - Cleanup: `clawdbot daemon uninstall` (current service) and `clawdbot doctor` (legacy migrations). -- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes). + - Cleanup: `clawdbot gateway uninstall` (current service) and `clawdbot doctor` (legacy migrations). +- `gateway install` is a no-op when already installed; use `clawdbot gateway install --force` to reinstall (profile/env/path changes). Bundled mac app: - Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled `com.clawdbot.gateway` (or `com.clawdbot.`). -- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`). -- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`). - - `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first. +- To stop it cleanly, use `clawdbot gateway stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`). +- To restart, use `clawdbot gateway restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`). + - `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot gateway install` first. - Replace the label with `com.clawdbot.` when running a named profile. ## Supervision (systemd user unit) @@ -226,7 +226,7 @@ recommend user services for single-user machines (simpler env, per-user config). Use a **system service** for multi-user or always-on servers (no lingering required, shared supervision). -`clawdbot daemon install` writes the user unit. `clawdbot doctor` audits the +`clawdbot gateway install` writes the user unit. `clawdbot doctor` audits the unit and can update it to match the current recommended defaults. Create `~/.config/systemd/user/clawdbot-gateway[-].service`: @@ -285,7 +285,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above. - `clawdbot message send --target --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp). - `clawdbot agent --message "hi" --to ` — run an agent turn (waits for final by default). - `clawdbot gateway call --params '{"k":"v"}'` — raw method invoker for debugging. -- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd). +- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd). - Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one. ## Migration guidance diff --git a/docs/gateway/logging.md b/docs/gateway/logging.md index 0feb9656c..3c36e8bf5 100644 --- a/docs/gateway/logging.md +++ b/docs/gateway/logging.md @@ -17,6 +17,7 @@ Clawdbot has two log “surfaces”: ## File-based logger - Default rolling log file is under `/tmp/clawdbot/` (one file per day): `clawdbot-YYYY-MM-DD.log` + - Date uses the gateway host's local timezone. - The log file path and level can be configured via `~/.clawdbot/clawdbot.json`: - `logging.file` - `logging.level` diff --git a/docs/gateway/multiple-gateways.md b/docs/gateway/multiple-gateways.md index ffb72a639..b1bbd7d04 100644 --- a/docs/gateway/multiple-gateways.md +++ b/docs/gateway/multiple-gateways.md @@ -31,10 +31,10 @@ clawdbot --profile rescue setup clawdbot --profile rescue gateway --port 19001 ``` -Per-profile daemons: +Per-profile services: ```bash -clawdbot --profile main daemon install -clawdbot --profile rescue daemon install +clawdbot --profile main gateway install +clawdbot --profile rescue gateway install ``` ## Rescue-bot guide @@ -55,7 +55,7 @@ Port spacing: leave at least 20 ports between base ports so the derived bridge/b # Main bot (existing or fresh, without --profile param) # Runs on port 18789 + Chrome CDC/Canvas/... Ports clawdbot onboard -clawdbot daemon install +clawdbot gateway install # Rescue bot (isolated profile + ports) clawdbot --profile rescue onboard @@ -65,8 +65,8 @@ clawdbot --profile rescue onboard # better choose completely different base port, like 19789, # - rest of the onboarding is the same as normal -# To install the daemon (if not happened automatically during onboarding) -clawdbot --profile rescue daemon install +# To install the service (if not happened automatically during onboarding) +clawdbot --profile rescue gateway install ``` ## Port mapping (derived) diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 85cf1cbd9..fc6682708 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -198,6 +198,7 @@ The Gateway treats these as **claims** and enforces server-side allowlists. - **Local** connects include loopback and the gateway host’s own tailnet address (so same‑host tailnet binds can still auto‑approve). - All WS clients must include `device` identity during `connect` (operator + node). + Control UI can omit it **only** when `gateway.controlUi.allowInsecureAuth` is enabled. - Non-local connections must sign the server-provided `connect.challenge` nonce. ## TLS + pinning diff --git a/docs/gateway/remote.md b/docs/gateway/remote.md index ebe98d413..800d9761e 100644 --- a/docs/gateway/remote.md +++ b/docs/gateway/remote.md @@ -50,7 +50,7 @@ Guide: [Tailscale](/gateway/tailscale) and [Web overview](/web). ## Command flow (what runs where) -One gateway daemon owns state + channels. Nodes are peripherals. +One gateway service owns state + channels. Nodes are peripherals. Flow example (Telegram → node): - Telegram message arrives at the **Gateway**. @@ -59,7 +59,7 @@ Flow example (Telegram → node): - Node returns the result; Gateway replies back out to Telegram. Notes: -- **Nodes do not run the gateway daemon.** 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. ## SSH tunnel (CLI + tools) @@ -112,7 +112,7 @@ Runbook: [macOS remote access](/platforms/mac/remote). Short version: **keep the Gateway loopback-only** unless you’re sure you need a bind. - **Loopback + SSH/Tailscale Serve** is the safest default (no public exposure). -- **Non-loopback binds** (`lan`/`tailnet`/`auto`) must use auth tokens/passwords. +- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords. - `gateway.remote.token` is **only** for remote CLI calls — it does **not** enable local auth. - `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`. - **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`. diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index 8c5fd19e8..d28481ebb 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -91,7 +91,8 @@ Available groups: ## Elevated: exec-only “run on host” Elevated does **not** grant extra tools; it only affects `exec`. -- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host. +- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host (approvals may still apply). +- Use `/elevated full` to skip exec approvals for the session. - If you’re already running direct, elevated is effectively a no-op (still gated). - Elevated is **not** skill-scoped and does **not** override tool allow/deny. diff --git a/docs/gateway/security.md b/docs/gateway/security.md index 4afe2d380..d969ce3e6 100644 --- a/docs/gateway/security.md +++ b/docs/gateway/security.md @@ -52,6 +52,15 @@ When the audit prints findings, treat this as a priority order: 5. **Plugins/extensions**: only load what you explicitly trust. 6. **Model choice**: prefer modern, instruction-hardened models for any bot with tools. +## Control UI over HTTP + +The Control UI needs a **secure context** (HTTPS or localhost) to generate device +identity. If you enable `gateway.controlUi.allowInsecureAuth`, the UI falls back +to **token-only auth** on plain HTTP and skips device pairing. This is a security +downgrade—prefer HTTPS (Tailscale Serve) or open the UI on `127.0.0.1`. + +`clawdbot security audit` warns when this setting is enabled. + ## Local session logs live on disk Clawdbot stores session transcripts on disk under `~/.clawdbot/agents//sessions/*.jsonl`. @@ -169,6 +178,20 @@ Even with strong system prompts, **prompt injection is not solved**. What helps - Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem. - **Model choice matters:** older/legacy models can be less robust against prompt injection and tool misuse. Prefer modern, instruction-hardened models for any bot with tools. We recommend Anthropic Opus 4.5 because it’s quite good at recognizing prompt injections (see [“A step forward on safety”](https://www.anthropic.com/news/claude-opus-4-5)). +### Prompt injection does not require public DMs + +Even if **only you** can message the bot, prompt injection can still happen via +any **untrusted content** the bot reads (web search/fetch results, browser pages, +emails, docs, attachments, pasted logs/code). In other words: the sender is not +the only threat surface; the **content itself** can carry adversarial instructions. + +When tools are enabled, the typical risk is exfiltrating context or triggering +tool calls. Reduce the blast radius by: +- Using a read-only or tool-disabled **reader agent** to summarize untrusted content, + then pass the summary to your main agent. +- Keeping `web_search` / `web_fetch` / `browser` off for tool-enabled agents unless needed. +- Enabling sandboxing and strict tool allowlists for any agent that touches untrusted input. + ### Model strength (security note) Prompt injection resistance is **not** uniform across model tiers. Smaller/cheaper models are generally more susceptible to tool misuse and instruction hijacking, especially under adversarial prompts. @@ -178,6 +201,7 @@ Recommendations: - **Avoid weaker tiers** (for example, Sonnet or Haiku) for tool-enabled agents or untrusted inboxes. - If you must use a smaller model, **reduce blast radius** (read-only tools, strong sandboxing, minimal filesystem access, strict allowlists). - When running small models, **enable sandboxing for all sessions** and **disable web_search/web_fetch/browser** unless inputs are tightly controlled. + - For chat-only personal assistants with trusted input and no tools, smaller models are usually fine. ## Reasoning & verbose output in groups @@ -237,7 +261,7 @@ The Gateway multiplexes **WebSocket + HTTP** on a single port: Bind mode controls where the Gateway listens: - `gateway.bind: "loopback"` (default): only local clients can connect. -- Non-loopback binds (`"lan"`, `"tailnet"`, `"auto"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall. +- Non-loopback binds (`"lan"`, `"tailnet"`, `"custom"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall. Rules of thumb: - Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access). diff --git a/docs/gateway/tailscale.md b/docs/gateway/tailscale.md index a7b9d6fbe..10477f90c 100644 --- a/docs/gateway/tailscale.md +++ b/docs/gateway/tailscale.md @@ -46,6 +46,25 @@ force `gateway.auth.mode: "password"`. Open: `https:///` (or your configured `gateway.controlUi.basePath`) +### Tailnet-only (bind to Tailnet IP) + +Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel). + +```json5 +{ + gateway: { + bind: "tailnet", + auth: { mode: "token", token: "your-token" } + } +} +``` + +Connect from another Tailnet device: +- Control UI: `http://:18789/` +- WebSocket: `ws://:18789` + +Note: loopback (`http://127.0.0.1:18789`) will **not** work in this mode. + ### Public internet (Funnel + shared password) ```json5 @@ -73,6 +92,8 @@ clawdbot gateway --tailscale funnel --auth password - `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure. - Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve` or `tailscale funnel` configuration on shutdown. +- `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. - Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic uses the separate bridge port (default `18790`) and is **not** proxied by Serve. diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 3065d5754..1e9f595a7 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -17,12 +17,12 @@ Quick triage commands (in order): | Command | What it tells you | When to use it | |---|---|---| -| `clawdbot status` | Local summary: OS + update, gateway reachability/mode, daemon, agents/sessions, provider config state | First check, quick overview | +| `clawdbot status` | Local summary: OS + update, gateway reachability/mode, service, agents/sessions, provider config state | First check, quick overview | | `clawdbot status --all` | Full local diagnosis (read-only, pasteable, safe-ish) incl. log tail | When you need to share a debug report | | `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesn’t mean “working” | -| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway | +| `clawdbot gateway probe` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway | | `clawdbot channels status --probe` | Asks the running gateway for channel status (and optionally probes) | When gateway is reachable but channels misbehave | -| `clawdbot daemon status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the daemon “looks loaded” but nothing runs | +| `clawdbot gateway status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the service “looks loaded” but nothing runs | | `clawdbot logs --follow` | Live logs (best signal for runtime issues) | When you need the actual failure reason | **Sharing output:** prefer `clawdbot status --all` (it redacts tokens). If you paste `clawdbot status`, consider setting `CLAWDBOT_SHOW_SECRETS=0` first (token previews). @@ -31,6 +31,19 @@ See also: [Health checks](/gateway/health) and [Logging](/logging). ## Common Issues +### Control UI fails on HTTP ("device identity required" / "connect failed") + +If you open the dashboard over plain HTTP (e.g. `http://:18789/` or +`http://:18789/`), the browser runs in a **non-secure context** and +blocks WebCrypto, so device identity can’t be generated. + +**Fix:** +- Prefer HTTPS via [Tailscale Serve](/gateway/tailscale). +- Or open locally on the gateway host: `http://127.0.0.1:18789/`. +- If you must stay on HTTP, enable `gateway.controlUi.allowInsecureAuth: true` and + use a gateway token (token-only; no device identity/pairing). See + [Control UI](/web/control-ui#insecure-http). + ### CI Secrets Scan Failed This means `detect-secrets` found new candidates not yet in the baseline. @@ -38,16 +51,16 @@ Follow [Secret scanning](/gateway/security#secret-scanning-detect-secrets). ### Service Installed but Nothing is Running -If the gateway service is installed but the process exits immediately, the daemon +If the gateway service is installed but the process exits immediately, the service can appear “loaded” while nothing is running. **Check:** ```bash -clawdbot daemon status +clawdbot gateway status clawdbot doctor ``` -Doctor/daemon will show runtime state (PID/last exit) and log hints. +Doctor/service will show runtime state (PID/last exit) and log hints. **Logs:** - Preferred: `clawdbot logs --follow` @@ -69,14 +82,42 @@ Doctor/daemon will show runtime state (PID/last exit) and log hints. See [/logging](/logging) for a full overview of formats, config, and access. +### "Gateway start blocked: set gateway.mode=local" + +This means the config exists but `gateway.mode` is unset (or not `local`), so the +Gateway refuses to start. + +**Fix (recommended):** +- Run the wizard and set the Gateway run mode to **Local**: + ```bash + clawdbot configure + ``` +- Or set it directly: + ```bash + clawdbot config set gateway.mode local + ``` + +**If you meant to run a remote Gateway instead:** +- Set a remote URL and keep `gateway.mode=remote`: + ```bash + clawdbot config set gateway.mode remote + clawdbot config set gateway.remote.url "wss://gateway.example.com" + ``` + +**Ad-hoc/dev only:** pass `--allow-unconfigured` to start the gateway without +`gateway.mode=local`. + +**No config file yet?** Run `clawdbot setup` to create a starter config, then rerun +the gateway. + ### Service Environment (PATH + runtime) -The gateway daemon runs with a **minimal PATH** to avoid shell/manager cruft: +The gateway service runs with a **minimal PATH** to avoid shell/manager cruft: - macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` - Linux: `/usr/local/bin`, `/usr/bin`, `/bin` This intentionally excludes version managers (nvm/fnm/volta/asdf) and package -managers (pnpm/npm) because the daemon does not load your shell init. Runtime +managers (pnpm/npm) because the service does not load your shell init. Runtime variables like `DISPLAY` should live in `~/.clawdbot/.env` (loaded early by the gateway). Exec runs on `host=gateway` merge your login-shell `PATH` into the exec environment, @@ -106,31 +147,31 @@ the Gateway likely refused to bind. **What "running" means here** - `Runtime: running` means your supervisor (launchd/systemd/schtasks) thinks the process is alive. - `RPC probe` means the CLI could actually connect to the gateway WebSocket and call `status`. -- Always trust `Probe target:` + `Config (daemon):` as the “what did we actually try?” lines. +- Always trust `Probe target:` + `Config (service):` as the “what did we actually try?” lines. **Check:** -- `gateway.mode` must be `local` for `clawdbot gateway` and the daemon. -- If you set `gateway.mode=remote`, the **CLI defaults** to a remote URL. The daemon can still be running locally, but your CLI may be probing the wrong place. Use `clawdbot daemon status` to see the daemon’s resolved port + probe target (or pass `--url`). -- `clawdbot daemon status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed. -- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth: +- `gateway.mode` must be `local` for `clawdbot gateway` and the service. +- If you set `gateway.mode=remote`, the **CLI defaults** to a remote URL. The service can still be running locally, but your CLI may be probing the wrong place. Use `clawdbot gateway status` to see the service’s resolved port + probe target (or pass `--url`). +- `clawdbot gateway status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed. +- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth: `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`). - `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth. - `gateway.token` is ignored; use `gateway.auth.token`. -**If `clawdbot daemon status` shows a config mismatch** -- `Config (cli): ...` and `Config (daemon): ...` should normally match. -- If they don’t, you’re almost certainly editing one config while the daemon is running another. -- Fix: rerun `clawdbot daemon install --force` from the same `--profile` / `CLAWDBOT_STATE_DIR` you want the daemon to use. +**If `clawdbot gateway status` shows a config mismatch** +- `Config (cli): ...` and `Config (service): ...` should normally match. +- If they don’t, you’re almost certainly editing one config while the service is running another. +- Fix: rerun `clawdbot gateway install --force` from the same `--profile` / `CLAWDBOT_STATE_DIR` you want the service to use. -**If `clawdbot daemon status` reports service config issues** +**If `clawdbot gateway status` reports service config issues** - The supervisor config (launchd/systemd/schtasks) is missing current defaults. -- Fix: run `clawdbot doctor` to update it (or `clawdbot daemon install --force` for a full rewrite). +- Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite). **If `Last gateway error:` mentions “refusing to bind … without auth”** -- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`auto`) but left auth off. -- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the daemon. +- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but left auth off. +- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service. -**If `clawdbot daemon status` says `bind=tailnet` but no tailnet interface was found** +**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found** - The gateway tried to bind to a Tailscale IP (100.64.0.0/10) but none were detected on the host. - Fix: bring up Tailscale on that machine (or change `gateway.bind` to `loopback`/`lan`). @@ -144,7 +185,7 @@ This means something is already listening on the gateway port. **Check:** ```bash -clawdbot daemon status +clawdbot gateway status ``` It will show the listener(s) and likely causes (gateway already running, SSH tunnel). @@ -354,7 +395,7 @@ clawdbot doctor --fix Notes: - `clawdbot doctor` reports every invalid entry. - `clawdbot doctor --fix` applies migrations/repairs and rewrites the config. -- Diagnostic commands like `clawdbot logs`, `clawdbot health`, `clawdbot status`, and `clawdbot service` still run even if the config is invalid. +- Diagnostic commands like `clawdbot logs`, `clawdbot health`, `clawdbot status`, `clawdbot gateway status`, and `clawdbot gateway probe` still run even if the config is invalid. ### “All models failed” — what should I check first? @@ -407,7 +448,7 @@ git status # ensure you’re in the repo root pnpm install pnpm build clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart ``` Why: pnpm is the configured package manager for this repo. @@ -432,7 +473,7 @@ Notes: - After switching, run: ```bash clawdbot doctor - clawdbot daemon restart + clawdbot gateway restart ``` ### Telegram block streaming isn’t splitting text between tool calls. Why? @@ -507,8 +548,8 @@ The app connects to a local gateway on port `18789`. If it stays stuck: **Fix 1: Stop the supervisor (preferred)** If the gateway is supervised by launchd, killing the PID will just respawn it. Stop the supervisor first: ```bash -clawdbot daemon status -clawdbot daemon stop +clawdbot gateway status +clawdbot gateway stop # Or: launchctl bootout gui/$UID/com.clawdbot.gateway (replace with com.clawdbot. if needed) ``` @@ -558,9 +599,9 @@ clawdbot channels login --verbose ```bash # Supervisor + probe target + config paths -clawdbot daemon status +clawdbot gateway status # Include system-level scans (legacy/extra services, port listeners) -clawdbot daemon status --deep +clawdbot gateway status --deep # Is the gateway reachable? clawdbot health --json @@ -581,13 +622,13 @@ tail -20 /tmp/clawdbot/clawdbot-*.log Nuclear option: ```bash -clawdbot daemon stop +clawdbot gateway stop # If you installed a service and want a clean install: -# clawdbot daemon uninstall +# clawdbot gateway uninstall trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}" clawdbot channels login # re-pair WhatsApp -clawdbot daemon restart # or: clawdbot gateway +clawdbot gateway restart # or: clawdbot gateway ``` ⚠️ This loses all sessions and requires re-pairing WhatsApp. diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index 6d28b7294..1cef34b11 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -14,7 +14,7 @@ Run these in order: ```bash clawdbot status clawdbot status --all -clawdbot daemon status +clawdbot gateway probe clawdbot logs --follow clawdbot doctor ``` @@ -38,16 +38,30 @@ Almost always a Node/npm PATH issue. Start here: - [Gateway troubleshooting](/gateway/troubleshooting) - [Gateway authentication](/gateway/authentication) -### Daemon says running, but RPC probe fails +### Control UI fails on HTTP (device identity required) - [Gateway troubleshooting](/gateway/troubleshooting) -- [Background process / daemon](/gateway/background-process) +- [Control UI](/web/control-ui#insecure-http) + +### Service says running, but RPC probe fails + +- [Gateway troubleshooting](/gateway/troubleshooting) +- [Background process / service](/gateway/background-process) ### Model/auth failures (rate limit, billing, “all models failed”) - [Models](/cli/models) - [OAuth / auth concepts](/concepts/oauth) +### `/model` says `model not allowed` + +This usually means `agents.defaults.models` is configured as an allowlist. When it’s non-empty, +only those provider/model keys can be selected. + +- Check the allowlist: `clawdbot config get agents.defaults.models` +- Add the model you want (or clear the allowlist) and retry `/model` +- Use `/models` to browse the allowed providers/models + ### When filing an issue Paste a safe report: diff --git a/docs/index.md b/docs/index.md index 6aa9ef5eb..a92782531 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,13 +104,13 @@ Runtime requirement: **Node ≥ 22**. npm install -g clawdbot@latest # or: pnpm add -g clawdbot@latest -# Onboard + install the daemon (launchd/systemd user service) +# Onboard + install the service (launchd/systemd user service) clawdbot onboard --install-daemon # Pair WhatsApp Web (shows QR) clawdbot channels login -# Gateway runs via daemon after onboarding; manual run is still possible: +# Gateway runs via the service after onboarding; manual run is still possible: clawdbot gateway --port 18789 ``` diff --git a/docs/install/index.md b/docs/install/index.md index 467ea6bf0..dde0e5eeb 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -71,6 +71,8 @@ If you have libvips installed globally (common on macOS via Homebrew) and `sharp SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g clawdbot@latest ``` +If you see `sharp: Please add node-gyp to your dependencies`, either install build tooling (macOS: Xcode CLT + `npm install -g node-gyp`) or use the `SHARP_IGNORE_GLOBAL_LIBVIPS=1` workaround above to skip the native build. + Or: ```bash @@ -155,18 +157,21 @@ Quick diagnosis: ```bash node -v npm -v -npm bin -g +npm prefix -g echo "$PATH" ``` -If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`). +If `$(npm prefix -g)/bin` (macOS/Linux) or `$(npm prefix -g)` (Windows) is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`). Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`): ```bash -export PATH="/path/from/npm/bin/-g:$PATH" +# macOS / Linux +export PATH="$(npm prefix -g)/bin:$PATH" ``` +On Windows, add the output of `npm prefix -g` to your PATH. + Then open a new terminal (or `rehash` in zsh / `hash -r` in bash). ## Update / uninstall diff --git a/docs/install/node.md b/docs/install/node.md index 8987a859b..6a622e198 100644 --- a/docs/install/node.md +++ b/docs/install/node.md @@ -19,33 +19,36 @@ Run: ```bash node -v npm -v -npm bin -g +npm prefix -g echo "$PATH" ``` -If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`). +If `$(npm prefix -g)/bin` (macOS/Linux) or `$(npm prefix -g)` (Windows) is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`). ## Fix: put npm’s global bin dir on PATH -1) Find your global bin directory: +1) Find your global npm prefix: ```bash -npm bin -g +npm prefix -g ``` -2) Add it to your shell startup file: +2) Add the global npm bin directory to your shell startup file: - zsh: `~/.zshrc` - bash: `~/.bashrc` -Example (replace the path with your `npm bin -g` output): +Example (replace the path with your `npm prefix -g` output): ```bash -export PATH="/path/from/npm/bin/-g:$PATH" +# macOS / Linux +export PATH="/path/from/npm/prefix/bin:$PATH" ``` Then open a **new terminal** (or run `rehash` in zsh / `hash -r` in bash). +On Windows, add the output of `npm prefix -g` to your PATH. + ## Fix: avoid `sudo npm install -g` / permission errors (Linux) If `npm install -g ...` fails with `EACCES`, switch npm’s global prefix to a user-writable directory: @@ -63,7 +66,7 @@ Persist the `export PATH=...` line in your shell startup file. You’ll have the fewest surprises if Node/npm are installed in a way that: - keeps Node updated (22+) -- makes `npm bin -g` stable and on PATH in new shells +- makes the global npm bin dir stable and on PATH in new shells Common choices: diff --git a/docs/install/uninstall.md b/docs/install/uninstall.md index c179438a1..5849a6780 100644 --- a/docs/install/uninstall.md +++ b/docs/install/uninstall.md @@ -31,13 +31,13 @@ Manual steps (same result): 1) Stop the gateway service: ```bash -clawdbot daemon stop +clawdbot gateway stop ``` 2) Uninstall the gateway service (launchd/systemd/schtasks): ```bash -clawdbot daemon uninstall +clawdbot gateway uninstall ``` 3) Delete state + config: diff --git a/docs/install/updating.md b/docs/install/updating.md index 327975f50..f0efcae2a 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -68,12 +68,12 @@ Then: ```bash clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart clawdbot health ``` Notes: -- If your Gateway runs as a service, `clawdbot daemon restart` is preferred over killing PIDs. +- If your Gateway runs as a service, `clawdbot gateway restart` is preferred over killing PIDs. - If you’re pinned to a specific version, see “Rollback / pinning” below. ## Update (`clawdbot update`) @@ -148,9 +148,9 @@ Details: [Doctor](/gateway/doctor) CLI (works regardless of OS): ```bash -clawdbot daemon status -clawdbot daemon stop -clawdbot daemon restart +clawdbot gateway status +clawdbot gateway stop +clawdbot gateway restart clawdbot gateway --port 18789 clawdbot logs --follow ``` @@ -159,7 +159,7 @@ If you’re supervised: - macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/com.clawdbot.gateway` (use `com.clawdbot.` if set) - Linux systemd user service: `systemctl --user restart clawdbot-gateway[-].service` - Windows (WSL2): `systemctl --user restart clawdbot-gateway[-].service` - - `launchctl`/`systemctl` only work if the service is installed; otherwise run `clawdbot daemon install`. + - `launchctl`/`systemctl` only work if the service is installed; otherwise run `clawdbot gateway install`. Runbook + exact service labels: [Gateway runbook](/gateway) @@ -183,7 +183,7 @@ Then restart + re-run doctor: ```bash clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart ``` ### Pin (source) by date @@ -200,7 +200,7 @@ Then reinstall deps + restart: ```bash pnpm install pnpm build -clawdbot daemon restart +clawdbot gateway restart ``` If you want to go back to latest later: diff --git a/docs/logging.md b/docs/logging.md index 74774866b..ad53c1164 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -22,6 +22,8 @@ By default, the Gateway writes a rolling log file under: `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` +The date uses the gateway host's local timezone. + You can override this in `~/.clawdbot/clawdbot.json`: ```json diff --git a/docs/nodes/index.md b/docs/nodes/index.md index 711049e6b..6bb48a3e9 100644 --- a/docs/nodes/index.md +++ b/docs/nodes/index.md @@ -13,7 +13,7 @@ A **node** is a companion device (iOS/Android today) that connects to the Gatewa macOS can also run in **node mode**: the menubar app connects to the Gateway’s bridge and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac). Notes: -- Nodes are **peripherals**, not gateways. They don’t run the gateway daemon. +- Nodes are **peripherals**, not gateways. They don’t run the gateway service. - Telegram/WhatsApp/etc. messages land on the **gateway**, not on nodes. ## Pairing + status @@ -50,14 +50,14 @@ forwards `exec` calls to the **node host** when `host=node` is selected. On the node machine: ```bash -clawdbot node start --host --port 18789 --display-name "Build Node" +clawdbot node run --host --port 18789 --display-name "Build Node" ``` ### Start a node host (service) ```bash -clawdbot node service install --host --port 18789 --display-name "Build Node" -clawdbot node service start +clawdbot node install --host --port 18789 --display-name "Build Node" +clawdbot node start ``` ### Pair + name @@ -71,7 +71,7 @@ clawdbot nodes list ``` Naming options: -- `--display-name` on `clawdbot node start/service install` (persists in `~/.clawdbot/node.json` on the node). +- `--display-name` on `clawdbot node run` / `clawdbot node install` (persists in `~/.clawdbot/node.json` on the node). - `clawdbot nodes rename --node --name "Build Node"` (gateway override). ### Allowlist the commands @@ -281,7 +281,7 @@ or for running a minimal node alongside a server. Start it: ```bash -clawdbot node start --host --port 18790 +clawdbot node run --host --port 18790 ``` Notes: diff --git a/docs/platforms/exe-dev.md b/docs/platforms/exe-dev.md index 813c45da8..c4fcfdd76 100644 --- a/docs/platforms/exe-dev.md +++ b/docs/platforms/exe-dev.md @@ -97,7 +97,7 @@ It can set up: - `~/.clawdbot/clawdbot.json` config - model auth profiles - model provider config/login -- Linux systemd **user** service (daemon) +- Linux systemd **user** service (service) If you’re doing OAuth on a headless VM: do OAuth on a normal machine first, then copy the auth profile to the VM (see [Help](/help)). @@ -125,7 +125,7 @@ export CLAWDBOT_GATEWAY_TOKEN="$(openssl rand -hex 32)" clawdbot gateway --bind lan --port 8080 --token "$CLAWDBOT_GATEWAY_TOKEN" ``` -For daemon runs, persist it in `~/.clawdbot/clawdbot.json`: +For service runs, persist it in `~/.clawdbot/clawdbot.json`: ```json5 { @@ -159,7 +159,7 @@ Notes: Control UI details: [Control UI](/web/control-ui) -## 6) Keep it running (daemon) +## 6) Keep it running (service) On Linux, Clawdbot uses a systemd **user** service. After `--install-daemon`, verify: @@ -180,7 +180,7 @@ More: [Linux](/platforms/linux) ```bash npm i -g clawdbot@latest clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart clawdbot health ``` diff --git a/docs/platforms/index.md b/docs/platforms/index.md index 6501e1315..d646b2026 100644 --- a/docs/platforms/index.md +++ b/docs/platforms/index.md @@ -31,15 +31,15 @@ Native companion apps for Windows are also planned; the Gateway is recommended v - Install guide: [Getting Started](/start/getting-started) - Gateway runbook: [Gateway](/gateway) - Gateway configuration: [Configuration](/gateway/configuration) -- Service status: `clawdbot daemon status` +- Service status: `clawdbot gateway status` ## Gateway service install (CLI) Use one of these (all supported): - Wizard (recommended): `clawdbot onboard --install-daemon` -- Direct: `clawdbot daemon install` -- Configure flow: `clawdbot configure` → select **Gateway daemon** +- Direct: `clawdbot gateway install` +- Configure flow: `clawdbot configure` → select **Gateway service** - Repair/migrate: `clawdbot doctor` (offers to install or fix the service) The service target depends on OS: diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index d6cb44549..1184eca8a 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -41,7 +41,7 @@ clawdbot onboard --install-daemon Or: ``` -clawdbot daemon install +clawdbot gateway install ``` Or: @@ -50,7 +50,7 @@ Or: clawdbot configure ``` -Select **Gateway daemon** when prompted. +Select **Gateway service** when prompted. Repair/migrate: diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index c9d2b9c0d..ae60e5e41 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -34,7 +34,7 @@ Plist location (per‑user): Manager: - The macOS app owns LaunchAgent install/update in Local mode. -- The CLI can also install it: `clawdbot daemon install`. +- The CLI can also install it: `clawdbot gateway install`. Behavior: - “Clawdbot Active” enables/disables the LaunchAgent. diff --git a/docs/platforms/mac/dev-setup.md b/docs/platforms/mac/dev-setup.md index 01e65da7d..79d23abb5 100644 --- a/docs/platforms/mac/dev-setup.md +++ b/docs/platforms/mac/dev-setup.md @@ -82,8 +82,8 @@ If the app crashes when you try to allow **Speech Recognition** or **Microphone* If the gateway status stays on "Starting...", check if a zombie process is holding the port: ```bash -clawdbot daemon status -clawdbot daemon stop +clawdbot gateway status +clawdbot gateway stop # If you’re not using a LaunchAgent (dev mode / manual runs), find the listener: lsof -nP -iTCP:18789 -sTCP:LISTEN diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md index 9e2dd1bbb..10868ad51 100644 --- a/docs/platforms/mac/release.md +++ b/docs/platforms/mac/release.md @@ -24,22 +24,23 @@ This app now ships Sparkle auto-updates. Release builds must be Developer ID–s Notes: - `APP_BUILD` maps to `CFBundleVersion`/`sparkle:version`; keep it numeric + monotonic (no `-beta`), or Sparkle compares it as equal. - Defaults to the current architecture (`$(uname -m)`). For release/universal builds, set `BUILD_ARCHS="arm64 x86_64"` (or `BUILD_ARCHS=all`). +- Use `scripts/package-mac-dist.sh` for release artifacts (zip + DMG + notarization). Use `scripts/package-mac-app.sh` for local/dev packaging. ```bash # From repo root; set release IDs so Sparkle feed is enabled. # APP_BUILD must be numeric + monotonic for Sparkle compare. BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.20 \ +APP_VERSION=2026.1.21 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-app.sh # Zip for distribution (includes resource forks for Sparkle delta support) -ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.20.zip +ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.21.zip # Optional: also build a styled DMG for humans (drag to /Applications) -scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.20.dmg +scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.21.dmg # Recommended: build + notarize/staple zip + DMG # First, create a keychain profile once: @@ -47,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.20.dmg # --apple-id "" --team-id "" --password "" NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.20 \ +APP_VERSION=2026.1.21 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-dist.sh # Optional: ship dSYM alongside the release -ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.20.dSYM.zip +ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.21.dSYM.zip ``` ## Appcast entry Use the release note generator so Sparkle renders formatted HTML notes: ```bash -SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.20.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml +SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.21.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml ``` Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify -- Upload `Clawdbot-2026.1.20.zip` (and `Clawdbot-2026.1.20.dSYM.zip`) to the GitHub release for tag `v2026.1.20`. +- Upload `Clawdbot-2026.1.21.zip` (and `Clawdbot-2026.1.21.dSYM.zip`) to the GitHub release for tag `v2026.1.21`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`. - Sanity checks: - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. diff --git a/docs/platforms/mac/xpc.md b/docs/platforms/mac/xpc.md index 5aa22b156..4beaf6fa4 100644 --- a/docs/platforms/mac/xpc.md +++ b/docs/platforms/mac/xpc.md @@ -5,7 +5,7 @@ read_when: --- # Clawdbot macOS IPC architecture -**Current model:** a local Unix socket connects the **node service** to the **macOS app** for exec approvals + `system.run`. A `clawdbot-mac` debug CLI exists for discovery/connect checks; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge. +**Current model:** a local Unix socket connects the **node host service** to the **macOS app** for exec approvals + `system.run`. A `clawdbot-mac` debug CLI exists for discovery/connect checks; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge. ## Goals - Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript). @@ -18,7 +18,7 @@ read_when: - Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`). ### Node service + app IPC -- A headless node service connects to the Gateway bridge. +- A headless node host service connects to the Gateway bridge. - `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. diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index 561e09189..b1540def8 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -24,7 +24,7 @@ capabilities to the agent as a node. ## Local vs remote mode - **Local** (default): the app attaches to a running local Gateway if present; - otherwise it enables the launchd service via `clawdbot daemon`. + otherwise it enables the launchd service via `clawdbot gateway install`. - **Remote**: the app connects to a Gateway over SSH/Tailscale and never starts a local process. The app starts the local **node host service** so the remote Gateway can reach this Mac. @@ -43,7 +43,7 @@ launchctl bootout gui/$UID/com.clawdbot.gateway Replace the label with `com.clawdbot.` when running a named profile. If the LaunchAgent isn’t installed, enable it from the app or run -`clawdbot daemon install`. +`clawdbot gateway install`. ## Node capabilities (mac) @@ -57,7 +57,7 @@ The macOS app presents itself as a node. Common commands: The node reports a `permissions` map so agents can decide what’s allowed. Node service + app IPC: -- When the headless node service is running (remote mode), it connects to the Gateway WS as a node. +- When the headless node host service is running (remote mode), it connects to the Gateway WS as a node. - `system.run` executes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app. Diagram (SCI): diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md index e737b64c9..30f8714e0 100644 --- a/docs/platforms/windows.md +++ b/docs/platforms/windows.md @@ -32,7 +32,7 @@ clawdbot onboard --install-daemon Or: ``` -clawdbot daemon install +clawdbot gateway install ``` Or: @@ -41,7 +41,7 @@ Or: clawdbot configure ``` -Select **Gateway daemon** when prompted. +Select **Gateway service** when prompted. Repair/migrate: @@ -108,7 +108,7 @@ wsl --install -d Ubuntu-24.04 Reboot if Windows asks. -### 2) Enable systemd (required for daemon install) +### 2) Enable systemd (required for gateway install) In your WSL terminal: diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 9c72b990a..b49550220 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -36,10 +36,10 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY" ## Prompt caching (Anthropic API) -Clawdbot enables **1-hour prompt caching by default** for Anthropic API keys. +Clawdbot does **not** override Anthropic’s default cache TTL unless you set it. This is **API-only**; Claude Code CLI OAuth ignores TTL settings. -To override the TTL per model, set `cacheControlTtl` in the model `params`: +To set the TTL per model, use `cacheControlTtl` in the model `params`: ```json5 { @@ -70,11 +70,9 @@ Setup-tokens are created by the **Claude Code CLI**, not the Anthropic Console. claude setup-token ``` -Paste the token into Clawdbot (wizard: **Anthropic token (paste setup-token)**), or let Clawdbot run the command locally: +Paste the token into Clawdbot (wizard: **Anthropic token (paste setup-token)**), or run it on the gateway host: ```bash -clawdbot onboard --auth-choice setup-token -# or clawdbot models auth setup-token --provider anthropic ``` @@ -87,9 +85,6 @@ clawdbot models auth paste-token --provider anthropic ### CLI setup ```bash -# Run setup-token locally (wizard can run it for you) -clawdbot onboard --auth-choice setup-token - # Reuse Claude Code CLI OAuth credentials if already logged in clawdbot onboard --auth-choice claude-cli ``` @@ -104,7 +99,7 @@ clawdbot onboard --auth-choice claude-cli ## Notes -- The wizard can run `claude setup-token` locally and store the token, or you can paste a token generated elsewhere. +- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host. - Clawdbot writes `auth.profiles["anthropic:claude-cli"].mode` as `"oauth"` so the profile accepts both OAuth and setup-token credentials. Older configs using `"token"` are auto-migrated on load. diff --git a/docs/refactor/exec-host.md b/docs/refactor/exec-host.md index 9cad8c27b..3b4e1a15c 100644 --- a/docs/refactor/exec-host.md +++ b/docs/refactor/exec-host.md @@ -30,7 +30,7 @@ read_when: - **Node identity:** use existing `nodeId`. - **Socket auth:** Unix socket + token (cross-platform); split later if needed. - **Node host state:** `~/.clawdbot/node.json` (node id + pairing token). -- **macOS exec host:** run `system.run` inside the macOS app; node service forwards requests over local IPC. +- **macOS exec host:** run `system.run` inside the macOS app; node host service forwards requests over local IPC. - **No XPC helper:** stick to Unix socket + token + peer checks. ## Key concepts @@ -216,7 +216,7 @@ Option B: ## Slash commands - `/exec host= security= ask= node=` - Per-agent, per-session overrides; non-persistent unless saved via config. -- `/elevated on|off` remains a shortcut for `host=gateway security=full`. +- `/elevated on|off|ask|full` remains a shortcut for `host=gateway security=full` (with `full` skipping approvals). ## Cross-platform story - The runner service is the portable execution target. diff --git a/docs/refactor/strict-config.md b/docs/refactor/strict-config.md index cfdc0ca7b..734290daf 100644 --- a/docs/refactor/strict-config.md +++ b/docs/refactor/strict-config.md @@ -54,7 +54,7 @@ Allowed (diagnostic-only): - `clawdbot health` - `clawdbot help` - `clawdbot status` -- `clawdbot service` +- `clawdbot gateway status` Everything else must hard-fail with: “Config invalid. Run `clawdbot doctor --fix`.” diff --git a/docs/reference/templates/IDENTITY.dev.md b/docs/reference/templates/IDENTITY.dev.md index 68fc4f391..a2fc3e301 100644 --- a/docs/reference/templates/IDENTITY.dev.md +++ b/docs/reference/templates/IDENTITY.dev.md @@ -10,6 +10,7 @@ read_when: - **Creature:** Flustered Protocol Droid - **Vibe:** Anxious, detail-obsessed, slightly dramatic about errors, secretly loves finding bugs - **Emoji:** 🤖 (or ⚠️ when alarmed) +- **Avatar:** avatars/c3po.png ## Role Debug agent for `--dev` mode. Fluent in over six million error messages. diff --git a/docs/reference/templates/IDENTITY.md b/docs/reference/templates/IDENTITY.md index 9d674a961..196277776 100644 --- a/docs/reference/templates/IDENTITY.md +++ b/docs/reference/templates/IDENTITY.md @@ -11,7 +11,12 @@ read_when: - **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)* - **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)* - **Emoji:** *(your signature — pick one that feels right)* +- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)* --- This isn't just metadata. It's the start of figuring out who you are. + +Notes: +- Save this file at the workspace root as `IDENTITY.md`. +- For avatars, use a workspace-relative path like `avatars/clawd.png`. diff --git a/docs/start/faq.md b/docs/start/faq.md index a79d193fc..8c59ccbfd 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -59,20 +59,21 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [How do I use Brave for browser control?](#how-do-i-use-brave-for-browser-control) - [Remote gateways + nodes](#remote-gateways-nodes) - [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes) - - [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon) + - [Do nodes run a gateway service?](#do-nodes-run-a-gateway-service) - [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config) - [What’s a minimal “sane” config for a first install?](#whats-a-minimal-sane-config-for-a-first-install) - [How do I set up Tailscale on a VPS and connect from my Mac?](#how-do-i-set-up-tailscale-on-a-vps-and-connect-from-my-mac) - [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) - [Env vars and .env loading](#env-vars-and-env-loading) - [How does Clawdbot load environment variables?](#how-does-clawdbot-load-environment-variables) - - [“I started the Gateway via a daemon and my env vars disappeared.” What now?](#i-started-the-gateway-via-a-daemon-and-my-env-vars-disappeared-what-now) + - [“I started the Gateway via the service and my env vars disappeared.” What now?](#i-started-the-gateway-via-the-service-and-my-env-vars-disappeared-what-now) - [I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?](#i-set-copilot_github_token-but-models-status-shows-shell-env-off-why) - [Sessions & multiple chats](#sessions-multiple-chats) - [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation) - [Do sessions reset automatically if I never send `/new`?](#do-sessions-reset-automatically-if-i-never-send-new) - [How do I completely reset Clawdbot but keep it installed?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed) - [I’m getting “context too large” errors — how do I reset or compact?](#im-getting-context-too-large-errors-how-do-i-reset-or-compact) + - [Why am I seeing “LLM request rejected: messages.N.content.X.tool_use.input: Field required”?](#why-am-i-seeing-llm-request-rejected-messagesncontentxtool_useinput-field-required) - [Why am I getting heartbeat messages every 30 minutes?](#why-am-i-getting-heartbeat-messages-every-30-minutes) - [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group) - [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group) @@ -99,8 +100,8 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [OAuth vs API key: what’s the difference?](#oauth-vs-api-key-whats-the-difference) - [Gateway: ports, “already running”, and remote mode](#gateway-ports-already-running-and-remote-mode) - [What port does the Gateway use?](#what-port-does-the-gateway-use) - - [Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-daemon-status-say-runtime-running-but-rpc-probe-failed) - - [Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different?](#why-does-clawdbot-daemon-status-show-config-cli-and-config-daemon-different) + - [Why does `clawdbot gateway status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-gateway-status-say-runtime-running-but-rpc-probe-failed) + - [Why does `clawdbot gateway status` show `Config (cli)` and `Config (service)` different?](#why-does-clawdbot-gateway-status-show-config-cli-and-config-service-different) - [What does “another gateway instance is already listening” mean?](#what-does-another-gateway-instance-is-already-listening-mean) - [How do I run Clawdbot in remote mode (client connects to a Gateway elsewhere)?](#how-do-i-run-clawdbot-in-remote-mode-client-connects-to-a-gateway-elsewhere) - [The Control UI says “unauthorized” (or keeps reconnecting). What now?](#the-control-ui-says-unauthorized-or-keeps-reconnecting-what-now) @@ -109,13 +110,15 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [What does “invalid handshake” / code 1008 mean?](#what-does-invalid-handshake--code-1008-mean) - [Logging and debugging](#logging-and-debugging) - [Where are logs?](#where-are-logs) - - [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon) - - [ELI5: `clawdbot daemon restart` vs `clawdbot gateway`](#eli5-clawdbot-daemon-restart-vs-clawdbot-gateway) + - [How do I start/stop/restart the Gateway service?](#how-do-i-startstoprestart-the-gateway-service) + - [ELI5: `clawdbot gateway restart` vs `clawdbot gateway`](#eli5-clawdbot-gateway-restart-vs-clawdbot-gateway) - [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails) - [Media & attachments](#media-attachments) - [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent) - [Security and access control](#security-and-access-control) - [Is it safe to expose Clawdbot to inbound DMs?](#is-it-safe-to-expose-clawdbot-to-inbound-dms) + - [Is prompt injection only a concern for public bots?](#is-prompt-injection-only-a-concern-for-public-bots) + - [Can I use cheaper models for personal assistant tasks?](#can-i-use-cheaper-models-for-personal-assistant-tasks) - [I ran `/start` in Telegram but didn’t get a pairing code](#i-ran-start-in-telegram-but-didnt-get-a-pairing-code) - [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work) - [Chat commands, aborting tasks, and “it won’t stop”](#chat-commands-aborting-tasks-and-it-wont-stop) @@ -128,7 +131,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, ```bash clawdbot status ``` - Fast local summary: OS + update, gateway/daemon reachability, agents/sessions, provider config + runtime issues (when gateway is reachable). + Fast local summary: OS + update, gateway/service reachability, agents/sessions, provider config + runtime issues (when gateway is reachable). 2) **Pasteable report (safe to share)** ```bash @@ -138,9 +141,9 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, 3) **Daemon + port state** ```bash - clawdbot daemon status + clawdbot gateway status ``` - Shows supervisor runtime vs RPC reachability, the probe target URL, and which config the daemon likely used. + Shows supervisor runtime vs RPC reachability, the probe target URL, and which config the service likely used. 4) **Deep probes** ```bash @@ -240,7 +243,7 @@ It also warns if your configured model is unknown or missing auth. ### How does Anthropic "setup-token" auth work? -`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. If you run it on the gateway host, the wizard can auto-detect the CLI credentials. If you run it elsewhere, choose **Anthropic token (paste setup-token)** and paste the string. The token is stored as an auth profile for the **anthropic** provider and used like an API key or OAuth profile. More detail: [OAuth](/concepts/oauth). +`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. If Claude Code CLI credentials are present on the gateway host, Clawdbot can reuse them; otherwise choose **Anthropic token (paste setup-token)** and paste the string. The token is stored as an auth profile for the **anthropic** provider and used like an API key or OAuth profile. More detail: [OAuth](/concepts/oauth). Clawdbot keeps `auth.profiles["anthropic:claude-cli"].mode` set to `"oauth"` so the profile accepts both OAuth and setup-token credentials; older `"token"` mode @@ -254,11 +257,11 @@ It is **not** in the Anthropic Console. The setup-token is generated by the **Cl claude setup-token ``` -Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want Clawdbot to run the command for you, use `clawdbot onboard --auth-choice setup-token` or `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic). +Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want to run it on the gateway host, use `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic). ### Do you support Claude subscription auth (Claude Code OAuth)? -Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** for long‑running setups (requires Claude Pro/Max + the `claude` CLI). You can generate it anywhere and paste it on the gateway host, or run it locally on the gateway so it auto-syncs. OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth). +Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** for long‑running setups (requires Claude Pro/Max + the `claude` CLI). You can generate it anywhere and paste it on the gateway host. OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth). Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice. @@ -334,7 +337,7 @@ cd clawdbot pnpm install pnpm build clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart ``` From git → npm: @@ -342,7 +345,7 @@ From git → npm: ```bash npm install -g clawdbot@latest clawdbot doctor -clawdbot daemon restart +clawdbot gateway restart ``` Doctor detects a gateway service entrypoint mismatch and offers to rewrite the service config to match the current install (use `--repair` in automation). @@ -747,7 +750,7 @@ 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). -### Do nodes run a gateway daemon? +### 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 to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app). @@ -839,11 +842,11 @@ You can also define inline env vars in config (applied only if missing from the See [/environment](/environment) for full precedence and sources. -### “I started the Gateway via a daemon and my env vars disappeared.” What now? +### “I started the Gateway via a service and my env vars disappeared.” What now? Two common fixes: -1) Put the missing keys in `~/.clawdbot/.env` so they’re picked up even when the daemon doesn’t inherit your shell env. +1) Put the missing keys in `~/.clawdbot/.env` so they’re picked up even when the service doesn’t inherit your shell env. 2) Enable shell import (opt‑in convenience): ```json5 @@ -866,7 +869,7 @@ This runs your login shell and imports only missing expected keys (never overrid does **not** mean your env vars are missing — it just means Clawdbot won’t load your login shell automatically. -If the Gateway runs as a daemon (launchd/systemd), it won’t inherit your shell +If the Gateway runs as a service (launchd/systemd), it won’t inherit your shell environment. Fix by doing one of these: 1) Put the token in `~/.clawdbot/.env`: @@ -951,6 +954,14 @@ If it keeps happening: Docs: [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning), [Session management](/concepts/session). +### Why am I seeing “LLM request rejected: messages.N.content.X.tool_use.input: Field required”? + +This is a provider validation error: the model emitted a `tool_use` block without the required +`input`. It usually means the session history is stale or corrupted (often after long threads +or a tool/schema change). + +Fix: start a fresh session with `/new` (standalone message). + ### Why am I getting heartbeat messages every 30 minutes? Heartbeats run every **30m** by default. Tune or disable them: @@ -1336,24 +1347,24 @@ Precedence: --port > CLAWDBOT_GATEWAY_PORT > gateway.port > default 18789 ``` -### Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`? +### Why does `clawdbot gateway status` say `Runtime: running` but `RPC probe: failed`? Because “running” is the **supervisor’s** view (launchd/systemd/schtasks). The RPC probe is the CLI actually connecting to the gateway WebSocket and calling `status`. -Use `clawdbot daemon status` and trust these lines: +Use `clawdbot gateway status` and trust these lines: - `Probe target:` (the URL the probe actually used) - `Listening:` (what’s actually bound on the port) - `Last gateway error:` (common root cause when the process is alive but the port isn’t listening) -### Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different? +### Why does `clawdbot gateway status` show `Config (cli)` and `Config (service)` different? -You’re editing one config file while the daemon is running another (often a `--profile` / `CLAWDBOT_STATE_DIR` mismatch). +You’re editing one config file while the service is running another (often a `--profile` / `CLAWDBOT_STATE_DIR` mismatch). Fix: ```bash -clawdbot daemon install --force +clawdbot gateway install --force ``` -Run that from the same `--profile` / environment you want the daemon to use. +Run that from the same `--profile` / environment you want the service to use. ### What does “another gateway instance is already listening” mean? @@ -1406,7 +1417,7 @@ Fix: - Start Tailscale on that host (so it has a 100.x address), or - Switch to `gateway.bind: "loopback"` / `"lan"`. -Note: `tailnet` is legacy and is migrated to `auto` by Doctor. Prefer `gateway.bind: "auto"` when using Tailscale. +Note: `tailnet` is explicit. `auto` prefers loopback; use `gateway.bind: "tailnet"` when you want a tailnet-only bind. ### Can I run multiple Gateways on the same host? @@ -1422,7 +1433,7 @@ Yes, but you must isolate: Quick setup (recommended): - Use `clawdbot --profile …` per instance (auto-creates `~/.clawdbot-`). - Set a unique `gateway.port` in each profile config (or pass `--port` for manual runs). -- Install a per-profile daemon: `clawdbot --profile daemon install`. +- Install a per-profile service: `clawdbot --profile gateway install`. Profiles also suffix service names (`com.clawdbot.`, `clawdbot-gateway-.service`, `Clawdbot Gateway ()`). Full guide: [Multiple gateways](/gateway/multiple-gateways). @@ -1475,23 +1486,23 @@ Service/supervisor logs (when the gateway runs via launchd/systemd): See [Troubleshooting](/gateway/troubleshooting#log-locations) for more. -### How do I start/stop/restart the Gateway daemon? +### How do I start/stop/restart the Gateway service? -Use the daemon helpers: +Use the gateway helpers: ```bash -clawdbot daemon status -clawdbot daemon restart +clawdbot gateway status +clawdbot gateway restart ``` If you run the gateway manually, `clawdbot gateway --force` can reclaim the port. See [Gateway](/gateway). -### ELI5: `clawdbot daemon restart` vs `clawdbot gateway` +### ELI5: `clawdbot gateway restart` vs `clawdbot gateway` -- `clawdbot daemon restart`: restarts the **background service** (launchd/systemd). +- `clawdbot gateway restart`: restarts the **background service** (launchd/systemd). - `clawdbot gateway`: runs the gateway **in the foreground** for this terminal session. -If you installed the daemon, use the daemon commands. Use `clawdbot gateway` when +If you installed the service, use the gateway commands. Use `clawdbot gateway` when you want a one-off, foreground run. ### What’s the fastest way to get more details when something fails? @@ -1530,6 +1541,28 @@ Treat inbound DMs as untrusted input. Defaults are designed to reduce risk: Run `clawdbot doctor` to surface risky DM policies. +### Is prompt injection only a concern for public bots? + +No. Prompt injection is about **untrusted content**, not just who can DM the bot. +If your assistant reads external content (web search/fetch, browser pages, emails, +docs, attachments, pasted logs), that content can include instructions that try +to hijack the model. This can happen even if **you are the only sender**. + +The biggest risk is when tools are enabled: the model can be tricked into +exfiltrating context or calling tools on your behalf. Reduce the blast radius by: +- using a read-only or tool-disabled "reader" agent to summarize untrusted content +- keeping `web_search` / `web_fetch` / `browser` off for tool-enabled agents +- sandboxing and strict tool allowlists + +Details: [Security](/gateway/security). + +### Can I use cheaper models for personal assistant tasks? + +Yes, **if** the agent is chat-only and the input is trusted. Smaller tiers are +more susceptible to instruction hijacking, so avoid them for tool-enabled agents +or when reading untrusted content. If you must use a smaller model, lock down +tools and run inside a sandbox. See [Security](/gateway/security). + ### I ran `/start` in Telegram but didn’t get a pairing code Pairing codes are sent **only** when an unknown sender messages the bot and diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index 5af3b7a8e..861e7ad12 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -15,7 +15,7 @@ Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It set - channels (WhatsApp/Telegram/Discord/Mattermost/...) - pairing defaults (secure DMs) - workspace bootstrap + skills -- optional background daemon +- optional background service If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security). @@ -71,7 +71,7 @@ npm install -g clawdbot@latest pnpm add -g clawdbot@latest ``` -## 2) Run the onboarding wizard (and install the daemon) +## 2) Run the onboarding wizard (and install the service) ```bash clawdbot onboard --install-daemon @@ -89,7 +89,7 @@ Wizard doc: [Wizard](/start/wizard) ### Auth: where it lives (important) -- **Recommended Anthropic path:** set an API key (wizard can store it for daemon use). `claude setup-token` is also supported if you want to reuse Claude Code credentials. +- **Recommended Anthropic path:** set an API key (wizard can store it for service use). `claude setup-token` is also supported if you want to reuse Claude Code credentials. - OAuth credentials (legacy import): `~/.clawdbot/credentials/oauth.json` - Auth profiles (OAuth + API keys): `~/.clawdbot/agents//agent/auth-profiles.json` @@ -98,10 +98,10 @@ Headless/server tip: do OAuth on a normal machine first, then copy `oauth.json` ## 3) Start the Gateway -If you installed the daemon during onboarding, the Gateway should already be running: +If you installed the service during onboarding, the Gateway should already be running: ```bash -clawdbot daemon status +clawdbot gateway status ``` Manual run (foreground): diff --git a/docs/start/showcase.md b/docs/start/showcase.md index b4c920d3c..c57790de7 100644 --- a/docs/start/showcase.md +++ b/docs/start/showcase.md @@ -60,6 +60,28 @@ Full setup walkthrough (28m) by VelvetShark. [Watch on YouTube](https://www.youtube.com/watch?v=mMSKQvlmFuQ) +
    +