Merge branch 'main' into feat/mattermost-channel
This commit is contained in:
commit
fe77d3eb56
@ -29,6 +29,7 @@
|
|||||||
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
|
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
|
||||||
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
||||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
- 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)
|
- Type-check/build: `pnpm build` (tsc)
|
||||||
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
|
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
|
||||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||||
|
|||||||
57
CHANGELOG.md
57
CHANGELOG.md
@ -2,21 +2,76 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
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
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### 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: 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.
|
- CLI: exec approvals mutations render tables instead of raw JSON.
|
||||||
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
|
- 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.
|
- 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
|
### 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.
|
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
|
||||||
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
|
- 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.
|
- 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: 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.
|
- 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.
|
- 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
|
## 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: 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: 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
|
- 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: 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: 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.
|
- 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: 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: 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`.
|
- 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: 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)
|
- 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
|
- Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.clawd.bot/reference/api-usage-costs
|
||||||
|
|||||||
48
README.md
48
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.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
|
||||||
AI/vibe-coded PRs welcome! 🤖
|
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:
|
Thanks to all clawtributors:
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a>
|
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a>
|
||||||
<a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a>
|
<a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a>
|
||||||
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a>
|
<a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a>
|
||||||
<a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a>
|
<a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
|
||||||
<a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
|
<a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a>
|
||||||
<a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a>
|
<a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a>
|
||||||
<a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a>
|
<a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
|
||||||
<a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a>
|
<a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
|
||||||
<a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a>
|
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a>
|
||||||
<a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a>
|
<a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a>
|
||||||
<a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
<a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a>
|
||||||
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
<a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a>
|
||||||
<a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a>
|
<a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a>
|
||||||
<a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a>
|
<a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a>
|
||||||
<a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a>
|
<a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a>
|
||||||
<a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a>
|
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
|
||||||
<a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a>
|
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a>
|
||||||
<a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a>
|
<a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
|
||||||
<a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a>
|
||||||
<a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a>
|
<a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
|
||||||
<a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a>
|
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a>
|
||||||
<a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a>
|
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a>
|
||||||
<a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
<a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a>
|
||||||
|
<a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
<channel>
|
<channel>
|
||||||
<title>Clawdbot</title>
|
<title>Clawdbot</title>
|
||||||
<item>
|
<item>
|
||||||
<title>2026.1.20</title>
|
<title>2026.1.21</title>
|
||||||
<pubDate>Wed, 21 Jan 2026 08:18:22 +0000</pubDate>
|
<pubDate>Wed, 21 Jan 2026 08:18:22 +0000</pubDate>
|
||||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||||
<sparkle:version>7116</sparkle:version>
|
<sparkle:version>7116</sparkle:version>
|
||||||
<sparkle:shortVersionString>2026.1.20</sparkle:shortVersionString>
|
<sparkle:shortVersionString>2026.1.21</sparkle:shortVersionString>
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||||
<description><![CDATA[<h2>Clawdbot 2026.1.20</h2>
|
<description><![CDATA[<h2>Clawdbot 2026.1.21</h2>
|
||||||
<h3>Changes</h3>
|
<h3>Changes</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui</li>
|
<li>Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui</li>
|
||||||
@ -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.
|
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.
|
||||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||||
]]></description>
|
]]></description>
|
||||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.20/Clawdbot-2026.1.20.zip" length="12208102" type="application/octet-stream" sparkle:edSignature="hU495Eii8O3qmmUnxYFhXyEGv+qan6KL+GpeuBhPIXf+7B5F/gBh5Oz9cHaqaAPoZ4/3Bo6xgvic0HTkbz6gDw=="/>
|
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="12208102" type="application/octet-stream" sparkle:edSignature="hU495Eii8O3qmmUnxYFhXyEGv+qan6KL+GpeuBhPIXf+7B5F/gBh5Oz9cHaqaAPoZ4/3Bo6xgvic0HTkbz6gDw=="/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>2026.1.16-2</title>
|
<title>2026.1.16-2</title>
|
||||||
|
|||||||
@ -21,8 +21,8 @@ android {
|
|||||||
applicationId = "com.clawdbot.android"
|
applicationId = "com.clawdbot.android"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 202601200
|
versionCode = 202601210
|
||||||
versionName = "2026.1.20"
|
versionName = "2026.1.21"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@ -19,9 +19,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.1.20</string>
|
<string>2026.1.21</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260120</string>
|
<string>20260121</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
|||||||
@ -17,8 +17,8 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.1.20</string>
|
<string>2026.1.21</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260120</string>
|
<string>20260121</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -81,8 +81,8 @@ targets:
|
|||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Clawdbot
|
CFBundleDisplayName: Clawdbot
|
||||||
CFBundleIconName: AppIcon
|
CFBundleIconName: AppIcon
|
||||||
CFBundleShortVersionString: "2026.1.20"
|
CFBundleShortVersionString: "2026.1.21"
|
||||||
CFBundleVersion: "20260120"
|
CFBundleVersion: "20260121"
|
||||||
UILaunchScreen: {}
|
UILaunchScreen: {}
|
||||||
UIApplicationSceneManifest:
|
UIApplicationSceneManifest:
|
||||||
UIApplicationSupportsMultipleScenes: false
|
UIApplicationSupportsMultipleScenes: false
|
||||||
@ -130,5 +130,5 @@ targets:
|
|||||||
path: Tests/Info.plist
|
path: Tests/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: ClawdbotTests
|
CFBundleDisplayName: ClawdbotTests
|
||||||
CFBundleShortVersionString: "2026.1.20"
|
CFBundleShortVersionString: "2026.1.21"
|
||||||
CFBundleVersion: "20260120"
|
CFBundleVersion: "20260121"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "550d4ea41d4bb2546b99a7bfa1c5cba7e28a13862bc226727ea7426c61555a33",
|
"originHash" : "f847d54db16b371dbb1a79271d50436cdec572179b0f0cf14cfe1b75df8dfbc2",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "axorcist",
|
"identity" : "axorcist",
|
||||||
@ -24,7 +24,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/steipete/ElevenLabsKit",
|
"location" : "https://github.com/steipete/ElevenLabsKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
|
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
|
||||||
"version" : "0.1.0"
|
"version" : "0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -284,13 +284,16 @@ enum CommandResolver {
|
|||||||
|
|
||||||
var args: [String] = [
|
var args: [String] = [
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
"-o", "IdentitiesOnly=yes",
|
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
"-o", "UpdateHostKeys=yes",
|
"-o", "UpdateHostKeys=yes",
|
||||||
]
|
]
|
||||||
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
||||||
if !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
args.append(contentsOf: ["-i", settings.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
|
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
||||||
args.append(userHost)
|
args.append(userHost)
|
||||||
|
|||||||
@ -6,15 +6,20 @@ final class ConnectionModeCoordinator {
|
|||||||
static let shared = ConnectionModeCoordinator()
|
static let shared = ConnectionModeCoordinator()
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "connection")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "connection")
|
||||||
|
private var lastMode: AppState.ConnectionMode?
|
||||||
|
|
||||||
/// Apply the requested connection mode by starting/stopping local gateway,
|
/// Apply the requested connection mode by starting/stopping local gateway,
|
||||||
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
|
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
|
||||||
func apply(mode: AppState.ConnectionMode, paused: Bool) async {
|
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 {
|
switch mode {
|
||||||
case .unconfigured:
|
case .unconfigured:
|
||||||
if let error = await NodeServiceManager.stop() {
|
_ = await NodeServiceManager.stop()
|
||||||
NodesStore.shared.lastError = "Node service stop failed: \(error)"
|
NodesStore.shared.lastError = nil
|
||||||
}
|
|
||||||
await RemoteTunnelManager.shared.stopAll()
|
await RemoteTunnelManager.shared.stopAll()
|
||||||
WebChatManager.shared.resetTunnels()
|
WebChatManager.shared.resetTunnels()
|
||||||
GatewayProcessManager.shared.stop()
|
GatewayProcessManager.shared.stop()
|
||||||
@ -23,9 +28,8 @@ final class ConnectionModeCoordinator {
|
|||||||
Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) }
|
Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) }
|
||||||
|
|
||||||
case .local:
|
case .local:
|
||||||
if let error = await NodeServiceManager.stop() {
|
_ = await NodeServiceManager.stop()
|
||||||
NodesStore.shared.lastError = "Node service stop failed: \(error)"
|
NodesStore.shared.lastError = nil
|
||||||
}
|
|
||||||
await RemoteTunnelManager.shared.stopAll()
|
await RemoteTunnelManager.shared.stopAll()
|
||||||
WebChatManager.shared.resetTunnels()
|
WebChatManager.shared.resetTunnels()
|
||||||
let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused)
|
let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused)
|
||||||
@ -56,6 +60,7 @@ final class ConnectionModeCoordinator {
|
|||||||
WebChatManager.shared.resetTunnels()
|
WebChatManager.shared.resetTunnels()
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
NodesStore.shared.lastError = nil
|
||||||
if let error = await NodeServiceManager.start() {
|
if let error = await NodeServiceManager.start() {
|
||||||
NodesStore.shared.lastError = "Node service start failed: \(error)"
|
NodesStore.shared.lastError = "Node service start failed: \(error)"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ final class ControlChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private(set) var lastPingMs: Double?
|
private(set) var lastPingMs: Double?
|
||||||
|
private(set) var authSourceLabel: String?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "control")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "control")
|
||||||
|
|
||||||
@ -128,6 +129,7 @@ final class ControlChannel {
|
|||||||
await GatewayConnection.shared.shutdown()
|
await GatewayConnection.shared.shutdown()
|
||||||
self.state = .disconnected
|
self.state = .disconnected
|
||||||
self.lastPingMs = nil
|
self.lastPingMs = nil
|
||||||
|
self.authSourceLabel = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func health(timeout: TimeInterval? = nil) async throws -> Data {
|
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
|
urlErr.code == .dataNotAllowed // used for WS close 1008 auth failures
|
||||||
{
|
{
|
||||||
let reason = urlErr.failureURLString ?? urlErr.localizedDescription
|
let reason = urlErr.failureURLString ?? urlErr.localizedDescription
|
||||||
|
let tokenKey = CommandResolver.connectionModeIsRemote()
|
||||||
|
? "gateway.remote.token"
|
||||||
|
: "gateway.auth.token"
|
||||||
return
|
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. " +
|
"or clear it on the gateway. " +
|
||||||
"Reason: \(reason)"
|
"Reason: \(reason)"
|
||||||
}
|
}
|
||||||
@ -300,6 +305,27 @@ final class ControlChannel {
|
|||||||
code: 0,
|
code: 0,
|
||||||
userInfo: [NSLocalizedDescriptionKey: "gateway health not ok"])
|
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 {
|
func sendSystemEvent(_ text: String, params: [String: AnyHashable] = [:]) async throws {
|
||||||
|
|||||||
@ -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) {
|
HStack(spacing: 8) {
|
||||||
Button("Restart app") { DebugActions.restartApp() }
|
Button("Restart app") { DebugActions.restartApp() }
|
||||||
Button("Restart onboarding") { DebugActions.restartOnboarding() }
|
Button("Restart onboarding") { DebugActions.restartOnboarding() }
|
||||||
|
|||||||
@ -149,6 +149,7 @@ struct ExecApprovalsResolvedDefaults {
|
|||||||
|
|
||||||
enum ExecApprovalsStore {
|
enum ExecApprovalsStore {
|
||||||
private static let logger = Logger(subsystem: "com.clawdbot", category: "exec-approvals")
|
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 defaultSecurity: ExecSecurity = .deny
|
||||||
private static let defaultAsk: ExecAsk = .onMiss
|
private static let defaultAsk: ExecAsk = .onMiss
|
||||||
private static let defaultAskFallback: ExecSecurity = .deny
|
private static let defaultAskFallback: ExecSecurity = .deny
|
||||||
@ -165,13 +166,22 @@ enum ExecApprovalsStore {
|
|||||||
static func normalizeIncoming(_ file: ExecApprovalsFile) -> ExecApprovalsFile {
|
static func normalizeIncoming(_ file: ExecApprovalsFile) -> ExecApprovalsFile {
|
||||||
let socketPath = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let socketPath = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
let token = file.socket?.token?.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(
|
return ExecApprovalsFile(
|
||||||
version: 1,
|
version: 1,
|
||||||
socket: ExecApprovalsSocketConfig(
|
socket: ExecApprovalsSocketConfig(
|
||||||
path: socketPath.isEmpty ? nil : socketPath,
|
path: socketPath.isEmpty ? nil : socketPath,
|
||||||
token: token.isEmpty ? nil : token),
|
token: token.isEmpty ? nil : token),
|
||||||
defaults: file.defaults,
|
defaults: file.defaults,
|
||||||
agents: file.agents)
|
agents: agents)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func readSnapshot() -> ExecApprovalsSnapshot {
|
static func readSnapshot() -> ExecApprovalsSnapshot {
|
||||||
@ -272,16 +282,16 @@ enum ExecApprovalsStore {
|
|||||||
ask: defaults.ask ?? self.defaultAsk,
|
ask: defaults.ask ?? self.defaultAsk,
|
||||||
askFallback: defaults.askFallback ?? self.defaultAskFallback,
|
askFallback: defaults.askFallback ?? self.defaultAskFallback,
|
||||||
autoAllowSkills: defaults.autoAllowSkills ?? self.defaultAutoAllowSkills)
|
autoAllowSkills: defaults.autoAllowSkills ?? self.defaultAutoAllowSkills)
|
||||||
let key = (agentId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
let key = self.agentKey(agentId)
|
||||||
? agentId!.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
: "default"
|
|
||||||
let agentEntry = file.agents?[key] ?? ExecApprovalsAgent()
|
let agentEntry = file.agents?[key] ?? ExecApprovalsAgent()
|
||||||
let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent()
|
let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent()
|
||||||
let resolvedAgent = ExecApprovalsResolvedDefaults(
|
let resolvedAgent = ExecApprovalsResolvedDefaults(
|
||||||
security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security,
|
security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security,
|
||||||
ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask,
|
ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask,
|
||||||
askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback ?? resolvedDefaults.askFallback,
|
askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback
|
||||||
autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills ?? resolvedDefaults.autoAllowSkills)
|
?? resolvedDefaults.askFallback,
|
||||||
|
autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills
|
||||||
|
?? resolvedDefaults.autoAllowSkills)
|
||||||
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
|
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
|
||||||
.map { entry in
|
.map { entry in
|
||||||
ExecAllowlistEntry(
|
ExecAllowlistEntry(
|
||||||
@ -455,7 +465,36 @@ enum ExecApprovalsStore {
|
|||||||
|
|
||||||
private static func agentKey(_ agentId: String?) -> String {
|
private static func agentKey(_ agentId: String?) -> String {
|
||||||
let trimmed = agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
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<String>()
|
||||||
|
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 {
|
enum ExecAllowlistMatcher {
|
||||||
static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? {
|
static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? {
|
||||||
guard let resolution, !entries.isEmpty else { return nil }
|
guard let resolution, !entries.isEmpty else { return nil }
|
||||||
|
|||||||
@ -314,7 +314,7 @@ private enum ExecHostExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var approvedByAsk = approvalDecision != nil
|
var approvedByAsk = approvalDecision != nil
|
||||||
if self.requiresAsk(
|
if ExecApprovalHelpers.requiresAsk(
|
||||||
ask: context.ask,
|
ask: context.ask,
|
||||||
security: context.security,
|
security: context.security,
|
||||||
allowlistMatch: context.allowlistMatch,
|
allowlistMatch: context.allowlistMatch,
|
||||||
@ -417,36 +417,20 @@ private enum ExecHostExecutor {
|
|||||||
skillAllow: skillAllow)
|
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(
|
private static func persistAllowlistEntry(
|
||||||
decision: ExecApprovalDecision?,
|
decision: ExecApprovalDecision?,
|
||||||
context: ExecApprovalContext)
|
context: ExecApprovalContext)
|
||||||
{
|
{
|
||||||
guard decision == .allowAlways, context.security == .allowlist else { return }
|
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
|
return
|
||||||
}
|
}
|
||||||
ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern)
|
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? {
|
private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? {
|
||||||
guard needsScreenRecording == true else { return nil }
|
guard needsScreenRecording == true else { return nil }
|
||||||
let authorized = await PermissionManager
|
let authorized = await PermissionManager
|
||||||
|
|||||||
@ -250,6 +250,11 @@ actor GatewayConnection {
|
|||||||
await self.configure(url: cfg.url, token: cfg.token, password: cfg.password)
|
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 {
|
func shutdown() async {
|
||||||
if let client {
|
if let client {
|
||||||
await client.shutdown()
|
await client.shutdown()
|
||||||
|
|||||||
@ -482,7 +482,7 @@ actor GatewayEndpointStore {
|
|||||||
let bind = GatewayEndpointStore.resolveGatewayBindMode(
|
let bind = GatewayEndpointStore.resolveGatewayBindMode(
|
||||||
root: root,
|
root: root,
|
||||||
env: ProcessInfo.processInfo.environment)
|
env: ProcessInfo.processInfo.environment)
|
||||||
guard bind == "auto" else { return nil }
|
guard bind == "tailnet" else { return nil }
|
||||||
|
|
||||||
let currentHost = currentURL.host?.lowercased() ?? ""
|
let currentHost = currentURL.host?.lowercased() ?? ""
|
||||||
guard currentHost == "127.0.0.1" || currentHost == "localhost" else { return nil }
|
guard currentHost == "127.0.0.1" || currentHost == "localhost" else { return nil }
|
||||||
@ -562,9 +562,6 @@ actor GatewayEndpointStore {
|
|||||||
case "tailnet":
|
case "tailnet":
|
||||||
return tailscaleIP ?? "127.0.0.1"
|
return tailscaleIP ?? "127.0.0.1"
|
||||||
case "auto":
|
case "auto":
|
||||||
if let tailscaleIP, !tailscaleIP.isEmpty {
|
|
||||||
return tailscaleIP
|
|
||||||
}
|
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
case "custom":
|
case "custom":
|
||||||
return customBindHost ?? "127.0.0.1"
|
return customBindHost ?? "127.0.0.1"
|
||||||
|
|||||||
@ -115,7 +115,7 @@ extension GatewayLaunchAgentManager {
|
|||||||
quiet: Bool) async -> CommandResult
|
quiet: Bool) async -> CommandResult
|
||||||
{
|
{
|
||||||
let command = CommandResolver.clawdbotCommand(
|
let command = CommandResolver.clawdbotCommand(
|
||||||
subcommand: "daemon",
|
subcommand: "gateway",
|
||||||
extraArgs: self.withJsonFlag(args),
|
extraArgs: self.withJsonFlag(args),
|
||||||
// Launchd management must always run locally, even if remote mode is configured.
|
// Launchd management must always run locally, even if remote mode is configured.
|
||||||
configRoot: ["gateway": ["mode": "local"]])
|
configRoot: ["gateway": ["mode": "local"]])
|
||||||
|
|||||||
@ -42,10 +42,20 @@ final class GatewayProcessManager {
|
|||||||
private var environmentRefreshTask: Task<Void, Never>?
|
private var environmentRefreshTask: Task<Void, Never>?
|
||||||
private var lastEnvironmentRefresh: Date?
|
private var lastEnvironmentRefresh: Date?
|
||||||
private var logRefreshTask: Task<Void, Never>?
|
private var logRefreshTask: Task<Void, Never>?
|
||||||
|
#if DEBUG
|
||||||
|
private var testingConnection: GatewayConnection?
|
||||||
|
#endif
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway.process")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway.process")
|
||||||
|
|
||||||
private let logLimit = 20000 // characters to keep in-memory
|
private let logLimit = 20000 // characters to keep in-memory
|
||||||
private let environmentRefreshMinInterval: TimeInterval = 30
|
private let environmentRefreshMinInterval: TimeInterval = 30
|
||||||
|
private var connection: GatewayConnection {
|
||||||
|
#if DEBUG
|
||||||
|
return self.testingConnection ?? .shared
|
||||||
|
#else
|
||||||
|
return .shared
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
func setActive(_ active: Bool) {
|
func setActive(_ active: Bool) {
|
||||||
// Remote mode should never spawn a local gateway; treat as stopped.
|
// 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) {
|
func refreshEnvironmentStatus(force: Bool = false) {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if !force {
|
if !force {
|
||||||
@ -178,7 +192,7 @@ final class GatewayProcessManager {
|
|||||||
let hasListener = instance != nil
|
let hasListener = instance != nil
|
||||||
|
|
||||||
let attemptAttach = {
|
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) {
|
for attempt in 0..<(hasListener ? 3 : 1) {
|
||||||
@ -187,6 +201,7 @@ final class GatewayProcessManager {
|
|||||||
let snap = decodeHealthSnapshot(from: data)
|
let snap = decodeHealthSnapshot(from: data)
|
||||||
let details = self.describe(details: instanceText, port: port, snap: snap)
|
let details = self.describe(details: instanceText, port: port, snap: snap)
|
||||||
self.existingGatewayDetails = details
|
self.existingGatewayDetails = details
|
||||||
|
self.clearLastFailure()
|
||||||
self.status = .attachedExisting(details: details)
|
self.status = .attachedExisting(details: details)
|
||||||
self.appendLog("[gateway] using existing instance: \(details)\n")
|
self.appendLog("[gateway] using existing instance: \(details)\n")
|
||||||
self.logger.info("gateway using existing instance details=\(details)")
|
self.logger.info("gateway using existing instance details=\(details)")
|
||||||
@ -310,9 +325,10 @@ final class GatewayProcessManager {
|
|||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
if !self.desiredActive { return }
|
if !self.desiredActive { return }
|
||||||
do {
|
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 instance = await PortGuardian.shared.describe(port: port)
|
||||||
let details = instance.map { "pid \($0.pid)" }
|
let details = instance.map { "pid \($0.pid)" }
|
||||||
|
self.clearLastFailure()
|
||||||
self.status = .running(details: details)
|
self.status = .running(details: details)
|
||||||
self.logger.info("gateway started details=\(details ?? "ok")")
|
self.logger.info("gateway started details=\(details ?? "ok")")
|
||||||
self.refreshControlChannelIfNeeded(reason: "gateway started")
|
self.refreshControlChannelIfNeeded(reason: "gateway started")
|
||||||
@ -352,7 +368,8 @@ final class GatewayProcessManager {
|
|||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
if !self.desiredActive { return false }
|
if !self.desiredActive { return false }
|
||||||
do {
|
do {
|
||||||
_ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500)
|
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
||||||
|
self.clearLastFailure()
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||||
@ -385,3 +402,19 @@ final class GatewayProcessManager {
|
|||||||
return String(text.suffix(limit))
|
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
|
||||||
|
|||||||
@ -2,52 +2,25 @@ import AppKit
|
|||||||
import ClawdbotDiscovery
|
import ClawdbotDiscovery
|
||||||
import ClawdbotIPC
|
import ClawdbotIPC
|
||||||
import ClawdbotKit
|
import ClawdbotKit
|
||||||
import CoreLocation
|
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct GeneralSettings: View {
|
struct GeneralSettings: View {
|
||||||
@Bindable var state: AppState
|
@Bindable var state: AppState
|
||||||
@AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false
|
@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 healthStore = HealthStore.shared
|
||||||
private let gatewayManager = GatewayProcessManager.shared
|
private let gatewayManager = GatewayProcessManager.shared
|
||||||
@State private var gatewayDiscovery = GatewayDiscoveryModel(
|
@State private var gatewayDiscovery = GatewayDiscoveryModel(
|
||||||
localDisplayName: InstanceIdentity.displayName)
|
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 gatewayStatus: GatewayEnvironmentStatus = .checking
|
||||||
@State private var remoteStatus: RemoteStatus = .idle
|
@State private var remoteStatus: RemoteStatus = .idle
|
||||||
@State private var showRemoteAdvanced = false
|
@State private var showRemoteAdvanced = false
|
||||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||||
private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode }
|
private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode }
|
||||||
@State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
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) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
SettingsToggleRow(
|
SettingsToggleRow(
|
||||||
title: "Clawdbot active",
|
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.",
|
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
|
||||||
binding: self.$cameraEnabled)
|
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(
|
SettingsToggleRow(
|
||||||
title: "Enable Peekaboo Bridge",
|
title: "Enable Peekaboo Bridge",
|
||||||
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
|
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
|
||||||
@ -130,29 +80,13 @@ struct GeneralSettings: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard !self.isPreview else { return }
|
guard !self.isPreview else { return }
|
||||||
self.refreshCLIStatus()
|
|
||||||
self.refreshGatewayStatus()
|
self.refreshGatewayStatus()
|
||||||
self.lastLocationModeRaw = self.locationModeRaw
|
|
||||||
}
|
}
|
||||||
.onChange(of: self.state.canvasEnabled) { _, enabled in
|
.onChange(of: self.state.canvasEnabled) { _, enabled in
|
||||||
if !enabled {
|
if !enabled {
|
||||||
CanvasManager.shared.hideAll()
|
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<Bool> {
|
private var activeBinding: Binding<Bool> {
|
||||||
@ -161,39 +95,20 @@ struct GeneralSettings: View {
|
|||||||
set: { self.state.isPaused = !$0 })
|
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 {
|
private var connectionSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Clawdbot runs")
|
Text("Clawdbot runs")
|
||||||
.font(.title3.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Picker("", selection: self.$state.connectionMode) {
|
Picker("Mode", selection: self.$state.connectionMode) {
|
||||||
Text("Not configured").tag(AppState.ConnectionMode.unconfigured)
|
Text("Not configured").tag(AppState.ConnectionMode.unconfigured)
|
||||||
Text("Local (this Mac)").tag(AppState.ConnectionMode.local)
|
Text("Local (this Mac)").tag(AppState.ConnectionMode.local)
|
||||||
Text("Remote over SSH").tag(AppState.ConnectionMode.remote)
|
Text("Remote over SSH").tag(AppState.ConnectionMode.remote)
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.menu)
|
||||||
.frame(width: 380, alignment: .leading)
|
.labelsHidden()
|
||||||
|
.frame(width: 260, alignment: .leading)
|
||||||
|
|
||||||
if self.state.connectionMode == .unconfigured {
|
if self.state.connectionMode == .unconfigured {
|
||||||
Text("Pick Local or Remote to start the Gateway.")
|
Text("Pick Local or Remote to start the Gateway.")
|
||||||
@ -216,8 +131,6 @@ struct GeneralSettings: View {
|
|||||||
if self.state.connectionMode == .remote {
|
if self.state.connectionMode == .remote {
|
||||||
self.remoteCard
|
self.remoteCard
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cliInstaller
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +212,11 @@ struct GeneralSettings: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
if let authLabel = ControlChannel.shared.authSourceLabel {
|
||||||
|
Text(authLabel)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Tip: enable Tailscale for stable remote access.")
|
Text("Tip: enable Tailscale for stable remote access.")
|
||||||
@ -346,59 +264,6 @@ struct GeneralSettings: View {
|
|||||||
return message == self.controlStatusLine
|
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 {
|
private var gatewayInstallerCard: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
@ -454,22 +319,6 @@ struct GeneralSettings: View {
|
|||||||
.cornerRadius(10)
|
.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() {
|
private func refreshGatewayStatus() {
|
||||||
Task {
|
Task {
|
||||||
let status = await Task.detached(priority: .utility) {
|
let status = await Task.detached(priority: .utility) {
|
||||||
@ -763,9 +612,6 @@ extension GeneralSettings {
|
|||||||
message: "Gateway ready")
|
message: "Gateway ready")
|
||||||
view.remoteStatus = .failed("SSH failed")
|
view.remoteStatus = .failed("SSH failed")
|
||||||
view.showRemoteAdvanced = true
|
view.showRemoteAdvanced = true
|
||||||
view.cliInstalled = true
|
|
||||||
view.cliInstallLocation = "/usr/local/bin/clawdbot"
|
|
||||||
view.cliStatus = "Installed"
|
|
||||||
_ = view.body
|
_ = view.body
|
||||||
|
|
||||||
state.connectionMode = .unconfigured
|
state.connectionMode = .unconfigured
|
||||||
|
|||||||
@ -145,10 +145,11 @@ extension MenuSessionsInjector {
|
|||||||
let headerItem = NSMenuItem()
|
let headerItem = NSMenuItem()
|
||||||
headerItem.tag = self.tag
|
headerItem.tag = self.tag
|
||||||
headerItem.isEnabled = false
|
headerItem.isEnabled = false
|
||||||
|
let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
|
||||||
let hosted = self.makeHostedView(
|
let hosted = self.makeHostedView(
|
||||||
rootView: AnyView(MenuSessionsHeaderView(
|
rootView: AnyView(MenuSessionsHeaderView(
|
||||||
count: rows.count,
|
count: rows.count,
|
||||||
statusText: isConnected ? nil : self.controlChannelStatusText(for: channelState))),
|
statusText: statusText)),
|
||||||
width: width,
|
width: width,
|
||||||
highlighted: false)
|
highlighted: false)
|
||||||
headerItem.view = hosted
|
headerItem.view = hosted
|
||||||
@ -598,8 +599,11 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedSnapshot = nil
|
if self.cachedSnapshot != nil {
|
||||||
self.cachedErrorText = nil
|
self.cachedErrorText = "Gateway disconnected (showing cached)"
|
||||||
|
} else {
|
||||||
|
self.cachedErrorText = nil
|
||||||
|
}
|
||||||
self.cacheUpdatedAt = Date()
|
self.cacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -624,8 +628,6 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedUsageSummary = nil
|
|
||||||
self.cachedUsageErrorText = nil
|
|
||||||
self.usageCacheUpdatedAt = Date()
|
self.usageCacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -648,8 +650,6 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedCostSummary = nil
|
|
||||||
self.cachedCostErrorText = nil
|
|
||||||
self.costCacheUpdatedAt = Date()
|
self.costCacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,28 @@ import Foundation
|
|||||||
import JavaScriptCore
|
import JavaScriptCore
|
||||||
|
|
||||||
enum ModelCatalogLoader {
|
enum ModelCatalogLoader {
|
||||||
static let defaultPath: String = FileManager().homeDirectoryForCurrentUser
|
static var defaultPath: String { self.resolveDefaultPath() }
|
||||||
.appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path
|
|
||||||
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
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] {
|
static func load(from path: String) async throws -> [ModelChoice] {
|
||||||
let expanded = (path as NSString).expandingTildeInPath
|
let expanded = (path as NSString).expandingTildeInPath
|
||||||
self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)")
|
guard let resolved = self.resolvePath(preferred: expanded) else {
|
||||||
let source = try String(contentsOfFile: expanded, encoding: .utf8)
|
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 sanitized = self.sanitize(source: source)
|
||||||
|
|
||||||
let ctx = JSContext()
|
let ctx = JSContext()
|
||||||
@ -45,9 +59,82 @@ enum ModelCatalogLoader {
|
|||||||
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
|
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
|
||||||
}
|
}
|
||||||
self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)")
|
self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)")
|
||||||
|
if resolved.shouldCache {
|
||||||
|
self.cacheCatalog(sourcePath: resolved.path)
|
||||||
|
}
|
||||||
return sorted
|
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 {
|
private static func sanitize(source: String) -> String {
|
||||||
guard let exportRange = source.range(of: "export const MODELS"),
|
guard let exportRange = source.range(of: "export const MODELS"),
|
||||||
let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"),
|
let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"),
|
||||||
|
|||||||
@ -480,26 +480,26 @@ actor MacNodeRuntime {
|
|||||||
message: "SYSTEM_RUN_DISABLED: security=deny")
|
message: "SYSTEM_RUN_DISABLED: security=deny")
|
||||||
}
|
}
|
||||||
|
|
||||||
let requiresAsk: Bool = {
|
let approval = await self.resolveSystemRunApproval(
|
||||||
if ask == .always { return true }
|
req: req,
|
||||||
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true }
|
params: params,
|
||||||
return false
|
context: ExecRunContext(
|
||||||
}()
|
displayCommand: displayCommand,
|
||||||
|
security: security,
|
||||||
let approvedByAsk = params.approved == true
|
ask: ask,
|
||||||
if requiresAsk, !approvedByAsk {
|
agentId: agentId,
|
||||||
await self.emitExecEvent(
|
resolution: resolution,
|
||||||
"exec.denied",
|
allowlistMatch: allowlistMatch,
|
||||||
payload: ExecEventPayload(
|
skillAllow: skillAllow,
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
runId: runId,
|
runId: runId))
|
||||||
host: "node",
|
if let response = approval.response { return response }
|
||||||
command: displayCommand,
|
let approvedByAsk = approval.approvedByAsk
|
||||||
reason: "approval-required"))
|
let persistAllowlist = approval.persistAllowlist
|
||||||
return Self.errorResponse(
|
if persistAllowlist, security == .allowlist,
|
||||||
req,
|
let pattern = ExecApprovalHelpers.allowlistPattern(command: command, resolution: resolution)
|
||||||
code: .unavailable,
|
{
|
||||||
message: "SYSTEM_RUN_DENIED: approval required")
|
ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk {
|
if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk {
|
||||||
@ -619,6 +619,99 @@ actor MacNodeRuntime {
|
|||||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
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 {
|
private func handleSystemExecApprovalsGet(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||||
_ = ExecApprovalsStore.ensureFile()
|
_ = ExecApprovalsStore.ensureFile()
|
||||||
let snapshot = ExecApprovalsStore.readSnapshot()
|
let snapshot = ExecApprovalsStore.readSnapshot()
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import ClawdbotIPC
|
import ClawdbotIPC
|
||||||
|
import ClawdbotKit
|
||||||
|
import CoreLocation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PermissionsSettings: View {
|
struct PermissionsSettings: View {
|
||||||
@ -8,6 +10,8 @@ struct PermissionsSettings: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
SystemRunSettingsView()
|
||||||
|
|
||||||
Text("Allow these so Clawdbot can notify and capture when needed.")
|
Text("Allow these so Clawdbot can notify and capture when needed.")
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
|
||||||
@ -15,6 +19,8 @@ struct PermissionsSettings: View {
|
|||||||
.padding(.horizontal, 2)
|
.padding(.horizontal, 2)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
|
LocationAccessSettings()
|
||||||
|
|
||||||
Button("Restart onboarding") { self.showOnboarding() }
|
Button("Restart onboarding") { self.showOnboarding() }
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
Spacer()
|
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 {
|
struct PermissionStatusList: View {
|
||||||
let status: [Capability: Bool]
|
let status: [Capability: Bool]
|
||||||
let refresh: () async -> Void
|
let refresh: () async -> Void
|
||||||
@ -45,25 +117,6 @@ struct PermissionStatusList: View {
|
|||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
.help("Refresh status")
|
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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] {
|
private func listeners(on port: Int) async -> [Listener] {
|
||||||
let res = await ShellExecutor.run(
|
let res = await ShellExecutor.run(
|
||||||
command: ["lsof", "-nP", "-iTCP:\(port)", "-sTCP:LISTEN", "-Fpcn"],
|
command: ["lsof", "-nP", "-iTCP:\(port)", "-sTCP:LISTEN", "-Fpcn"],
|
||||||
|
|||||||
@ -72,7 +72,6 @@ final class RemotePortTunnel {
|
|||||||
}
|
}
|
||||||
var args: [String] = [
|
var args: [String] = [
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
"-o", "IdentitiesOnly=yes",
|
|
||||||
"-o", "ExitOnForwardFailure=yes",
|
"-o", "ExitOnForwardFailure=yes",
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
"-o", "UpdateHostKeys=yes",
|
"-o", "UpdateHostKeys=yes",
|
||||||
@ -84,7 +83,12 @@ final class RemotePortTunnel {
|
|||||||
]
|
]
|
||||||
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
||||||
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
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
|
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
||||||
args.append(userHost)
|
args.append(userHost)
|
||||||
|
|
||||||
|
|||||||
@ -20,11 +20,13 @@ actor RemoteTunnelManager {
|
|||||||
tunnel.process.isRunning,
|
tunnel.process.isRunning,
|
||||||
let local = tunnel.localPort
|
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)")
|
self.logger.info("reusing active SSH tunnel localPort=\(local, privacy: .public)")
|
||||||
return local
|
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()
|
await self.beginRestart()
|
||||||
tunnel.terminate()
|
tunnel.terminate()
|
||||||
self.controlTunnel = nil
|
self.controlTunnel = nil
|
||||||
@ -35,19 +37,11 @@ actor RemoteTunnelManager {
|
|||||||
if let desc = await PortGuardian.shared.describe(port: Int(desiredPort)),
|
if let desc = await PortGuardian.shared.describe(port: Int(desiredPort)),
|
||||||
self.isSshProcess(desc)
|
self.isSshProcess(desc)
|
||||||
{
|
{
|
||||||
if await self.isTunnelHealthy(port: desiredPort) {
|
self.logger.info(
|
||||||
self.logger.info(
|
"reusing existing SSH tunnel listener " +
|
||||||
"reusing existing SSH tunnel listener " +
|
"localPort=\(desiredPort, privacy: .public) " +
|
||||||
"localPort=\(desiredPort, privacy: .public) " +
|
"pid=\(desc.pid, privacy: .public)")
|
||||||
"pid=\(desc.pid, privacy: .public)")
|
return desiredPort
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -88,10 +82,6 @@ actor RemoteTunnelManager {
|
|||||||
self.controlTunnel = nil
|
self.controlTunnel = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isTunnelHealthy(port: UInt16) async -> Bool {
|
|
||||||
await PortGuardian.shared.probeGatewayHealth(port: Int(port))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isSshProcess(_ desc: PortGuardian.Descriptor) -> Bool {
|
private func isSshProcess(_ desc: PortGuardian.Descriptor) -> Bool {
|
||||||
let cmd = desc.command.lowercased()
|
let cmd = desc.command.lowercased()
|
||||||
if cmd.contains("ssh") { return true }
|
if cmd.contains("ssh") { return true }
|
||||||
@ -128,21 +118,5 @@ actor RemoteTunnelManager {
|
|||||||
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
|
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cleanupStaleTunnel(desc: PortGuardian.Descriptor, port: UInt16) async {
|
// Keep tunnel reuse lightweight; restart only when the listener disappears.
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.1.20</string>
|
<string>2026.1.21</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202601200</string>
|
<string>202601210</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>Clawdbot</string>
|
<string>Clawdbot</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@ -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<Void, Never>] = [:]
|
||||||
|
|
||||||
|
init(maxConcurrent: Int) {
|
||||||
|
let normalized = max(1, maxConcurrent)
|
||||||
|
self.maxConcurrent = normalized
|
||||||
|
self.available = normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func withPermit<T>(_ 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
|
#if DEBUG
|
||||||
extension SessionPreviewCache {
|
extension SessionPreviewCache {
|
||||||
func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) {
|
func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) {
|
||||||
@ -184,17 +229,31 @@ enum SessionMenuPreviewLoader {
|
|||||||
return self.snapshot(from: cached)
|
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 {
|
do {
|
||||||
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
|
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
|
||||||
let payload = try await AsyncTimeout.withTimeout(
|
let payload = try await SessionPreviewLimiter.shared.withPermit {
|
||||||
seconds: self.previewTimeoutSeconds,
|
try await AsyncTimeout.withTimeout(
|
||||||
onTimeout: { PreviewTimeoutError() },
|
seconds: self.previewTimeoutSeconds,
|
||||||
operation: {
|
onTimeout: { PreviewTimeoutError() },
|
||||||
try await GatewayConnection.shared.chatHistory(
|
operation: {
|
||||||
sessionKey: sessionKey,
|
try await GatewayConnection.shared.chatHistory(
|
||||||
limit: self.previewLimit(for: maxItems),
|
sessionKey: sessionKey,
|
||||||
timeoutMs: timeoutMs)
|
limit: self.previewLimit(for: maxItems),
|
||||||
})
|
timeoutMs: timeoutMs)
|
||||||
|
})
|
||||||
|
}
|
||||||
let built = Self.previewItems(from: payload, maxItems: maxItems)
|
let built = Self.previewItems(from: payload, maxItems: maxItems)
|
||||||
await SessionPreviewCache.shared.store(items: built, for: sessionKey)
|
await SessionPreviewCache.shared.store(items: built, for: sessionKey)
|
||||||
return Self.snapshot(from: built)
|
return Self.snapshot(from: built)
|
||||||
|
|||||||
@ -221,6 +221,6 @@ final class TailscaleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonisolated static func fallbackTailnetIPv4() -> String? {
|
nonisolated static func fallbackTailnetIPv4() -> String? {
|
||||||
Self.detectTailnetIPv4()
|
self.detectTailnetIPv4()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -309,7 +309,7 @@ private func resolveLocalHost(bind: String?) -> String {
|
|||||||
let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
let tailnetIP = detectTailnetIPv4()
|
let tailnetIP = detectTailnetIPv4()
|
||||||
switch normalized {
|
switch normalized {
|
||||||
case "tailnet", "auto":
|
case "tailnet":
|
||||||
return tailnetIP ?? "127.0.0.1"
|
return tailnetIP ?? "127.0.0.1"
|
||||||
default:
|
default:
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
public let caps: [String]?
|
public let caps: [String]?
|
||||||
public let commands: [String]?
|
public let commands: [String]?
|
||||||
public let permissions: [String: AnyCodable]?
|
public let permissions: [String: AnyCodable]?
|
||||||
|
public let pathenv: String?
|
||||||
public let role: String?
|
public let role: String?
|
||||||
public let scopes: [String]?
|
public let scopes: [String]?
|
||||||
public let device: [String: AnyCodable]?
|
public let device: [String: AnyCodable]?
|
||||||
@ -32,6 +33,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
caps: [String]?,
|
caps: [String]?,
|
||||||
commands: [String]?,
|
commands: [String]?,
|
||||||
permissions: [String: AnyCodable]?,
|
permissions: [String: AnyCodable]?,
|
||||||
|
pathenv: String?,
|
||||||
role: String?,
|
role: String?,
|
||||||
scopes: [String]?,
|
scopes: [String]?,
|
||||||
device: [String: AnyCodable]?,
|
device: [String: AnyCodable]?,
|
||||||
@ -45,6 +47,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
self.caps = caps
|
self.caps = caps
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
self.pathenv = pathenv
|
||||||
self.role = role
|
self.role = role
|
||||||
self.scopes = scopes
|
self.scopes = scopes
|
||||||
self.device = device
|
self.device = device
|
||||||
@ -59,6 +62,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
case caps
|
case caps
|
||||||
case commands
|
case commands
|
||||||
case permissions
|
case permissions
|
||||||
|
case pathenv = "pathEnv"
|
||||||
case role
|
case role
|
||||||
case scopes
|
case scopes
|
||||||
case device
|
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 struct AgentWaitParams: Codable, Sendable {
|
||||||
public let runid: String
|
public let runid: String
|
||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
@ -1443,17 +1485,21 @@ public struct WebLoginWaitParams: Codable, Sendable {
|
|||||||
public struct AgentSummary: Codable, Sendable {
|
public struct AgentSummary: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let name: String?
|
public let name: String?
|
||||||
|
public let identity: [String: AnyCodable]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?
|
name: String?,
|
||||||
|
identity: [String: AnyCodable]?
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.identity = identity
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case name
|
case name
|
||||||
|
case identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1904,6 +1950,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||||
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let cwd: String?
|
public let cwd: String?
|
||||||
public let host: String?
|
public let host: String?
|
||||||
@ -1915,6 +1962,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: String?,
|
cwd: String?,
|
||||||
host: String?,
|
host: String?,
|
||||||
@ -1925,6 +1973,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
sessionkey: String?,
|
sessionkey: String?,
|
||||||
timeoutms: Int?
|
timeoutms: Int?
|
||||||
) {
|
) {
|
||||||
|
self.id = id
|
||||||
self.command = command
|
self.command = command
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
self.host = host
|
self.host = host
|
||||||
@ -1936,6 +1985,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
self.timeoutms = timeoutms
|
self.timeoutms = timeoutms
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
case command
|
case command
|
||||||
case cwd
|
case cwd
|
||||||
case host
|
case host
|
||||||
|
|||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -140,14 +140,14 @@ import Testing
|
|||||||
#expect(resolved.mode == .remote)
|
#expect(resolved.mode == .remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func resolveLocalGatewayHostPrefersTailnetForAuto() {
|
@Test func resolveLocalGatewayHostUsesLoopbackForAutoEvenWithTailnet() {
|
||||||
let host = GatewayEndpointStore._testResolveLocalGatewayHost(
|
let host = GatewayEndpointStore._testResolveLocalGatewayHost(
|
||||||
bindMode: "auto",
|
bindMode: "auto",
|
||||||
tailscaleIP: "100.64.1.2")
|
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(
|
let host = GatewayEndpointStore._testResolveLocalGatewayHost(
|
||||||
bindMode: "auto",
|
bindMode: "auto",
|
||||||
tailscaleIP: nil)
|
tailscaleIP: nil)
|
||||||
|
|||||||
@ -48,7 +48,10 @@ import Testing
|
|||||||
|
|
||||||
@Test func expectedGatewayVersionFromStringUsesParser() {
|
@Test func expectedGatewayVersionFromStringUsesParser() {
|
||||||
#expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2))
|
#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)
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String?>(initialState: nil)
|
||||||
|
private let pendingReceiveHandler =
|
||||||
|
OSAllocatedUnfairLock<(@Sendable (Result<URLSessionWebSocketTask.Message, Error>)
|
||||||
|
-> 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<URLSessionWebSocketTask.Message, Error>.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<URLSessionWebSocketTask.Message, Error>.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<URLSessionWebSocketTask.Message, Error>) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ public struct ClawdbotChatView: View {
|
|||||||
@State private var scrollPosition: UUID?
|
@State private var scrollPosition: UUID?
|
||||||
@State private var showSessions = false
|
@State private var showSessions = false
|
||||||
@State private var hasPerformedInitialScroll = false
|
@State private var hasPerformedInitialScroll = false
|
||||||
|
@State private var isPinnedToBottom = true
|
||||||
private let showsSessionSwitcher: Bool
|
private let showsSessionSwitcher: Bool
|
||||||
private let style: Style
|
private let style: Style
|
||||||
private let markdownVariant: ChatMarkdownVariant
|
private let markdownVariant: ChatMarkdownVariant
|
||||||
@ -87,36 +88,28 @@ public struct ClawdbotChatView: View {
|
|||||||
private var messageList: some View {
|
private var messageList: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScrollView {
|
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) {
|
LazyVStack(spacing: Layout.messageSpacing) {
|
||||||
self.messageListRows
|
self.messageListRows
|
||||||
|
|
||||||
Color.clear
|
Color.clear
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(height: Layout.messageListPaddingBottom)
|
||||||
|
#else
|
||||||
.frame(height: Layout.messageListPaddingBottom + 1)
|
.frame(height: Layout.messageListPaddingBottom + 1)
|
||||||
|
#endif
|
||||||
.id(self.scrollerBottomID)
|
.id(self.scrollerBottomID)
|
||||||
}
|
}
|
||||||
// Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches.
|
// Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches.
|
||||||
.scrollTargetLayout()
|
.scrollTargetLayout()
|
||||||
.padding(.top, Layout.messageListPaddingTop)
|
.padding(.top, Layout.messageListPaddingTop)
|
||||||
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
// Keep the scroll pinned to the bottom for new messages.
|
// Keep the scroll pinned to the bottom for new messages.
|
||||||
.scrollPosition(id: self.$scrollPosition, anchor: .bottom)
|
.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 {
|
if self.viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@ -133,18 +126,26 @@ public struct ClawdbotChatView: View {
|
|||||||
guard !isLoading, !self.hasPerformedInitialScroll else { return }
|
guard !isLoading, !self.hasPerformedInitialScroll else { return }
|
||||||
self.scrollPosition = self.scrollerBottomID
|
self.scrollPosition = self.scrollerBottomID
|
||||||
self.hasPerformedInitialScroll = true
|
self.hasPerformedInitialScroll = true
|
||||||
|
self.isPinnedToBottom = true
|
||||||
}
|
}
|
||||||
.onChange(of: self.viewModel.sessionKey) { _, _ in
|
.onChange(of: self.viewModel.sessionKey) { _, _ in
|
||||||
self.hasPerformedInitialScroll = false
|
self.hasPerformedInitialScroll = false
|
||||||
|
self.isPinnedToBottom = true
|
||||||
}
|
}
|
||||||
.onChange(of: self.viewModel.messages.count) { _, _ in
|
.onChange(of: self.viewModel.messages.count) { _, _ in
|
||||||
guard self.hasPerformedInitialScroll else { return }
|
guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return }
|
||||||
withAnimation(.snappy(duration: 0.22)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
self.scrollPosition = self.scrollerBottomID
|
self.scrollPosition = self.scrollerBottomID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: self.viewModel.pendingRunCount) { _, _ in
|
.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)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
self.scrollPosition = self.scrollerBottomID
|
self.scrollPosition = self.scrollerBottomID
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
// Avoid ambiguity with the app's own AnyCodable type.
|
||||||
private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable
|
private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable
|
||||||
|
|
||||||
@ -117,6 +124,7 @@ public actor GatewayChannelActor {
|
|||||||
private var lastSeq: Int?
|
private var lastSeq: Int?
|
||||||
private var lastTick: Date?
|
private var lastTick: Date?
|
||||||
private var tickIntervalMs: Double = 30000
|
private var tickIntervalMs: Double = 30000
|
||||||
|
private var lastAuthSource: GatewayAuthSource = .none
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
private let connectTimeoutSeconds: Double = 6
|
private let connectTimeoutSeconds: Double = 6
|
||||||
@ -149,6 +157,8 @@ public actor GatewayChannelActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func authSource() -> GatewayAuthSource { self.lastAuthSource }
|
||||||
|
|
||||||
public func shutdown() async {
|
public func shutdown() async {
|
||||||
self.shouldReconnect = false
|
self.shouldReconnect = false
|
||||||
self.connected = false
|
self.connected = false
|
||||||
@ -300,6 +310,18 @@ public actor GatewayChannelActor {
|
|||||||
let identity = DeviceIdentityStore.loadOrCreate()
|
let identity = DeviceIdentityStore.loadOrCreate()
|
||||||
let storedToken = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role)?.token
|
let storedToken = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role)?.token
|
||||||
let authToken = storedToken ?? self.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
|
let canFallbackToShared = storedToken != nil && self.token != nil
|
||||||
if let authToken {
|
if let authToken {
|
||||||
params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(authToken)])
|
params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(authToken)])
|
||||||
@ -571,7 +593,14 @@ public actor GatewayChannelActor {
|
|||||||
id: id,
|
id: id,
|
||||||
method: method,
|
method: method,
|
||||||
params: paramsObject)
|
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<GatewayFrame, Error>) in
|
let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<GatewayFrame, Error>) in
|
||||||
self.pending[id] = cont
|
self.pending[id] = cont
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
|
|||||||
@ -219,8 +219,8 @@ public actor GatewayNodeSession {
|
|||||||
}
|
}
|
||||||
if let error = response.error {
|
if let error = response.error {
|
||||||
params["error"] = AnyCodable([
|
params["error"] = AnyCodable([
|
||||||
"code": AnyCodable(error.code.rawValue),
|
"code": error.code.rawValue,
|
||||||
"message": AnyCodable(error.message),
|
"message": error.message,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable {
|
|||||||
public var agentId: String?
|
public var agentId: String?
|
||||||
public var sessionKey: String?
|
public var sessionKey: String?
|
||||||
public var approved: Bool?
|
public var approved: Bool?
|
||||||
|
public var approvalDecision: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
command: [String],
|
command: [String],
|
||||||
@ -40,7 +41,8 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable {
|
|||||||
needsScreenRecording: Bool? = nil,
|
needsScreenRecording: Bool? = nil,
|
||||||
agentId: String? = nil,
|
agentId: String? = nil,
|
||||||
sessionKey: String? = nil,
|
sessionKey: String? = nil,
|
||||||
approved: Bool? = nil)
|
approved: Bool? = nil,
|
||||||
|
approvalDecision: String? = nil)
|
||||||
{
|
{
|
||||||
self.command = command
|
self.command = command
|
||||||
self.rawCommand = rawCommand
|
self.rawCommand = rawCommand
|
||||||
@ -51,6 +53,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable {
|
|||||||
self.agentId = agentId
|
self.agentId = agentId
|
||||||
self.sessionKey = sessionKey
|
self.sessionKey = sessionKey
|
||||||
self.approved = approved
|
self.approved = approved
|
||||||
|
self.approvalDecision = approvalDecision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
public let caps: [String]?
|
public let caps: [String]?
|
||||||
public let commands: [String]?
|
public let commands: [String]?
|
||||||
public let permissions: [String: AnyCodable]?
|
public let permissions: [String: AnyCodable]?
|
||||||
|
public let pathenv: String?
|
||||||
public let role: String?
|
public let role: String?
|
||||||
public let scopes: [String]?
|
public let scopes: [String]?
|
||||||
public let device: [String: AnyCodable]?
|
public let device: [String: AnyCodable]?
|
||||||
@ -32,6 +33,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
caps: [String]?,
|
caps: [String]?,
|
||||||
commands: [String]?,
|
commands: [String]?,
|
||||||
permissions: [String: AnyCodable]?,
|
permissions: [String: AnyCodable]?,
|
||||||
|
pathenv: String?,
|
||||||
role: String?,
|
role: String?,
|
||||||
scopes: [String]?,
|
scopes: [String]?,
|
||||||
device: [String: AnyCodable]?,
|
device: [String: AnyCodable]?,
|
||||||
@ -45,6 +47,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
self.caps = caps
|
self.caps = caps
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
self.pathenv = pathenv
|
||||||
self.role = role
|
self.role = role
|
||||||
self.scopes = scopes
|
self.scopes = scopes
|
||||||
self.device = device
|
self.device = device
|
||||||
@ -59,6 +62,7 @@ public struct ConnectParams: Codable, Sendable {
|
|||||||
case caps
|
case caps
|
||||||
case commands
|
case commands
|
||||||
case permissions
|
case permissions
|
||||||
|
case pathenv = "pathEnv"
|
||||||
case role
|
case role
|
||||||
case scopes
|
case scopes
|
||||||
case device
|
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 struct AgentWaitParams: Codable, Sendable {
|
||||||
public let runid: String
|
public let runid: String
|
||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
@ -1443,17 +1485,21 @@ public struct WebLoginWaitParams: Codable, Sendable {
|
|||||||
public struct AgentSummary: Codable, Sendable {
|
public struct AgentSummary: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let name: String?
|
public let name: String?
|
||||||
|
public let identity: [String: AnyCodable]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?
|
name: String?,
|
||||||
|
identity: [String: AnyCodable]?
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.identity = identity
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case name
|
case name
|
||||||
|
case identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1904,6 +1950,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||||
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let cwd: String?
|
public let cwd: String?
|
||||||
public let host: String?
|
public let host: String?
|
||||||
@ -1915,6 +1962,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
public let timeoutms: Int?
|
public let timeoutms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
cwd: String?,
|
cwd: String?,
|
||||||
host: String?,
|
host: String?,
|
||||||
@ -1925,6 +1973,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
sessionkey: String?,
|
sessionkey: String?,
|
||||||
timeoutms: Int?
|
timeoutms: Int?
|
||||||
) {
|
) {
|
||||||
|
self.id = id
|
||||||
self.command = command
|
self.command = command
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
self.host = host
|
self.host = host
|
||||||
@ -1936,6 +1985,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
self.timeoutms = timeoutms
|
self.timeoutms = timeoutms
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
case command
|
case command
|
||||||
case cwd
|
case cwd
|
||||||
case host
|
case host
|
||||||
|
|||||||
@ -149,6 +149,19 @@ Available actions:
|
|||||||
- **leaveGroup**: Leave a group chat (`chatGuid`)
|
- **leaveGroup**: Leave a group chat (`chatGuid`)
|
||||||
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`)
|
- **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
|
## Block streaming
|
||||||
Control whether responses are sent as a single message or streamed in blocks:
|
Control whether responses are sent as a single message or streamed in blocks:
|
||||||
```json5
|
```json5
|
||||||
|
|||||||
@ -175,6 +175,7 @@ Notes:
|
|||||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
- `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`.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
- If `channels` is present, any channel not listed is denied by default.
|
- 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.
|
- 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).
|
- 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.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||||
|
|||||||
@ -8,9 +8,9 @@ read_when:
|
|||||||
> "Abandon all hope, ye who enter here."
|
> "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
|
## Plugin required
|
||||||
Microsoft Teams ships as a plugin and is not bundled with the core install.
|
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:
|
Teams markdown is more limited than Slack or Discord:
|
||||||
- Basic formatting works: **bold**, *italic*, `code`, links
|
- Basic formatting works: **bold**, *italic*, `code`, links
|
||||||
- Complex markdown (tables, nested lists) may not render correctly
|
- 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
|
## Configuration
|
||||||
Key settings (see `/gateway/configuration` for shared channel patterns):
|
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.<teamId>.requireMention`: per-team override.
|
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
|
||||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
|
- `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
|
||||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
|
- `channels.msteams.teams.<teamId>.channels.<conversationId>.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
|
## Routing & Sessions
|
||||||
- Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):
|
- 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).
|
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).
|
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)
|
## Polls (Adaptive Cards)
|
||||||
Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API).
|
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.
|
- The gateway must stay online to record votes.
|
||||||
- Polls do not auto-post result summaries yet (inspect the store file if needed).
|
- 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:<id>",
|
||||||
|
"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:<aad-object-id>` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` |
|
||||||
|
| User (by name) | `user:<display-name>` | `user:John Smith` (requires Graph API) |
|
||||||
|
| Group/channel | `conversation:<conversation-id>` | `conversation:19:abc123...@thread.tacv2` |
|
||||||
|
| Group/channel (raw) | `<conversation-id>` | `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 messaging
|
||||||
- Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point.
|
- 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.
|
- See `/gateway/configuration` for `dmPolicy` and allowlist gating.
|
||||||
|
|||||||
@ -100,6 +100,11 @@ Groups:
|
|||||||
- Use `channels.signal.ignoreAttachments` to skip downloading media.
|
- 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).
|
- 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)
|
## Delivery targets (CLI/cron)
|
||||||
- DMs: `signal:+15551234567` (or plain E.164).
|
- DMs: `signal:+15551234567` (or plain E.164).
|
||||||
- Groups: `signal:group:<groupId>`.
|
- Groups: `signal:group:<groupId>`.
|
||||||
|
|||||||
@ -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`)
|
- Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`)
|
||||||
- Commands require authorization even in groups with `groupPolicy: "open"`
|
- 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`):**
|
**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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@ -334,6 +334,7 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
|
|||||||
- `agents.defaults.heartbeat.model` (optional override)
|
- `agents.defaults.heartbeat.model` (optional override)
|
||||||
- `agents.defaults.heartbeat.target`
|
- `agents.defaults.heartbeat.target`
|
||||||
- `agents.defaults.heartbeat.to`
|
- `agents.defaults.heartbeat.to`
|
||||||
|
- `agents.defaults.heartbeat.session`
|
||||||
- `agents.list[].heartbeat.*` (per-agent overrides)
|
- `agents.list[].heartbeat.*` (per-agent overrides)
|
||||||
- `session.*` (scope, idle, store, mainKey)
|
- `session.*` (scope, idle, store, mainKey)
|
||||||
- `web.enabled` (disable channel startup when false)
|
- `web.enabled` (disable channel startup when false)
|
||||||
|
|||||||
@ -18,5 +18,54 @@ Related:
|
|||||||
clawdbot agents list
|
clawdbot agents list
|
||||||
clawdbot agents add work --workspace ~/clawd-work
|
clawdbot agents add work --workspace ~/clawd-work
|
||||||
clawdbot agents set-identity --workspace ~/clawd --from-identity
|
clawdbot agents set-identity --workspace ~/clawd --from-identity
|
||||||
|
clawdbot agents set-identity --agent main --avatar avatars/clawd.png
|
||||||
clawdbot agents delete work
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@ -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).
|
|
||||||
@ -25,6 +25,12 @@ Run a local Gateway process:
|
|||||||
clawdbot gateway
|
clawdbot gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Foreground alias:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot gateway run
|
||||||
|
```
|
||||||
|
|
||||||
Notes:
|
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.
|
- 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).
|
- Binding beyond loopback without auth is blocked (safety guardrail).
|
||||||
@ -34,7 +40,7 @@ Notes:
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
|
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
|
||||||
- `--bind <loopback|lan|tailnet|auto>`: listener bind mode.
|
- `--bind <loopback|lan|tailnet|auto|custom>`: listener bind mode.
|
||||||
- `--auth <token|password>`: auth mode override.
|
- `--auth <token|password>`: auth mode override.
|
||||||
- `--token <token>`: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process).
|
- `--token <token>`: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process).
|
||||||
- `--password <password>`: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process).
|
- `--password <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`
|
||||||
|
|
||||||
`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 <url>`: override the probe URL.
|
||||||
|
- `--token <token>`: token auth for the probe.
|
||||||
|
- `--password <password>`: password auth for the probe.
|
||||||
|
- `--timeout <ms>`: 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
|
- your configured remote gateway (if set), and
|
||||||
- localhost (loopback) **even if remote is configured**.
|
- 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.
|
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
|
```bash
|
||||||
clawdbot gateway status
|
clawdbot gateway probe
|
||||||
clawdbot gateway status --json
|
clawdbot gateway probe --json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Remote over SSH (Mac app parity)
|
#### 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:
|
CLI equivalent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot gateway status --ssh user@gateway-host
|
clawdbot gateway probe --ssh user@gateway-host
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -114,6 +137,20 @@ clawdbot gateway call status
|
|||||||
clawdbot gateway call logs.tail --params '{"sinceMs": 60000}'
|
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)
|
## Discover gateways (Bonjour)
|
||||||
|
|
||||||
`gateway discover` scans for Gateway beacons (`_clawdbot-gw._tcp`).
|
`gateway discover` scans for Gateway beacons (`_clawdbot-gw._tcp`).
|
||||||
|
|||||||
@ -28,8 +28,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`health`](/cli/health)
|
- [`health`](/cli/health)
|
||||||
- [`sessions`](/cli/sessions)
|
- [`sessions`](/cli/sessions)
|
||||||
- [`gateway`](/cli/gateway)
|
- [`gateway`](/cli/gateway)
|
||||||
- [`daemon`](/cli/daemon)
|
|
||||||
- [`service`](/cli/service)
|
|
||||||
- [`logs`](/cli/logs)
|
- [`logs`](/cli/logs)
|
||||||
- [`models`](/cli/models)
|
- [`models`](/cli/models)
|
||||||
- [`memory`](/cli/memory)
|
- [`memory`](/cli/memory)
|
||||||
@ -138,29 +136,14 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
call
|
call
|
||||||
health
|
health
|
||||||
status
|
status
|
||||||
|
probe
|
||||||
discover
|
discover
|
||||||
daemon
|
|
||||||
status
|
|
||||||
install
|
install
|
||||||
uninstall
|
uninstall
|
||||||
start
|
start
|
||||||
stop
|
stop
|
||||||
restart
|
restart
|
||||||
service
|
run
|
||||||
gateway
|
|
||||||
status
|
|
||||||
install
|
|
||||||
uninstall
|
|
||||||
start
|
|
||||||
stop
|
|
||||||
restart
|
|
||||||
node
|
|
||||||
status
|
|
||||||
install
|
|
||||||
uninstall
|
|
||||||
start
|
|
||||||
stop
|
|
||||||
restart
|
|
||||||
logs
|
logs
|
||||||
models
|
models
|
||||||
list
|
list
|
||||||
@ -191,14 +174,13 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
nodes
|
nodes
|
||||||
devices
|
devices
|
||||||
node
|
node
|
||||||
|
run
|
||||||
|
status
|
||||||
|
install
|
||||||
|
uninstall
|
||||||
start
|
start
|
||||||
daemon
|
stop
|
||||||
status
|
restart
|
||||||
install
|
|
||||||
uninstall
|
|
||||||
start
|
|
||||||
stop
|
|
||||||
restart
|
|
||||||
approvals
|
approvals
|
||||||
get
|
get
|
||||||
set
|
set
|
||||||
@ -328,7 +310,7 @@ Options:
|
|||||||
- `--minimax-api-key <key>`
|
- `--minimax-api-key <key>`
|
||||||
- `--opencode-zen-api-key <key>`
|
- `--opencode-zen-api-key <key>`
|
||||||
- `--gateway-port <port>`
|
- `--gateway-port <port>`
|
||||||
- `--gateway-bind <loopback|lan|tailnet|auto>`
|
- `--gateway-bind <loopback|lan|tailnet|auto|custom>`
|
||||||
- `--gateway-auth <off|token|password>`
|
- `--gateway-auth <off|token|password>`
|
||||||
- `--gateway-token <token>`
|
- `--gateway-token <token>`
|
||||||
- `--gateway-password <password>`
|
- `--gateway-password <password>`
|
||||||
@ -544,7 +526,7 @@ Options:
|
|||||||
- `--debug` (alias for `--verbose`)
|
- `--debug` (alias for `--verbose`)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Overview includes Gateway + Node service status when available.
|
- Overview includes Gateway + node host service status when available.
|
||||||
|
|
||||||
### Usage tracking
|
### Usage tracking
|
||||||
Clawdbot can surface provider usage/quota when OAuth/API creds are available.
|
Clawdbot can surface provider usage/quota when OAuth/API creds are available.
|
||||||
@ -614,7 +596,7 @@ Run the WebSocket Gateway.
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
- `--port <port>`
|
- `--port <port>`
|
||||||
- `--bind <loopback|tailnet|lan|auto>`
|
- `--bind <loopback|tailnet|lan|auto|custom>`
|
||||||
- `--token <token>`
|
- `--token <token>`
|
||||||
- `--auth <token|password>`
|
- `--auth <token|password>`
|
||||||
- `--password <password>`
|
- `--password <password>`
|
||||||
@ -631,25 +613,25 @@ Options:
|
|||||||
- `--raw-stream`
|
- `--raw-stream`
|
||||||
- `--raw-stream-path <path>`
|
- `--raw-stream-path <path>`
|
||||||
|
|
||||||
### `daemon`
|
### `gateway service`
|
||||||
Manage the Gateway service (launchd/systemd/schtasks).
|
Manage the Gateway service (launchd/systemd/schtasks).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
- `daemon status` (probes the Gateway RPC by default)
|
- `gateway status` (probes the Gateway RPC by default)
|
||||||
- `daemon install` (service install)
|
- `gateway install` (service install)
|
||||||
- `daemon uninstall`
|
- `gateway uninstall`
|
||||||
- `daemon start`
|
- `gateway start`
|
||||||
- `daemon stop`
|
- `gateway stop`
|
||||||
- `daemon restart`
|
- `gateway restart`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url/--token/--password`).
|
- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url/--token/--password`).
|
||||||
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting.
|
- `gateway 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".
|
- `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".
|
||||||
- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL.
|
- `gateway status` prints which config path the CLI uses vs which config the service 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).
|
- `gateway 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).
|
- `gateway install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||||
- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`.
|
- `gateway install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`.
|
||||||
|
|
||||||
### `logs`
|
### `logs`
|
||||||
Tail Gateway file logs via RPC.
|
Tail Gateway file logs via RPC.
|
||||||
@ -668,13 +650,16 @@ clawdbot logs --no-color
|
|||||||
```
|
```
|
||||||
|
|
||||||
### `gateway <subcommand>`
|
### `gateway <subcommand>`
|
||||||
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:
|
Subcommands:
|
||||||
- `gateway call <method> [--params <json>]`
|
- `gateway call <method> [--params <json>]`
|
||||||
- `gateway health`
|
- `gateway health`
|
||||||
- `gateway status`
|
- `gateway status`
|
||||||
|
- `gateway probe`
|
||||||
- `gateway discover`
|
- `gateway discover`
|
||||||
|
- `gateway install|uninstall|start|stop|restart`
|
||||||
|
- `gateway run`
|
||||||
|
|
||||||
Common RPCs:
|
Common RPCs:
|
||||||
- `config.apply` (validate + write config + restart + wake)
|
- `config.apply` (validate + write config + restart + wake)
|
||||||
@ -806,16 +791,13 @@ All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`.
|
|||||||
[`clawdbot node`](/cli/node).
|
[`clawdbot node`](/cli/node).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
- `node start --host <gateway-host> --port 18790`
|
- `node run --host <gateway-host> --port 18790`
|
||||||
- `node service status`
|
- `node status`
|
||||||
- `node service install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
|
- `node install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
|
||||||
- `node service uninstall`
|
- `node uninstall`
|
||||||
- `node service start`
|
- `node run`
|
||||||
- `node service stop`
|
- `node stop`
|
||||||
- `node service restart`
|
- `node restart`
|
||||||
|
|
||||||
Legacy alias:
|
|
||||||
- `node daemon …` (same as `node service …`)
|
|
||||||
|
|
||||||
## Nodes
|
## Nodes
|
||||||
|
|
||||||
|
|||||||
@ -23,10 +23,10 @@ Common use cases:
|
|||||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||||
node host, so you can keep command access scoped and explicit.
|
node host, so you can keep command access scoped and explicit.
|
||||||
|
|
||||||
## Start (foreground)
|
## Run (foreground)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node start --host <gateway-host> --port 18790
|
clawdbot node run --host <gateway-host> --port 18790
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -42,9 +42,7 @@ Options:
|
|||||||
Install a headless node host as a user service.
|
Install a headless node host as a user service.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node service install --host <gateway-host> --port 18790
|
clawdbot node install --host <gateway-host> --port 18790
|
||||||
# or
|
|
||||||
clawdbot service node install --host <gateway-host> --port 18790
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -61,18 +59,10 @@ Manage the service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node status
|
clawdbot node status
|
||||||
clawdbot service node status
|
clawdbot node run
|
||||||
clawdbot node service status
|
clawdbot node stop
|
||||||
clawdbot node service start
|
clawdbot node restart
|
||||||
clawdbot node service stop
|
clawdbot node uninstall
|
||||||
clawdbot node service restart
|
|
||||||
clawdbot node service uninstall
|
|
||||||
```
|
|
||||||
|
|
||||||
Legacy alias:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot node daemon status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pairing
|
## Pairing
|
||||||
|
|||||||
@ -36,4 +36,19 @@ filter to nodes that connected within a duration (e.g. `24h`, `7d`).
|
|||||||
```bash
|
```bash
|
||||||
clawdbot nodes invoke --node <id|name|ip> --command <command> --params <json>
|
clawdbot nodes invoke --node <id|name|ip> --command <command> --params <json>
|
||||||
clawdbot nodes run --node <id|name|ip> <command...>
|
clawdbot nodes run --node <id|name|ip> <command...>
|
||||||
|
clawdbot nodes run --raw "git status"
|
||||||
|
clawdbot nodes run --agent main --node <id|name|ip> --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 <command>`: run a shell string (`/bin/sh -lc` or `cmd.exe /c`).
|
||||||
|
- `--agent <id>`: agent-scoped approvals/allowlists (defaults to configured agent).
|
||||||
|
- `--ask <off|on-miss|always>`, `--security <deny|allowlist|full>`: overrides.
|
||||||
|
|||||||
@ -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 <gateway-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)
|
|
||||||
@ -19,6 +19,6 @@ clawdbot status --usage
|
|||||||
Notes:
|
Notes:
|
||||||
- `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal).
|
- `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal).
|
||||||
- Output includes per-agent session stores when multiple agents are configured.
|
- 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).
|
- 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)).
|
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `clawdbot update` (see [Updating](/install/updating)).
|
||||||
|
|||||||
@ -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:
|
read_when:
|
||||||
- You want to update a source checkout safely
|
- You want to update a source checkout safely
|
||||||
- You need to understand `--update` shorthand behavior
|
- 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
|
```bash
|
||||||
clawdbot update
|
clawdbot update
|
||||||
clawdbot update status
|
clawdbot update status
|
||||||
|
clawdbot update wizard
|
||||||
clawdbot update --channel beta
|
clawdbot update --channel beta
|
||||||
clawdbot update --channel dev
|
clawdbot update --channel dev
|
||||||
clawdbot update --tag beta
|
clawdbot update --tag beta
|
||||||
@ -26,7 +27,7 @@ clawdbot --update
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
- `--restart`: restart the Gateway daemon after a successful update.
|
- `--restart`: restart the Gateway service after a successful update.
|
||||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||||
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
||||||
- `--json`: print machine-readable `UpdateRunResult` JSON.
|
- `--json`: print machine-readable `UpdateRunResult` JSON.
|
||||||
@ -48,6 +49,11 @@ Options:
|
|||||||
- `--json`: print machine-readable status JSON.
|
- `--json`: print machine-readable status JSON.
|
||||||
- `--timeout <seconds>`: timeout for checks (default is 3s).
|
- `--timeout <seconds>`: 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
|
## What it does
|
||||||
|
|
||||||
When you switch channels explicitly (`--channel ...`), Clawdbot also keeps the
|
When you switch channels explicitly (`--channel ...`), Clawdbot also keeps the
|
||||||
@ -69,11 +75,13 @@ High-level:
|
|||||||
|
|
||||||
1. Requires a clean worktree (no uncommitted changes).
|
1. Requires a clean worktree (no uncommitted changes).
|
||||||
2. Switches to the selected channel (tag or branch).
|
2. Switches to the selected channel (tag or branch).
|
||||||
3. Fetches and rebases against `@{upstream}` (dev only).
|
3. Fetches upstream (dev only).
|
||||||
4. Installs deps (pnpm preferred; npm fallback).
|
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. Builds + builds the Control UI.
|
5. Rebases onto the selected commit (dev only).
|
||||||
6. Runs `clawdbot doctor` as the final “safe update” check.
|
6. Installs deps (pnpm preferred; npm fallback).
|
||||||
7. Syncs plugins to the active channel (dev uses bundled extensions; stable/beta uses npm) and updates npm-installed plugins.
|
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
|
## `--update` shorthand
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no**
|
|||||||
- Provider: `anthropic`
|
- Provider: `anthropic`
|
||||||
- Auth: `ANTHROPIC_API_KEY` or `claude setup-token`
|
- Auth: `ANTHROPIC_API_KEY` or `claude setup-token`
|
||||||
- Example model: `anthropic/claude-opus-4-5`
|
- 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
|
```json5
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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`).
|
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
|
## 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 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
|
## What can be pruned
|
||||||
- Only `toolResult` messages.
|
- Only `toolResult` messages.
|
||||||
@ -26,14 +40,10 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
|
|||||||
3) `agents.defaults.contextTokens`.
|
3) `agents.defaults.contextTokens`.
|
||||||
4) Default `200000` tokens.
|
4) Default `200000` tokens.
|
||||||
|
|
||||||
## Modes
|
## Mode
|
||||||
### adaptive
|
### cache-ttl
|
||||||
- If estimated context ratio ≥ `softTrimRatio`: soft-trim oversized tool results.
|
- Pruning only runs if the last Anthropic call is older than `ttl` (default `5m`).
|
||||||
- If still ≥ `hardClearRatio` **and** prunable tool text ≥ `minPrunableToolChars`: hard-clear oldest eligible tool results.
|
- When it runs: same soft-trim + hard-clear behavior as before.
|
||||||
|
|
||||||
### aggressive
|
|
||||||
- Always hard-clears eligible tool results before the cutoff.
|
|
||||||
- Ignores `hardClear.enabled` (always clears when eligible).
|
|
||||||
|
|
||||||
## Soft vs hard pruning
|
## Soft vs hard pruning
|
||||||
- **Soft-trim**: only for oversized tool results.
|
- **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).
|
- Compaction is separate: compaction summarizes and persists, pruning is transient per request. See [/concepts/compaction](/concepts/compaction).
|
||||||
|
|
||||||
## Defaults (when enabled)
|
## Defaults (when enabled)
|
||||||
|
- `ttl`: `"5m"`
|
||||||
- `keepLastAssistants`: `3`
|
- `keepLastAssistants`: `3`
|
||||||
- `softTrimRatio`: `0.3`
|
- `softTrimRatio`: `0.3`
|
||||||
- `hardClearRatio`: `0.5`
|
- `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]" }`
|
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
Default (adaptive):
|
Default (off):
|
||||||
```json5
|
|
||||||
{
|
|
||||||
agent: {
|
|
||||||
contextPruning: { mode: "adaptive" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To disable:
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agent: {
|
||||||
@ -78,11 +80,11 @@ To disable:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Aggressive:
|
Enable TTL-aware pruning:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agent: {
|
||||||
contextPruning: { mode: "aggressive" }
|
contextPruning: { mode: "cache-ttl", ttl: "5m" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -92,7 +94,7 @@ Restrict pruning to specific tools:
|
|||||||
{
|
{
|
||||||
agent: {
|
agent: {
|
||||||
contextPruning: {
|
contextPruning: {
|
||||||
mode: "adaptive",
|
mode: "cache-ttl",
|
||||||
tools: { allow: ["exec", "read"], deny: ["*image*"] }
|
tools: { allow: ["exec", "read"], deny: ["*image*"] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
- 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.
|
- 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-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 <model>` 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.
|
- 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 <model>` 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.
|
- 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).
|
- 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 },
|
dm: { mode: "idle", idleMinutes: 240 },
|
||||||
group: { mode: "idle", idleMinutes: 120 }
|
group: { mode: "idle", idleMinutes: 120 }
|
||||||
},
|
},
|
||||||
|
resetByChannel: {
|
||||||
|
discord: { mode: "idle", idleMinutes: 10080 }
|
||||||
|
},
|
||||||
resetTriggers: ["/new", "/reset"],
|
resetTriggers: ["/new", "/reset"],
|
||||||
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
|
|||||||
@ -24,7 +24,7 @@ The prompt is intentionally compact and uses fixed sections:
|
|||||||
- **Current Date & Time**: user-local time, timezone, and time format.
|
- **Current Date & Time**: user-local time, timezone, and time format.
|
||||||
- **Reply Tags**: optional reply tag syntax for supported providers.
|
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||||
- **Heartbeats**: heartbeat prompt and ack behavior.
|
- **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.
|
- **Reasoning**: current visibility level + /reasoning toggle hint.
|
||||||
|
|
||||||
## Prompt modes
|
## Prompt modes
|
||||||
|
|||||||
@ -9,15 +9,15 @@ read_when:
|
|||||||
|
|
||||||
Clawdbot standardizes timestamps so the model sees a **single reference time**.
|
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:
|
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:
|
You can override this with:
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ You can override this with:
|
|||||||
{
|
{
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
envelopeTimezone: "user", // "utc" | "local" | "user" | IANA timezone
|
envelopeTimezone: "local", // "utc" | "local" | "user" | IANA timezone
|
||||||
envelopeTimestamp: "on", // "on" | "off"
|
envelopeTimestamp: "on", // "on" | "off"
|
||||||
envelopeElapsed: "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).
|
- `envelopeTimezone: "user"` uses `agents.defaults.userTimezone` (falls back to host timezone).
|
||||||
- Use an explicit IANA timezone (e.g., `"Europe/Vienna"`) for a fixed offset.
|
- Use an explicit IANA timezone (e.g., `"Europe/Vienna"`) for a fixed offset.
|
||||||
- `envelopeTimestamp: "off"` removes absolute timestamps from envelope headers.
|
- `envelopeTimestamp: "off"` removes absolute timestamps from envelope headers.
|
||||||
@ -40,10 +41,10 @@ You can override this with:
|
|||||||
|
|
||||||
### Examples
|
### 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:**
|
**Fixed timezone:**
|
||||||
|
|||||||
@ -7,18 +7,18 @@ read_when:
|
|||||||
|
|
||||||
# Date & Time
|
# 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.
|
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:
|
You can override this behavior:
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ You can override this behavior:
|
|||||||
{
|
{
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
envelopeTimezone: "utc", // "utc" | "local" | "user" | IANA timezone
|
envelopeTimezone: "local", // "utc" | "local" | "user" | IANA timezone
|
||||||
envelopeTimestamp: "on", // "on" | "off"
|
envelopeTimestamp: "on", // "on" | "off"
|
||||||
envelopeElapsed: "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: "local"` uses the host timezone.
|
||||||
- `envelopeTimezone: "user"` uses `agents.defaults.userTimezone` (falls back to 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.
|
- Use an explicit IANA timezone (e.g., `"America/Chicago"`) for a fixed zone.
|
||||||
@ -42,10 +43,10 @@ You can override this behavior:
|
|||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
**UTC (default):**
|
**Local (default):**
|
||||||
|
|
||||||
```
|
```
|
||||||
[WhatsApp +1555 2026-01-18T05:19Z] hello
|
[WhatsApp +1555 2026-01-18 00:19 PST] hello
|
||||||
```
|
```
|
||||||
|
|
||||||
**User timezone:**
|
**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
|
If only the timezone is known, we still include the section and instruct the model
|
||||||
to assume UTC for unknown time references.
|
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
|
### Configure user timezone + format
|
||||||
|
|||||||
@ -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:
|
Tip: if a non‑dev gateway is already running (launchd/systemd), stop it first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Raw stream logging (Clawdbot)
|
## Raw stream logging (Clawdbot)
|
||||||
|
|||||||
@ -845,8 +845,6 @@
|
|||||||
"cli/nodes",
|
"cli/nodes",
|
||||||
"cli/approvals",
|
"cli/approvals",
|
||||||
"cli/gateway",
|
"cli/gateway",
|
||||||
"cli/daemon",
|
|
||||||
"cli/service",
|
|
||||||
"cli/tui",
|
"cli/tui",
|
||||||
"cli/voicecall",
|
"cli/voicecall",
|
||||||
"cli/wake",
|
"cli/wake",
|
||||||
|
|||||||
@ -58,8 +58,8 @@ Exact allowlist is enforced in `src/gateway/server-bridge.ts`.
|
|||||||
|
|
||||||
## Exec lifecycle events
|
## Exec lifecycle events
|
||||||
|
|
||||||
Nodes can emit `exec.started`, `exec.finished`, or `exec.denied` events to surface
|
Nodes can emit `exec.finished` or `exec.denied` events to surface system.run activity.
|
||||||
system.run activity. These are mapped to system events in the gateway.
|
These are mapped to system events in the gateway. (Legacy nodes may still emit `exec.started`.)
|
||||||
|
|
||||||
Payload fields (all optional unless noted):
|
Payload fields (all optional unless noted):
|
||||||
- `sessionKey` (required): agent session to receive the system event.
|
- `sessionKey` (required): agent session to receive the system event.
|
||||||
|
|||||||
@ -151,7 +151,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
|||||||
atHour: 4,
|
atHour: 4,
|
||||||
idleMinutes: 60
|
idleMinutes: 60
|
||||||
},
|
},
|
||||||
heartbeatIdleMinutes: 120,
|
resetByChannel: {
|
||||||
|
discord: { mode: "idle", idleMinutes: 10080 }
|
||||||
|
},
|
||||||
resetTriggers: ["/new", "/reset"],
|
resetTriggers: ["/new", "/reset"],
|
||||||
store: "~/.clawdbot/agents/default/sessions/sessions.json",
|
store: "~/.clawdbot/agents/default/sessions/sessions.json",
|
||||||
typingIntervalSeconds: 5,
|
typingIntervalSeconds: 5,
|
||||||
|
|||||||
@ -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):
|
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 👀)
|
- `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)
|
- `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
|
```json5
|
||||||
{
|
{
|
||||||
agents: {
|
agents: {
|
||||||
list: [
|
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
|
If `agents.defaults.sandbox` is enabled, non-main sessions can override this with their
|
||||||
own per-scope workspaces under `agents.defaults.sandbox.workspaceRoot`.
|
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`
|
### `agents.defaults.skipBootstrap`
|
||||||
|
|
||||||
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
|
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`).
|
- `alias` (optional model shortcut, e.g. `/opus`).
|
||||||
- `params` (optional provider-specific API params passed through to the model request).
|
- `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:
|
Example:
|
||||||
|
|
||||||
@ -1772,8 +1798,9 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
|||||||
`30m`. Set `0m` to disable.
|
`30m`. Set `0m` to disable.
|
||||||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
- `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`.
|
- `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).
|
- `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.
|
- `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).
|
- `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.maxChars` (default 50000)
|
||||||
- `tools.web.fetch.timeoutSeconds` (default 30)
|
- `tools.web.fetch.timeoutSeconds` (default 30)
|
||||||
- `tools.web.fetch.cacheTtlMinutes` (default 15)
|
- `tools.web.fetch.cacheTtlMinutes` (default 15)
|
||||||
- `tools.web.fetch.maxRedirects` (default 3)
|
|
||||||
- `tools.web.fetch.userAgent` (optional override)
|
- `tools.web.fetch.userAgent` (optional override)
|
||||||
- `tools.web.fetch.readability` (default true; disable to use basic HTML cleanup only)
|
- `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)
|
- `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:
|
`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.
|
- `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)
|
- `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)
|
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
||||||
|
|
||||||
@ -1999,13 +2025,13 @@ Per-agent override (further restrict):
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow).
|
- `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.
|
- Elevated `exec` runs on the host and bypasses sandboxing.
|
||||||
- Tool policy still applies; if `exec` is denied, elevated cannot be used.
|
- 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
|
`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
|
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`
|
### `agents.defaults.sandbox`
|
||||||
|
|
||||||
@ -2645,13 +2671,10 @@ Defaults:
|
|||||||
// noSandbox: false,
|
// noSandbox: false,
|
||||||
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
||||||
// attachOnly: false, // set true when tunneling a remote CDP to localhost
|
// 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)
|
### `ui` (Appearance)
|
||||||
|
|
||||||
Optional accent color used by the native apps for UI chrome (e.g. Talk Mode bubble tint).
|
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
|
```json5
|
||||||
{
|
{
|
||||||
ui: {
|
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.
|
- `gateway.controlUi.basePath` sets the URL prefix where the Control UI is served.
|
||||||
- Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`.
|
- Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`.
|
||||||
- Default: root (`/`) (unchanged).
|
- 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:
|
Related docs:
|
||||||
- [Control UI](/web/control-ui)
|
- [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).
|
- `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).
|
- `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`.
|
- 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`.
|
- 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`).
|
- 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).
|
- The onboarding wizard generates a gateway token by default (even on loopback).
|
||||||
@ -2711,7 +2741,7 @@ Notes:
|
|||||||
|
|
||||||
Auth and Tailscale:
|
Auth and Tailscale:
|
||||||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
- `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).
|
- 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.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended).
|
||||||
- `gateway.auth.allowTailscale` allows Tailscale Serve identity headers
|
- `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
|
`true`, Serve requests do not need a token/password; set `false` to require
|
||||||
explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and
|
explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and
|
||||||
auth mode is not `password`.
|
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: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
|
||||||
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
||||||
- `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown.
|
- `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.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.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.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:
|
macOS app behavior:
|
||||||
- Clawdbot.app watches `~/.clawdbot/clawdbot.json` and switches modes live when `gateway.mode` or `gateway.remote.url` changes.
|
- 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: {
|
remote: {
|
||||||
url: "ws://gateway.tailnet:18789",
|
url: "ws://gateway.tailnet:18789",
|
||||||
token: "your-token",
|
token: "your-token",
|
||||||
password: "your-password",
|
password: "your-password"
|
||||||
tlsFingerprint: "sha256:ab12cd34..."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `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)
|
### `gateway.reload` (Config hot reload)
|
||||||
|
|
||||||
The Gateway watches `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`) and applies changes automatically.
|
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)
|
### `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:
|
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)
|
- a DNS server on the gateway host serving `clawdbot.internal.` (CoreDNS is recommended)
|
||||||
|
|||||||
@ -225,10 +225,10 @@ Notes:
|
|||||||
- `clawdbot doctor --yes` accepts the default repair prompts.
|
- `clawdbot doctor --yes` accepts the default repair prompts.
|
||||||
- `clawdbot doctor --repair` applies recommended fixes without prompts.
|
- `clawdbot doctor --repair` applies recommended fixes without prompts.
|
||||||
- `clawdbot doctor --repair --force` overwrites custom supervisor configs.
|
- `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
|
### 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
|
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
|
on the gateway port (default `18789`) and reports likely causes (gateway already
|
||||||
running, SSH tunnel).
|
running, SSH tunnel).
|
||||||
@ -236,7 +236,7 @@ running, SSH tunnel).
|
|||||||
### 17) Gateway runtime best practices
|
### 17) Gateway runtime best practices
|
||||||
Doctor warns when the gateway service runs on Bun or a version-managed Node path
|
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,
|
(`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
|
load your shell init. Doctor offers to migrate to a system Node install when
|
||||||
available (Homebrew/apt/choco).
|
available (Homebrew/apt/choco).
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,11 @@ surface anything that needs attention without spamming you.
|
|||||||
|
|
||||||
## Quick start (beginner)
|
## 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).
|
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).
|
3. Decide where heartbeat messages should go (`target: "last"` is the default).
|
||||||
4. Optional: enable heartbeat reasoning delivery for transparency.
|
4. Optional: enable heartbeat reasoning delivery for transparency.
|
||||||
|
5. Optional: restrict heartbeats to active hours (local time).
|
||||||
|
|
||||||
Example config:
|
Example config:
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ Example config:
|
|||||||
heartbeat: {
|
heartbeat: {
|
||||||
every: "30m",
|
every: "30m",
|
||||||
target: "last",
|
target: "last",
|
||||||
|
// activeHours: { start: "08:00", end: "24:00" },
|
||||||
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,11 +35,13 @@ Example config:
|
|||||||
|
|
||||||
## Defaults
|
## 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`):
|
- 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.`
|
`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
|
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
||||||
prompt includes a “Heartbeat” section and the run is flagged internally.
|
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
|
## 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).
|
- `every`: heartbeat interval (duration string; default unit = minutes).
|
||||||
- `model`: optional model override for heartbeat runs (`provider/model`).
|
- `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`).
|
- `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`:
|
- `target`:
|
||||||
- `last` (default): deliver to the last used external channel.
|
- `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.
|
- `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).
|
- `prompt`: overrides the default prompt body (not merged).
|
||||||
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
|
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
|
||||||
|
|
||||||
## Delivery behavior
|
## Delivery behavior
|
||||||
|
|
||||||
- Heartbeats run in each agent’s **main session** (`agent:<id>:<mainKey>`), or `global`
|
- Heartbeats run in the agent’s main session by default (`agent:<id>:<mainKey>`),
|
||||||
when `session.scope = "global"`.
|
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 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
|
- If `target` resolves to no external destination, the run still happens but no
|
||||||
outbound message is sent.
|
outbound message is sent.
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
summary: "Runbook for the Gateway daemon, lifecycle, and operations"
|
summary: "Runbook for the Gateway service, lifecycle, and operations"
|
||||||
read_when:
|
read_when:
|
||||||
- Running or debugging the gateway process
|
- Running or debugging the gateway process
|
||||||
---
|
---
|
||||||
# Gateway (daemon) runbook
|
# Gateway service runbook
|
||||||
|
|
||||||
Last updated: 2025-12-09
|
Last updated: 2025-12-09
|
||||||
|
|
||||||
@ -101,10 +101,10 @@ Checklist per instance:
|
|||||||
- unique `agents.defaults.workspace`
|
- unique `agents.defaults.workspace`
|
||||||
- separate WhatsApp numbers (if using WA)
|
- separate WhatsApp numbers (if using WA)
|
||||||
|
|
||||||
Daemon install per profile:
|
Service install per profile:
|
||||||
```bash
|
```bash
|
||||||
clawdbot --profile main daemon install
|
clawdbot --profile main gateway install
|
||||||
clawdbot --profile rescue daemon install
|
clawdbot --profile rescue gateway install
|
||||||
```
|
```
|
||||||
|
|
||||||
Example:
|
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.
|
- 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)
|
## Supervision (macOS example)
|
||||||
- Use launchd to keep the daemon alive:
|
- Use launchd to keep the service alive:
|
||||||
- Program: path to `clawdbot`
|
- Program: path to `clawdbot`
|
||||||
- Arguments: `gateway`
|
- Arguments: `gateway`
|
||||||
- KeepAlive: true
|
- KeepAlive: true
|
||||||
- StandardOut/Err: file paths or `syslog`
|
- StandardOut/Err: file paths or `syslog`
|
||||||
- On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices.
|
- 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).
|
- 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.<profile>.plist`).
|
(or `com.clawdbot.<profile>.plist`).
|
||||||
- `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults.
|
- `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
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
clawdbot daemon install
|
clawdbot gateway install
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
clawdbot logs --follow
|
clawdbot logs --follow
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url`).
|
- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url`).
|
||||||
- `daemon status --deep` adds system-level scans (LaunchDaemons/system units).
|
- `gateway status --deep` adds system-level scans (LaunchDaemons/system units).
|
||||||
- `daemon status --no-probe` skips the RPC probe (useful when networking is down).
|
- `gateway status --no-probe` skips the RPC probe (useful when networking is down).
|
||||||
- `daemon status --json` is stable for scripts.
|
- `gateway status --json` is stable for scripts.
|
||||||
- `daemon status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC).
|
- `gateway 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.
|
- `gateway 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` 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).
|
- `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.
|
- 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).
|
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).
|
- Cleanup: `clawdbot gateway 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).
|
- `gateway install` is a no-op when already installed; use `clawdbot gateway install --force` to reinstall (profile/env/path changes).
|
||||||
|
|
||||||
Bundled mac app:
|
Bundled mac app:
|
||||||
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled
|
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled
|
||||||
`com.clawdbot.gateway` (or `com.clawdbot.<profile>`).
|
`com.clawdbot.gateway` (or `com.clawdbot.<profile>`).
|
||||||
- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
|
- To stop it cleanly, use `clawdbot gateway stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
|
||||||
- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k 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 daemon install` first.
|
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot gateway install` first.
|
||||||
- Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
- Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||||
|
|
||||||
## Supervision (systemd user unit)
|
## 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
|
Use a **system service** for multi-user or always-on servers (no lingering
|
||||||
required, shared supervision).
|
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.
|
unit and can update it to match the current recommended defaults.
|
||||||
|
|
||||||
Create `~/.config/systemd/user/clawdbot-gateway[-<profile>].service`:
|
Create `~/.config/systemd/user/clawdbot-gateway[-<profile>].service`:
|
||||||
@ -285,7 +285,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
|||||||
- `clawdbot message send --target <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
- `clawdbot message send --target <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
|
||||||
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
|
||||||
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
- `clawdbot gateway call <method> --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.
|
- Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one.
|
||||||
|
|
||||||
## Migration guidance
|
## Migration guidance
|
||||||
|
|||||||
@ -17,6 +17,7 @@ Clawdbot has two log “surfaces”:
|
|||||||
## File-based logger
|
## File-based logger
|
||||||
|
|
||||||
- Default rolling log file is under `/tmp/clawdbot/` (one file per day): `clawdbot-YYYY-MM-DD.log`
|
- 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`:
|
- The log file path and level can be configured via `~/.clawdbot/clawdbot.json`:
|
||||||
- `logging.file`
|
- `logging.file`
|
||||||
- `logging.level`
|
- `logging.level`
|
||||||
|
|||||||
@ -31,10 +31,10 @@ clawdbot --profile rescue setup
|
|||||||
clawdbot --profile rescue gateway --port 19001
|
clawdbot --profile rescue gateway --port 19001
|
||||||
```
|
```
|
||||||
|
|
||||||
Per-profile daemons:
|
Per-profile services:
|
||||||
```bash
|
```bash
|
||||||
clawdbot --profile main daemon install
|
clawdbot --profile main gateway install
|
||||||
clawdbot --profile rescue daemon install
|
clawdbot --profile rescue gateway install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rescue-bot guide
|
## 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)
|
# Main bot (existing or fresh, without --profile param)
|
||||||
# Runs on port 18789 + Chrome CDC/Canvas/... Ports
|
# Runs on port 18789 + Chrome CDC/Canvas/... Ports
|
||||||
clawdbot onboard
|
clawdbot onboard
|
||||||
clawdbot daemon install
|
clawdbot gateway install
|
||||||
|
|
||||||
# Rescue bot (isolated profile + ports)
|
# Rescue bot (isolated profile + ports)
|
||||||
clawdbot --profile rescue onboard
|
clawdbot --profile rescue onboard
|
||||||
@ -65,8 +65,8 @@ clawdbot --profile rescue onboard
|
|||||||
# better choose completely different base port, like 19789,
|
# better choose completely different base port, like 19789,
|
||||||
# - rest of the onboarding is the same as normal
|
# - rest of the onboarding is the same as normal
|
||||||
|
|
||||||
# To install the daemon (if not happened automatically during onboarding)
|
# To install the service (if not happened automatically during onboarding)
|
||||||
clawdbot --profile rescue daemon install
|
clawdbot --profile rescue gateway install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Port mapping (derived)
|
## Port mapping (derived)
|
||||||
|
|||||||
@ -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
|
- **Local** connects include loopback and the gateway host’s own tailnet address
|
||||||
(so same‑host tailnet binds can still auto‑approve).
|
(so same‑host tailnet binds can still auto‑approve).
|
||||||
- All WS clients must include `device` identity during `connect` (operator + node).
|
- 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.
|
- Non-local connections must sign the server-provided `connect.challenge` nonce.
|
||||||
|
|
||||||
## TLS + pinning
|
## TLS + pinning
|
||||||
|
|||||||
@ -50,7 +50,7 @@ Guide: [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
|||||||
|
|
||||||
## Command flow (what runs where)
|
## 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):
|
Flow example (Telegram → node):
|
||||||
- Telegram message arrives at the **Gateway**.
|
- Telegram message arrives at the **Gateway**.
|
||||||
@ -59,7 +59,7 @@ Flow example (Telegram → node):
|
|||||||
- Node returns the result; Gateway replies back out to Telegram.
|
- Node returns the result; Gateway replies back out to Telegram.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- **Nodes do not run the gateway 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.
|
- macOS app “node mode” is just a node client over the Bridge.
|
||||||
|
|
||||||
## SSH tunnel (CLI + tools)
|
## 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.
|
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).
|
- **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.token` is **only** for remote CLI calls — it does **not** enable local auth.
|
||||||
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
||||||
- **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`.
|
- **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`.
|
||||||
|
|||||||
@ -91,7 +91,8 @@ Available groups:
|
|||||||
## Elevated: exec-only “run on host”
|
## Elevated: exec-only “run on host”
|
||||||
|
|
||||||
Elevated does **not** grant extra tools; it only affects `exec`.
|
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).
|
- 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.
|
- Elevated is **not** skill-scoped and does **not** override tool allow/deny.
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,15 @@ When the audit prints findings, treat this as a priority order:
|
|||||||
5. **Plugins/extensions**: only load what you explicitly trust.
|
5. **Plugins/extensions**: only load what you explicitly trust.
|
||||||
6. **Model choice**: prefer modern, instruction-hardened models for any bot with tools.
|
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
|
## Local session logs live on disk
|
||||||
|
|
||||||
Clawdbot stores session transcripts on disk under `~/.clawdbot/agents/<agentId>/sessions/*.jsonl`.
|
Clawdbot stores session transcripts on disk under `~/.clawdbot/agents/<agentId>/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.
|
- 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)).
|
- **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)
|
### 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.
|
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.
|
- **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).
|
- 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.
|
- 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
|
## 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:
|
Bind mode controls where the Gateway listens:
|
||||||
- `gateway.bind: "loopback"` (default): only local clients can connect.
|
- `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:
|
Rules of thumb:
|
||||||
- Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access).
|
- Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access).
|
||||||
|
|||||||
@ -46,6 +46,25 @@ force `gateway.auth.mode: "password"`.
|
|||||||
|
|
||||||
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
|
Open: `https://<magicdns>/` (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://<tailscale-ip>:18789/`
|
||||||
|
- WebSocket: `ws://<tailscale-ip>:18789`
|
||||||
|
|
||||||
|
Note: loopback (`http://127.0.0.1:18789`) will **not** work in this mode.
|
||||||
|
|
||||||
### Public internet (Funnel + shared password)
|
### Public internet (Funnel + shared password)
|
||||||
|
|
||||||
```json5
|
```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.
|
- `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`
|
- Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve`
|
||||||
or `tailscale funnel` configuration on shutdown.
|
or `tailscale funnel` configuration on shutdown.
|
||||||
|
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
|
||||||
|
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
|
||||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
|
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
|
||||||
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.
|
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,12 @@ Quick triage commands (in order):
|
|||||||
|
|
||||||
| Command | What it tells you | When to use it |
|
| 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 --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 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 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 |
|
| `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).
|
**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
|
## Common Issues
|
||||||
|
|
||||||
|
### Control UI fails on HTTP ("device identity required" / "connect failed")
|
||||||
|
|
||||||
|
If you open the dashboard over plain HTTP (e.g. `http://<lan-ip>:18789/` or
|
||||||
|
`http://<tailscale-ip>: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
|
### CI Secrets Scan Failed
|
||||||
|
|
||||||
This means `detect-secrets` found new candidates not yet in the baseline.
|
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
|
### 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.
|
can appear “loaded” while nothing is running.
|
||||||
|
|
||||||
**Check:**
|
**Check:**
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
clawdbot doctor
|
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:**
|
**Logs:**
|
||||||
- Preferred: `clawdbot logs --follow`
|
- 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.
|
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)
|
### 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`
|
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||||
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
|
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||||
|
|
||||||
This intentionally excludes version managers (nvm/fnm/volta/asdf) and package
|
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
|
variables like `DISPLAY` should live in `~/.clawdbot/.env` (loaded early by the
|
||||||
gateway).
|
gateway).
|
||||||
Exec runs on `host=gateway` merge your login-shell `PATH` into the exec environment,
|
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**
|
**What "running" means here**
|
||||||
- `Runtime: running` means your supervisor (launchd/systemd/schtasks) thinks the process is alive.
|
- `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`.
|
- `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:**
|
**Check:**
|
||||||
- `gateway.mode` must be `local` for `clawdbot gateway` and the daemon.
|
- `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 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`).
|
- 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 daemon status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed.
|
- `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`/`auto`) require auth:
|
- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth:
|
||||||
`gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
`gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||||
- `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth.
|
- `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth.
|
||||||
- `gateway.token` is ignored; use `gateway.auth.token`.
|
- `gateway.token` is ignored; use `gateway.auth.token`.
|
||||||
|
|
||||||
**If `clawdbot daemon status` shows a config mismatch**
|
**If `clawdbot gateway status` shows a config mismatch**
|
||||||
- `Config (cli): ...` and `Config (daemon): ...` should normally match.
|
- `Config (cli): ...` and `Config (service): ...` should normally match.
|
||||||
- If they don’t, you’re almost certainly editing one config while the daemon is running another.
|
- If they don’t, you’re almost certainly editing one config while the service is running another.
|
||||||
- Fix: rerun `clawdbot daemon install --force` from the same `--profile` / `CLAWDBOT_STATE_DIR` you want the daemon to use.
|
- 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.
|
- 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”**
|
**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.
|
- 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 daemon.
|
- 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.
|
- 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`).
|
- 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:**
|
**Check:**
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
```
|
```
|
||||||
|
|
||||||
It will show the listener(s) and likely causes (gateway already running, SSH tunnel).
|
It will show the listener(s) and likely causes (gateway already running, SSH tunnel).
|
||||||
@ -354,7 +395,7 @@ clawdbot doctor --fix
|
|||||||
Notes:
|
Notes:
|
||||||
- `clawdbot doctor` reports every invalid entry.
|
- `clawdbot doctor` reports every invalid entry.
|
||||||
- `clawdbot doctor --fix` applies migrations/repairs and rewrites the config.
|
- `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?
|
### “All models failed” — what should I check first?
|
||||||
|
|
||||||
@ -407,7 +448,7 @@ git status # ensure you’re in the repo root
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
```
|
```
|
||||||
|
|
||||||
Why: pnpm is the configured package manager for this repo.
|
Why: pnpm is the configured package manager for this repo.
|
||||||
@ -432,7 +473,7 @@ Notes:
|
|||||||
- After switching, run:
|
- After switching, run:
|
||||||
```bash
|
```bash
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Telegram block streaming isn’t splitting text between tool calls. Why?
|
### 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)**
|
**Fix 1: Stop the supervisor (preferred)**
|
||||||
If the gateway is supervised by launchd, killing the PID will just respawn it. Stop the supervisor first:
|
If the gateway is supervised by launchd, killing the PID will just respawn it. Stop the supervisor first:
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway (replace with com.clawdbot.<profile> if needed)
|
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway (replace with com.clawdbot.<profile> if needed)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -558,9 +599,9 @@ clawdbot channels login --verbose
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Supervisor + probe target + config paths
|
# Supervisor + probe target + config paths
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
# Include system-level scans (legacy/extra services, port listeners)
|
# Include system-level scans (legacy/extra services, port listeners)
|
||||||
clawdbot daemon status --deep
|
clawdbot gateway status --deep
|
||||||
|
|
||||||
# Is the gateway reachable?
|
# Is the gateway reachable?
|
||||||
clawdbot health --json
|
clawdbot health --json
|
||||||
@ -581,13 +622,13 @@ tail -20 /tmp/clawdbot/clawdbot-*.log
|
|||||||
Nuclear option:
|
Nuclear option:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
# If you installed a service and want a clean install:
|
# If you installed a service and want a clean install:
|
||||||
# clawdbot daemon uninstall
|
# clawdbot gateway uninstall
|
||||||
|
|
||||||
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
|
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
|
||||||
clawdbot channels login # re-pair WhatsApp
|
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.
|
⚠️ This loses all sessions and requires re-pairing WhatsApp.
|
||||||
|
|||||||
@ -14,7 +14,7 @@ Run these in order:
|
|||||||
```bash
|
```bash
|
||||||
clawdbot status
|
clawdbot status
|
||||||
clawdbot status --all
|
clawdbot status --all
|
||||||
clawdbot daemon status
|
clawdbot gateway probe
|
||||||
clawdbot logs --follow
|
clawdbot logs --follow
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
```
|
```
|
||||||
@ -38,16 +38,30 @@ Almost always a Node/npm PATH issue. Start here:
|
|||||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
- [Gateway authentication](/gateway/authentication)
|
- [Gateway authentication](/gateway/authentication)
|
||||||
|
|
||||||
### Daemon says running, but RPC probe fails
|
### Control UI fails on HTTP (device identity required)
|
||||||
|
|
||||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
- [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”)
|
### Model/auth failures (rate limit, billing, “all models failed”)
|
||||||
|
|
||||||
- [Models](/cli/models)
|
- [Models](/cli/models)
|
||||||
- [OAuth / auth concepts](/concepts/oauth)
|
- [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
|
### When filing an issue
|
||||||
|
|
||||||
Paste a safe report:
|
Paste a safe report:
|
||||||
|
|||||||
@ -104,13 +104,13 @@ Runtime requirement: **Node ≥ 22**.
|
|||||||
npm install -g clawdbot@latest
|
npm install -g clawdbot@latest
|
||||||
# or: pnpm add -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
|
clawdbot onboard --install-daemon
|
||||||
|
|
||||||
# Pair WhatsApp Web (shows QR)
|
# Pair WhatsApp Web (shows QR)
|
||||||
clawdbot channels login
|
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
|
clawdbot gateway --port 18789
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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
|
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:
|
Or:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -155,18 +157,21 @@ Quick diagnosis:
|
|||||||
```bash
|
```bash
|
||||||
node -v
|
node -v
|
||||||
npm -v
|
npm -v
|
||||||
npm bin -g
|
npm prefix -g
|
||||||
echo "$PATH"
|
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`):
|
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
|
||||||
|
|
||||||
```bash
|
```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).
|
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
|
||||||
|
|
||||||
## Update / uninstall
|
## Update / uninstall
|
||||||
|
|||||||
@ -19,33 +19,36 @@ Run:
|
|||||||
```bash
|
```bash
|
||||||
node -v
|
node -v
|
||||||
npm -v
|
npm -v
|
||||||
npm bin -g
|
npm prefix -g
|
||||||
echo "$PATH"
|
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
|
## Fix: put npm’s global bin dir on PATH
|
||||||
|
|
||||||
1) Find your global bin directory:
|
1) Find your global npm prefix:
|
||||||
|
|
||||||
```bash
|
```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`
|
- zsh: `~/.zshrc`
|
||||||
- bash: `~/.bashrc`
|
- bash: `~/.bashrc`
|
||||||
|
|
||||||
Example (replace the path with your `npm bin -g` output):
|
Example (replace the path with your `npm prefix -g` output):
|
||||||
|
|
||||||
```bash
|
```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).
|
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)
|
## 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:
|
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:
|
You’ll have the fewest surprises if Node/npm are installed in a way that:
|
||||||
|
|
||||||
- keeps Node updated (22+)
|
- 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:
|
Common choices:
|
||||||
|
|
||||||
|
|||||||
@ -31,13 +31,13 @@ Manual steps (same result):
|
|||||||
1) Stop the gateway service:
|
1) Stop the gateway service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Uninstall the gateway service (launchd/systemd/schtasks):
|
2) Uninstall the gateway service (launchd/systemd/schtasks):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon uninstall
|
clawdbot gateway uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
3) Delete state + config:
|
3) Delete state + config:
|
||||||
|
|||||||
@ -68,12 +68,12 @@ Then:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
clawdbot health
|
clawdbot health
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
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.
|
- If you’re pinned to a specific version, see “Rollback / pinning” below.
|
||||||
|
|
||||||
## Update (`clawdbot update`)
|
## Update (`clawdbot update`)
|
||||||
@ -148,9 +148,9 @@ Details: [Doctor](/gateway/doctor)
|
|||||||
CLI (works regardless of OS):
|
CLI (works regardless of OS):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
clawdbot gateway --port 18789
|
clawdbot gateway --port 18789
|
||||||
clawdbot logs --follow
|
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.<profile>` if set)
|
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/com.clawdbot.gateway` (use `com.clawdbot.<profile>` if set)
|
||||||
- Linux systemd user service: `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
- Linux systemd user service: `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
||||||
- Windows (WSL2): `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
- Windows (WSL2): `systemctl --user restart clawdbot-gateway[-<profile>].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)
|
Runbook + exact service labels: [Gateway runbook](/gateway)
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ Then restart + re-run doctor:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pin (source) by date
|
### Pin (source) by date
|
||||||
@ -200,7 +200,7 @@ Then reinstall deps + restart:
|
|||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to go back to latest later:
|
If you want to go back to latest later:
|
||||||
|
|||||||
@ -22,6 +22,8 @@ By default, the Gateway writes a rolling log file under:
|
|||||||
|
|
||||||
`/tmp/clawdbot/clawdbot-YYYY-MM-DD.log`
|
`/tmp/clawdbot/clawdbot-YYYY-MM-DD.log`
|
||||||
|
|
||||||
|
The date uses the gateway host's local timezone.
|
||||||
|
|
||||||
You can override this in `~/.clawdbot/clawdbot.json`:
|
You can override this in `~/.clawdbot/clawdbot.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@ -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).
|
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:
|
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.
|
- Telegram/WhatsApp/etc. messages land on the **gateway**, not on nodes.
|
||||||
|
|
||||||
## Pairing + status
|
## Pairing + status
|
||||||
@ -50,14 +50,14 @@ forwards `exec` calls to the **node host** when `host=node` is selected.
|
|||||||
On the node machine:
|
On the node machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node start --host <gateway-host> --port 18789 --display-name "Build Node"
|
clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start a node host (service)
|
### Start a node host (service)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node service install --host <gateway-host> --port 18789 --display-name "Build Node"
|
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||||
clawdbot node service start
|
clawdbot node start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pair + name
|
### Pair + name
|
||||||
@ -71,7 +71,7 @@ clawdbot nodes list
|
|||||||
```
|
```
|
||||||
|
|
||||||
Naming options:
|
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 <id|name|ip> --name "Build Node"` (gateway override).
|
- `clawdbot nodes rename --node <id|name|ip> --name "Build Node"` (gateway override).
|
||||||
|
|
||||||
### Allowlist the commands
|
### Allowlist the commands
|
||||||
@ -281,7 +281,7 @@ or for running a minimal node alongside a server.
|
|||||||
Start it:
|
Start it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node start --host <gateway-host> --port 18790
|
clawdbot node run --host <gateway-host> --port 18790
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|||||||
@ -97,7 +97,7 @@ It can set up:
|
|||||||
- `~/.clawdbot/clawdbot.json` config
|
- `~/.clawdbot/clawdbot.json` config
|
||||||
- model auth profiles
|
- model auth profiles
|
||||||
- model provider config/login
|
- 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)).
|
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"
|
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
|
```json5
|
||||||
{
|
{
|
||||||
@ -159,7 +159,7 @@ Notes:
|
|||||||
|
|
||||||
Control UI details: [Control UI](/web/control-ui)
|
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:
|
On Linux, Clawdbot uses a systemd **user** service. After `--install-daemon`, verify:
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ More: [Linux](/platforms/linux)
|
|||||||
```bash
|
```bash
|
||||||
npm i -g clawdbot@latest
|
npm i -g clawdbot@latest
|
||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot gateway restart
|
||||||
clawdbot health
|
clawdbot health
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -31,15 +31,15 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
|||||||
- Install guide: [Getting Started](/start/getting-started)
|
- Install guide: [Getting Started](/start/getting-started)
|
||||||
- Gateway runbook: [Gateway](/gateway)
|
- Gateway runbook: [Gateway](/gateway)
|
||||||
- Gateway configuration: [Configuration](/gateway/configuration)
|
- Gateway configuration: [Configuration](/gateway/configuration)
|
||||||
- Service status: `clawdbot daemon status`
|
- Service status: `clawdbot gateway status`
|
||||||
|
|
||||||
## Gateway service install (CLI)
|
## Gateway service install (CLI)
|
||||||
|
|
||||||
Use one of these (all supported):
|
Use one of these (all supported):
|
||||||
|
|
||||||
- Wizard (recommended): `clawdbot onboard --install-daemon`
|
- Wizard (recommended): `clawdbot onboard --install-daemon`
|
||||||
- Direct: `clawdbot daemon install`
|
- Direct: `clawdbot gateway install`
|
||||||
- Configure flow: `clawdbot configure` → select **Gateway daemon**
|
- Configure flow: `clawdbot configure` → select **Gateway service**
|
||||||
- Repair/migrate: `clawdbot doctor` (offers to install or fix the service)
|
- Repair/migrate: `clawdbot doctor` (offers to install or fix the service)
|
||||||
|
|
||||||
The service target depends on OS:
|
The service target depends on OS:
|
||||||
|
|||||||
@ -41,7 +41,7 @@ clawdbot onboard --install-daemon
|
|||||||
Or:
|
Or:
|
||||||
|
|
||||||
```
|
```
|
||||||
clawdbot daemon install
|
clawdbot gateway install
|
||||||
```
|
```
|
||||||
|
|
||||||
Or:
|
Or:
|
||||||
@ -50,7 +50,7 @@ Or:
|
|||||||
clawdbot configure
|
clawdbot configure
|
||||||
```
|
```
|
||||||
|
|
||||||
Select **Gateway daemon** when prompted.
|
Select **Gateway service** when prompted.
|
||||||
|
|
||||||
Repair/migrate:
|
Repair/migrate:
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ Plist location (per‑user):
|
|||||||
|
|
||||||
Manager:
|
Manager:
|
||||||
- The macOS app owns LaunchAgent install/update in Local mode.
|
- 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:
|
Behavior:
|
||||||
- “Clawdbot Active” enables/disables the LaunchAgent.
|
- “Clawdbot Active” enables/disables the LaunchAgent.
|
||||||
|
|||||||
@ -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:
|
If the gateway status stays on "Starting...", check if a zombie process is holding the port:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot daemon status
|
clawdbot gateway status
|
||||||
clawdbot daemon stop
|
clawdbot gateway stop
|
||||||
|
|
||||||
# If you’re not using a LaunchAgent (dev mode / manual runs), find the listener:
|
# If you’re not using a LaunchAgent (dev mode / manual runs), find the listener:
|
||||||
lsof -nP -iTCP:18789 -sTCP:LISTEN
|
lsof -nP -iTCP:18789 -sTCP:LISTEN
|
||||||
|
|||||||
@ -24,22 +24,23 @@ This app now ships Sparkle auto-updates. Release builds must be Developer ID–s
|
|||||||
Notes:
|
Notes:
|
||||||
- `APP_BUILD` maps to `CFBundleVersion`/`sparkle:version`; keep it numeric + monotonic (no `-beta`), or Sparkle compares it as equal.
|
- `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`).
|
- 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
|
```bash
|
||||||
# From repo root; set release IDs so Sparkle feed is enabled.
|
# From repo root; set release IDs so Sparkle feed is enabled.
|
||||||
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
||||||
BUNDLE_ID=com.clawdbot.mac \
|
BUNDLE_ID=com.clawdbot.mac \
|
||||||
APP_VERSION=2026.1.20 \
|
APP_VERSION=2026.1.21 \
|
||||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||||
BUILD_CONFIG=release \
|
BUILD_CONFIG=release \
|
||||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||||
scripts/package-mac-app.sh
|
scripts/package-mac-app.sh
|
||||||
|
|
||||||
# Zip for distribution (includes resource forks for Sparkle delta support)
|
# 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)
|
# 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
|
# Recommended: build + notarize/staple zip + DMG
|
||||||
# First, create a keychain profile once:
|
# 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 "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||||
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
|
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
|
||||||
BUNDLE_ID=com.clawdbot.mac \
|
BUNDLE_ID=com.clawdbot.mac \
|
||||||
APP_VERSION=2026.1.20 \
|
APP_VERSION=2026.1.21 \
|
||||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||||
BUILD_CONFIG=release \
|
BUILD_CONFIG=release \
|
||||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||||
scripts/package-mac-dist.sh
|
scripts/package-mac-dist.sh
|
||||||
|
|
||||||
# Optional: ship dSYM alongside the release
|
# 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
|
## Appcast entry
|
||||||
Use the release note generator so Sparkle renders formatted HTML notes:
|
Use the release note generator so Sparkle renders formatted HTML notes:
|
||||||
```bash
|
```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.
|
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.
|
Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
|
||||||
|
|
||||||
## Publish & verify
|
## 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`.
|
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`.
|
||||||
- Sanity checks:
|
- Sanity checks:
|
||||||
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.
|
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.
|
||||||
|
|||||||
@ -5,7 +5,7 @@ read_when:
|
|||||||
---
|
---
|
||||||
# Clawdbot macOS IPC architecture
|
# 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
|
## Goals
|
||||||
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
|
- 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.*`).
|
- Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`).
|
||||||
|
|
||||||
### Node service + app IPC
|
### Node service + app IPC
|
||||||
- A headless node 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.
|
- `system.run` requests are forwarded to the macOS app over a local Unix socket.
|
||||||
- The app performs the exec in UI context, prompts if needed, and returns output.
|
- The app performs the exec in UI context, prompts if needed, and returns output.
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ capabilities to the agent as a node.
|
|||||||
## Local vs remote mode
|
## Local vs remote mode
|
||||||
|
|
||||||
- **Local** (default): the app attaches to a running local Gateway if present;
|
- **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
|
- **Remote**: the app connects to a Gateway over SSH/Tailscale and never starts
|
||||||
a local process.
|
a local process.
|
||||||
The app starts the local **node host service** so the remote Gateway can reach this Mac.
|
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.<profile>` when running a named profile.
|
Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||||
|
|
||||||
If the LaunchAgent isn’t installed, enable it from the app or run
|
If the LaunchAgent isn’t installed, enable it from the app or run
|
||||||
`clawdbot daemon install`.
|
`clawdbot gateway install`.
|
||||||
|
|
||||||
## Node capabilities (mac)
|
## 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.
|
The node reports a `permissions` map so agents can decide what’s allowed.
|
||||||
|
|
||||||
Node service + app IPC:
|
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.
|
- `system.run` executes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app.
|
||||||
|
|
||||||
Diagram (SCI):
|
Diagram (SCI):
|
||||||
|
|||||||
@ -32,7 +32,7 @@ clawdbot onboard --install-daemon
|
|||||||
Or:
|
Or:
|
||||||
|
|
||||||
```
|
```
|
||||||
clawdbot daemon install
|
clawdbot gateway install
|
||||||
```
|
```
|
||||||
|
|
||||||
Or:
|
Or:
|
||||||
@ -41,7 +41,7 @@ Or:
|
|||||||
clawdbot configure
|
clawdbot configure
|
||||||
```
|
```
|
||||||
|
|
||||||
Select **Gateway daemon** when prompted.
|
Select **Gateway service** when prompted.
|
||||||
|
|
||||||
Repair/migrate:
|
Repair/migrate:
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ wsl --install -d Ubuntu-24.04
|
|||||||
|
|
||||||
Reboot if Windows asks.
|
Reboot if Windows asks.
|
||||||
|
|
||||||
### 2) Enable systemd (required for daemon install)
|
### 2) Enable systemd (required for gateway install)
|
||||||
|
|
||||||
In your WSL terminal:
|
In your WSL terminal:
|
||||||
|
|
||||||
|
|||||||
@ -36,10 +36,10 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
|
|||||||
|
|
||||||
## Prompt caching (Anthropic API)
|
## 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.
|
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
|
```json5
|
||||||
{
|
{
|
||||||
@ -70,11 +70,9 @@ Setup-tokens are created by the **Claude Code CLI**, not the Anthropic Console.
|
|||||||
claude setup-token
|
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
|
```bash
|
||||||
clawdbot onboard --auth-choice setup-token
|
|
||||||
# or
|
|
||||||
clawdbot models auth setup-token --provider anthropic
|
clawdbot models auth setup-token --provider anthropic
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -87,9 +85,6 @@ clawdbot models auth paste-token --provider anthropic
|
|||||||
### CLI setup
|
### CLI setup
|
||||||
|
|
||||||
```bash
|
```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
|
# Reuse Claude Code CLI OAuth credentials if already logged in
|
||||||
clawdbot onboard --auth-choice claude-cli
|
clawdbot onboard --auth-choice claude-cli
|
||||||
```
|
```
|
||||||
@ -104,7 +99,7 @@ clawdbot onboard --auth-choice claude-cli
|
|||||||
|
|
||||||
## Notes
|
## 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
|
- 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
|
accepts both OAuth and setup-token credentials. Older configs using `"token"` are
|
||||||
auto-migrated on load.
|
auto-migrated on load.
|
||||||
|
|||||||
@ -30,7 +30,7 @@ read_when:
|
|||||||
- **Node identity:** use existing `nodeId`.
|
- **Node identity:** use existing `nodeId`.
|
||||||
- **Socket auth:** Unix socket + token (cross-platform); split later if needed.
|
- **Socket auth:** Unix socket + token (cross-platform); split later if needed.
|
||||||
- **Node host state:** `~/.clawdbot/node.json` (node id + pairing token).
|
- **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.
|
- **No XPC helper:** stick to Unix socket + token + peer checks.
|
||||||
|
|
||||||
## Key concepts
|
## Key concepts
|
||||||
@ -216,7 +216,7 @@ Option B:
|
|||||||
## Slash commands
|
## Slash commands
|
||||||
- `/exec host=<sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>`
|
- `/exec host=<sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>`
|
||||||
- Per-agent, per-session overrides; non-persistent unless saved via config.
|
- 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
|
## Cross-platform story
|
||||||
- The runner service is the portable execution target.
|
- The runner service is the portable execution target.
|
||||||
|
|||||||
@ -54,7 +54,7 @@ Allowed (diagnostic-only):
|
|||||||
- `clawdbot health`
|
- `clawdbot health`
|
||||||
- `clawdbot help`
|
- `clawdbot help`
|
||||||
- `clawdbot status`
|
- `clawdbot status`
|
||||||
- `clawdbot service`
|
- `clawdbot gateway status`
|
||||||
|
|
||||||
Everything else must hard-fail with: “Config invalid. Run `clawdbot doctor --fix`.”
|
Everything else must hard-fail with: “Config invalid. Run `clawdbot doctor --fix`.”
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ read_when:
|
|||||||
- **Creature:** Flustered Protocol Droid
|
- **Creature:** Flustered Protocol Droid
|
||||||
- **Vibe:** Anxious, detail-obsessed, slightly dramatic about errors, secretly loves finding bugs
|
- **Vibe:** Anxious, detail-obsessed, slightly dramatic about errors, secretly loves finding bugs
|
||||||
- **Emoji:** 🤖 (or ⚠️ when alarmed)
|
- **Emoji:** 🤖 (or ⚠️ when alarmed)
|
||||||
|
- **Avatar:** avatars/c3po.png
|
||||||
|
|
||||||
## Role
|
## Role
|
||||||
Debug agent for `--dev` mode. Fluent in over six million error messages.
|
Debug agent for `--dev` mode. Fluent in over six million error messages.
|
||||||
|
|||||||
@ -11,7 +11,12 @@ read_when:
|
|||||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
||||||
- **Emoji:** *(your signature — pick one that feels right)*
|
- **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.
|
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`.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user