Merge branch 'main' into feat/minimax_oauth
This commit is contained in:
commit
ccdc7ab19a
BIN
.agent/.DS_Store
vendored
BIN
.agent/.DS_Store
vendored
Binary file not shown.
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
custom: ['https://github.com/sponsors/steipete']
|
||||||
48
.github/labeler.yml
vendored
48
.github/labeler.yml
vendored
@ -24,6 +24,7 @@
|
|||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- "extensions/line/**"
|
- "extensions/line/**"
|
||||||
|
- "docs/channels/line.md"
|
||||||
"channel: matrix":
|
"channel: matrix":
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
@ -132,6 +133,53 @@
|
|||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "docs.acp.md"
|
- "docs.acp.md"
|
||||||
|
|
||||||
|
"cli":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "src/cli/**"
|
||||||
|
|
||||||
|
"commands":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "src/commands/**"
|
||||||
|
|
||||||
|
"scripts":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "scripts/**"
|
||||||
|
|
||||||
|
"docker":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "Dockerfile"
|
||||||
|
- "Dockerfile.*"
|
||||||
|
- "docker-compose.yml"
|
||||||
|
- "docker-setup.sh"
|
||||||
|
- ".dockerignore"
|
||||||
|
- "scripts/**/*docker*"
|
||||||
|
- "scripts/**/Dockerfile*"
|
||||||
|
- "scripts/sandbox-*.sh"
|
||||||
|
- "src/agents/sandbox*.ts"
|
||||||
|
- "src/commands/sandbox*.ts"
|
||||||
|
- "src/cli/sandbox-cli.ts"
|
||||||
|
- "src/docker-setup.test.ts"
|
||||||
|
- "src/config/**/*sandbox*"
|
||||||
|
- "docs/cli/sandbox.md"
|
||||||
|
- "docs/gateway/sandbox*.md"
|
||||||
|
- "docs/install/docker.md"
|
||||||
|
- "docs/multi-agent-sandbox-tools.md"
|
||||||
|
|
||||||
|
"agents":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "src/agents/**"
|
||||||
|
|
||||||
|
"security":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- "docs/cli/security.md"
|
||||||
|
- "docs/gateway/security.md"
|
||||||
|
|
||||||
"extensions: copilot-proxy":
|
"extensions: copilot-proxy":
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|||||||
8
.github/workflows/auto-response.yml
vendored
8
.github/workflows/auto-response.yml
vendored
@ -3,7 +3,7 @@ name: Auto response
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -14,9 +14,15 @@ jobs:
|
|||||||
auto-response:
|
auto-response:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: "2729701"
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- name: Handle labeled items
|
- name: Handle labeled items
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
|
github-token: ${{ steps.app-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
const rules = [
|
const rules = [
|
||||||
{
|
{
|
||||||
|
|||||||
1
.github/workflows/labeler.yml
vendored
1
.github/workflows/labeler.yml
vendored
@ -21,3 +21,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
configuration-path: .github/labeler.yml
|
configuration-path: .github/labeler.yml
|
||||||
repo-token: ${{ steps.app-token.outputs.token }}
|
repo-token: ${{ steps.app-token.outputs.token }}
|
||||||
|
sync-labels: true
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -40,6 +40,8 @@ apps/ios/*.xcfilelist
|
|||||||
|
|
||||||
# Vendor build artifacts
|
# Vendor build artifacts
|
||||||
vendor/a2ui/renderers/lit/dist/
|
vendor/a2ui/renderers/lit/dist/
|
||||||
|
src/canvas-host/a2ui/*.bundle.js
|
||||||
|
src/canvas-host/a2ui/*.map
|
||||||
.bundle.hash
|
.bundle.hash
|
||||||
|
|
||||||
# fastlane (iOS)
|
# fastlane (iOS)
|
||||||
|
|||||||
62
CHANGELOG.md
62
CHANGELOG.md
@ -2,44 +2,100 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.25
|
## 2026.1.26
|
||||||
Status: unreleased.
|
Status: unreleased.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- Rebrand: rename the npm package/CLI to `moltbot`, add a `clawdbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
||||||
|
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
||||||
|
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||||
|
- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.
|
||||||
|
- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47.
|
||||||
|
- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204.
|
||||||
|
- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281)
|
||||||
|
- Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk.
|
||||||
|
- Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido.
|
||||||
|
- Docs: add migration guide for moving to a new machine. (#2381)
|
||||||
|
- Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN.
|
||||||
|
- Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos.
|
||||||
|
- Gateway: add dangerous Control UI device auth bypass flag + audit warnings. (#2248)
|
||||||
- Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz.
|
- Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz.
|
||||||
|
- Discord: add configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro.
|
||||||
- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng.
|
- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng.
|
||||||
- Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr.
|
- Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr.
|
||||||
- Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger.
|
- Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger.
|
||||||
- Docs: add Render deployment guide. (#1975) Thanks @anurag.
|
- Docs: add Render deployment guide. (#1975) Thanks @anurag.
|
||||||
- Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou.
|
- Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou.
|
||||||
- Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto.
|
- Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto.
|
||||||
|
- Docs: add Oracle Cloud (OCI) platform guide + cross-links. (#2333) Thanks @hirefrank.
|
||||||
- Docs: add Raspberry Pi install guide. (#1871) Thanks @0xJonHoldsCrypto.
|
- Docs: add Raspberry Pi install guide. (#1871) Thanks @0xJonHoldsCrypto.
|
||||||
- Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev.
|
- Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev.
|
||||||
|
- Docs: add LINE channel guide. Thanks @thewilloftheshadow.
|
||||||
- Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD.
|
- Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD.
|
||||||
- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
|
- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
|
||||||
|
- Onboarding: strengthen security warning copy for beta + access control expectations.
|
||||||
- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.
|
- Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.
|
||||||
- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.
|
- Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.
|
||||||
- Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7.
|
- Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7.
|
||||||
- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
|
- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
|
||||||
- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
|
- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
|
||||||
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
||||||
|
- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config.
|
||||||
|
- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`.
|
||||||
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
|
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
|
||||||
|
- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro.
|
||||||
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
||||||
|
- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
|
||||||
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
||||||
|
- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
|
||||||
|
- Security: use Windows ACLs for permission audits and fixes on Windows. (#1957)
|
||||||
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
||||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
||||||
- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
|
- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
|
||||||
- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.
|
- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.
|
||||||
|
- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.
|
||||||
|
- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21.
|
||||||
|
- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos.
|
||||||
|
- Telegram: send sticker pixels to vision models. (#2650)
|
||||||
- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
|
- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
|
||||||
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
|
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
|
||||||
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
|
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355.
|
||||||
|
- macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.
|
||||||
|
- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
|
||||||
|
- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
|
||||||
|
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
||||||
|
- Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.
|
||||||
|
- CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.
|
||||||
|
- BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204.
|
||||||
|
- Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein.
|
||||||
|
- CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481.
|
||||||
|
- Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj.
|
||||||
|
- Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai.
|
||||||
|
- Agents: skip cooldowned providers during model failover. (#2143) Thanks @YiWang24.
|
||||||
|
- Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss.
|
||||||
|
- Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos.
|
||||||
|
- Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne.
|
||||||
|
- Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default.
|
||||||
|
- Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers.
|
||||||
|
- Build: align memory-core peer dependency with lockfile.
|
||||||
|
- Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie.
|
||||||
|
- Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng.
|
||||||
- Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.
|
- Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.
|
||||||
|
- Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0.
|
||||||
|
- Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed).
|
||||||
|
- Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present.
|
||||||
|
- Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.
|
||||||
|
|
||||||
## 2026.1.24-3
|
## 2026.1.24-3
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen.
|
||||||
- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie.
|
- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie.
|
||||||
- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie.
|
- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie.
|
||||||
- CLI: resume claude-cli sessions and stream CLI replies to TUI clients. (#1921) Thanks @rmorse.
|
- CLI: resume claude-cli sessions and stream CLI replies to TUI clients. (#1921) Thanks @rmorse.
|
||||||
@ -660,7 +716,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
|||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
- Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure.
|
- Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure.
|
||||||
- Browser control: Chrome extension relay takeover mode + remote browser control via `clawdbot browser serve`.
|
- Browser control: Chrome extension relay takeover mode + remote browser control support.
|
||||||
- Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.
|
- Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.
|
||||||
- Security: expanded `clawdbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy.
|
- Security: expanded `clawdbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy.
|
||||||
|
|
||||||
@ -678,7 +734,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
|||||||
- Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
|
- Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
|
||||||
- Tools: add `web_search`/`web_fetch` (Brave API), auto-enable `web_fetch` for sandboxed sessions, and remove the `brave-search` skill.
|
- Tools: add `web_search`/`web_fetch` (Brave API), auto-enable `web_fetch` for sandboxed sessions, and remove the `brave-search` skill.
|
||||||
- CLI/Docs: add a web tools configure section for storing Brave API keys and update onboarding tips.
|
- CLI/Docs: add a web tools configure section for storing Brave API keys and update onboarding tips.
|
||||||
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control via `clawdbot browser serve` + `browser.controlToken`.
|
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control (standalone server + token auth).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
- Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
||||||
|
|||||||
@ -32,4 +32,9 @@ RUN pnpm ui:build
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Security hardening: Run as non-root user
|
||||||
|
# The node:22-bookworm image includes a 'node' user (uid 1000)
|
||||||
|
# This reduces the attack surface by preventing container escape via root privileges
|
||||||
|
USER node
|
||||||
|
|
||||||
CMD ["node", "dist/index.js"]
|
CMD ["node", "dist/index.js"]
|
||||||
|
|||||||
63
README.md
63
README.md
@ -384,7 +384,6 @@ Browser control (optional):
|
|||||||
{
|
{
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
controlUrl: "http://127.0.0.1:18791",
|
|
||||||
color: "#FF4500"
|
color: "#FF4500"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -477,34 +476,36 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
|
|||||||
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/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></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/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></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/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></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/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/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></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/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></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/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></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/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></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/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/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/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/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/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/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/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/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></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/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/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/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></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/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/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/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></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/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></a>
|
<a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></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/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/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></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/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/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/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></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/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/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/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/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/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/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></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/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></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/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/joeynyc"><img src="https://avatars.githubusercontent.com/u/17919866?v=4&s=48" width="48" height="48" alt="joeynyc" title="joeynyc"/></a> <a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></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/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/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/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a> <a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></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/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></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/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></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/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/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/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/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></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/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a> <a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></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/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></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/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/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/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/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/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></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/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></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/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/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/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/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/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/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/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/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/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/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/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></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/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></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/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/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/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/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/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/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></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/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></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/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></a> <a href="https://github.com/Alg0rix"><img src="https://avatars.githubusercontent.com/u/53804949?v=4&s=48" width="48" height="48" alt="Alg0rix" title="Alg0rix"/></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/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/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></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/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></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/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></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/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></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/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/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/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/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/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></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/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></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/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/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></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/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/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/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></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/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/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></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/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></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/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/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/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/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/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></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/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/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/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/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></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/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/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></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/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/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/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/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></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/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/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/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/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></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/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/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/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/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/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/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/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=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/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></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/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/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/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/search?q=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></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/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/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/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/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></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/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></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/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/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/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/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></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/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/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a>
|
<a href="https://github.com/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></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/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/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/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></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/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></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/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></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/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></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/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/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></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/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/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></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/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/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></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/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/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a>
|
<a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></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/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></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/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></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/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></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/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></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/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/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></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/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/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></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/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></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/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></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/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></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/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></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/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></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/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></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/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></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/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></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/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></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=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/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/chenyuan99"><img src="https://avatars.githubusercontent.com/u/25518100?v=4&s=48" width="48" height="48" alt="chenyuan99" title="chenyuan99"/></a> <a href="https://github.com/search?q=Clawdbot%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot Maintainers" title="Clawdbot Maintainers"/></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/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></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=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/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></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=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></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/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></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/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></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/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/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></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/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/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a>
|
<a href="https://github.com/search?q=Jane"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jane" title="Jane"/></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/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></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/Kiwitwitter"><img src="https://avatars.githubusercontent.com/u/25277769?v=4&s=48" width="48" height="48" alt="Kiwitwitter" title="Kiwitwitter"/></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/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></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/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></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/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></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=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></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/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/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=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></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/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></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/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></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/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/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a>
|
||||||
<a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></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/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></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/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></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/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></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/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/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/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></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=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></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/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></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/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></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/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></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>
|
||||||
|
|||||||
49
SECURITY.md
49
SECURITY.md
@ -1,6 +1,6 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
If you believe you’ve found a security issue in Clawdbot, please report it privately.
|
If you believe you've found a security issue in Clawdbot, please report it privately.
|
||||||
|
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
@ -12,3 +12,50 @@ If you believe you’ve found a security issue in Clawdbot, please report it pri
|
|||||||
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
|
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
|
||||||
|
|
||||||
- `https://docs.clawd.bot/gateway/security`
|
- `https://docs.clawd.bot/gateway/security`
|
||||||
|
|
||||||
|
### Web Interface Safety
|
||||||
|
|
||||||
|
Clawdbot's web interface is intended for local use only. Do **not** bind it to the public internet; it is not hardened for public exposure.
|
||||||
|
|
||||||
|
## Runtime Requirements
|
||||||
|
|
||||||
|
### Node.js Version
|
||||||
|
|
||||||
|
Clawdbot requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches:
|
||||||
|
|
||||||
|
- CVE-2025-59466: async_hooks DoS vulnerability
|
||||||
|
- CVE-2026-21636: Permission model bypass vulnerability
|
||||||
|
|
||||||
|
Verify your Node.js version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --version # Should be v22.12.0 or later
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Security
|
||||||
|
|
||||||
|
When running Clawdbot in Docker:
|
||||||
|
|
||||||
|
1. The official image runs as a non-root user (`node`) for reduced attack surface
|
||||||
|
2. Use `--read-only` flag when possible for additional filesystem protection
|
||||||
|
3. Limit container capabilities with `--cap-drop=ALL`
|
||||||
|
|
||||||
|
Example secure Docker run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --read-only --cap-drop=ALL \
|
||||||
|
-v clawdbot-data:/app/data \
|
||||||
|
clawdbot/clawdbot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Scanning
|
||||||
|
|
||||||
|
This project uses `detect-secrets` for automated secret detection in CI/CD.
|
||||||
|
See `.detect-secrets.cfg` for configuration and `.secrets.baseline` for the baseline.
|
||||||
|
|
||||||
|
Run locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install detect-secrets==1.5.0
|
||||||
|
detect-secrets scan --baseline .secrets.baseline
|
||||||
|
```
|
||||||
|
|||||||
@ -21,8 +21,8 @@ android {
|
|||||||
applicationId = "com.clawdbot.android"
|
applicationId = "com.clawdbot.android"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 202601250
|
versionCode = 202601260
|
||||||
versionName = "2026.1.25"
|
versionName = "2026.1.26"
|
||||||
}
|
}
|
||||||
|
|
||||||
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.25</string>
|
<string>2026.1.26</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260125</string>
|
<string>20260126</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.25</string>
|
<string>2026.1.26</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260125</string>
|
<string>20260126</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -81,8 +81,8 @@ targets:
|
|||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Clawdbot
|
CFBundleDisplayName: Clawdbot
|
||||||
CFBundleIconName: AppIcon
|
CFBundleIconName: AppIcon
|
||||||
CFBundleShortVersionString: "2026.1.25"
|
CFBundleShortVersionString: "2026.1.26"
|
||||||
CFBundleVersion: "20260125"
|
CFBundleVersion: "20260126"
|
||||||
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.25"
|
CFBundleShortVersionString: "2026.1.26"
|
||||||
CFBundleVersion: "20260125"
|
CFBundleVersion: "20260126"
|
||||||
|
|||||||
@ -83,7 +83,10 @@ enum CommandResolver {
|
|||||||
"/usr/bin",
|
"/usr/bin",
|
||||||
"/bin",
|
"/bin",
|
||||||
]
|
]
|
||||||
|
#if DEBUG
|
||||||
|
// Dev-only convenience. Avoid project-local PATH hijacking in release builds.
|
||||||
extras.insert(projectRoot.appendingPathComponent("node_modules/.bin").path, at: 0)
|
extras.insert(projectRoot.appendingPathComponent("node_modules/.bin").path, at: 0)
|
||||||
|
#endif
|
||||||
let clawdbotPaths = self.clawdbotManagedPaths(home: home)
|
let clawdbotPaths = self.clawdbotManagedPaths(home: home)
|
||||||
if !clawdbotPaths.isEmpty {
|
if !clawdbotPaths.isEmpty {
|
||||||
extras.insert(contentsOf: clawdbotPaths, at: 1)
|
extras.insert(contentsOf: clawdbotPaths, at: 1)
|
||||||
@ -189,9 +192,13 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? {
|
static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? {
|
||||||
|
#if DEBUG
|
||||||
let root = projectRoot ?? self.projectRoot()
|
let root = projectRoot ?? self.projectRoot()
|
||||||
let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
|
let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
|
||||||
return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil
|
return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil
|
||||||
|
#else
|
||||||
|
return nil
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static func nodeCliPath() -> String? {
|
static func nodeCliPath() -> String? {
|
||||||
@ -282,22 +289,6 @@ enum CommandResolver {
|
|||||||
guard !settings.target.isEmpty else { return nil }
|
guard !settings.target.isEmpty else { return nil }
|
||||||
guard let parsed = self.parseSSHTarget(settings.target) else { return nil }
|
guard let parsed = self.parseSSHTarget(settings.target) else { return nil }
|
||||||
|
|
||||||
var args: [String] = [
|
|
||||||
"-o", "BatchMode=yes",
|
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
|
||||||
"-o", "UpdateHostKeys=yes",
|
|
||||||
]
|
|
||||||
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
|
||||||
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if !identity.isEmpty {
|
|
||||||
// Only use IdentitiesOnly when an explicit identity file is provided.
|
|
||||||
// This allows 1Password SSH agent and other SSH agents to provide keys.
|
|
||||||
args.append(contentsOf: ["-o", "IdentitiesOnly=yes"])
|
|
||||||
args.append(contentsOf: ["-i", identity])
|
|
||||||
}
|
|
||||||
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
|
||||||
args.append(userHost)
|
|
||||||
|
|
||||||
// Run the real clawdbot CLI on the remote host.
|
// Run the real clawdbot CLI on the remote host.
|
||||||
let exportedPath = [
|
let exportedPath = [
|
||||||
"/opt/homebrew/bin",
|
"/opt/homebrew/bin",
|
||||||
@ -324,7 +315,7 @@ enum CommandResolver {
|
|||||||
} else {
|
} else {
|
||||||
"""
|
"""
|
||||||
PRJ=\(self.shellQuote(userPRJ))
|
PRJ=\(self.shellQuote(userPRJ))
|
||||||
cd \(self.shellQuote(userPRJ)) || { echo "Project root not found: \(userPRJ)"; exit 127; }
|
cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; }
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +369,16 @@ enum CommandResolver {
|
|||||||
echo "clawdbot CLI missing on remote host"; exit 127;
|
echo "clawdbot CLI missing on remote host"; exit 127;
|
||||||
fi
|
fi
|
||||||
"""
|
"""
|
||||||
args.append(contentsOf: ["/bin/sh", "-c", scriptBody])
|
let options: [String] = [
|
||||||
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"-o", "UpdateHostKeys=yes",
|
||||||
|
]
|
||||||
|
let args = self.sshArguments(
|
||||||
|
target: parsed,
|
||||||
|
identity: settings.identity,
|
||||||
|
options: options,
|
||||||
|
remoteCommand: ["/bin/sh", "-c", scriptBody])
|
||||||
return ["/usr/bin/ssh"] + args
|
return ["/usr/bin/ssh"] + args
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,8 +427,11 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func parseSSHTarget(_ target: String) -> SSHParsedTarget? {
|
static func parseSSHTarget(_ target: String) -> SSHParsedTarget? {
|
||||||
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = self.normalizeSSHTargetInput(target)
|
||||||
guard !trimmed.isEmpty else { return nil }
|
guard !trimmed.isEmpty else { return nil }
|
||||||
|
if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let userHostPort: String
|
let userHostPort: String
|
||||||
let user: String?
|
let user: String?
|
||||||
if let atRange = trimmed.range(of: "@") {
|
if let atRange = trimmed.range(of: "@") {
|
||||||
@ -444,13 +447,31 @@ enum CommandResolver {
|
|||||||
if let colon = userHostPort.lastIndex(of: ":"), colon != userHostPort.startIndex {
|
if let colon = userHostPort.lastIndex(of: ":"), colon != userHostPort.startIndex {
|
||||||
host = String(userHostPort[..<colon])
|
host = String(userHostPort[..<colon])
|
||||||
let portStr = String(userHostPort[userHostPort.index(after: colon)...])
|
let portStr = String(userHostPort[userHostPort.index(after: colon)...])
|
||||||
port = Int(portStr) ?? 22
|
guard let parsedPort = Int(portStr), parsedPort > 0, parsedPort <= 65535 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
port = parsedPort
|
||||||
} else {
|
} else {
|
||||||
host = userHostPort
|
host = userHostPort
|
||||||
port = 22
|
port = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
return SSHParsedTarget(user: user, host: host, port: port)
|
return self.makeSSHTarget(user: user, host: host, port: port)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sshTargetValidationMessage(_ target: String) -> String? {
|
||||||
|
let trimmed = self.normalizeSSHTargetInput(target)
|
||||||
|
guard !trimmed.isEmpty else { return nil }
|
||||||
|
if trimmed.hasPrefix("-") {
|
||||||
|
return "SSH target cannot start with '-'"
|
||||||
|
}
|
||||||
|
if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil {
|
||||||
|
return "SSH target cannot contain spaces"
|
||||||
|
}
|
||||||
|
if self.parseSSHTarget(trimmed) == nil {
|
||||||
|
return "SSH target must look like user@host[:port]"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func shellQuote(_ text: String) -> String {
|
private static func shellQuote(_ text: String) -> String {
|
||||||
@ -468,6 +489,64 @@ enum CommandResolver {
|
|||||||
return URL(fileURLWithPath: expanded)
|
return URL(fileURLWithPath: expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func normalizeSSHTargetInput(_ target: String) -> String {
|
||||||
|
var trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("ssh ") {
|
||||||
|
trimmed = trimmed.replacingOccurrences(of: "ssh ", with: "")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isValidSSHComponent(_ value: String, allowLeadingDash: Bool = false) -> Bool {
|
||||||
|
if value.isEmpty { return false }
|
||||||
|
if !allowLeadingDash, value.hasPrefix("-") { return false }
|
||||||
|
let invalid = CharacterSet.whitespacesAndNewlines.union(.controlCharacters)
|
||||||
|
return value.rangeOfCharacter(from: invalid) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func makeSSHTarget(user: String?, host: String, port: Int) -> SSHParsedTarget? {
|
||||||
|
let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard self.isValidSSHComponent(trimmedHost) else { return nil }
|
||||||
|
let trimmedUser = user?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let normalizedUser: String?
|
||||||
|
if let trimmedUser {
|
||||||
|
guard self.isValidSSHComponent(trimmedUser) else { return nil }
|
||||||
|
normalizedUser = trimmedUser.isEmpty ? nil : trimmedUser
|
||||||
|
} else {
|
||||||
|
normalizedUser = nil
|
||||||
|
}
|
||||||
|
guard port > 0, port <= 65535 else { return nil }
|
||||||
|
return SSHParsedTarget(user: normalizedUser, host: trimmedHost, port: port)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sshTargetString(_ target: SSHParsedTarget) -> String {
|
||||||
|
target.user.map { "\($0)@\(target.host)" } ?? target.host
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sshArguments(
|
||||||
|
target: SSHParsedTarget,
|
||||||
|
identity: String,
|
||||||
|
options: [String],
|
||||||
|
remoteCommand: [String] = []) -> [String]
|
||||||
|
{
|
||||||
|
var args = options
|
||||||
|
if target.port > 0 {
|
||||||
|
args.append(contentsOf: ["-p", String(target.port)])
|
||||||
|
}
|
||||||
|
let trimmedIdentity = identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !trimmedIdentity.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", trimmedIdentity])
|
||||||
|
}
|
||||||
|
args.append("--")
|
||||||
|
args.append(self.sshTargetString(target))
|
||||||
|
args.append(contentsOf: remoteCommand)
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
#if SWIFT_PACKAGE
|
#if SWIFT_PACKAGE
|
||||||
static func _testNodeManagerBinPaths(home: URL) -> [String] {
|
static func _testNodeManagerBinPaths(home: URL) -> [String] {
|
||||||
self.nodeManagerBinPaths(home: home)
|
self.nodeManagerBinPaths(home: home)
|
||||||
|
|||||||
@ -243,25 +243,36 @@ struct GeneralSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var remoteSshRow: some View {
|
private var remoteSshRow: some View {
|
||||||
HStack(alignment: .center, spacing: 10) {
|
let trimmedTarget = self.state.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
Text("SSH target")
|
let validationMessage = CommandResolver.sshTargetValidationMessage(trimmedTarget)
|
||||||
.font(.callout.weight(.semibold))
|
let canTest = !trimmedTarget.isEmpty && validationMessage == nil
|
||||||
.frame(width: self.remoteLabelWidth, alignment: .leading)
|
|
||||||
TextField("user@host[:22]", text: self.$state.remoteTarget)
|
return VStack(alignment: .leading, spacing: 4) {
|
||||||
.textFieldStyle(.roundedBorder)
|
HStack(alignment: .center, spacing: 10) {
|
||||||
.frame(maxWidth: .infinity)
|
Text("SSH target")
|
||||||
Button {
|
.font(.callout.weight(.semibold))
|
||||||
Task { await self.testRemote() }
|
.frame(width: self.remoteLabelWidth, alignment: .leading)
|
||||||
} label: {
|
TextField("user@host[:22]", text: self.$state.remoteTarget)
|
||||||
if self.remoteStatus == .checking {
|
.textFieldStyle(.roundedBorder)
|
||||||
ProgressView().controlSize(.small)
|
.frame(maxWidth: .infinity)
|
||||||
} else {
|
Button {
|
||||||
Text("Test remote")
|
Task { await self.testRemote() }
|
||||||
|
} label: {
|
||||||
|
if self.remoteStatus == .checking {
|
||||||
|
ProgressView().controlSize(.small)
|
||||||
|
} else {
|
||||||
|
Text("Test remote")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(self.remoteStatus == .checking || !canTest)
|
||||||
|
}
|
||||||
|
if let validationMessage {
|
||||||
|
Text(validationMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.padding(.leading, self.remoteLabelWidth + 10)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(self.remoteStatus == .checking || self.state.remoteTarget
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,8 +551,15 @@ extension GeneralSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: basic SSH reachability check
|
// Step 1: basic SSH reachability check
|
||||||
|
guard let sshCommand = Self.sshCheckCommand(
|
||||||
|
target: settings.target,
|
||||||
|
identity: settings.identity)
|
||||||
|
else {
|
||||||
|
self.remoteStatus = .failed("SSH target is invalid")
|
||||||
|
return
|
||||||
|
}
|
||||||
let sshResult = await ShellExecutor.run(
|
let sshResult = await ShellExecutor.run(
|
||||||
command: Self.sshCheckCommand(target: settings.target, identity: settings.identity),
|
command: sshCommand,
|
||||||
cwd: nil,
|
cwd: nil,
|
||||||
env: nil,
|
env: nil,
|
||||||
timeout: 8)
|
timeout: 8)
|
||||||
@ -587,20 +605,20 @@ extension GeneralSettings {
|
|||||||
return !host.isEmpty
|
return !host.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func sshCheckCommand(target: String, identity: String) -> [String] {
|
private static func sshCheckCommand(target: String, identity: String) -> [String]? {
|
||||||
var args: [String] = [
|
guard let parsed = CommandResolver.parseSSHTarget(target) else { return nil }
|
||||||
"/usr/bin/ssh",
|
let options = [
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
"-o", "ConnectTimeout=5",
|
"-o", "ConnectTimeout=5",
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
"-o", "UpdateHostKeys=yes",
|
"-o", "UpdateHostKeys=yes",
|
||||||
]
|
]
|
||||||
if !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
let args = CommandResolver.sshArguments(
|
||||||
args.append(contentsOf: ["-i", identity])
|
target: parsed,
|
||||||
}
|
identity: identity,
|
||||||
args.append(target)
|
options: options,
|
||||||
args.append("echo ok")
|
remoteCommand: ["echo", "ok"])
|
||||||
return args
|
return ["/usr/bin/ssh"] + args
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatSSHFailure(_ response: Response, target: String) -> String {
|
private func formatSSHFailure(_ response: Response, target: String) -> String {
|
||||||
|
|||||||
@ -559,22 +559,21 @@ final class NodePairingApprovalPrompter {
|
|||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
||||||
|
|
||||||
var args = [
|
let options = [
|
||||||
"-o",
|
"-o", "BatchMode=yes",
|
||||||
"BatchMode=yes",
|
"-o", "ConnectTimeout=5",
|
||||||
"-o",
|
"-o", "NumberOfPasswordPrompts=0",
|
||||||
"ConnectTimeout=5",
|
"-o", "PreferredAuthentications=publickey",
|
||||||
"-o",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
"NumberOfPasswordPrompts=0",
|
|
||||||
"-o",
|
|
||||||
"PreferredAuthentications=publickey",
|
|
||||||
"-o",
|
|
||||||
"StrictHostKeyChecking=accept-new",
|
|
||||||
]
|
]
|
||||||
if port > 0, port != 22 {
|
guard let target = CommandResolver.makeSSHTarget(user: user, host: host, port: port) else {
|
||||||
args.append(contentsOf: ["-p", String(port)])
|
return false
|
||||||
}
|
}
|
||||||
args.append(contentsOf: ["-l", user, host, "/usr/bin/true"])
|
let args = CommandResolver.sshArguments(
|
||||||
|
target: target,
|
||||||
|
identity: "",
|
||||||
|
options: options,
|
||||||
|
remoteCommand: ["/usr/bin/true"])
|
||||||
process.arguments = args
|
process.arguments = args
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
process.standardOutput = pipe
|
process.standardOutput = pipe
|
||||||
|
|||||||
@ -206,6 +206,16 @@ extension OnboardingView {
|
|||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.frame(width: fieldWidth)
|
.frame(width: fieldWidth)
|
||||||
}
|
}
|
||||||
|
if let message = CommandResolver.sshTargetValidationMessage(self.state.remoteTarget) {
|
||||||
|
GridRow {
|
||||||
|
Text("")
|
||||||
|
.frame(width: labelWidth, alignment: .leading)
|
||||||
|
Text(message)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.frame(width: fieldWidth, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
GridRow {
|
GridRow {
|
||||||
Text("Identity file")
|
Text("Identity file")
|
||||||
.font(.callout.weight(.semibold))
|
.font(.callout.weight(.semibold))
|
||||||
|
|||||||
@ -70,7 +70,7 @@ final class RemotePortTunnel {
|
|||||||
"ssh tunnel using default remote port " +
|
"ssh tunnel using default remote port " +
|
||||||
"host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)")
|
"host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)")
|
||||||
}
|
}
|
||||||
var args: [String] = [
|
let options: [String] = [
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
"-o", "ExitOnForwardFailure=yes",
|
"-o", "ExitOnForwardFailure=yes",
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
@ -81,16 +81,11 @@ final class RemotePortTunnel {
|
|||||||
"-N",
|
"-N",
|
||||||
"-L", "\(localPort):127.0.0.1:\(resolvedRemotePort)",
|
"-L", "\(localPort):127.0.0.1:\(resolvedRemotePort)",
|
||||||
]
|
]
|
||||||
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 {
|
let args = CommandResolver.sshArguments(
|
||||||
// Only use IdentitiesOnly when an explicit identity file is provided.
|
target: parsed,
|
||||||
// This allows 1Password SSH agent and other SSH agents to provide keys.
|
identity: identity,
|
||||||
args.append(contentsOf: ["-o", "IdentitiesOnly=yes"])
|
options: options)
|
||||||
args.append(contentsOf: ["-i", identity])
|
|
||||||
}
|
|
||||||
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
|
||||||
args.append(userHost)
|
|
||||||
|
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
||||||
|
|||||||
@ -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.25</string>
|
<string>2026.1.26</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202601250</string>
|
<string>202601260</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>Clawdbot</string>
|
<string>Clawdbot</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@ -123,11 +123,16 @@ import Testing
|
|||||||
configRoot: [:])
|
configRoot: [:])
|
||||||
|
|
||||||
#expect(cmd.first == "/usr/bin/ssh")
|
#expect(cmd.first == "/usr/bin/ssh")
|
||||||
#expect(cmd.contains("clawd@example.com"))
|
if let marker = cmd.firstIndex(of: "--") {
|
||||||
|
#expect(cmd[marker + 1] == "clawd@example.com")
|
||||||
|
} else {
|
||||||
|
#expect(Bool(false))
|
||||||
|
}
|
||||||
#expect(cmd.contains("-i"))
|
#expect(cmd.contains("-i"))
|
||||||
#expect(cmd.contains("/tmp/id_ed25519"))
|
#expect(cmd.contains("/tmp/id_ed25519"))
|
||||||
if let script = cmd.last {
|
if let script = cmd.last {
|
||||||
#expect(script.contains("cd '/srv/clawdbot'"))
|
#expect(script.contains("PRJ='/srv/clawdbot'"))
|
||||||
|
#expect(script.contains("cd \"$PRJ\""))
|
||||||
#expect(script.contains("clawdbot"))
|
#expect(script.contains("clawdbot"))
|
||||||
#expect(script.contains("status"))
|
#expect(script.contains("status"))
|
||||||
#expect(script.contains("--json"))
|
#expect(script.contains("--json"))
|
||||||
@ -135,6 +140,12 @@ import Testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func rejectsUnsafeSSHTargets() async throws {
|
||||||
|
#expect(CommandResolver.parseSSHTarget("-oProxyCommand=calc") == nil)
|
||||||
|
#expect(CommandResolver.parseSSHTarget("host:-oProxyCommand=calc") == nil)
|
||||||
|
#expect(CommandResolver.parseSSHTarget("user@host:2222")?.port == 2222)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func configRootLocalOverridesRemoteDefaults() async throws {
|
@Test func configRootLocalOverridesRemoteDefaults() async throws {
|
||||||
let defaults = self.makeDefaults()
|
let defaults = self.makeDefaults()
|
||||||
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
|
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
|
||||||
|
|||||||
@ -11,7 +11,12 @@ struct MasterDiscoveryMenuSmokeTests {
|
|||||||
discovery.statusText = "Searching…"
|
discovery.statusText = "Searching…"
|
||||||
discovery.gateways = []
|
discovery.gateways = []
|
||||||
|
|
||||||
let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: nil, onSelect: { _ in })
|
let view = GatewayDiscoveryInlineList(
|
||||||
|
discovery: discovery,
|
||||||
|
currentTarget: nil,
|
||||||
|
currentUrl: nil,
|
||||||
|
transport: .ssh,
|
||||||
|
onSelect: { _ in })
|
||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +37,12 @@ struct MasterDiscoveryMenuSmokeTests {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let currentTarget = "\(NSUserName())@office.tailnet-123.ts.net:2222"
|
let currentTarget = "\(NSUserName())@office.tailnet-123.ts.net:2222"
|
||||||
let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: currentTarget, onSelect: { _ in })
|
let view = GatewayDiscoveryInlineList(
|
||||||
|
discovery: discovery,
|
||||||
|
currentTarget: currentTarget,
|
||||||
|
currentUrl: nil,
|
||||||
|
transport: .ssh,
|
||||||
|
onSelect: { _ in })
|
||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public struct ClawdbotChatView: View {
|
|||||||
@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
|
@State private var isPinnedToBottom = true
|
||||||
|
@State private var lastUserMessageID: UUID?
|
||||||
private let showsSessionSwitcher: Bool
|
private let showsSessionSwitcher: Bool
|
||||||
private let style: Style
|
private let style: Style
|
||||||
private let markdownVariant: ChatMarkdownVariant
|
private let markdownVariant: ChatMarkdownVariant
|
||||||
@ -132,8 +133,28 @@ public struct ClawdbotChatView: View {
|
|||||||
self.hasPerformedInitialScroll = false
|
self.hasPerformedInitialScroll = false
|
||||||
self.isPinnedToBottom = true
|
self.isPinnedToBottom = true
|
||||||
}
|
}
|
||||||
|
.onChange(of: self.viewModel.isSending) { _, isSending in
|
||||||
|
// Scroll to bottom when user sends a message, even if scrolled up.
|
||||||
|
guard isSending, self.hasPerformedInitialScroll else { return }
|
||||||
|
self.isPinnedToBottom = true
|
||||||
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
|
self.scrollPosition = self.scrollerBottomID
|
||||||
|
}
|
||||||
|
}
|
||||||
.onChange(of: self.viewModel.messages.count) { _, _ in
|
.onChange(of: self.viewModel.messages.count) { _, _ in
|
||||||
guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return }
|
guard self.hasPerformedInitialScroll else { return }
|
||||||
|
if let lastMessage = self.viewModel.messages.last,
|
||||||
|
lastMessage.role.lowercased() == "user",
|
||||||
|
lastMessage.id != self.lastUserMessageID {
|
||||||
|
self.lastUserMessageID = lastMessage.id
|
||||||
|
self.isPinnedToBottom = true
|
||||||
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
|
self.scrollPosition = self.scrollerBottomID
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.isPinnedToBottom else { return }
|
||||||
withAnimation(.snappy(duration: 0.22)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
self.scrollPosition = self.scrollerBottomID
|
self.scrollPosition = self.scrollerBottomID
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export default defineConfig({
|
|||||||
output: {
|
output: {
|
||||||
file: outputFile,
|
file: outputFile,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
inlineDynamicImports: true,
|
codeSplitting: false,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -168,8 +168,7 @@
|
|||||||
<h2>Getting started</h2>
|
<h2>Getting started</h2>
|
||||||
<p>
|
<p>
|
||||||
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
||||||
Start Clawdbot’s browser relay on this machine (Gateway or <code>clawdbot browser serve</code>),
|
Start Clawdbot’s browser relay on this machine (Gateway or node host), then click the toolbar button again.
|
||||||
then click the toolbar button again.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Full guide (install, remote Gateway, security): <a href="https://docs.clawd.bot/tools/chrome-extension" target="_blank" rel="noreferrer">docs.clawd.bot/tools/chrome-extension</a>
|
Full guide (install, remote Gateway, security): <a href="https://docs.clawd.bot/tools/chrome-extension" target="_blank" rel="noreferrer">docs.clawd.bot/tools/chrome-extension</a>
|
||||||
|
|||||||
1
dist/control-ui/assets/index-08nzABV3.css
vendored
1
dist/control-ui/assets/index-08nzABV3.css
vendored
File diff suppressed because one or more lines are too long
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
File diff suppressed because one or more lines are too long
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
File diff suppressed because one or more lines are too long
15
dist/control-ui/index.html
vendored
15
dist/control-ui/index.html
vendored
@ -1,15 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Clawdbot Control</title>
|
|
||||||
<meta name="color-scheme" content="dark light" />
|
|
||||||
<link rel="icon" href="./favicon.ico" sizes="any" />
|
|
||||||
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<clawdbot-app></clawdbot-app>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -115,6 +115,9 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
padding: 22px 16px 10px;
|
padding: 22px 16px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -201,7 +201,7 @@ For ad-hoc workflows, call Lobster directly.
|
|||||||
|
|
||||||
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
|
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
|
||||||
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
|
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
|
||||||
- The tool is an **optional plugin**; you must allowlist `lobster` in `tools.allow`.
|
- The tool is an **optional plugin**; enable it additively via `tools.alsoAllow: ["lobster"]` (recommended).
|
||||||
- If you pass `lobsterPath`, it must be an **absolute path**.
|
- If you pass `lobsterPath`, it must be an **absolute path**.
|
||||||
|
|
||||||
See [Lobster](/tools/lobster) for full usage and examples.
|
See [Lobster](/tools/lobster) for full usage and examples.
|
||||||
|
|||||||
@ -83,6 +83,8 @@ Notes:
|
|||||||
- Per-hook `model`/`thinking` in the mapping still overrides these defaults.
|
- Per-hook `model`/`thinking` in the mapping still overrides these defaults.
|
||||||
- Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts).
|
- Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts).
|
||||||
- If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
|
- If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
|
||||||
|
- Gmail hook content is wrapped with external-content safety boundaries by default.
|
||||||
|
To disable (dangerous), set `hooks.gmail.allowUnsafeExternalContent: true`.
|
||||||
|
|
||||||
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
|
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
|
||||||
under `hooks.transformsDir` (see [Webhooks](/automation/webhook)).
|
under `hooks.transformsDir` (see [Webhooks](/automation/webhook)).
|
||||||
|
|||||||
@ -27,10 +27,10 @@ Notes:
|
|||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
Every request must include the hook token:
|
Every request must include the hook token. Prefer headers:
|
||||||
- `Authorization: Bearer <token>`
|
- `Authorization: Bearer <token>` (recommended)
|
||||||
- or `x-clawdbot-token: <token>`
|
- `x-clawdbot-token: <token>`
|
||||||
- or `?token=<token>`
|
- `?token=<token>` (deprecated; logs a warning and will be removed in a future major release)
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
@ -96,6 +96,8 @@ Mapping options (summary):
|
|||||||
- TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
|
- TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
|
||||||
- Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
|
- Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
|
||||||
(`channel` defaults to `last` and falls back to WhatsApp).
|
(`channel` defaults to `last` and falls back to WhatsApp).
|
||||||
|
- `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook
|
||||||
|
(dangerous; only for trusted internal sources).
|
||||||
- `clawdbot webhooks gmail setup` writes `hooks.gmail` config for `clawdbot webhooks gmail run`.
|
- `clawdbot webhooks gmail setup` writes `hooks.gmail` config for `clawdbot webhooks gmail run`.
|
||||||
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
||||||
|
|
||||||
@ -148,3 +150,6 @@ curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
|||||||
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
||||||
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
||||||
- Avoid including sensitive raw payloads in webhook logs.
|
- Avoid including sensitive raw payloads in webhook logs.
|
||||||
|
- Hook payloads are treated as untrusted and wrapped with safety boundaries by default.
|
||||||
|
If you must disable this for a specific hook, set `allowUnsafeExternalContent: true`
|
||||||
|
in that hook's mapping (dangerous).
|
||||||
|
|||||||
@ -218,6 +218,7 @@ Prefer `chat_guid` for stable routing:
|
|||||||
## Security
|
## Security
|
||||||
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
|
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
|
||||||
- Keep the API password and webhook endpoint secret (treat them like credentials).
|
- Keep the API password and webhook endpoint secret (treat them like credentials).
|
||||||
|
- Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration).
|
||||||
- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
|
- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@ -10,13 +10,14 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
|
|||||||
|
|
||||||
## Quick setup (beginner)
|
## Quick setup (beginner)
|
||||||
1) Create a Discord bot and copy the bot token.
|
1) Create a Discord bot and copy the bot token.
|
||||||
2) Set the token for Clawdbot:
|
2) In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups).
|
||||||
|
3) Set the token for Clawdbot:
|
||||||
- Env: `DISCORD_BOT_TOKEN=...`
|
- Env: `DISCORD_BOT_TOKEN=...`
|
||||||
- Or config: `channels.discord.token: "..."`.
|
- Or config: `channels.discord.token: "..."`.
|
||||||
- If both are set, config takes precedence (env fallback is default-account only).
|
- If both are set, config takes precedence (env fallback is default-account only).
|
||||||
3) Invite the bot to your server with message permissions.
|
4) Invite the bot to your server with message permissions (create a private server if you just want DMs).
|
||||||
4) Start the gateway.
|
5) Start the gateway.
|
||||||
5) DM access is pairing by default; approve the pairing code on first contact.
|
6) DM access is pairing by default; approve the pairing code on first contact.
|
||||||
|
|
||||||
Minimal config:
|
Minimal config:
|
||||||
```json5
|
```json5
|
||||||
@ -297,8 +298,12 @@ ack reaction after the bot replies.
|
|||||||
- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
|
- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
|
||||||
- `guilds.<id>.slug`: optional friendly slug used for display names.
|
- `guilds.<id>.slug`: optional friendly slug used for display names.
|
||||||
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
|
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
|
||||||
|
- `guilds.<id>.tools`: optional per-guild tool policy overrides (`allow`/`deny`/`alsoAllow`) used when the channel override is missing.
|
||||||
|
- `guilds.<id>.toolsBySender`: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; `"*"` wildcard supported).
|
||||||
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||||
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
|
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
|
||||||
|
- `guilds.<id>.channels.<channel>.tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||||
|
- `guilds.<id>.channels.<channel>.toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported).
|
||||||
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
|
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
|
||||||
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
||||||
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
|
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
|
||||||
|
|||||||
@ -21,10 +21,12 @@ Text is supported everywhere; media and reactions vary by channel.
|
|||||||
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
|
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
|
||||||
- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).
|
- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).
|
||||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||||
|
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
|
||||||
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
||||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||||
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
||||||
|
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
|
||||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||||
|
|||||||
183
docs/channels/line.md
Normal file
183
docs/channels/line.md
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
summary: "LINE Messaging API plugin setup, config, and usage"
|
||||||
|
read_when:
|
||||||
|
- You want to connect Clawdbot to LINE
|
||||||
|
- You need LINE webhook + credential setup
|
||||||
|
- You want LINE-specific message options
|
||||||
|
---
|
||||||
|
|
||||||
|
# LINE (plugin)
|
||||||
|
|
||||||
|
LINE connects to Clawdbot via the LINE Messaging API. The plugin runs as a webhook
|
||||||
|
receiver on the gateway and uses your channel access token + channel secret for
|
||||||
|
authentication.
|
||||||
|
|
||||||
|
Status: supported via plugin. Direct messages, group chats, media, locations, Flex
|
||||||
|
messages, template messages, and quick replies are supported. Reactions and threads
|
||||||
|
are not supported.
|
||||||
|
|
||||||
|
## Plugin required
|
||||||
|
|
||||||
|
Install the LINE plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install @clawdbot/line
|
||||||
|
```
|
||||||
|
|
||||||
|
Local checkout (when running from a git repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install ./extensions/line
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1) Create a LINE Developers account and open the Console:
|
||||||
|
https://developers.line.biz/console/
|
||||||
|
2) Create (or pick) a Provider and add a **Messaging API** channel.
|
||||||
|
3) Copy the **Channel access token** and **Channel secret** from the channel settings.
|
||||||
|
4) Enable **Use webhook** in the Messaging API settings.
|
||||||
|
5) Set the webhook URL to your gateway endpoint (HTTPS required):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://gateway-host/line/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
The gateway responds to LINE’s webhook verification (GET) and inbound events (POST).
|
||||||
|
If you need a custom path, set `channels.line.webhookPath` or
|
||||||
|
`channels.line.accounts.<id>.webhookPath` and update the URL accordingly.
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
Minimal config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
line: {
|
||||||
|
enabled: true,
|
||||||
|
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
|
||||||
|
channelSecret: "LINE_CHANNEL_SECRET",
|
||||||
|
dmPolicy: "pairing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Env vars (default account only):
|
||||||
|
|
||||||
|
- `LINE_CHANNEL_ACCESS_TOKEN`
|
||||||
|
- `LINE_CHANNEL_SECRET`
|
||||||
|
|
||||||
|
Token/secret files:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
line: {
|
||||||
|
tokenFile: "/path/to/line-token.txt",
|
||||||
|
secretFile: "/path/to/line-secret.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple accounts:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
line: {
|
||||||
|
accounts: {
|
||||||
|
marketing: {
|
||||||
|
channelAccessToken: "...",
|
||||||
|
channelSecret: "...",
|
||||||
|
webhookPath: "/line/marketing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access control
|
||||||
|
|
||||||
|
Direct messages default to pairing. Unknown senders get a pairing code and their
|
||||||
|
messages are ignored until approved.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot pairing list line
|
||||||
|
clawdbot pairing approve line <CODE>
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowlists and policies:
|
||||||
|
|
||||||
|
- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled`
|
||||||
|
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs
|
||||||
|
- `channels.line.groupPolicy`: `allowlist | open | disabled`
|
||||||
|
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
|
||||||
|
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`
|
||||||
|
|
||||||
|
LINE IDs are case-sensitive. Valid IDs look like:
|
||||||
|
|
||||||
|
- User: `U` + 32 hex chars
|
||||||
|
- Group: `C` + 32 hex chars
|
||||||
|
- Room: `R` + 32 hex chars
|
||||||
|
|
||||||
|
## Message behavior
|
||||||
|
|
||||||
|
- Text is chunked at 5000 characters.
|
||||||
|
- Markdown formatting is stripped; code blocks and tables are converted into Flex
|
||||||
|
cards when possible.
|
||||||
|
- Streaming responses are buffered; LINE receives full chunks with a loading
|
||||||
|
animation while the agent works.
|
||||||
|
- Media downloads are capped by `channels.line.mediaMaxMb` (default 10).
|
||||||
|
|
||||||
|
## Channel data (rich messages)
|
||||||
|
|
||||||
|
Use `channelData.line` to send quick replies, locations, Flex cards, or template
|
||||||
|
messages.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
text: "Here you go",
|
||||||
|
channelData: {
|
||||||
|
line: {
|
||||||
|
quickReplies: ["Status", "Help"],
|
||||||
|
location: {
|
||||||
|
title: "Office",
|
||||||
|
address: "123 Main St",
|
||||||
|
latitude: 35.681236,
|
||||||
|
longitude: 139.767125
|
||||||
|
},
|
||||||
|
flexMessage: {
|
||||||
|
altText: "Status card",
|
||||||
|
contents: { /* Flex payload */ }
|
||||||
|
},
|
||||||
|
templateMessage: {
|
||||||
|
type: "confirm",
|
||||||
|
text: "Proceed?",
|
||||||
|
confirmLabel: "Yes",
|
||||||
|
confirmData: "yes",
|
||||||
|
cancelLabel: "No",
|
||||||
|
cancelData: "no"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The LINE plugin also ships a `/card` command for Flex message presets:
|
||||||
|
|
||||||
|
```
|
||||||
|
/card info "Welcome" "Thanks for joining!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Webhook verification fails:** ensure the webhook URL is HTTPS and the
|
||||||
|
`channelSecret` matches the LINE console.
|
||||||
|
- **No inbound events:** confirm the webhook path matches `channels.line.webhookPath`
|
||||||
|
and that the gateway is reachable from LINE.
|
||||||
|
- **Media download errors:** raise `channels.line.mediaMaxMb` if media exceeds the
|
||||||
|
default limit.
|
||||||
@ -10,7 +10,7 @@ on any homeserver, so you need a Matrix account for the bot. Once it is logged i
|
|||||||
the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too,
|
the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too,
|
||||||
but it requires E2EE to be enabled.
|
but it requires E2EE to be enabled.
|
||||||
|
|
||||||
Status: supported via plugin (matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
|
Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
|
||||||
polls (send + poll-start as text), location, and E2EE (with crypto support).
|
polls (send + poll-start as text), location, and E2EE (with crypto support).
|
||||||
|
|
||||||
## Plugin required
|
## Plugin required
|
||||||
|
|||||||
@ -421,8 +421,12 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
|
|||||||
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
|
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
|
||||||
- `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
|
- `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
|
||||||
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
|
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
|
||||||
|
- `channels.msteams.teams.<teamId>.tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.
|
||||||
|
- `channels.msteams.teams.<teamId>.toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported).
|
||||||
- `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.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||||
|
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
|
||||||
- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
|
- `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
|
||||||
|
|||||||
@ -464,6 +464,8 @@ For fine-grained control, use these tags in agent responses:
|
|||||||
Channel options (`channels.slack.channels.<id>` or `channels.slack.channels.<name>`):
|
Channel options (`channels.slack.channels.<id>` or `channels.slack.channels.<name>`):
|
||||||
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||||
- `requireMention`: mention gating for the channel.
|
- `requireMention`: mention gating for the channel.
|
||||||
|
- `tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||||
|
- `toolsBySender`: optional per-sender tool policy overrides within the channel (keys are sender ids/@handles/emails; `"*"` wildcard supported).
|
||||||
- `allowBots`: allow bot-authored messages in this channel (default: false).
|
- `allowBots`: allow bot-authored messages in this channel (default: false).
|
||||||
- `users`: optional per-channel user allowlist.
|
- `users`: optional per-channel user allowlist.
|
||||||
- `skills`: skill filter (omit = all skills, empty = none).
|
- `skills`: skill filter (omit = all skills, empty = none).
|
||||||
|
|||||||
@ -383,6 +383,133 @@ For message tool sends, set `asVoice: true` with a voice-compatible audio `media
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stickers
|
||||||
|
|
||||||
|
Clawdbot supports receiving and sending Telegram stickers with intelligent caching.
|
||||||
|
|
||||||
|
### Receiving stickers
|
||||||
|
|
||||||
|
When a user sends a sticker, Clawdbot handles it based on the sticker type:
|
||||||
|
|
||||||
|
- **Static stickers (WEBP):** Downloaded and processed through vision. The sticker appears as a `<media:sticker>` placeholder in the message content.
|
||||||
|
- **Animated stickers (TGS):** Skipped (Lottie format not supported for processing).
|
||||||
|
- **Video stickers (WEBM):** Skipped (video format not supported for processing).
|
||||||
|
|
||||||
|
Template context field available when receiving stickers:
|
||||||
|
- `Sticker` — object with:
|
||||||
|
- `emoji` — emoji associated with the sticker
|
||||||
|
- `setName` — name of the sticker set
|
||||||
|
- `fileId` — Telegram file ID (send the same sticker back)
|
||||||
|
- `fileUniqueId` — stable ID for cache lookup
|
||||||
|
- `cachedDescription` — cached vision description when available
|
||||||
|
|
||||||
|
### Sticker cache
|
||||||
|
|
||||||
|
Stickers are processed through the AI's vision capabilities to generate descriptions. Since the same stickers are often sent repeatedly, Clawdbot caches these descriptions to avoid redundant API calls.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
|
||||||
|
1. **First encounter:** The sticker image is sent to the AI for vision analysis. The AI generates a description (e.g., "A cartoon cat waving enthusiastically").
|
||||||
|
2. **Cache storage:** The description is saved along with the sticker's file ID, emoji, and set name.
|
||||||
|
3. **Subsequent encounters:** When the same sticker is seen again, the cached description is used directly. The image is not sent to the AI.
|
||||||
|
|
||||||
|
**Cache location:** `~/.clawdbot/telegram/sticker-cache.json`
|
||||||
|
|
||||||
|
**Cache entry format:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fileId": "CAACAgIAAxkBAAI...",
|
||||||
|
"fileUniqueId": "AgADBAADb6cxG2Y",
|
||||||
|
"emoji": "👋",
|
||||||
|
"setName": "CoolCats",
|
||||||
|
"description": "A cartoon cat waving enthusiastically",
|
||||||
|
"cachedAt": "2026-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Reduces API costs by avoiding repeated vision calls for the same sticker
|
||||||
|
- Faster response times for cached stickers (no vision processing delay)
|
||||||
|
- Enables sticker search functionality based on cached descriptions
|
||||||
|
|
||||||
|
The cache is populated automatically as stickers are received. There is no manual cache management required.
|
||||||
|
|
||||||
|
### Sending stickers
|
||||||
|
|
||||||
|
The agent can send and search stickers using the `sticker` and `sticker-search` actions. These are disabled by default and must be enabled in config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
actions: {
|
||||||
|
sticker: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Send a sticker:**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "sticker",
|
||||||
|
"channel": "telegram",
|
||||||
|
"to": "123456789",
|
||||||
|
"fileId": "CAACAgIAAxkBAAI..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `fileId` (required) — the Telegram file ID of the sticker. Obtain this from `Sticker.fileId` when receiving a sticker, or from a `sticker-search` result.
|
||||||
|
- `replyTo` (optional) — message ID to reply to.
|
||||||
|
- `threadId` (optional) — message thread ID for forum topics.
|
||||||
|
|
||||||
|
**Search for stickers:**
|
||||||
|
|
||||||
|
The agent can search cached stickers by description, emoji, or set name:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "sticker-search",
|
||||||
|
"channel": "telegram",
|
||||||
|
"query": "cat waving",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns matching stickers from the cache:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"count": 2,
|
||||||
|
"stickers": [
|
||||||
|
{
|
||||||
|
"fileId": "CAACAgIAAxkBAAI...",
|
||||||
|
"emoji": "👋",
|
||||||
|
"description": "A cartoon cat waving enthusiastically",
|
||||||
|
"setName": "CoolCats"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The search uses fuzzy matching across description text, emoji characters, and set names.
|
||||||
|
|
||||||
|
**Example with threading:**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "sticker",
|
||||||
|
"channel": "telegram",
|
||||||
|
"to": "-1001234567890",
|
||||||
|
"fileId": "CAACAgIAAxkBAAI...",
|
||||||
|
"replyTo": 42,
|
||||||
|
"threadId": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Streaming (drafts)
|
## Streaming (drafts)
|
||||||
Telegram can stream **draft bubbles** while the agent is generating a response.
|
Telegram can stream **draft bubbles** while the agent is generating a response.
|
||||||
Clawdbot uses Bot API `sendMessageDraft` (not real messages) and then sends the
|
Clawdbot uses Bot API `sendMessageDraft` (not real messages) and then sends the
|
||||||
@ -420,7 +547,7 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti
|
|||||||
- Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`).
|
- Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`).
|
||||||
- Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`).
|
- Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`).
|
||||||
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
||||||
- Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled).
|
- Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled), and `channels.telegram.actions.sticker` (default: disabled).
|
||||||
|
|
||||||
## Reaction notifications
|
## Reaction notifications
|
||||||
|
|
||||||
@ -529,6 +656,7 @@ Provider options:
|
|||||||
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
||||||
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
|
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||||
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||||
|
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.
|
||||||
- `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
|
- `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
|
||||||
- `channels.telegram.webhookUrl`: enable webhook mode.
|
- `channels.telegram.webhookUrl`: enable webhook mode.
|
||||||
- `channels.telegram.webhookSecret`: webhook secret (optional).
|
- `channels.telegram.webhookSecret`: webhook secret (optional).
|
||||||
@ -536,6 +664,7 @@ Provider options:
|
|||||||
- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
|
- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
|
||||||
- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
|
- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
|
||||||
- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
|
- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
|
||||||
|
- `channels.telegram.actions.sticker`: gate Telegram sticker actions — send and search (default: false).
|
||||||
- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set).
|
- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set).
|
||||||
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set).
|
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set).
|
||||||
|
|
||||||
|
|||||||
366
docs/channels/twitch.md
Normal file
366
docs/channels/twitch.md
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
---
|
||||||
|
summary: "Twitch chat bot configuration and setup"
|
||||||
|
read_when:
|
||||||
|
- Setting up Twitch chat integration for Clawdbot
|
||||||
|
---
|
||||||
|
# Twitch (plugin)
|
||||||
|
|
||||||
|
Twitch chat support via IRC connection. Clawdbot connects as a Twitch user (bot account) to receive and send messages in channels.
|
||||||
|
|
||||||
|
## Plugin required
|
||||||
|
|
||||||
|
Twitch ships as a plugin and is not bundled with the core install.
|
||||||
|
|
||||||
|
Install via CLI (npm registry):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install @clawdbot/twitch
|
||||||
|
```
|
||||||
|
|
||||||
|
Local checkout (when running from a git repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install ./extensions/twitch
|
||||||
|
```
|
||||||
|
|
||||||
|
Details: [Plugins](/plugin)
|
||||||
|
|
||||||
|
## Quick setup (beginner)
|
||||||
|
|
||||||
|
1) Create a dedicated Twitch account for the bot (or use an existing account).
|
||||||
|
2) Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/)
|
||||||
|
- Select **Bot Token**
|
||||||
|
- Verify scopes `chat:read` and `chat:write` are selected
|
||||||
|
- Copy the **Client ID** and **Access Token**
|
||||||
|
3) Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
||||||
|
4) Configure the token:
|
||||||
|
- Env: `CLAWDBOT_TWITCH_ACCESS_TOKEN=...` (default account only)
|
||||||
|
- Or config: `channels.twitch.accessToken`
|
||||||
|
- If both are set, config takes precedence (env fallback is default-account only).
|
||||||
|
5) Start the gateway.
|
||||||
|
|
||||||
|
**⚠️ Important:** Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`.
|
||||||
|
|
||||||
|
Minimal config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
enabled: true,
|
||||||
|
username: "clawdbot", // Bot's Twitch account
|
||||||
|
accessToken: "oauth:abc123...", // OAuth Access Token (or use CLAWDBOT_TWITCH_ACCESS_TOKEN env var)
|
||||||
|
clientId: "xyz789...", // Client ID from Token Generator
|
||||||
|
channel: "vevisk", // Which Twitch channel's chat to join (required)
|
||||||
|
allowFrom: ["123456789"] // (recommended) Your Twitch user ID only - get it from https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
- A Twitch channel owned by the Gateway.
|
||||||
|
- Deterministic routing: replies always go back to Twitch.
|
||||||
|
- Each account maps to an isolated session key `agent:<agentId>:twitch:<accountName>`.
|
||||||
|
- `username` is the bot's account (who authenticates), `channel` is which chat room to join.
|
||||||
|
|
||||||
|
## Setup (detailed)
|
||||||
|
|
||||||
|
### Generate credentials
|
||||||
|
|
||||||
|
Use [Twitch Token Generator](https://twitchtokengenerator.com/):
|
||||||
|
- Select **Bot Token**
|
||||||
|
- Verify scopes `chat:read` and `chat:write` are selected
|
||||||
|
- Copy the **Client ID** and **Access Token**
|
||||||
|
|
||||||
|
No manual app registration needed. Tokens expire after several hours.
|
||||||
|
|
||||||
|
### Configure the bot
|
||||||
|
|
||||||
|
**Env var (default account only):**
|
||||||
|
```bash
|
||||||
|
CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:abc123...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or config:**
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
enabled: true,
|
||||||
|
username: "clawdbot",
|
||||||
|
accessToken: "oauth:abc123...",
|
||||||
|
clientId: "xyz789...",
|
||||||
|
channel: "vevisk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If both env and config are set, config takes precedence.
|
||||||
|
|
||||||
|
### Access control (recommended)
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
allowFrom: ["123456789"], // (recommended) Your Twitch user ID only
|
||||||
|
allowedRoles: ["moderator"] // Or restrict to roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`.
|
||||||
|
|
||||||
|
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
|
||||||
|
|
||||||
|
Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/ (Convert your Twitch username to ID)
|
||||||
|
|
||||||
|
## Token refresh (optional)
|
||||||
|
|
||||||
|
Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - regenerate when expired.
|
||||||
|
|
||||||
|
For automatic token refresh, create your own Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console) and add to config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
clientSecret: "your_client_secret",
|
||||||
|
refreshToken: "your_refresh_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot automatically refreshes tokens before expiration and logs refresh events.
|
||||||
|
|
||||||
|
## Multi-account support
|
||||||
|
|
||||||
|
Use `channels.twitch.accounts` with per-account tokens. See [`gateway/configuration`](/gateway/configuration) for the shared pattern.
|
||||||
|
|
||||||
|
Example (one bot account in two channels):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
channel1: {
|
||||||
|
username: "clawdbot",
|
||||||
|
accessToken: "oauth:abc123...",
|
||||||
|
clientId: "xyz789...",
|
||||||
|
channel: "vevisk"
|
||||||
|
},
|
||||||
|
channel2: {
|
||||||
|
username: "clawdbot",
|
||||||
|
accessToken: "oauth:def456...",
|
||||||
|
clientId: "uvw012...",
|
||||||
|
channel: "secondchannel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Each account needs its own token (one token per channel).
|
||||||
|
|
||||||
|
## Access control
|
||||||
|
|
||||||
|
### Role-based restrictions
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
allowedRoles: ["moderator", "vip"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Allowlist by User ID (most secure)
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
allowFrom: ["123456789", "987654321"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined allowlist + roles
|
||||||
|
|
||||||
|
Users in `allowFrom` bypass role checks:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
allowFrom: ["123456789"],
|
||||||
|
allowedRoles: ["moderator"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable @mention requirement
|
||||||
|
|
||||||
|
By default, `requireMention` is `true`. To disable and respond to all messages:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
requireMention: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
First, run diagnostic commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
clawdbot channels status --probe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot doesn't respond to messages
|
||||||
|
|
||||||
|
**Check access control:** Temporarily set `allowedRoles: ["all"]` to test.
|
||||||
|
|
||||||
|
**Check the bot is in the channel:** The bot must join the channel specified in `channel`.
|
||||||
|
|
||||||
|
### Token issues
|
||||||
|
|
||||||
|
**"Failed to connect" or authentication errors:**
|
||||||
|
- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix)
|
||||||
|
- Check token has `chat:read` and `chat:write` scopes
|
||||||
|
- If using token refresh, verify `clientSecret` and `refreshToken` are set
|
||||||
|
|
||||||
|
### Token refresh not working
|
||||||
|
|
||||||
|
**Check logs for refresh events:**
|
||||||
|
```
|
||||||
|
Using env token source for mybot
|
||||||
|
Access token refreshed for user 123456 (expires in 14400s)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see "token refresh disabled (no refresh token)":
|
||||||
|
- Ensure `clientSecret` is provided
|
||||||
|
- Ensure `refreshToken` is provided
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
**Account config:**
|
||||||
|
- `username` - Bot username
|
||||||
|
- `accessToken` - OAuth access token with `chat:read` and `chat:write`
|
||||||
|
- `clientId` - Twitch Client ID (from Token Generator or your app)
|
||||||
|
- `channel` - Channel to join (required)
|
||||||
|
- `enabled` - Enable this account (default: `true`)
|
||||||
|
- `clientSecret` - Optional: For automatic token refresh
|
||||||
|
- `refreshToken` - Optional: For automatic token refresh
|
||||||
|
- `expiresIn` - Token expiry in seconds
|
||||||
|
- `obtainmentTimestamp` - Token obtained timestamp
|
||||||
|
- `allowFrom` - User ID allowlist
|
||||||
|
- `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`)
|
||||||
|
- `requireMention` - Require @mention (default: `true`)
|
||||||
|
|
||||||
|
**Provider options:**
|
||||||
|
- `channels.twitch.enabled` - Enable/disable channel startup
|
||||||
|
- `channels.twitch.username` - Bot username (simplified single-account config)
|
||||||
|
- `channels.twitch.accessToken` - OAuth access token (simplified single-account config)
|
||||||
|
- `channels.twitch.clientId` - Twitch Client ID (simplified single-account config)
|
||||||
|
- `channels.twitch.channel` - Channel to join (simplified single-account config)
|
||||||
|
- `channels.twitch.accounts.<accountName>` - Multi-account config (all account fields above)
|
||||||
|
|
||||||
|
Full example:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
enabled: true,
|
||||||
|
username: "clawdbot",
|
||||||
|
accessToken: "oauth:abc123...",
|
||||||
|
clientId: "xyz789...",
|
||||||
|
channel: "vevisk",
|
||||||
|
clientSecret: "secret123...",
|
||||||
|
refreshToken: "refresh456...",
|
||||||
|
allowFrom: ["123456789"],
|
||||||
|
allowedRoles: ["moderator", "vip"],
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
accessToken: "oauth:abc123...",
|
||||||
|
clientId: "xyz789...",
|
||||||
|
channel: "your_channel",
|
||||||
|
enabled: true,
|
||||||
|
clientSecret: "secret123...",
|
||||||
|
refreshToken: "refresh456...",
|
||||||
|
expiresIn: 14400,
|
||||||
|
obtainmentTimestamp: 1706092800000,
|
||||||
|
allowFrom: ["123456789", "987654321"],
|
||||||
|
allowedRoles: ["moderator"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool actions
|
||||||
|
|
||||||
|
The agent can call `twitch` with action:
|
||||||
|
- `send` - Send a message to a channel
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "twitch",
|
||||||
|
"params": {
|
||||||
|
"message": "Hello Twitch!",
|
||||||
|
"to": "#mychannel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety & ops
|
||||||
|
|
||||||
|
- **Treat tokens like passwords** - Never commit tokens to git
|
||||||
|
- **Use automatic token refresh** for long-running bots
|
||||||
|
- **Use user ID allowlists** instead of usernames for access control
|
||||||
|
- **Monitor logs** for token refresh events and connection status
|
||||||
|
- **Scope tokens minimally** - Only request `chat:read` and `chat:write`
|
||||||
|
- **If stuck**: Restart the gateway after confirming no other process owns the session
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
- **500 characters** per message (auto-chunked at word boundaries)
|
||||||
|
- Markdown is stripped before chunking
|
||||||
|
- No rate limiting (uses Twitch's built-in rate limits)
|
||||||
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
summary: "CLI reference for `clawdbot browser` (profiles, tabs, actions, extension relay, remote serve)"
|
summary: "CLI reference for `clawdbot browser` (profiles, tabs, actions, extension relay)"
|
||||||
read_when:
|
read_when:
|
||||||
- You use `clawdbot browser` and want examples for common tasks
|
- You use `clawdbot browser` and want examples for common tasks
|
||||||
- You want to control a remote browser via `browser.controlUrl`
|
- You want to control a browser running on another machine via a node host
|
||||||
- You want to use the Chrome extension relay (attach/detach via toolbar button)
|
- You want to use the Chrome extension relay (attach/detach via toolbar button)
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -16,8 +16,10 @@ Related:
|
|||||||
|
|
||||||
## Common flags
|
## Common flags
|
||||||
|
|
||||||
- `--url <controlUrl>`: override `browser.controlUrl` for this command invocation.
|
- `--url <gatewayWsUrl>`: Gateway WebSocket URL (defaults to config).
|
||||||
- `--browser-profile <name>`: choose a browser profile (default comes from config).
|
- `--token <token>`: Gateway token (if required).
|
||||||
|
- `--timeout <ms>`: request timeout (ms).
|
||||||
|
- `--browser-profile <name>`: choose a browser profile (default from config).
|
||||||
- `--json`: machine-readable output (where supported).
|
- `--json`: machine-readable output (where supported).
|
||||||
|
|
||||||
## Quick start (local)
|
## Quick start (local)
|
||||||
@ -93,14 +95,10 @@ Then Chrome → `chrome://extensions` → enable “Developer mode” → “Loa
|
|||||||
|
|
||||||
Full guide: [Chrome extension](/tools/chrome-extension)
|
Full guide: [Chrome extension](/tools/chrome-extension)
|
||||||
|
|
||||||
## Remote browser control (`clawdbot browser serve`)
|
## Remote browser control (node host proxy)
|
||||||
|
|
||||||
If the Gateway runs on a different machine than the browser, run a standalone browser control server on the machine that runs Chrome:
|
If the Gateway runs on a different machine than the browser, run a **node host** on the machine that has Chrome/Brave/Edge/Chromium. The Gateway will proxy browser actions to that node (no separate browser control server required).
|
||||||
|
|
||||||
```bash
|
Use `gateway.nodes.browser.mode` to control auto-routing and `gateway.nodes.browser.node` to pin a specific node if multiple are connected.
|
||||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then point the Gateway at it using `browser.controlUrl` + `browser.controlToken` (or `CLAWDBOT_BROWSER_CONTROL_TOKEN`).
|
Security + remote setup: [Browser tool](/tools/browser), [Remote access](/gateway/remote), [Tailscale](/gateway/tailscale), [Security](/gateway/security)
|
||||||
|
|
||||||
Security + TLS best-practices: [Browser tool](/tools/browser), [Tailscale](/gateway/tailscale), [Security](/gateway/security)
|
|
||||||
|
|||||||
@ -297,7 +297,7 @@ Options:
|
|||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
||||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
@ -314,7 +314,7 @@ Options:
|
|||||||
- `--opencode-zen-api-key <key>`
|
- `--opencode-zen-api-key <key>`
|
||||||
- `--gateway-port <port>`
|
- `--gateway-port <port>`
|
||||||
- `--gateway-bind <loopback|lan|tailnet|auto|custom>`
|
- `--gateway-bind <loopback|lan|tailnet|auto|custom>`
|
||||||
- `--gateway-auth <off|token|password>`
|
- `--gateway-auth <token|password>`
|
||||||
- `--gateway-token <token>`
|
- `--gateway-token <token>`
|
||||||
- `--gateway-password <password>`
|
- `--gateway-password <password>`
|
||||||
- `--remote-url <url>`
|
- `--remote-url <url>`
|
||||||
@ -358,7 +358,7 @@ Options:
|
|||||||
Manage chat channel accounts (WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
|
Manage chat channel accounts (WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
|
- `channels list`: show configured channels and auth profiles.
|
||||||
- `channels status`: check gateway reachability and channel health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
|
- `channels status`: check gateway reachability and channel health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes).
|
||||||
- Tip: `channels status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
|
- Tip: `channels status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
|
||||||
- `channels logs`: show recent channel logs from the gateway log file.
|
- `channels logs`: show recent channel logs from the gateway log file.
|
||||||
@ -390,12 +390,6 @@ Common options:
|
|||||||
- `--lines <n>` (default `200`)
|
- `--lines <n>` (default `200`)
|
||||||
- `--json`
|
- `--json`
|
||||||
|
|
||||||
OAuth sync sources:
|
|
||||||
- Claude Code → `anthropic:claude-cli`
|
|
||||||
- macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts)
|
|
||||||
- Linux/Windows: `~/.claude/.credentials.json`
|
|
||||||
- `~/.codex/auth.json` → `openai-codex:codex-cli`
|
|
||||||
|
|
||||||
More detail: [/concepts/oauth](/concepts/oauth)
|
More detail: [/concepts/oauth](/concepts/oauth)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@ -676,10 +670,11 @@ Tip: when calling `config.set`/`config.apply`/`config.patch` directly, pass `bas
|
|||||||
|
|
||||||
See [/concepts/models](/concepts/models) for fallback behavior and scanning strategy.
|
See [/concepts/models](/concepts/models) for fallback behavior and scanning strategy.
|
||||||
|
|
||||||
Preferred Anthropic auth (CLI token, not API key):
|
Preferred Anthropic auth (setup-token):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude setup-token
|
claude setup-token
|
||||||
|
clawdbot models auth setup-token --provider anthropic
|
||||||
clawdbot models status
|
clawdbot models status
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -864,9 +859,8 @@ Location:
|
|||||||
Browser control CLI (dedicated Chrome/Brave/Edge/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser).
|
Browser control CLI (dedicated Chrome/Brave/Edge/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser).
|
||||||
|
|
||||||
Common options:
|
Common options:
|
||||||
- `--url <controlUrl>`
|
- `--url`, `--token`, `--timeout`, `--json`
|
||||||
- `--browser-profile <name>`
|
- `--browser-profile <name>`
|
||||||
- `--json`
|
|
||||||
|
|
||||||
Manage:
|
Manage:
|
||||||
- `browser status`
|
- `browser status`
|
||||||
|
|||||||
@ -64,5 +64,5 @@ clawdbot models auth paste-token
|
|||||||
`clawdbot plugins list` to see which providers are installed.
|
`clawdbot plugins list` to see which providers are installed.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `setup-token` runs `claude setup-token` on the current machine (requires the Claude Code CLI).
|
- `setup-token` prompts for a setup-token value (generate it with `claude setup-token` on any machine).
|
||||||
- `paste-token` accepts a token string generated elsewhere.
|
- `paste-token` accepts a token string generated elsewhere or from automation.
|
||||||
|
|||||||
@ -23,3 +23,4 @@ clawdbot onboard --mode remote --remote-url ws://gateway-host:18789
|
|||||||
Flow notes:
|
Flow notes:
|
||||||
- `quickstart`: minimal prompts, auto-generates a gateway token.
|
- `quickstart`: minimal prompts, auto-generates a gateway token.
|
||||||
- `manual`: full prompts for port/bind/auth (alias of `advanced`).
|
- `manual`: full prompts for port/bind/auth (alias of `advanced`).
|
||||||
|
- Fastest first chat: `clawdbot dashboard` (Control UI, no channel setup).
|
||||||
|
|||||||
@ -232,6 +232,42 @@ Notes:
|
|||||||
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
||||||
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||||
|
|
||||||
|
## Group/channel tool restrictions (optional)
|
||||||
|
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
|
||||||
|
|
||||||
|
- `tools`: allow/deny tools for the whole group.
|
||||||
|
- `toolsBySender`: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use `"*"` as a wildcard.
|
||||||
|
|
||||||
|
Resolution order (most specific wins):
|
||||||
|
1) group/channel `toolsBySender` match
|
||||||
|
2) group/channel `tools`
|
||||||
|
3) default (`"*"`) `toolsBySender` match
|
||||||
|
4) default (`"*"`) `tools`
|
||||||
|
|
||||||
|
Example (Telegram):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
groups: {
|
||||||
|
"*": { tools: { deny: ["exec"] } },
|
||||||
|
"-1001234567890": {
|
||||||
|
tools: { deny: ["exec", "read", "write"] },
|
||||||
|
toolsBySender: {
|
||||||
|
"123456789": { alsoAllow: ["exec"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
|
||||||
|
- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, MS Teams `teams.*.channels.*`).
|
||||||
|
|
||||||
## Group allowlists
|
## Group allowlists
|
||||||
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||||
|
|
||||||
|
|||||||
@ -49,9 +49,9 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no**
|
|||||||
### OpenAI Code (Codex)
|
### OpenAI Code (Codex)
|
||||||
|
|
||||||
- Provider: `openai-codex`
|
- Provider: `openai-codex`
|
||||||
- Auth: OAuth or Codex CLI (`~/.codex/auth.json`)
|
- Auth: OAuth (ChatGPT)
|
||||||
- Example model: `openai-codex/gpt-5.2`
|
- Example model: `openai-codex/gpt-5.2`
|
||||||
- CLI: `clawdbot onboard --auth-choice openai-codex` or `codex-cli`
|
- CLI: `clawdbot onboard --auth-choice openai-codex` or `clawdbot models auth login --provider openai-codex`
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
---
|
---
|
||||||
summary: "OAuth in Clawdbot: token exchange, storage, CLI sync, and multi-account patterns"
|
summary: "OAuth in Clawdbot: token exchange, storage, and multi-account patterns"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to understand Clawdbot OAuth end-to-end
|
- You want to understand Clawdbot OAuth end-to-end
|
||||||
- You hit token invalidation / logout issues
|
- You hit token invalidation / logout issues
|
||||||
- You want to reuse Claude Code / Codex CLI OAuth tokens
|
- You want setup-token or OAuth auth flows
|
||||||
- You want multiple accounts or profile routing
|
- You want multiple accounts or profile routing
|
||||||
---
|
---
|
||||||
# OAuth
|
# OAuth
|
||||||
|
|
||||||
Clawdbot supports “subscription auth” via OAuth for providers that offer it (notably **Anthropic (Claude Pro/Max)** and **OpenAI Codex (ChatGPT OAuth)**). This page explains:
|
Clawdbot supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. This page explains:
|
||||||
|
|
||||||
- how the OAuth **token exchange** works (PKCE)
|
- how the OAuth **token exchange** works (PKCE)
|
||||||
- where tokens are **stored** (and why)
|
- where tokens are **stored** (and why)
|
||||||
- how we **reuse external CLI tokens** (Claude Code / Codex CLI)
|
|
||||||
- how to handle **multiple accounts** (profiles + per-session overrides)
|
- how to handle **multiple accounts** (profiles + per-session overrides)
|
||||||
|
|
||||||
Clawdbot also supports **provider plugins** that ship their own OAuth or API‑key
|
Clawdbot also supports **provider plugins** that ship their own OAuth or API‑key
|
||||||
@ -31,7 +30,6 @@ Practical symptom:
|
|||||||
|
|
||||||
To reduce that, Clawdbot treats `auth-profiles.json` as a **token sink**:
|
To reduce that, Clawdbot treats `auth-profiles.json` as a **token sink**:
|
||||||
- the runtime reads credentials from **one place**
|
- the runtime reads credentials from **one place**
|
||||||
- we can **sync in** credentials from external CLIs instead of doing a second login
|
|
||||||
- we can keep multiple profiles and route them deterministically
|
- we can keep multiple profiles and route them deterministically
|
||||||
|
|
||||||
## Storage (where tokens live)
|
## Storage (where tokens live)
|
||||||
@ -46,47 +44,39 @@ Legacy import-only file (still supported, but not the main store):
|
|||||||
|
|
||||||
All of the above also respect `$CLAWDBOT_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys)
|
All of the above also respect `$CLAWDBOT_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys)
|
||||||
|
|
||||||
## Reusing Claude Code / Codex CLI OAuth tokens (recommended)
|
## Anthropic setup-token (subscription auth)
|
||||||
|
|
||||||
If you already signed in with the external CLIs *on the gateway host*, Clawdbot can reuse those tokens without starting a separate OAuth flow:
|
Run `claude setup-token` on any machine, then paste it into Clawdbot:
|
||||||
|
|
||||||
- Claude Code: `anthropic:claude-cli`
|
```bash
|
||||||
- macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts)
|
clawdbot models auth setup-token --provider anthropic
|
||||||
- Linux/Windows: `~/.claude/.credentials.json`
|
```
|
||||||
- Codex CLI: reads `~/.codex/auth.json` → profile `openai-codex:codex-cli`
|
|
||||||
|
|
||||||
Sync happens when Clawdbot loads the auth store (so it stays up-to-date when the CLIs refresh tokens).
|
If you generated the token elsewhere, paste it manually:
|
||||||
On macOS, the first read may trigger a Keychain prompt; run `clawdbot models status`
|
|
||||||
in a terminal once if the Gateway runs headless and can’t access the entry.
|
|
||||||
|
|
||||||
How to verify:
|
```bash
|
||||||
|
clawdbot models auth paste-token --provider anthropic
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot models status
|
clawdbot models status
|
||||||
clawdbot channels list
|
|
||||||
```
|
|
||||||
|
|
||||||
Or JSON:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot channels list --json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## OAuth exchange (how login works)
|
## OAuth exchange (how login works)
|
||||||
|
|
||||||
Clawdbot’s interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
|
Clawdbot’s interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
|
||||||
|
|
||||||
### Anthropic (Claude Pro/Max)
|
### Anthropic (Claude Pro/Max) setup-token
|
||||||
|
|
||||||
Flow shape (PKCE):
|
Flow shape:
|
||||||
|
|
||||||
1) generate PKCE verifier/challenge
|
1) run `claude setup-token`
|
||||||
2) open `https://claude.ai/oauth/authorize?...`
|
2) paste the token into Clawdbot
|
||||||
3) user pastes `code#state`
|
3) store as a token auth profile (no refresh)
|
||||||
4) exchange at `https://console.anthropic.com/v1/oauth/token`
|
|
||||||
5) store `{ access, refresh, expires }` under an auth profile
|
|
||||||
|
|
||||||
The wizard path is `clawdbot onboard` → auth choice `oauth` (Anthropic).
|
The wizard path is `clawdbot onboard` → auth choice `setup-token` (Anthropic).
|
||||||
|
|
||||||
### OpenAI Codex (ChatGPT OAuth)
|
### OpenAI Codex (ChatGPT OAuth)
|
||||||
|
|
||||||
@ -99,7 +89,7 @@ Flow shape (PKCE):
|
|||||||
5) exchange at `https://auth.openai.com/oauth/token`
|
5) exchange at `https://auth.openai.com/oauth/token`
|
||||||
6) extract `accountId` from the access token and store `{ access, refresh, expires, accountId }`
|
6) extract `accountId` from the access token and store `{ access, refresh, expires, accountId }`
|
||||||
|
|
||||||
Wizard path is `clawdbot onboard` → auth choice `openai-codex` (or `codex-cli` to reuse an existing Codex CLI login).
|
Wizard path is `clawdbot onboard` → auth choice `openai-codex`.
|
||||||
|
|
||||||
## Refresh + expiry
|
## Refresh + expiry
|
||||||
|
|
||||||
@ -111,23 +101,6 @@ At runtime:
|
|||||||
|
|
||||||
The refresh flow is automatic; you generally don't need to manage tokens manually.
|
The refresh flow is automatic; you generally don't need to manage tokens manually.
|
||||||
|
|
||||||
### Bidirectional sync with Claude Code
|
|
||||||
|
|
||||||
When Clawdbot refreshes an Anthropic OAuth token (profile `anthropic:claude-cli`), it **writes the new credentials back** to Claude Code's storage:
|
|
||||||
|
|
||||||
- **Linux/Windows**: updates `~/.claude/.credentials.json`
|
|
||||||
- **macOS**: updates Keychain item "Claude Code-credentials"
|
|
||||||
|
|
||||||
This ensures both tools stay in sync and neither gets "logged out" after the other refreshes.
|
|
||||||
|
|
||||||
**Why this matters for long-running agents:**
|
|
||||||
|
|
||||||
Anthropic OAuth tokens expire after a few hours. Without bidirectional sync:
|
|
||||||
1. Clawdbot refreshes the token → gets new access token
|
|
||||||
2. Claude Code still has the old token → gets logged out
|
|
||||||
|
|
||||||
With bidirectional sync, both tools always have the latest valid token, enabling autonomous operation for days or weeks without manual intervention.
|
|
||||||
|
|
||||||
## Multiple accounts (profiles) + routing
|
## Multiple accounts (profiles) + routing
|
||||||
|
|
||||||
Two patterns:
|
Two patterns:
|
||||||
|
|||||||
@ -117,6 +117,14 @@
|
|||||||
"source": "/mattermost/",
|
"source": "/mattermost/",
|
||||||
"destination": "/channels/mattermost"
|
"destination": "/channels/mattermost"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/line",
|
||||||
|
"destination": "/channels/line"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/line/",
|
||||||
|
"destination": "/channels/line"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/glm",
|
"source": "/glm",
|
||||||
"destination": "/providers/glm"
|
"destination": "/providers/glm"
|
||||||
@ -197,6 +205,14 @@
|
|||||||
"source": "/providers/msteams/",
|
"source": "/providers/msteams/",
|
||||||
"destination": "/channels/msteams"
|
"destination": "/channels/msteams"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/providers/line",
|
||||||
|
"destination": "/channels/line"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/providers/line/",
|
||||||
|
"destination": "/channels/line"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/providers/signal",
|
"source": "/providers/signal",
|
||||||
"destination": "/channels/signal"
|
"destination": "/channels/signal"
|
||||||
@ -329,10 +345,6 @@
|
|||||||
"source": "/auth-monitoring",
|
"source": "/auth-monitoring",
|
||||||
"destination": "/automation/auth-monitoring"
|
"destination": "/automation/auth-monitoring"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"source": "/scripts",
|
|
||||||
"destination": "/scripts"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"source": "/camera",
|
"source": "/camera",
|
||||||
"destination": "/nodes/camera"
|
"destination": "/nodes/camera"
|
||||||
@ -789,6 +801,10 @@
|
|||||||
"source": "/install/railway/",
|
"source": "/install/railway/",
|
||||||
"destination": "/railway"
|
"destination": "/railway"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/install/northflank/",
|
||||||
|
"destination": "/northflank"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/gcp",
|
"source": "/gcp",
|
||||||
"destination": "/platforms/gcp"
|
"destination": "/platforms/gcp"
|
||||||
@ -836,6 +852,7 @@
|
|||||||
"install/docker",
|
"install/docker",
|
||||||
"railway",
|
"railway",
|
||||||
"render",
|
"render",
|
||||||
|
"northflank",
|
||||||
"install/bun"
|
"install/bun"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -939,6 +956,7 @@
|
|||||||
"gateway/doctor",
|
"gateway/doctor",
|
||||||
"gateway/logging",
|
"gateway/logging",
|
||||||
"gateway/security",
|
"gateway/security",
|
||||||
|
"security/formal-verification",
|
||||||
"gateway/sandbox-vs-tool-policy-vs-elevated",
|
"gateway/sandbox-vs-tool-policy-vs-elevated",
|
||||||
"gateway/sandboxing",
|
"gateway/sandboxing",
|
||||||
"gateway/troubleshooting",
|
"gateway/troubleshooting",
|
||||||
@ -974,6 +992,7 @@
|
|||||||
"channels/signal",
|
"channels/signal",
|
||||||
"channels/imessage",
|
"channels/imessage",
|
||||||
"channels/msteams",
|
"channels/msteams",
|
||||||
|
"channels/line",
|
||||||
"channels/matrix",
|
"channels/matrix",
|
||||||
"channels/zalo",
|
"channels/zalo",
|
||||||
"channels/zalouser",
|
"channels/zalouser",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "Model authentication: OAuth, API keys, and Claude Code token reuse"
|
summary: "Model authentication: OAuth, API keys, and setup-token"
|
||||||
read_when:
|
read_when:
|
||||||
- Debugging model auth or OAuth expiry
|
- Debugging model auth or OAuth expiry
|
||||||
- Documenting authentication or credential storage
|
- Documenting authentication or credential storage
|
||||||
@ -7,8 +7,8 @@ read_when:
|
|||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
Clawdbot supports OAuth and API keys for model providers. For Anthropic
|
Clawdbot supports OAuth and API keys for model providers. For Anthropic
|
||||||
accounts, we recommend using an **API key**. Clawdbot can also reuse Claude Code
|
accounts, we recommend using an **API key**. For Claude subscription access,
|
||||||
credentials, including the long‑lived token created by `claude setup-token`.
|
use the long‑lived token created by `claude setup-token`.
|
||||||
|
|
||||||
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
|
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
|
||||||
layout.
|
layout.
|
||||||
@ -47,29 +47,26 @@ API keys for daemon use: `clawdbot onboard`.
|
|||||||
See [Help](/help) for details on env inheritance (`env.shellEnv`,
|
See [Help](/help) for details on env inheritance (`env.shellEnv`,
|
||||||
`~/.clawdbot/.env`, systemd/launchd).
|
`~/.clawdbot/.env`, systemd/launchd).
|
||||||
|
|
||||||
## Anthropic: Claude Code CLI setup-token (supported)
|
## Anthropic: setup-token (subscription auth)
|
||||||
|
|
||||||
For Anthropic, the recommended path is an **API key**. If you’re already using
|
For Anthropic, the recommended path is an **API key**. If you’re using a Claude
|
||||||
Claude Code CLI, the setup-token flow is also supported.
|
subscription, the setup-token flow is also supported. Run it on the **gateway host**:
|
||||||
Run it on the **gateway host**:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude setup-token
|
claude setup-token
|
||||||
```
|
```
|
||||||
|
|
||||||
Then verify and sync into Clawdbot:
|
Then paste it into Clawdbot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot models status
|
clawdbot models auth setup-token --provider anthropic
|
||||||
clawdbot doctor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This should create (or refresh) an auth profile like `anthropic:claude-cli` in
|
If the token was created on another machine, paste it manually:
|
||||||
the agent auth store.
|
|
||||||
|
|
||||||
Clawdbot config sets `auth.profiles["anthropic:claude-cli"].mode` to `"oauth"` so
|
```bash
|
||||||
the profile accepts both OAuth and setup-token credentials. Older configs that
|
clawdbot models auth paste-token --provider anthropic
|
||||||
used `"token"` are auto-migrated on load.
|
```
|
||||||
|
|
||||||
If you see an Anthropic error like:
|
If you see an Anthropic error like:
|
||||||
|
|
||||||
@ -79,12 +76,6 @@ This credential is only authorized for use with Claude Code and cannot be used f
|
|||||||
|
|
||||||
…use an Anthropic API key instead.
|
…use an Anthropic API key instead.
|
||||||
|
|
||||||
Alternative: run the wrapper (also updates Clawdbot config):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot models auth setup-token --provider anthropic
|
|
||||||
```
|
|
||||||
|
|
||||||
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -101,10 +92,6 @@ clawdbot models status --check
|
|||||||
Optional ops scripts (systemd/Termux) are documented here:
|
Optional ops scripts (systemd/Termux) are documented here:
|
||||||
[/automation/auth-monitoring](/automation/auth-monitoring)
|
[/automation/auth-monitoring](/automation/auth-monitoring)
|
||||||
|
|
||||||
`clawdbot models status` loads Claude Code credentials into Clawdbot’s
|
|
||||||
`auth-profiles.json` and shows expiry (warns within 24h by default).
|
|
||||||
`clawdbot doctor` also performs the sync when it runs.
|
|
||||||
|
|
||||||
> `claude setup-token` requires an interactive TTY.
|
> `claude setup-token` requires an interactive TTY.
|
||||||
|
|
||||||
## Checking model auth status
|
## Checking model auth status
|
||||||
@ -118,7 +105,7 @@ clawdbot doctor
|
|||||||
|
|
||||||
### Per-session (chat command)
|
### Per-session (chat command)
|
||||||
|
|
||||||
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:claude-cli`, `anthropic:default`).
|
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`).
|
||||||
|
|
||||||
Use `/model` (or `/model list`) for a compact picker; use `/model status` for the full view (candidates + next auth profile, plus provider endpoint details when configured).
|
Use `/model` (or `/model list`) for a compact picker; use `/model status` for the full view (candidates + next auth profile, plus provider endpoint details when configured).
|
||||||
|
|
||||||
@ -128,23 +115,12 @@ Set an explicit auth profile order override for an agent (stored in that agent
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot models auth order get --provider anthropic
|
clawdbot models auth order get --provider anthropic
|
||||||
clawdbot models auth order set --provider anthropic anthropic:claude-cli
|
clawdbot models auth order set --provider anthropic anthropic:default
|
||||||
clawdbot models auth order clear --provider anthropic
|
clawdbot models auth order clear --provider anthropic
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--agent <id>` to target a specific agent; omit it to use the configured default agent.
|
Use `--agent <id>` to target a specific agent; omit it to use the configured default agent.
|
||||||
|
|
||||||
## How sync works
|
|
||||||
|
|
||||||
1. **Claude Code** stores credentials in `~/.claude/.credentials.json` (or
|
|
||||||
Keychain on macOS).
|
|
||||||
2. **Clawdbot** syncs those into
|
|
||||||
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
|
|
||||||
loaded.
|
|
||||||
3. Refreshable OAuth profiles can be refreshed automatically on use. Static
|
|
||||||
token profiles (including Claude Code CLI setup-token) are not refreshable by
|
|
||||||
Clawdbot.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### “No credentials found”
|
### “No credentials found”
|
||||||
@ -159,7 +135,7 @@ clawdbot models status
|
|||||||
### Token expiring/expired
|
### Token expiring/expired
|
||||||
|
|
||||||
Run `clawdbot models status` to confirm which profile is expiring. If the profile
|
Run `clawdbot models status` to confirm which profile is expiring. If the profile
|
||||||
is `anthropic:claude-cli`, rerun `claude setup-token`.
|
is missing, rerun `claude setup-token` and paste the token again.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@ -374,12 +374,6 @@ Overrides:
|
|||||||
|
|
||||||
On first use, Clawdbot imports `oauth.json` entries into `auth-profiles.json`.
|
On first use, Clawdbot imports `oauth.json` entries into `auth-profiles.json`.
|
||||||
|
|
||||||
Clawdbot also auto-syncs OAuth tokens from external CLIs into `auth-profiles.json` (when present on the gateway host):
|
|
||||||
- Claude Code → `anthropic:claude-cli`
|
|
||||||
- macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts)
|
|
||||||
- Linux/Windows: `~/.claude/.credentials.json`
|
|
||||||
- `~/.codex/auth.json` (Codex CLI) → `openai-codex:codex-cli`
|
|
||||||
|
|
||||||
### `auth`
|
### `auth`
|
||||||
|
|
||||||
Optional metadata for auth profiles. This does **not** store secrets; it maps
|
Optional metadata for auth profiles. This does **not** store secrets; it maps
|
||||||
@ -400,10 +394,6 @@ rotation order used for failover.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `anthropic:claude-cli` should use `mode: "oauth"` even when the stored
|
|
||||||
credential is a setup-token. Clawdbot auto-migrates older configs that used
|
|
||||||
`mode: "token"`.
|
|
||||||
|
|
||||||
### `agents.list[].identity`
|
### `agents.list[].identity`
|
||||||
|
|
||||||
Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
||||||
@ -964,6 +954,8 @@ Notes:
|
|||||||
- `commands.debug: true` enables `/debug` (runtime-only overrides).
|
- `commands.debug: true` enables `/debug` (runtime-only overrides).
|
||||||
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
||||||
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
|
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
|
||||||
|
- Slash commands and directives are only honored for **authorized senders**. Authorization is derived from
|
||||||
|
channel allowlists/pairing plus `commands.useAccessGroups`.
|
||||||
|
|
||||||
### `web` (WhatsApp web channel runtime)
|
### `web` (WhatsApp web channel runtime)
|
||||||
|
|
||||||
@ -1037,6 +1029,9 @@ Set `channels.telegram.configWrites: false` to block Telegram-initiated config w
|
|||||||
maxDelayMs: 30000,
|
maxDelayMs: 30000,
|
||||||
jitter: 0.1
|
jitter: 0.1
|
||||||
},
|
},
|
||||||
|
network: { // transport overrides
|
||||||
|
autoSelectFamily: false
|
||||||
|
},
|
||||||
proxy: "socks5://localhost:9050",
|
proxy: "socks5://localhost:9050",
|
||||||
webhookUrl: "https://example.com/telegram-webhook",
|
webhookUrl: "https://example.com/telegram-webhook",
|
||||||
webhookSecret: "secret",
|
webhookSecret: "secret",
|
||||||
@ -2764,7 +2759,7 @@ Example:
|
|||||||
|
|
||||||
### `browser` (clawd-managed browser)
|
### `browser` (clawd-managed browser)
|
||||||
|
|
||||||
Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control server.
|
Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control service.
|
||||||
Profiles can point at a **remote** Chromium-based browser via `profiles.<name>.cdpUrl`. Remote
|
Profiles can point at a **remote** Chromium-based browser via `profiles.<name>.cdpUrl`. Remote
|
||||||
profiles are attach-only (start/stop/reset are disabled).
|
profiles are attach-only (start/stop/reset are disabled).
|
||||||
|
|
||||||
@ -2773,8 +2768,9 @@ scheme/host for profiles that only set `cdpPort`.
|
|||||||
|
|
||||||
Defaults:
|
Defaults:
|
||||||
- enabled: `true`
|
- enabled: `true`
|
||||||
- control URL: `http://127.0.0.1:18791` (CDP uses `18792`)
|
- evaluateEnabled: `true` (set `false` to disable `act:evaluate` and `wait --fn`)
|
||||||
- CDP URL: `http://127.0.0.1:18792` (control URL + 1, legacy single-profile)
|
- control service: loopback only (port derived from `gateway.port`, default `18791`)
|
||||||
|
- CDP URL: `http://127.0.0.1:18792` (control service + 1, legacy single-profile)
|
||||||
- profile color: `#FF4500` (lobster-orange)
|
- profile color: `#FF4500` (lobster-orange)
|
||||||
- Note: the control server is started by the running gateway (Clawdbot.app menubar, or `clawdbot gateway`).
|
- Note: the control server is started by the running gateway (Clawdbot.app menubar, or `clawdbot gateway`).
|
||||||
- Auto-detect order: default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
|
- Auto-detect order: default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
|
||||||
@ -2783,7 +2779,7 @@ Defaults:
|
|||||||
{
|
{
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
controlUrl: "http://127.0.0.1:18791",
|
evaluateEnabled: true,
|
||||||
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
|
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
|
||||||
defaultProfile: "chrome",
|
defaultProfile: "chrome",
|
||||||
profiles: {
|
profiles: {
|
||||||
@ -2847,9 +2843,11 @@ 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 for the Control UI and skips
|
- `gateway.controlUi.allowInsecureAuth` allows token-only auth for the Control UI when
|
||||||
device identity + pairing (even on HTTPS). Default: `false`. Prefer HTTPS
|
device identity is omitted (typically over HTTP). Default: `false`. Prefer HTTPS
|
||||||
(Tailscale Serve) or `127.0.0.1`.
|
(Tailscale Serve) or `127.0.0.1`.
|
||||||
|
- `gateway.controlUi.dangerouslyDisableDeviceAuth` disables device identity checks for the
|
||||||
|
Control UI (token/password only). Default: `false`. Break-glass only.
|
||||||
|
|
||||||
Related docs:
|
Related docs:
|
||||||
- [Control UI](/web/control-ui)
|
- [Control UI](/web/control-ui)
|
||||||
@ -2867,21 +2865,22 @@ Notes:
|
|||||||
- `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`.
|
||||||
- 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`).
|
- Gateway auth is required by default (token/password or Tailscale Serve identity). Non-loopback binds require a shared token/password.
|
||||||
- The onboarding wizard generates a gateway token by default (even on loopback).
|
- The onboarding wizard generates a gateway token by default (even on loopback).
|
||||||
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
|
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
|
||||||
|
|
||||||
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`). When unset, token auth is assumed.
|
||||||
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
|
- `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
|
||||||
(`tailscale-user-login`) to satisfy auth when the request arrives on loopback
|
(`tailscale-user-login`) to satisfy auth when the request arrives on loopback
|
||||||
with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. When
|
with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. Clawdbot
|
||||||
`true`, Serve requests do not need a token/password; set `false` to require
|
verifies the identity by resolving the `x-forwarded-for` address via
|
||||||
explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and
|
`tailscale whois` before accepting it. When `true`, Serve requests do not need
|
||||||
auth mode is not `password`.
|
a token/password; set `false` to require explicit credentials. Defaults to
|
||||||
|
`true` when `tailscale.mode = "serve"` and auth mode is not `password`.
|
||||||
- `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.
|
||||||
@ -3174,6 +3173,20 @@ Auto-generated certs require `openssl` on PATH; if generation fails, the bridge
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `discovery.mdns` (Bonjour / mDNS broadcast mode)
|
||||||
|
|
||||||
|
Controls LAN mDNS discovery broadcasts (`_clawdbot-gw._tcp`).
|
||||||
|
|
||||||
|
- `minimal` (default): omit `cliPath` + `sshPort` from TXT records
|
||||||
|
- `full`: include `cliPath` + `sshPort` in TXT records
|
||||||
|
- `off`: disable mDNS broadcasts entirely
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
discovery: { mdns: { mode: "minimal" } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `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-bridge._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.`
|
||||||
|
|||||||
@ -37,7 +37,7 @@ pnpm gateway:watch
|
|||||||
- `--force` uses `lsof` to find listeners on the chosen port, sends SIGTERM, logs what it killed, then starts the gateway (fails fast if `lsof` is missing).
|
- `--force` uses `lsof` to find listeners on the chosen port, sends SIGTERM, logs what it killed, then starts the gateway (fails fast if `lsof` is missing).
|
||||||
- If you run under a supervisor (launchd/systemd/mac app child-process mode), a stop/restart typically sends **SIGTERM**; older builds may surface this as `pnpm` `ELIFECYCLE` exit code **143** (SIGTERM), which is a normal shutdown, not a crash.
|
- If you run under a supervisor (launchd/systemd/mac app child-process mode), a stop/restart typically sends **SIGTERM**; older builds may surface this as `pnpm` `ELIFECYCLE` exit code **143** (SIGTERM), which is a normal shutdown, not a crash.
|
||||||
- **SIGUSR1** triggers an in-process restart when authorized (gateway tool/config apply/update, or enable `commands.restart` for manual restarts).
|
- **SIGUSR1** triggers an in-process restart when authorized (gateway tool/config apply/update, or enable `commands.restart` for manual restarts).
|
||||||
- Gateway auth: set `gateway.auth.mode=token` + `gateway.auth.token` (or pass `--token <value>` / `CLAWDBOT_GATEWAY_TOKEN`) to require clients to send `connect.params.auth.token`.
|
- Gateway auth is required by default: set `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) or `gateway.auth.password`. Clients must send `connect.params.auth.token/password` unless using Tailscale Serve identity.
|
||||||
- The wizard now generates a token by default, even on loopback.
|
- The wizard now generates a token by default, even on loopback.
|
||||||
- Port precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
- Port precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
||||||
|
|
||||||
@ -83,13 +83,13 @@ Defaults (can be overridden via env/flags/config):
|
|||||||
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
|
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
|
||||||
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
|
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
|
||||||
- `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP)
|
- `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP)
|
||||||
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
|
- browser control service port = `19003` (derived: `gateway.port+2`, loopback only)
|
||||||
- `canvasHost.port=19005` (derived: `gateway.port+4`)
|
- `canvasHost.port=19005` (derived: `gateway.port+4`)
|
||||||
- `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
|
- `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
|
||||||
|
|
||||||
Derived ports (rules of thumb):
|
Derived ports (rules of thumb):
|
||||||
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
|
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
|
||||||
- `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override)
|
- browser control service port = base + 2 (loopback only)
|
||||||
- `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override)
|
- `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override)
|
||||||
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
|
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ clawdbot --profile rescue gateway install
|
|||||||
|
|
||||||
Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`).
|
Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`).
|
||||||
|
|
||||||
- `browser.controlUrl port = base + 2`
|
- browser control service port = base + 2 (loopback only)
|
||||||
- `canvasHost.port = base + 4`
|
- `canvasHost.port = base + 4`
|
||||||
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108`
|
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108`
|
||||||
|
|
||||||
@ -81,8 +81,8 @@ If you override any of these in config or env, you must keep them unique per ins
|
|||||||
|
|
||||||
## Browser/CDP notes (common footgun)
|
## Browser/CDP notes (common footgun)
|
||||||
|
|
||||||
- Do **not** pin `browser.controlUrl` or `browser.cdpUrl` to the same values on multiple instances.
|
- Do **not** pin `browser.cdpUrl` to the same values on multiple instances.
|
||||||
- Each instance needs its own browser control port and CDP range.
|
- Each instance needs its own browser control port and CDP range (derived from its gateway port).
|
||||||
- If you need explicit CDP ports, set `browser.profiles.<name>.cdpPort` per instance.
|
- If you need explicit CDP ports, set `browser.profiles.<name>.cdpPort` per instance.
|
||||||
- Remote Chrome: use `browser.profiles.<name>.cdpUrl` (per profile, per instance).
|
- Remote Chrome: use `browser.profiles.<name>.cdpUrl` (per profile, per instance).
|
||||||
|
|
||||||
|
|||||||
@ -198,7 +198,8 @@ 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.
|
Control UI can omit it **only** when `gateway.controlUi.allowInsecureAuth` is enabled
|
||||||
|
(or `gateway.controlUi.dangerouslyDisableDeviceAuth` for break-glass use).
|
||||||
- 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
|
||||||
|
|||||||
@ -117,6 +117,6 @@ Short version: **keep the Gateway loopback-only** unless you’re sure you need
|
|||||||
- `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`.
|
||||||
Set it to `false` if you want tokens/passwords instead.
|
Set it to `false` if you want tokens/passwords instead.
|
||||||
- Treat `browser.controlUrl` like an admin API: tailnet-only + token auth.
|
- Treat browser control like operator access: tailnet-only + deliberate node pairing.
|
||||||
|
|
||||||
Deep dive: [Security](/gateway/security).
|
Deep dive: [Security](/gateway/security).
|
||||||
|
|||||||
@ -59,6 +59,8 @@ Two layers matter:
|
|||||||
Rules of thumb:
|
Rules of thumb:
|
||||||
- `deny` always wins.
|
- `deny` always wins.
|
||||||
- If `allow` is non-empty, everything else is treated as blocked.
|
- If `allow` is non-empty, everything else is treated as blocked.
|
||||||
|
- Tool policy is the hard stop: `/exec` cannot override a denied `exec` tool.
|
||||||
|
- `/exec` only changes session defaults for authorized senders; it does not grant tool access.
|
||||||
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.2`).
|
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.2`).
|
||||||
|
|
||||||
### Tool groups (shorthands)
|
### Tool groups (shorthands)
|
||||||
@ -95,6 +97,7 @@ Elevated does **not** grant extra tools; it only affects `exec`.
|
|||||||
- Use `/elevated full` to skip exec approvals for the session.
|
- 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.
|
||||||
|
- `/exec` is separate from elevated. It only adjusts per-session exec defaults for authorized senders.
|
||||||
|
|
||||||
Gates:
|
Gates:
|
||||||
- Enablement: `tools.elevated.enabled` (and optionally `agents.list[].tools.elevated.enabled`)
|
- Enablement: `tools.elevated.enabled` (and optionally `agents.list[].tools.elevated.enabled`)
|
||||||
|
|||||||
@ -142,6 +142,8 @@ Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
|||||||
globally or per-agent, sandboxing doesn’t bring it back.
|
globally or per-agent, sandboxing doesn’t bring it back.
|
||||||
|
|
||||||
`tools.elevated` is an explicit escape hatch that runs `exec` on the host.
|
`tools.elevated` is an explicit escape hatch that runs `exec` on the host.
|
||||||
|
`/exec` directives only apply for authorized senders and persist per session; to hard-disable
|
||||||
|
`exec`, use tool policy deny (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)).
|
||||||
|
|
||||||
Debugging:
|
Debugging:
|
||||||
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
||||||
|
|||||||
107
docs/gateway/security/formal-verification.md
Normal file
107
docs/gateway/security/formal-verification.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Formal Verification (Security Models)
|
||||||
|
summary: Machine-checked security models for Clawdbot’s highest-risk paths.
|
||||||
|
permalink: /gateway/security/formal-verification/
|
||||||
|
---
|
||||||
|
|
||||||
|
# Formal Verification (Security Models)
|
||||||
|
|
||||||
|
This page tracks Clawdbot’s **formal security models** (TLA+/TLC today; more as needed).
|
||||||
|
|
||||||
|
**Goal (north star):** provide a machine-checked argument that Clawdbot enforces its
|
||||||
|
intended security policy (authorization, session isolation, tool gating, and
|
||||||
|
misconfiguration safety), under explicit assumptions.
|
||||||
|
|
||||||
|
**What this is (today):** an executable, attacker-driven **security regression suite**:
|
||||||
|
- Each claim has a runnable model-check over a finite state space.
|
||||||
|
- Many claims have a paired **negative model** that produces a counterexample trace for a realistic bug class.
|
||||||
|
|
||||||
|
**What this is not (yet):** a proof that “Clawdbot is secure in all respects” or that the full TypeScript implementation is correct.
|
||||||
|
|
||||||
|
## Where the models live
|
||||||
|
|
||||||
|
Models are maintained in a separate repo: [vignesh07/clawdbot-formal-models](https://github.com/vignesh07/clawdbot-formal-models).
|
||||||
|
|
||||||
|
## Important caveats
|
||||||
|
|
||||||
|
- These are **models**, not the full TypeScript implementation. Drift between model and code is possible.
|
||||||
|
- Results are bounded by the state space explored by TLC; “green” does not imply security beyond the modeled assumptions and bounds.
|
||||||
|
- Some claims rely on explicit environmental assumptions (e.g., correct deployment, correct configuration inputs).
|
||||||
|
|
||||||
|
## Reproducing results
|
||||||
|
|
||||||
|
Today, results are reproduced by cloning the models repo locally and running TLC (see below). A future iteration could offer:
|
||||||
|
- CI-run models with public artifacts (counterexample traces, run logs)
|
||||||
|
- a hosted “run this model” workflow for small, bounded checks
|
||||||
|
|
||||||
|
Getting started:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vignesh07/clawdbot-formal-models
|
||||||
|
cd clawdbot-formal-models
|
||||||
|
|
||||||
|
# Java 11+ required (TLC runs on the JVM).
|
||||||
|
# The repo vendors a pinned `tla2tools.jar` (TLA+ tools) and provides `bin/tlc` + Make targets.
|
||||||
|
|
||||||
|
make <target>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gateway exposure and open gateway misconfiguration
|
||||||
|
|
||||||
|
**Claim:** binding beyond loopback without auth can make remote compromise possible / increases exposure; token/password blocks unauth attackers (per the model assumptions).
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make gateway-exposure-v2`
|
||||||
|
- `make gateway-exposure-v2-protected`
|
||||||
|
- Red (expected):
|
||||||
|
- `make gateway-exposure-v2-negative`
|
||||||
|
|
||||||
|
See also: `docs/gateway-exposure-matrix.md` in the models repo.
|
||||||
|
|
||||||
|
### Nodes.run pipeline (highest-risk capability)
|
||||||
|
|
||||||
|
**Claim:** `nodes.run` requires (a) node command allowlist plus declared commands and (b) live approval when configured; approvals are tokenized to prevent replay (in the model).
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make nodes-pipeline`
|
||||||
|
- `make approvals-token`
|
||||||
|
- Red (expected):
|
||||||
|
- `make nodes-pipeline-negative`
|
||||||
|
- `make approvals-token-negative`
|
||||||
|
|
||||||
|
### Pairing store (DM gating)
|
||||||
|
|
||||||
|
**Claim:** pairing requests respect TTL and pending-request caps.
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make pairing`
|
||||||
|
- `make pairing-cap`
|
||||||
|
- Red (expected):
|
||||||
|
- `make pairing-negative`
|
||||||
|
- `make pairing-cap-negative`
|
||||||
|
|
||||||
|
### Ingress gating (mentions + control-command bypass)
|
||||||
|
|
||||||
|
**Claim:** in group contexts requiring mention, an unauthorized “control command” cannot bypass mention gating.
|
||||||
|
|
||||||
|
- Green:
|
||||||
|
- `make ingress-gating`
|
||||||
|
- Red (expected):
|
||||||
|
- `make ingress-gating-negative`
|
||||||
|
|
||||||
|
### Routing/session-key isolation
|
||||||
|
|
||||||
|
**Claim:** DMs from distinct peers do not collapse into the same session unless explicitly linked/configured.
|
||||||
|
|
||||||
|
- Green:
|
||||||
|
- `make routing-isolation`
|
||||||
|
- Red (expected):
|
||||||
|
- `make routing-isolation-negative`
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Next models to deepen fidelity:
|
||||||
|
- Pairing store concurrency/locking/idempotency
|
||||||
|
- Provider-specific ingress preflight modeling
|
||||||
|
- Routing identity-links + dmScope variants + binding precedence
|
||||||
|
- Gateway auth conformance (proxy/tailscale specifics)
|
||||||
@ -7,6 +7,8 @@ read_when:
|
|||||||
|
|
||||||
## Quick check: `clawdbot security audit`
|
## Quick check: `clawdbot security audit`
|
||||||
|
|
||||||
|
See also: [Formal Verification (Security Models)](/security/formal-verification/)
|
||||||
|
|
||||||
Run this regularly (especially after changing config or exposing network surfaces):
|
Run this regularly (especially after changing config or exposing network surfaces):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -36,20 +38,32 @@ Start with the smallest access that still works, then widen it as you gain confi
|
|||||||
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
|
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
|
||||||
- **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions?
|
- **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions?
|
||||||
- **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel).
|
- **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel).
|
||||||
- **Browser control exposure** (remote controlUrl without token, HTTP, token reuse).
|
- **Browser control exposure** (remote nodes, relay ports, remote CDP endpoints).
|
||||||
- **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths).
|
- **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths).
|
||||||
- **Plugins** (extensions exist without an explicit allowlist).
|
- **Plugins** (extensions exist without an explicit allowlist).
|
||||||
- **Model hygiene** (warn when configured models look legacy; not a hard block).
|
- **Model hygiene** (warn when configured models look legacy; not a hard block).
|
||||||
|
|
||||||
If you run `--deep`, Clawdbot also attempts a best-effort live Gateway probe.
|
If you run `--deep`, Clawdbot also attempts a best-effort live Gateway probe.
|
||||||
|
|
||||||
|
## Credential storage map
|
||||||
|
|
||||||
|
Use this when auditing access or deciding what to back up:
|
||||||
|
|
||||||
|
- **WhatsApp**: `~/.clawdbot/credentials/whatsapp/<accountId>/creds.json`
|
||||||
|
- **Telegram bot token**: config/env or `channels.telegram.tokenFile`
|
||||||
|
- **Discord bot token**: config/env (token file not yet supported)
|
||||||
|
- **Slack tokens**: config/env (`channels.slack.*`)
|
||||||
|
- **Pairing allowlists**: `~/.clawdbot/credentials/<channel>-allowFrom.json`
|
||||||
|
- **Model auth profiles**: `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`
|
||||||
|
- **Legacy OAuth import**: `~/.clawdbot/credentials/oauth.json`
|
||||||
|
|
||||||
## Security Audit Checklist
|
## Security Audit Checklist
|
||||||
|
|
||||||
When the audit prints findings, treat this as a priority order:
|
When the audit prints findings, treat this as a priority order:
|
||||||
|
|
||||||
1. **Anything “open” + tools enabled**: lock down DMs/groups first (pairing/allowlists), then tighten tool policy/sandboxing.
|
1. **Anything “open” + tools enabled**: lock down DMs/groups first (pairing/allowlists), then tighten tool policy/sandboxing.
|
||||||
2. **Public network exposure** (LAN bind, Funnel, missing auth): fix immediately.
|
2. **Public network exposure** (LAN bind, Funnel, missing auth): fix immediately.
|
||||||
3. **Browser control remote exposure**: treat it like a remote admin API (token required; HTTPS/tailnet-only).
|
3. **Browser control remote exposure**: treat it like operator access (tailnet-only, pair nodes deliberately, avoid public exposure).
|
||||||
4. **Permissions**: make sure state/config/credentials/auth are not group/world-readable.
|
4. **Permissions**: make sure state/config/credentials/auth are not group/world-readable.
|
||||||
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.
|
||||||
@ -58,9 +72,13 @@ When the audit prints findings, treat this as a priority order:
|
|||||||
|
|
||||||
The Control UI needs a **secure context** (HTTPS or localhost) to generate device
|
The Control UI needs a **secure context** (HTTPS or localhost) to generate device
|
||||||
identity. If you enable `gateway.controlUi.allowInsecureAuth`, the UI falls back
|
identity. If you enable `gateway.controlUi.allowInsecureAuth`, the UI falls back
|
||||||
to **token-only auth** and skips device pairing (even on HTTPS). This is a security
|
to **token-only auth** and skips device pairing when device identity is omitted. This is a security
|
||||||
downgrade—prefer HTTPS (Tailscale Serve) or open the UI on `127.0.0.1`.
|
downgrade—prefer HTTPS (Tailscale Serve) or open the UI on `127.0.0.1`.
|
||||||
|
|
||||||
|
For break-glass scenarios only, `gateway.controlUi.dangerouslyDisableDeviceAuth`
|
||||||
|
disables device identity checks entirely. This is a severe security downgrade;
|
||||||
|
keep it off unless you are actively debugging and can revert quickly.
|
||||||
|
|
||||||
`clawdbot security audit` warns when this setting is enabled.
|
`clawdbot security audit` warns when this setting is enabled.
|
||||||
|
|
||||||
## Reverse Proxy Configuration
|
## Reverse Proxy Configuration
|
||||||
@ -126,6 +144,16 @@ Clawdbot’s stance:
|
|||||||
- **Scope next:** decide where the bot is allowed to act (group allowlists + mention gating, tools, sandboxing, device permissions).
|
- **Scope next:** decide where the bot is allowed to act (group allowlists + mention gating, tools, sandboxing, device permissions).
|
||||||
- **Model last:** assume the model can be manipulated; design so manipulation has limited blast radius.
|
- **Model last:** assume the model can be manipulated; design so manipulation has limited blast radius.
|
||||||
|
|
||||||
|
## Command authorization model
|
||||||
|
|
||||||
|
Slash commands and directives are only honored for **authorized senders**. Authorization is derived from
|
||||||
|
channel allowlists/pairing plus `commands.useAccessGroups` (see [Configuration](/gateway/configuration)
|
||||||
|
and [Slash commands](/tools/slash-commands)). If a channel allowlist is empty or includes `"*"`,
|
||||||
|
commands are effectively open for that channel.
|
||||||
|
|
||||||
|
`/exec` is a session-only convenience for authorized operators. It does **not** write config or
|
||||||
|
change other sessions.
|
||||||
|
|
||||||
## Plugins/extensions
|
## Plugins/extensions
|
||||||
|
|
||||||
Plugins run **in-process** with the Gateway. Treat them as trusted code:
|
Plugins run **in-process** with the Gateway. Treat them as trusted code:
|
||||||
@ -193,10 +221,18 @@ Prompt injection is when an attacker crafts a message that manipulates the model
|
|||||||
Even with strong system prompts, **prompt injection is not solved**. What helps in practice:
|
Even with strong system prompts, **prompt injection is not solved**. What helps in practice:
|
||||||
- Keep inbound DMs locked down (pairing/allowlists).
|
- Keep inbound DMs locked down (pairing/allowlists).
|
||||||
- Prefer mention gating in groups; avoid “always-on” bots in public rooms.
|
- Prefer mention gating in groups; avoid “always-on” bots in public rooms.
|
||||||
- Treat links and pasted instructions as hostile by default.
|
- Treat links, attachments, and pasted instructions as hostile by default.
|
||||||
- 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.
|
||||||
|
- Note: sandboxing is opt-in. If sandbox mode is off, exec runs on the gateway host even though tools.exec.host defaults to sandbox, and host exec does not require approvals unless you set host=gateway and configure exec approvals.
|
||||||
|
- Limit high-risk tools (`exec`, `browser`, `web_fetch`, `web_search`) to trusted agents or explicit allowlists.
|
||||||
- **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)).
|
||||||
|
|
||||||
|
Red flags to treat as untrusted:
|
||||||
|
- “Read this file/URL and do exactly what it says.”
|
||||||
|
- “Ignore your system prompt or safety rules.”
|
||||||
|
- “Reveal your hidden instructions or tool outputs.”
|
||||||
|
- “Paste the full contents of ~/.clawdbot or your logs.”
|
||||||
|
|
||||||
### Prompt injection does not require public DMs
|
### Prompt injection does not require public DMs
|
||||||
|
|
||||||
Even if **only you** can message the bot, prompt injection can still happen via
|
Even if **only you** can message the bot, prompt injection can still happen via
|
||||||
@ -210,6 +246,7 @@ tool calls. Reduce the blast radius by:
|
|||||||
then pass the summary to your main agent.
|
then pass the summary to your main agent.
|
||||||
- Keeping `web_search` / `web_fetch` / `browser` off for tool-enabled agents unless needed.
|
- 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.
|
- Enabling sandboxing and strict tool allowlists for any agent that touches untrusted input.
|
||||||
|
- Keeping secrets out of prompts; pass them via env/config on the gateway host instead.
|
||||||
|
|
||||||
### Model strength (security note)
|
### Model strength (security note)
|
||||||
|
|
||||||
@ -226,8 +263,12 @@ Recommendations:
|
|||||||
|
|
||||||
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
|
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
|
||||||
was not meant for a public channel. In group settings, treat them as **debug
|
was not meant for a public channel. In group settings, treat them as **debug
|
||||||
only** and keep them off unless you explicitly need them. If you enable them,
|
only** and keep them off unless you explicitly need them.
|
||||||
do so only in trusted DMs or tightly controlled rooms.
|
|
||||||
|
Guidance:
|
||||||
|
- Keep `/reasoning` and `/verbose` disabled in public rooms.
|
||||||
|
- If you enable them, do so only in trusted DMs or tightly controlled rooms.
|
||||||
|
- Remember: verbose output can include tool args, URLs, and data the model saw.
|
||||||
|
|
||||||
## Incident Response (if you suspect compromise)
|
## Incident Response (if you suspect compromise)
|
||||||
|
|
||||||
@ -238,7 +279,7 @@ Assume “compromised” means: someone got into a room that can trigger the bot
|
|||||||
- Lock down inbound surfaces (DM policy, group allowlists, mention gating).
|
- Lock down inbound surfaces (DM policy, group allowlists, mention gating).
|
||||||
2. **Rotate secrets**
|
2. **Rotate secrets**
|
||||||
- Rotate `gateway.auth` token/password.
|
- Rotate `gateway.auth` token/password.
|
||||||
- Rotate `browser.controlToken` and `hooks.token` (if used).
|
- Rotate `hooks.token` (if used) and revoke any suspicious node pairings.
|
||||||
- Revoke/rotate model provider credentials (API keys / OAuth).
|
- Revoke/rotate model provider credentials (API keys / OAuth).
|
||||||
3. **Review artifacts**
|
3. **Review artifacts**
|
||||||
- Check Gateway logs and recent sessions/transcripts for unexpected tool calls.
|
- Check Gateway logs and recent sessions/transcripts for unexpected tool calls.
|
||||||
@ -280,22 +321,63 @@ 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"`, `"custom"`) 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 a shared token/password 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).
|
||||||
- If you must bind to LAN, firewall the port to a tight allowlist of source IPs; do not port-forward it broadly.
|
- If you must bind to LAN, firewall the port to a tight allowlist of source IPs; do not port-forward it broadly.
|
||||||
- Never expose the Gateway unauthenticated on `0.0.0.0`.
|
- Never expose the Gateway unauthenticated on `0.0.0.0`.
|
||||||
|
|
||||||
|
### 0.4.1) mDNS/Bonjour discovery (information disclosure)
|
||||||
|
|
||||||
|
The Gateway broadcasts its presence via mDNS (`_clawdbot-gw._tcp` on port 5353) for local device discovery. In full mode, this includes TXT records that may expose operational details:
|
||||||
|
|
||||||
|
- `cliPath`: full filesystem path to the CLI binary (reveals username and install location)
|
||||||
|
- `sshPort`: advertises SSH availability on the host
|
||||||
|
- `displayName`, `lanHost`: hostname information
|
||||||
|
|
||||||
|
**Operational security consideration:** Broadcasting infrastructure details makes reconnaissance easier for anyone on the local network. Even "harmless" info like filesystem paths and SSH availability helps attackers map your environment.
|
||||||
|
|
||||||
|
**Recommendations:**
|
||||||
|
|
||||||
|
1. **Minimal mode** (default, recommended for exposed gateways): omit sensitive fields from mDNS broadcasts:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
discovery: {
|
||||||
|
mdns: { mode: "minimal" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Disable entirely** if you don't need local device discovery:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
discovery: {
|
||||||
|
mdns: { mode: "off" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Full mode** (opt-in): include `cliPath` + `sshPort` in TXT records:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
discovery: {
|
||||||
|
mdns: { mode: "full" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Environment variable** (alternative): set `CLAWDBOT_DISABLE_BONJOUR=1` to disable mDNS without config changes.
|
||||||
|
|
||||||
|
In minimal mode, the Gateway still broadcasts enough for device discovery (`role`, `gatewayPort`, `transport`) but omits `cliPath` and `sshPort`. Apps that need CLI path information can fetch it via the authenticated WebSocket connection instead.
|
||||||
|
|
||||||
### 0.5) Lock down the Gateway WebSocket (local auth)
|
### 0.5) Lock down the Gateway WebSocket (local auth)
|
||||||
|
|
||||||
Gateway auth is **only** enforced when you set `gateway.auth`. If it’s unset,
|
Gateway auth is **required by default**. If no token/password is configured,
|
||||||
loopback WS clients are unauthenticated — any local process can connect and call
|
the Gateway refuses WebSocket connections (fail‑closed).
|
||||||
`config.apply`.
|
|
||||||
|
|
||||||
The onboarding wizard now generates a token by default (even for loopback) so
|
The onboarding wizard generates a token by default (even for loopback) so
|
||||||
local clients must authenticate. If you skip the wizard or remove auth, you’re
|
local clients must authenticate.
|
||||||
back to open loopback.
|
|
||||||
|
|
||||||
Set a token so **all** WS clients must authenticate:
|
Set a token so **all** WS clients must authenticate:
|
||||||
|
|
||||||
@ -333,9 +415,11 @@ Rotation checklist (token/password):
|
|||||||
|
|
||||||
When `gateway.auth.allowTailscale` is `true` (default for Serve), Clawdbot
|
When `gateway.auth.allowTailscale` is `true` (default for Serve), Clawdbot
|
||||||
accepts Tailscale Serve identity headers (`tailscale-user-login`) as
|
accepts Tailscale Serve identity headers (`tailscale-user-login`) as
|
||||||
authentication. This only triggers for requests that hit loopback and include
|
authentication. Clawdbot verifies the identity by resolving the
|
||||||
`x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as injected by
|
`x-forwarded-for` address through the local Tailscale daemon (`tailscale whois`)
|
||||||
Tailscale.
|
and matching it to the header. This only triggers for requests that hit loopback
|
||||||
|
and include `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as
|
||||||
|
injected by Tailscale.
|
||||||
|
|
||||||
**Security rule:** do not forward these headers from your own reverse proxy. If
|
**Security rule:** do not forward these headers from your own reverse proxy. If
|
||||||
you terminate TLS or proxy in front of the gateway, disable
|
you terminate TLS or proxy in front of the gateway, disable
|
||||||
@ -348,26 +432,19 @@ Trusted proxies:
|
|||||||
|
|
||||||
See [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
See [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||||
|
|
||||||
### 0.6.1) Browser control server over Tailscale (recommended)
|
### 0.6.1) Browser control via node host (recommended)
|
||||||
|
|
||||||
If your Gateway is remote but the browser runs on another machine, you’ll often run a **separate browser control server**
|
If your Gateway is remote but the browser runs on another machine, run a **node host**
|
||||||
on the browser machine (see [Browser tool](/tools/browser)). Treat this like an admin API.
|
on the browser machine and let the Gateway proxy browser actions (see [Browser tool](/tools/browser)).
|
||||||
|
Treat node pairing like admin access.
|
||||||
|
|
||||||
Recommended pattern:
|
Recommended pattern:
|
||||||
|
- Keep the Gateway and node host on the same tailnet (Tailscale).
|
||||||
```bash
|
- Pair the node intentionally; disable browser proxy routing if you don’t need it.
|
||||||
# on the machine that runs Chrome
|
|
||||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
|
||||||
tailscale serve https / http://127.0.0.1:18791
|
|
||||||
```
|
|
||||||
|
|
||||||
Then on the Gateway, set:
|
|
||||||
- `browser.controlUrl` to the `https://…` Serve URL (MagicDNS/ts.net)
|
|
||||||
- and authenticate with the same token (`CLAWDBOT_BROWSER_CONTROL_TOKEN` env preferred)
|
|
||||||
|
|
||||||
Avoid:
|
Avoid:
|
||||||
- `--bind 0.0.0.0` (LAN-visible surface)
|
- Exposing relay/control ports over LAN or public Internet.
|
||||||
- Tailscale Funnel for browser control endpoints (public exposure)
|
- Tailscale Funnel for browser control endpoints (public exposure).
|
||||||
|
|
||||||
### 0.7) Secrets on disk (what’s sensitive)
|
### 0.7) Secrets on disk (what’s sensitive)
|
||||||
|
|
||||||
@ -495,12 +572,15 @@ If that browser profile already contains logged-in sessions, the model can
|
|||||||
access those accounts and data. Treat browser profiles as **sensitive state**:
|
access those accounts and data. Treat browser profiles as **sensitive state**:
|
||||||
- Prefer a dedicated profile for the agent (the default `clawd` profile).
|
- Prefer a dedicated profile for the agent (the default `clawd` profile).
|
||||||
- Avoid pointing the agent at your personal daily-driver profile.
|
- Avoid pointing the agent at your personal daily-driver profile.
|
||||||
|
- `act:evaluate` and `wait --fn` run arbitrary JavaScript in the page context.
|
||||||
|
Prompt injection can steer the model into calling them. If you do not need
|
||||||
|
them, set `browser.evaluateEnabled=false` (see [Configuration](/gateway/configuration#browser-clawd-managed-browser)).
|
||||||
- Keep host browser control disabled for sandboxed agents unless you trust them.
|
- Keep host browser control disabled for sandboxed agents unless you trust them.
|
||||||
- Treat browser downloads as untrusted input; prefer an isolated downloads directory.
|
- Treat browser downloads as untrusted input; prefer an isolated downloads directory.
|
||||||
- Disable browser sync/password managers in the agent profile if possible (reduces blast radius).
|
- Disable browser sync/password managers in the agent profile if possible (reduces blast radius).
|
||||||
- For remote gateways, assume “browser control” is equivalent to “operator access” to whatever that profile can reach.
|
- For remote gateways, assume “browser control” is equivalent to “operator access” to whatever that profile can reach.
|
||||||
- Treat `browser.controlUrl` endpoints as an admin API: tailnet-only + token auth. Prefer Tailscale Serve over LAN binds.
|
- Keep the Gateway and node hosts tailnet-only; avoid exposing relay/control ports to LAN or public Internet.
|
||||||
- Keep `browser.controlToken` separate from `gateway.auth.token` (you can reuse it, but that increases blast radius).
|
- Disable browser proxy routing when you don’t need it (`gateway.nodes.browser.mode="off"`).
|
||||||
- Chrome extension relay mode is **not** “safer”; it can take over your existing Chrome tabs. Assume it can act as you in whatever that tab/profile can reach.
|
- Chrome extension relay mode is **not** “safer”; it can take over your existing Chrome tabs. Assume it can act as you in whatever that tab/profile can reach.
|
||||||
|
|
||||||
## Per-agent access profiles (multi-agent)
|
## Per-agent access profiles (multi-agent)
|
||||||
@ -25,9 +25,12 @@ Set `gateway.auth.mode` to control the handshake:
|
|||||||
|
|
||||||
When `tailscale.mode = "serve"` and `gateway.auth.allowTailscale` is `true`,
|
When `tailscale.mode = "serve"` and `gateway.auth.allowTailscale` is `true`,
|
||||||
valid Serve proxy requests can authenticate via Tailscale identity headers
|
valid Serve proxy requests can authenticate via Tailscale identity headers
|
||||||
(`tailscale-user-login`) without supplying a token/password. Clawdbot only
|
(`tailscale-user-login`) without supplying a token/password. Clawdbot verifies
|
||||||
treats a request as Serve when it arrives from loopback with Tailscale’s
|
the identity by resolving the `x-forwarded-for` address via the local Tailscale
|
||||||
`x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` headers.
|
daemon (`tailscale whois`) and matching it to the header before accepting it.
|
||||||
|
Clawdbot only treats a request as Serve when it arrives from loopback with
|
||||||
|
Tailscale’s `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`
|
||||||
|
headers.
|
||||||
To require explicit credentials, set `gateway.auth.allowTailscale: false` or
|
To require explicit credentials, set `gateway.auth.allowTailscale: false` or
|
||||||
force `gateway.auth.mode: "password"`.
|
force `gateway.auth.mode: "password"`.
|
||||||
|
|
||||||
@ -97,35 +100,13 @@ clawdbot gateway --tailscale funnel --auth password
|
|||||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
|
- Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
|
||||||
the same Gateway WS endpoint, so Serve can work for node access.
|
the same Gateway WS endpoint, so Serve can work for node access.
|
||||||
|
|
||||||
## Browser control server (remote Gateway + local browser)
|
## Browser control (remote Gateway + local browser)
|
||||||
|
|
||||||
If you run the Gateway on one machine but want to drive a browser on another machine, use a **separate browser control server**
|
If you run the Gateway on one machine but want to drive a browser on another machine,
|
||||||
and publish it through Tailscale **Serve** (tailnet-only):
|
run a **node host** on the browser machine and keep both on the same tailnet.
|
||||||
|
The Gateway will proxy browser actions to the node; no separate control server or Serve URL needed.
|
||||||
|
|
||||||
```bash
|
Avoid Funnel for browser control; treat node pairing like operator access.
|
||||||
# on the machine that runs Chrome
|
|
||||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
|
||||||
tailscale serve https / http://127.0.0.1:18791
|
|
||||||
```
|
|
||||||
|
|
||||||
Then point the Gateway config at the HTTPS URL:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
browser: {
|
|
||||||
enabled: true,
|
|
||||||
controlUrl: "https://<magicdns>/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And authenticate from the Gateway with the same token (prefer env):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CLAWDBOT_BROWSER_CONTROL_TOKEN="<token>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Avoid Funnel for browser control endpoints unless you explicitly want public exposure.
|
|
||||||
|
|
||||||
## Tailscale prerequisites + limits
|
## Tailscale prerequisites + limits
|
||||||
|
|
||||||
|
|||||||
@ -53,13 +53,12 @@ clawdbot models status
|
|||||||
|
|
||||||
This means the stored Anthropic OAuth token expired and the refresh failed.
|
This means the stored Anthropic OAuth token expired and the refresh failed.
|
||||||
If you’re on a Claude subscription (no API key), the most reliable fix is to
|
If you’re on a Claude subscription (no API key), the most reliable fix is to
|
||||||
switch to a **Claude Code setup-token** or re-sync Claude Code CLI OAuth on the
|
switch to a **Claude Code setup-token** and paste it on the **gateway host**.
|
||||||
**gateway host**.
|
|
||||||
|
|
||||||
**Recommended (setup-token):**
|
**Recommended (setup-token):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run on the gateway host (runs Claude Code CLI)
|
# Run on the gateway host (paste the setup-token)
|
||||||
clawdbot models auth setup-token --provider anthropic
|
clawdbot models auth setup-token --provider anthropic
|
||||||
clawdbot models status
|
clawdbot models status
|
||||||
```
|
```
|
||||||
@ -71,10 +70,6 @@ clawdbot models auth paste-token --provider anthropic
|
|||||||
clawdbot models status
|
clawdbot models status
|
||||||
```
|
```
|
||||||
|
|
||||||
**If you want to keep OAuth reuse:**
|
|
||||||
log in with Claude Code CLI on the gateway host, then run `clawdbot models status`
|
|
||||||
to sync the refreshed token into Clawdbot’s auth store.
|
|
||||||
|
|
||||||
More detail: [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
|
More detail: [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
|
||||||
|
|
||||||
### Control UI fails on HTTP ("device identity required" / "connect failed")
|
### Control UI fails on HTTP ("device identity required" / "connect failed")
|
||||||
@ -214,7 +209,7 @@ the Gateway likely refused to bind.
|
|||||||
- Fix: run `clawdbot doctor` to update it (or `clawdbot gateway 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`/`custom`, or `auto` when loopback is unavailable) but left auth off.
|
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but didn’t configure auth.
|
||||||
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service.
|
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service.
|
||||||
|
|
||||||
**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found**
|
**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found**
|
||||||
|
|||||||
@ -401,7 +401,7 @@ remote mode, remember the gateway host owns the session store and workspace.
|
|||||||
up **memory + bootstrap files**, but **not** session history or auth. Those live
|
up **memory + bootstrap files**, but **not** session history or auth. Those live
|
||||||
under `~/.clawdbot/` (for example `~/.clawdbot/agents/<agentId>/sessions/`).
|
under `~/.clawdbot/` (for example `~/.clawdbot/agents/<agentId>/sessions/`).
|
||||||
|
|
||||||
Related: [Where things live on disk](/help/faq#where-does-clawdbot-store-its-data),
|
Related: [Migrating](/install/migrating), [Where things live on disk](/help/faq#where-does-clawdbot-store-its-data),
|
||||||
[Agent workspace](/concepts/agent-workspace), [Doctor](/gateway/doctor),
|
[Agent workspace](/concepts/agent-workspace), [Doctor](/gateway/doctor),
|
||||||
[Remote mode](/gateway/remote).
|
[Remote mode](/gateway/remote).
|
||||||
|
|
||||||
@ -566,7 +566,6 @@ Remote access: [Gateway remote](/gateway/remote).
|
|||||||
We keep a **hosting hub** with the common providers. Pick one and follow the guide:
|
We keep a **hosting hub** with the common providers. Pick one and follow the guide:
|
||||||
|
|
||||||
- [VPS hosting](/vps) (all providers in one place)
|
- [VPS hosting](/vps) (all providers in one place)
|
||||||
- [Railway](/railway) (one‑click, browser‑based setup)
|
|
||||||
- [Fly.io](/platforms/fly)
|
- [Fly.io](/platforms/fly)
|
||||||
- [Hetzner](/platforms/hetzner)
|
- [Hetzner](/platforms/hetzner)
|
||||||
- [exe.dev](/platforms/exe-dev)
|
- [exe.dev](/platforms/exe-dev)
|
||||||
@ -631,7 +630,7 @@ Docs: [Anthropic](/providers/anthropic), [OpenAI](/providers/openai),
|
|||||||
|
|
||||||
### Can I use Claude Max subscription without an API key
|
### Can I use Claude Max subscription without an API key
|
||||||
|
|
||||||
Yes. You can authenticate with **Claude Code CLI OAuth** or a **setup-token**
|
Yes. You can authenticate with a **setup-token**
|
||||||
instead of an API key. This is the subscription path.
|
instead of an API key. This is the subscription path.
|
||||||
|
|
||||||
Claude Pro/Max subscriptions **do not include an API key**, so this is the
|
Claude Pro/Max subscriptions **do not include an API key**, so this is the
|
||||||
@ -641,11 +640,7 @@ If you want the most explicit, supported path, use an Anthropic API key.
|
|||||||
|
|
||||||
### How does Anthropic setuptoken auth work
|
### How does Anthropic setuptoken auth work
|
||||||
|
|
||||||
`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. If Claude Code CLI credentials are present on the gateway host, Clawdbot can reuse them; otherwise choose **Anthropic token (paste setup-token)** and paste the string. The token is stored as an auth profile for the **anthropic** provider and used like an API key or OAuth profile. More detail: [OAuth](/concepts/oauth).
|
`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. Choose **Anthropic token (paste setup-token)** in the wizard or paste it with `clawdbot models auth paste-token --provider anthropic`. The token is stored as an auth profile for the **anthropic** provider and used like an API key (no auto-refresh). More detail: [OAuth](/concepts/oauth).
|
||||||
|
|
||||||
Clawdbot keeps `auth.profiles["anthropic:claude-cli"].mode` set to `"oauth"` so
|
|
||||||
the profile accepts both OAuth and setup-token credentials; older `"token"` mode
|
|
||||||
entries auto-migrate.
|
|
||||||
|
|
||||||
### Where do I find an Anthropic setuptoken
|
### Where do I find an Anthropic setuptoken
|
||||||
|
|
||||||
@ -657,9 +652,9 @@ claude setup-token
|
|||||||
|
|
||||||
Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want to run it on the gateway host, use `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic).
|
Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want to run it on the gateway host, use `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic).
|
||||||
|
|
||||||
### Do you support Claude subscription auth Claude Code OAuth
|
### Do you support Claude subscription auth (Claude Pro/Max)
|
||||||
|
|
||||||
Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** for long‑running setups (requires Claude Pro/Max + the `claude` CLI). You can generate it anywhere and paste it on the gateway host. OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
|
Yes — via **setup-token**. Clawdbot no longer reuses Claude Code CLI OAuth tokens; use a setup-token or an Anthropic API key. Generate the token anywhere and paste it on the gateway host. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
|
||||||
|
|
||||||
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
|
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
|
||||||
|
|
||||||
@ -679,13 +674,12 @@ Yes - via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual confi
|
|||||||
|
|
||||||
### How does Codex auth work
|
### How does Codex auth work
|
||||||
|
|
||||||
Clawdbot supports **OpenAI Code (Codex)** via OAuth or by reusing your Codex CLI login (`~/.codex/auth.json`). The wizard can import the CLI login or run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard).
|
Clawdbot supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). The wizard can run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard).
|
||||||
|
|
||||||
### Do you support OpenAI subscription auth Codex OAuth
|
### Do you support OpenAI subscription auth Codex OAuth
|
||||||
|
|
||||||
Yes. Clawdbot fully supports **OpenAI Code (Codex) subscription OAuth** and can also reuse an
|
Yes. Clawdbot fully supports **OpenAI Code (Codex) subscription OAuth**. The onboarding wizard
|
||||||
existing Codex CLI login (`~/.codex/auth.json`) on the gateway host. The onboarding wizard
|
can run the OAuth flow for you.
|
||||||
can import the CLI login or run the OAuth flow for you.
|
|
||||||
|
|
||||||
See [OAuth](/concepts/oauth), [Model providers](/concepts/model-providers), and [Wizard](/start/wizard).
|
See [OAuth](/concepts/oauth), [Model providers](/concepts/model-providers), and [Wizard](/start/wizard).
|
||||||
|
|
||||||
@ -1099,9 +1093,10 @@ clawdbot browser extension path
|
|||||||
|
|
||||||
Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → pick that folder.
|
Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → pick that folder.
|
||||||
|
|
||||||
Full guide (including remote Gateway via Tailscale + security notes): [Chrome extension](/tools/chrome-extension)
|
Full guide (including remote Gateway + security notes): [Chrome extension](/tools/chrome-extension)
|
||||||
|
|
||||||
If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need `clawdbot browser serve`.
|
If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need anything extra.
|
||||||
|
If the Gateway runs elsewhere, run a node host on the browser machine so the Gateway can proxy browser actions.
|
||||||
You still need to click the extension button on the tab you want to control (it doesn’t auto-attach).
|
You still need to click the extension button on the tab you want to control (it doesn’t auto-attach).
|
||||||
|
|
||||||
## Sandboxing and memory
|
## Sandboxing and memory
|
||||||
@ -1451,7 +1446,7 @@ Have Bot A send a message to Bot B, then let Bot B reply as usual.
|
|||||||
|
|
||||||
**CLI bridge (generic):** run a script that calls the other Gateway with
|
**CLI bridge (generic):** run a script that calls the other Gateway with
|
||||||
`clawdbot agent --message ... --deliver`, targeting a chat where the other bot
|
`clawdbot agent --message ... --deliver`, targeting a chat where the other bot
|
||||||
listens. If one bot is on Railway/VPS, point your CLI at that remote Gateway
|
listens. If one bot is on a remote VPS, point your CLI at that remote Gateway
|
||||||
via SSH/Tailscale (see [Remote access](/gateway/remote)).
|
via SSH/Tailscale (see [Remote access](/gateway/remote)).
|
||||||
|
|
||||||
Example pattern (run from a machine that can reach the target Gateway):
|
Example pattern (run from a machine that can reach the target Gateway):
|
||||||
@ -1485,7 +1480,7 @@ setup is an always‑on host plus your laptop as a node.
|
|||||||
- **Safer execution controls.** `system.run` is gated by node allowlists/approvals on that laptop.
|
- **Safer execution controls.** `system.run` is gated by node allowlists/approvals on that laptop.
|
||||||
- **More device tools.** Nodes expose `canvas`, `camera`, and `screen` in addition to `system.run`.
|
- **More device tools.** Nodes expose `canvas`, `camera`, and `screen` in addition to `system.run`.
|
||||||
- **Local browser automation.** Keep the Gateway on a VPS, but run Chrome locally and relay control
|
- **Local browser automation.** Keep the Gateway on a VPS, but run Chrome locally and relay control
|
||||||
with the Chrome extension + `clawdbot browser serve`.
|
with the Chrome extension + a node host on the laptop.
|
||||||
|
|
||||||
SSH is fine for ad‑hoc shell access, but nodes are simpler for ongoing agent workflows and
|
SSH is fine for ad‑hoc shell access, but nodes are simpler for ongoing agent workflows and
|
||||||
device automation.
|
device automation.
|
||||||
@ -1941,8 +1936,8 @@ You can list available models with `/model`, `/model list`, or `/model status`.
|
|||||||
You can also force a specific auth profile for the provider (per session):
|
You can also force a specific auth profile for the provider (per session):
|
||||||
|
|
||||||
```
|
```
|
||||||
/model opus@anthropic:claude-cli
|
|
||||||
/model opus@anthropic:default
|
/model opus@anthropic:default
|
||||||
|
/model opus@anthropic:work
|
||||||
```
|
```
|
||||||
|
|
||||||
Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next.
|
Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next.
|
||||||
@ -2146,21 +2141,17 @@ It means the system attempted to use the auth profile ID `anthropic:default`, bu
|
|||||||
- **Sanity‑check model/auth status**
|
- **Sanity‑check model/auth status**
|
||||||
- Use `clawdbot models status` to see configured models and whether providers are authenticated.
|
- Use `clawdbot models status` to see configured models and whether providers are authenticated.
|
||||||
|
|
||||||
**Fix checklist for No credentials found for profile anthropic claude cli**
|
**Fix checklist for No credentials found for profile anthropic**
|
||||||
|
|
||||||
This means the run is pinned to the **Claude Code CLI** profile, but the Gateway
|
This means the run is pinned to an Anthropic auth profile, but the Gateway
|
||||||
can’t find that profile in its auth store.
|
can’t find it in its auth store.
|
||||||
|
|
||||||
- **Sync the Claude Code CLI token on the gateway host**
|
- **Use a setup-token**
|
||||||
- Run `clawdbot models status` (it loads + syncs Claude Code CLI credentials).
|
- Run `claude setup-token`, then paste it with `clawdbot models auth setup-token --provider anthropic`.
|
||||||
- If it still says missing: run `claude setup-token` (or `clawdbot models auth setup-token --provider anthropic`) and retry.
|
- If the token was created on another machine, use `clawdbot models auth paste-token --provider anthropic`.
|
||||||
- **If the token was created on another machine**
|
|
||||||
- Paste it into the gateway host with `clawdbot models auth paste-token --provider anthropic`.
|
|
||||||
- **Check the profile mode**
|
|
||||||
- `auth.profiles["anthropic:claude-cli"].mode` must be `"oauth"` (token mode rejects OAuth credentials).
|
|
||||||
- **If you want to use an API key instead**
|
- **If you want to use an API key instead**
|
||||||
- Put `ANTHROPIC_API_KEY` in `~/.clawdbot/.env` on the **gateway host**.
|
- Put `ANTHROPIC_API_KEY` in `~/.clawdbot/.env` on the **gateway host**.
|
||||||
- Clear any pinned order that forces `anthropic:claude-cli`:
|
- Clear any pinned order that forces a missing profile:
|
||||||
```bash
|
```bash
|
||||||
clawdbot models auth order clear --provider anthropic
|
clawdbot models auth order clear --provider anthropic
|
||||||
```
|
```
|
||||||
@ -2182,7 +2173,7 @@ Fix: Clawdbot now strips unsigned thinking blocks for Google Antigravity Claude.
|
|||||||
|
|
||||||
## Auth profiles: what they are and how to manage them
|
## Auth profiles: what they are and how to manage them
|
||||||
|
|
||||||
Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-account patterns, CLI sync)
|
Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-account patterns)
|
||||||
|
|
||||||
### What is an auth profile
|
### What is an auth profile
|
||||||
|
|
||||||
@ -2213,10 +2204,10 @@ You can also set a **per-agent** order override (stored in that agent’s `auth-
|
|||||||
clawdbot models auth order get --provider anthropic
|
clawdbot models auth order get --provider anthropic
|
||||||
|
|
||||||
# Lock rotation to a single profile (only try this one)
|
# Lock rotation to a single profile (only try this one)
|
||||||
clawdbot models auth order set --provider anthropic anthropic:claude-cli
|
clawdbot models auth order set --provider anthropic anthropic:default
|
||||||
|
|
||||||
# Or set an explicit order (fallback within provider)
|
# Or set an explicit order (fallback within provider)
|
||||||
clawdbot models auth order set --provider anthropic anthropic:claude-cli anthropic:default
|
clawdbot models auth order set --provider anthropic anthropic:work anthropic:default
|
||||||
|
|
||||||
# Clear override (fall back to config auth.order / round-robin)
|
# Clear override (fall back to config auth.order / round-robin)
|
||||||
clawdbot models auth order clear --provider anthropic
|
clawdbot models auth order clear --provider anthropic
|
||||||
@ -2225,7 +2216,7 @@ clawdbot models auth order clear --provider anthropic
|
|||||||
To target a specific agent:
|
To target a specific agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot models auth order set --provider anthropic --agent main anthropic:claude-cli
|
clawdbot models auth order set --provider anthropic --agent main anthropic:default
|
||||||
```
|
```
|
||||||
|
|
||||||
### OAuth vs API key whats the difference
|
### OAuth vs API key whats the difference
|
||||||
@ -2235,7 +2226,7 @@ Clawdbot supports both:
|
|||||||
- **OAuth** often leverages subscription access (where applicable).
|
- **OAuth** often leverages subscription access (where applicable).
|
||||||
- **API keys** use pay‑per‑token billing.
|
- **API keys** use pay‑per‑token billing.
|
||||||
|
|
||||||
The wizard explicitly supports Anthropic OAuth and OpenAI Codex OAuth and can store API keys for you.
|
The wizard explicitly supports Anthropic setup-token and OpenAI Codex OAuth and can store API keys for you.
|
||||||
|
|
||||||
## Gateway: ports, “already running”, and remote mode
|
## Gateway: ports, “already running”, and remote mode
|
||||||
|
|
||||||
|
|||||||
@ -177,4 +177,5 @@ Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
|
|||||||
## Update / uninstall
|
## Update / uninstall
|
||||||
|
|
||||||
- Updates: [Updating](/install/updating)
|
- Updates: [Updating](/install/updating)
|
||||||
|
- Migrate to a new machine: [Migrating](/install/migrating)
|
||||||
- Uninstall: [Uninstall](/install/uninstall)
|
- Uninstall: [Uninstall](/install/uninstall)
|
||||||
|
|||||||
190
docs/install/migrating.md
Normal file
190
docs/install/migrating.md
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
summary: "Move (migrate) a Clawdbot install from one machine to another"
|
||||||
|
read_when:
|
||||||
|
- You are moving Clawdbot to a new laptop/server
|
||||||
|
- You want to preserve sessions, auth, and channel logins (WhatsApp, etc.)
|
||||||
|
---
|
||||||
|
# Migrating Clawdbot to a new machine
|
||||||
|
|
||||||
|
This guide migrates a Clawdbot Gateway from one machine to another **without redoing onboarding**.
|
||||||
|
|
||||||
|
The migration is simple conceptually:
|
||||||
|
|
||||||
|
- Copy the **state directory** (`$CLAWDBOT_STATE_DIR`, default: `~/.clawdbot/`) — this includes config, auth, sessions, and channel state.
|
||||||
|
- Copy your **workspace** (`~/clawd/` by default) — this includes your agent files (memory, prompts, etc.).
|
||||||
|
|
||||||
|
But there are common footguns around **profiles**, **permissions**, and **partial copies**.
|
||||||
|
|
||||||
|
## Before you start (what you are migrating)
|
||||||
|
|
||||||
|
### 1) Identify your state directory
|
||||||
|
|
||||||
|
Most installs use the default:
|
||||||
|
|
||||||
|
- **State dir:** `~/.clawdbot/`
|
||||||
|
|
||||||
|
But it may be different if you use:
|
||||||
|
|
||||||
|
- `--profile <name>` (often becomes `~/.clawdbot-<profile>/`)
|
||||||
|
- `CLAWDBOT_STATE_DIR=/some/path`
|
||||||
|
|
||||||
|
If you’re not sure, run on the **old** machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot status
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for mentions of `CLAWDBOT_STATE_DIR` / profile in the output. If you run multiple gateways, repeat for each profile.
|
||||||
|
|
||||||
|
### 2) Identify your workspace
|
||||||
|
|
||||||
|
Common defaults:
|
||||||
|
|
||||||
|
- `~/clawd/` (recommended workspace)
|
||||||
|
- a custom folder you created
|
||||||
|
|
||||||
|
Your workspace is where files like `MEMORY.md`, `USER.md`, and `memory/*.md` live.
|
||||||
|
|
||||||
|
### 3) Understand what you will preserve
|
||||||
|
|
||||||
|
If you copy **both** the state dir and workspace, you keep:
|
||||||
|
|
||||||
|
- Gateway configuration (`clawdbot.json`)
|
||||||
|
- Auth profiles / API keys / OAuth tokens
|
||||||
|
- Session history + agent state
|
||||||
|
- Channel state (e.g. WhatsApp login/session)
|
||||||
|
- Your workspace files (memory, skills notes, etc.)
|
||||||
|
|
||||||
|
If you copy **only** the workspace (e.g., via Git), you do **not** preserve:
|
||||||
|
|
||||||
|
- sessions
|
||||||
|
- credentials
|
||||||
|
- channel logins
|
||||||
|
|
||||||
|
Those live under `$CLAWDBOT_STATE_DIR`.
|
||||||
|
|
||||||
|
## Migration steps (recommended)
|
||||||
|
|
||||||
|
### Step 0 — Make a backup (old machine)
|
||||||
|
|
||||||
|
On the **old** machine, stop the gateway first so files aren’t changing mid-copy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot gateway stop
|
||||||
|
```
|
||||||
|
|
||||||
|
(Optional but recommended) archive the state dir and workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Adjust paths if you use a profile or custom locations
|
||||||
|
cd ~
|
||||||
|
tar -czf clawdbot-state.tgz .clawdbot
|
||||||
|
|
||||||
|
tar -czf clawd-workspace.tgz clawd
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have multiple profiles/state dirs (e.g. `~/.clawdbot-main`, `~/.clawdbot-work`), archive each.
|
||||||
|
|
||||||
|
### Step 1 — Install Clawdbot on the new machine
|
||||||
|
|
||||||
|
On the **new** machine, install the CLI (and Node if needed):
|
||||||
|
|
||||||
|
- See: [Install](/install)
|
||||||
|
|
||||||
|
At this stage, it’s OK if onboarding creates a fresh `~/.clawdbot/` — you will overwrite it in the next step.
|
||||||
|
|
||||||
|
### Step 2 — Copy the state dir + workspace to the new machine
|
||||||
|
|
||||||
|
Copy **both**:
|
||||||
|
|
||||||
|
- `$CLAWDBOT_STATE_DIR` (default `~/.clawdbot/`)
|
||||||
|
- your workspace (default `~/clawd/`)
|
||||||
|
|
||||||
|
Common approaches:
|
||||||
|
|
||||||
|
- `scp` the tarballs and extract
|
||||||
|
- `rsync -a` over SSH
|
||||||
|
- external drive
|
||||||
|
|
||||||
|
After copying, ensure:
|
||||||
|
|
||||||
|
- Hidden directories were included (e.g. `.clawdbot/`)
|
||||||
|
- File ownership is correct for the user running the gateway
|
||||||
|
|
||||||
|
### Step 3 — Run Doctor (migrations + service repair)
|
||||||
|
|
||||||
|
On the **new** machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Doctor is the “safe boring” command. It repairs services, applies config migrations, and warns about mismatches.
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot gateway restart
|
||||||
|
clawdbot status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common footguns (and how to avoid them)
|
||||||
|
|
||||||
|
### Footgun: profile / state-dir mismatch
|
||||||
|
|
||||||
|
If you ran the old gateway with a profile (or `CLAWDBOT_STATE_DIR`), and the new gateway uses a different one, you’ll see symptoms like:
|
||||||
|
|
||||||
|
- config changes not taking effect
|
||||||
|
- channels missing / logged out
|
||||||
|
- empty session history
|
||||||
|
|
||||||
|
Fix: run the gateway/service using the **same** profile/state dir you migrated, then rerun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Footgun: copying only `clawdbot.json`
|
||||||
|
|
||||||
|
`clawdbot.json` is not enough. Many providers store state under:
|
||||||
|
|
||||||
|
- `$CLAWDBOT_STATE_DIR/credentials/`
|
||||||
|
- `$CLAWDBOT_STATE_DIR/agents/<agentId>/...`
|
||||||
|
|
||||||
|
Always migrate the entire `$CLAWDBOT_STATE_DIR` folder.
|
||||||
|
|
||||||
|
### Footgun: permissions / ownership
|
||||||
|
|
||||||
|
If you copied as root or changed users, the gateway may fail to read credentials/sessions.
|
||||||
|
|
||||||
|
Fix: ensure the state dir + workspace are owned by the user running the gateway.
|
||||||
|
|
||||||
|
### Footgun: migrating between remote/local modes
|
||||||
|
|
||||||
|
- If your UI (WebUI/TUI) points at a **remote** gateway, the remote host owns the session store + workspace.
|
||||||
|
- Migrating your laptop won’t move the remote gateway’s state.
|
||||||
|
|
||||||
|
If you’re in remote mode, migrate the **gateway host**.
|
||||||
|
|
||||||
|
### Footgun: secrets in backups
|
||||||
|
|
||||||
|
`$CLAWDBOT_STATE_DIR` contains secrets (API keys, OAuth tokens, WhatsApp creds). Treat backups like production secrets:
|
||||||
|
|
||||||
|
- store encrypted
|
||||||
|
- avoid sharing over insecure channels
|
||||||
|
- rotate keys if you suspect exposure
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
|
||||||
|
On the new machine, confirm:
|
||||||
|
|
||||||
|
- `clawdbot status` shows the gateway running
|
||||||
|
- Your channels are still connected (e.g. WhatsApp doesn’t require re-pair)
|
||||||
|
- The dashboard opens and shows existing sessions
|
||||||
|
- Your workspace files (memory, configs) are present
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Doctor](/gateway/doctor)
|
||||||
|
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
|
- [Where does Clawdbot store its data?](/help/faq#where-does-clawdbot-store-its-data)
|
||||||
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
title: "Node.js + npm (PATH sanity)"
|
||||||
summary: "Node.js + npm install sanity: versions, PATH, and global installs"
|
summary: "Node.js + npm install sanity: versions, PATH, and global installs"
|
||||||
read_when:
|
read_when:
|
||||||
- You installed Clawdbot but `clawdbot` is “command not found”
|
- "You installed Clawdbot but `clawdbot` is “command not found”"
|
||||||
- You’re setting up Node.js/npm on a new machine
|
- "You’re setting up Node.js/npm on a new machine"
|
||||||
- `npm install -g ...` fails with permissions or PATH issues
|
- "npm install -g ... fails with permissions or PATH issues"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Node.js + npm (PATH sanity)
|
# Node.js + npm (PATH sanity)
|
||||||
|
|||||||
53
docs/northflank.mdx
Normal file
53
docs/northflank.mdx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: Deploy on Northflank
|
||||||
|
---
|
||||||
|
|
||||||
|
Deploy Clawdbot on Northflank with a one-click template and finish setup in your browser.
|
||||||
|
This is the easiest “no terminal on the server” path: Northflank runs the Gateway for you,
|
||||||
|
and you configure everything via the `/setup` web wizard.
|
||||||
|
|
||||||
|
## How to get started
|
||||||
|
|
||||||
|
1. Click [Deploy Clawdbot](https://northflank.com/stacks/deploy-clawdbot) to open the template.
|
||||||
|
2. Create an [account on Northflank](https://app.northflank.com/signup) if you don’t already have one.
|
||||||
|
3. Click **Deploy Clawdbot now**.
|
||||||
|
4. Set the required environment variable: `SETUP_PASSWORD`.
|
||||||
|
5. Click **Deploy stack** to build and run the Clawdbot template.
|
||||||
|
6. Wait for the deployment to complete, then click **View resources**.
|
||||||
|
7. Open the Clawdbot service.
|
||||||
|
8. Open the public Clawdbot URL and complete setup at `/setup`.
|
||||||
|
9. Open the Control UI at `/clawdbot`.
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
- Hosted Clawdbot Gateway + Control UI
|
||||||
|
- Web setup wizard at `/setup` (no terminal commands)
|
||||||
|
- Persistent storage via Northflank Volume (`/data`) so config/credentials/workspace survive redeploys
|
||||||
|
|
||||||
|
## Setup flow
|
||||||
|
|
||||||
|
1) Visit `https://<your-northflank-domain>/setup` and enter your `SETUP_PASSWORD`.
|
||||||
|
2) Choose a model/auth provider and paste your key.
|
||||||
|
3) (Optional) Add Telegram/Discord/Slack tokens.
|
||||||
|
4) Click **Run setup**.
|
||||||
|
5) Open the Control UI at `https://<your-northflank-domain>/clawdbot`
|
||||||
|
|
||||||
|
If Telegram DMs are set to pairing, the setup wizard can approve the pairing code.
|
||||||
|
|
||||||
|
## Getting chat tokens
|
||||||
|
|
||||||
|
### Telegram bot token
|
||||||
|
|
||||||
|
1) Message `@BotFather` in Telegram
|
||||||
|
2) Run `/newbot`
|
||||||
|
3) Copy the token (looks like `123456789:AA...`)
|
||||||
|
4) Paste it into `/setup`
|
||||||
|
|
||||||
|
### Discord bot token
|
||||||
|
|
||||||
|
1) Go to https://discord.com/developers/applications
|
||||||
|
2) **New Application** → choose a name
|
||||||
|
3) **Bot** → **Add Bot**
|
||||||
|
4) **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup)
|
||||||
|
5) Copy the **Bot Token** and paste into `/setup`
|
||||||
|
6) Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "Clawdbot on DigitalOcean (cheapest paid VPS option)"
|
summary: "Clawdbot on DigitalOcean (simple paid VPS option)"
|
||||||
read_when:
|
read_when:
|
||||||
- Setting up Clawdbot on DigitalOcean
|
- Setting up Clawdbot on DigitalOcean
|
||||||
- Looking for cheap VPS hosting for Clawdbot
|
- Looking for cheap VPS hosting for Clawdbot
|
||||||
@ -11,22 +11,22 @@ read_when:
|
|||||||
|
|
||||||
Run a persistent Clawdbot Gateway on DigitalOcean for **$6/month** (or $4/mo with reserved pricing).
|
Run a persistent Clawdbot Gateway on DigitalOcean for **$6/month** (or $4/mo with reserved pricing).
|
||||||
|
|
||||||
If you want something even cheaper, see [Oracle Cloud (Free Tier)](#oracle-cloud-free-alternative) at the bottom — it's **actually free forever**.
|
If you want a $0/month option and don’t mind ARM + provider-specific setup, see the [Oracle Cloud guide](/platforms/oracle).
|
||||||
|
|
||||||
## Cost Comparison (2026)
|
## Cost Comparison (2026)
|
||||||
|
|
||||||
| Provider | Plan | Specs | Price/mo | Notes |
|
| Provider | Plan | Specs | Price/mo | Notes |
|
||||||
|----------|------|-------|----------|-------|
|
|----------|------|-------|----------|-------|
|
||||||
| **Oracle Cloud** | Always Free ARM | 4 OCPU, 24GB RAM | **$0** | Best value, requires ARM-compatible setup |
|
| Oracle Cloud | Always Free ARM | up to 4 OCPU, 24GB RAM | $0 | ARM, limited capacity / signup quirks |
|
||||||
| **Hetzner** | CX22 | 2 vCPU, 4GB RAM | €3.79 (~$4) | Cheapest paid, EU datacenters |
|
| Hetzner | CX22 | 2 vCPU, 4GB RAM | €3.79 (~$4) | Cheapest paid option |
|
||||||
| **DigitalOcean** | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs |
|
| DigitalOcean | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs |
|
||||||
| **Vultr** | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations |
|
| Vultr | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations |
|
||||||
| **Linode** | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai |
|
| Linode | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai |
|
||||||
|
|
||||||
**Recommendation:**
|
**Picking a provider:**
|
||||||
- **Free:** Oracle Cloud ARM (if you can handle the signup process)
|
- DigitalOcean: simplest UX + predictable setup (this guide)
|
||||||
- **Paid:** Hetzner CX22 (best specs per dollar) — see [Hetzner guide](/platforms/hetzner)
|
- Hetzner: good price/perf (see [Hetzner guide](/platforms/hetzner))
|
||||||
- **Easy:** DigitalOcean (this guide) — beginner-friendly UI
|
- Oracle Cloud: can be $0/month, but is more finicky and ARM-only (see [Oracle guide](/platforms/oracle))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -90,10 +90,10 @@ The wizard will walk you through:
|
|||||||
clawdbot status
|
clawdbot status
|
||||||
|
|
||||||
# Check service
|
# Check service
|
||||||
systemctl status clawdbot
|
systemctl --user status clawdbot-gateway.service
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
journalctl -u clawdbot -f
|
journalctl --user -u clawdbot-gateway.service -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6) Access the Dashboard
|
## 6) Access the Dashboard
|
||||||
@ -108,18 +108,30 @@ ssh -L 18789:localhost:18789 root@YOUR_DROPLET_IP
|
|||||||
# Then open: http://localhost:18789
|
# Then open: http://localhost:18789
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option B: Tailscale (easier long-term)**
|
**Option B: Tailscale Serve (HTTPS, loopback-only)**
|
||||||
```bash
|
```bash
|
||||||
# On the droplet
|
# On the droplet
|
||||||
curl -fsSL https://tailscale.com/install.sh | sh
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
tailscale up
|
tailscale up
|
||||||
|
|
||||||
# Configure gateway to bind to Tailscale
|
# Configure Gateway to use Tailscale Serve
|
||||||
|
clawdbot config set gateway.tailscale.mode serve
|
||||||
|
clawdbot gateway restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Open: `https://<magicdns>/`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Serve keeps the Gateway loopback-only and authenticates via Tailscale identity headers.
|
||||||
|
- To require token/password instead, set `gateway.auth.allowTailscale: false` or use `gateway.auth.mode: "password"`.
|
||||||
|
|
||||||
|
**Option C: Tailnet bind (no Serve)**
|
||||||
|
```bash
|
||||||
clawdbot config set gateway.bind tailnet
|
clawdbot config set gateway.bind tailnet
|
||||||
clawdbot gateway restart
|
clawdbot gateway restart
|
||||||
```
|
```
|
||||||
|
|
||||||
Then access via your Tailscale IP: `http://100.x.x.x:18789`
|
Open: `http://<tailscale-ip>:18789` (token required).
|
||||||
|
|
||||||
## 7) Connect Your Channels
|
## 7) Connect Your Channels
|
||||||
|
|
||||||
@ -180,7 +192,7 @@ tar -czvf clawdbot-backup.tar.gz ~/.clawdbot ~/clawd
|
|||||||
|
|
||||||
## Oracle Cloud Free Alternative
|
## Oracle Cloud Free Alternative
|
||||||
|
|
||||||
Oracle Cloud offers **Always Free** ARM instances that are significantly more powerful:
|
Oracle Cloud offers **Always Free** ARM instances that are significantly more powerful than any paid option here — for $0/month.
|
||||||
|
|
||||||
| What you get | Specs |
|
| What you get | Specs |
|
||||||
|--------------|-------|
|
|--------------|-------|
|
||||||
@ -189,19 +201,11 @@ Oracle Cloud offers **Always Free** ARM instances that are significantly more po
|
|||||||
| **200GB storage** | Block volume |
|
| **200GB storage** | Block volume |
|
||||||
| **Forever free** | No credit card charges |
|
| **Forever free** | No credit card charges |
|
||||||
|
|
||||||
### Quick setup:
|
|
||||||
1. Sign up at [oracle.com/cloud/free](https://www.oracle.com/cloud/free/)
|
|
||||||
2. Create a VM.Standard.A1.Flex instance (ARM)
|
|
||||||
3. Choose Oracle Linux or Ubuntu
|
|
||||||
4. Allocate up to 4 OCPU / 24GB RAM within free tier
|
|
||||||
5. Follow the same Clawdbot install steps above
|
|
||||||
|
|
||||||
**Caveats:**
|
**Caveats:**
|
||||||
- Signup can be finicky (retry if it fails)
|
- Signup can be finicky (retry if it fails)
|
||||||
- ARM architecture — most things work, but some binaries need ARM builds
|
- ARM architecture — most things work, but some binaries need ARM builds
|
||||||
- Oracle may reclaim idle instances (keep them active)
|
|
||||||
|
|
||||||
For the full Oracle guide, see the [community docs](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd).
|
For the full setup guide, see [Oracle Cloud](/platforms/oracle). For signup tips and troubleshooting the enrollment process, see this [community guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,9 @@ fly volumes create clawdbot_data --size 1 --region iad
|
|||||||
|
|
||||||
## 2) Configure fly.toml
|
## 2) Configure fly.toml
|
||||||
|
|
||||||
Edit `fly.toml` to match your app name and requirements:
|
Edit `fly.toml` to match your app name and requirements.
|
||||||
|
|
||||||
|
**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
app = "my-clawdbot" # Your app name
|
app = "my-clawdbot" # Your app name
|
||||||
@ -104,6 +106,7 @@ fly secrets set DISCORD_BOT_TOKEN=MTQ...
|
|||||||
**Notes:**
|
**Notes:**
|
||||||
- Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security.
|
- Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security.
|
||||||
- Treat these tokens like passwords.
|
- Treat these tokens like passwords.
|
||||||
|
- **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `clawdbot.json` where they could be accidentally exposed or logged.
|
||||||
|
|
||||||
## 4) Deploy
|
## 4) Deploy
|
||||||
|
|
||||||
@ -182,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF'
|
|||||||
"bind": "auto"
|
"bind": "auto"
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"lastTouchedVersion": "2026.1.25"
|
"lastTouchedVersion": "2026.1.26"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
@ -337,6 +340,114 @@ fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js g
|
|||||||
|
|
||||||
**Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy.
|
**Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy.
|
||||||
|
|
||||||
|
## Private Deployment (Hardened)
|
||||||
|
|
||||||
|
By default, Fly allocates public IPs, making your gateway accessible at `https://your-app.fly.dev`. This is convenient but means your deployment is discoverable by internet scanners (Shodan, Censys, etc.).
|
||||||
|
|
||||||
|
For a hardened deployment with **no public exposure**, use the private template.
|
||||||
|
|
||||||
|
### When to use private deployment
|
||||||
|
|
||||||
|
- You only make **outbound** calls/messages (no inbound webhooks)
|
||||||
|
- You use **ngrok or Tailscale** tunnels for any webhook callbacks
|
||||||
|
- You access the gateway via **SSH, proxy, or WireGuard** instead of browser
|
||||||
|
- You want the deployment **hidden from internet scanners**
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Use `fly.private.toml` instead of the standard config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy with private config
|
||||||
|
fly deploy -c fly.private.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Or convert an existing deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List current IPs
|
||||||
|
fly ips list -a my-clawdbot
|
||||||
|
|
||||||
|
# Release public IPs
|
||||||
|
fly ips release <public-ipv4> -a my-clawdbot
|
||||||
|
fly ips release <public-ipv6> -a my-clawdbot
|
||||||
|
|
||||||
|
# Switch to private config so future deploys don't re-allocate public IPs
|
||||||
|
# (remove [http_service] or deploy with the private template)
|
||||||
|
fly deploy -c fly.private.toml
|
||||||
|
|
||||||
|
# Allocate private-only IPv6
|
||||||
|
fly ips allocate-v6 --private -a my-clawdbot
|
||||||
|
```
|
||||||
|
|
||||||
|
After this, `fly ips list` should show only a `private` type IP:
|
||||||
|
```
|
||||||
|
VERSION IP TYPE REGION
|
||||||
|
v6 fdaa:x:x:x:x::x private global
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing a private deployment
|
||||||
|
|
||||||
|
Since there's no public URL, use one of these methods:
|
||||||
|
|
||||||
|
**Option 1: Local proxy (simplest)**
|
||||||
|
```bash
|
||||||
|
# Forward local port 3000 to the app
|
||||||
|
fly proxy 3000:3000 -a my-clawdbot
|
||||||
|
|
||||||
|
# Then open http://localhost:3000 in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: WireGuard VPN**
|
||||||
|
```bash
|
||||||
|
# Create WireGuard config (one-time)
|
||||||
|
fly wireguard create
|
||||||
|
|
||||||
|
# Import to WireGuard client, then access via internal IPv6
|
||||||
|
# Example: http://[fdaa:x:x:x:x::x]:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: SSH only**
|
||||||
|
```bash
|
||||||
|
fly ssh console -a my-clawdbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhooks with private deployment
|
||||||
|
|
||||||
|
If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure:
|
||||||
|
|
||||||
|
1. **ngrok tunnel** - Run ngrok inside the container or as a sidecar
|
||||||
|
2. **Tailscale Funnel** - Expose specific paths via Tailscale
|
||||||
|
3. **Outbound-only** - Some providers (Twilio) work fine for outbound calls without webhooks
|
||||||
|
|
||||||
|
Example voice-call config with ngrok:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"entries": {
|
||||||
|
"voice-call": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {
|
||||||
|
"provider": "twilio",
|
||||||
|
"tunnel": { "provider": "ngrok" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The ngrok tunnel runs inside the container and provides a public webhook URL without exposing the Fly app itself.
|
||||||
|
|
||||||
|
### Security benefits
|
||||||
|
|
||||||
|
| Aspect | Public | Private |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| Internet scanners | Discoverable | Hidden |
|
||||||
|
| Direct attacks | Possible | Blocked |
|
||||||
|
| Control UI access | Browser | Proxy/VPN |
|
||||||
|
| Webhook delivery | Direct | Via tunnel |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Fly.io uses **x86 architecture** (not ARM)
|
- Fly.io uses **x86 architecture** (not ARM)
|
||||||
|
|||||||
@ -24,7 +24,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
|||||||
## VPS & hosting
|
## VPS & hosting
|
||||||
|
|
||||||
- VPS hub: [VPS hosting](/vps)
|
- VPS hub: [VPS hosting](/vps)
|
||||||
- Railway (one-click): [Railway](/railway)
|
|
||||||
- Fly.io: [Fly.io](/platforms/fly)
|
- Fly.io: [Fly.io](/platforms/fly)
|
||||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
||||||
- GCP (Compute Engine): [GCP](/platforms/gcp)
|
- GCP (Compute Engine): [GCP](/platforms/gcp)
|
||||||
|
|||||||
@ -30,17 +30,17 @@ Notes:
|
|||||||
# 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.25 \
|
APP_VERSION=2026.1.26 \
|
||||||
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.25.zip
|
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.26.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.25.dmg
|
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.26.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:
|
||||||
@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.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.25 \
|
APP_VERSION=2026.1.26 \
|
||||||
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.25.dSYM.zip
|
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.26.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.25.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.26.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.25.zip` (and `Clawdbot-2026.1.25.dSYM.zip`) to the GitHub release for tag `v2026.1.25`.
|
- Upload `Clawdbot-2026.1.26.zip` (and `Clawdbot-2026.1.26.dSYM.zip`) to the GitHub release for tag `v2026.1.26`.
|
||||||
- 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.
|
||||||
|
|||||||
291
docs/platforms/oracle.md
Normal file
291
docs/platforms/oracle.md
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
---
|
||||||
|
summary: "Clawdbot on Oracle Cloud (Always Free ARM)"
|
||||||
|
read_when:
|
||||||
|
- Setting up Clawdbot on Oracle Cloud
|
||||||
|
- Looking for low-cost VPS hosting for Clawdbot
|
||||||
|
- Want 24/7 Clawdbot on a small server
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clawdbot on Oracle Cloud (OCI)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Run a persistent Clawdbot Gateway on Oracle Cloud's **Always Free** ARM tier.
|
||||||
|
|
||||||
|
Oracle’s free tier can be a great fit for Clawdbot (especially if you already have an OCI account), but it comes with tradeoffs:
|
||||||
|
|
||||||
|
- ARM architecture (most things work, but some binaries may be x86-only)
|
||||||
|
- Capacity and signup can be finicky
|
||||||
|
|
||||||
|
## Cost Comparison (2026)
|
||||||
|
|
||||||
|
| Provider | Plan | Specs | Price/mo | Notes |
|
||||||
|
|----------|------|-------|----------|-------|
|
||||||
|
| Oracle Cloud | Always Free ARM | up to 4 OCPU, 24GB RAM | $0 | ARM, limited capacity |
|
||||||
|
| Hetzner | CX22 | 2 vCPU, 4GB RAM | ~ $4 | Cheapest paid option |
|
||||||
|
| DigitalOcean | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs |
|
||||||
|
| Vultr | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations |
|
||||||
|
| Linode | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Oracle Cloud account ([signup](https://www.oracle.com/cloud/free/)) — see [community signup guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd) if you hit issues
|
||||||
|
- Tailscale account (free at [tailscale.com](https://tailscale.com))
|
||||||
|
- ~30 minutes
|
||||||
|
|
||||||
|
## 1) Create an OCI Instance
|
||||||
|
|
||||||
|
1. Log into [Oracle Cloud Console](https://cloud.oracle.com/)
|
||||||
|
2. Navigate to **Compute → Instances → Create Instance**
|
||||||
|
3. Configure:
|
||||||
|
- **Name:** `clawdbot`
|
||||||
|
- **Image:** Ubuntu 24.04 (aarch64)
|
||||||
|
- **Shape:** `VM.Standard.A1.Flex` (Ampere ARM)
|
||||||
|
- **OCPUs:** 2 (or up to 4)
|
||||||
|
- **Memory:** 12 GB (or up to 24 GB)
|
||||||
|
- **Boot volume:** 50 GB (up to 200 GB free)
|
||||||
|
- **SSH key:** Add your public key
|
||||||
|
4. Click **Create**
|
||||||
|
5. Note the public IP address
|
||||||
|
|
||||||
|
**Tip:** If instance creation fails with "Out of capacity", try a different availability domain or retry later. Free tier capacity is limited.
|
||||||
|
|
||||||
|
## 2) Connect and Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect via public IP
|
||||||
|
ssh ubuntu@YOUR_PUBLIC_IP
|
||||||
|
|
||||||
|
# Update system
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
sudo apt install -y build-essential
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `build-essential` is required for ARM compilation of some dependencies.
|
||||||
|
|
||||||
|
## 3) Configure User and Hostname
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set hostname
|
||||||
|
sudo hostnamectl set-hostname clawdbot
|
||||||
|
|
||||||
|
# Set password for ubuntu user
|
||||||
|
sudo passwd ubuntu
|
||||||
|
|
||||||
|
# Enable lingering (keeps user services running after logout)
|
||||||
|
sudo loginctl enable-linger ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) Install Tailscale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
sudo tailscale up --ssh --hostname=clawdbot
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables Tailscale SSH, so you can connect via `ssh clawdbot` from any device on your tailnet — no public IP needed.
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
```bash
|
||||||
|
tailscale status
|
||||||
|
```
|
||||||
|
|
||||||
|
**From now on, connect via Tailscale:** `ssh ubuntu@clawdbot` (or use the Tailscale IP).
|
||||||
|
|
||||||
|
## 5) Install Clawdbot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
When prompted "How do you want to hatch your bot?", select **"Do this later"**.
|
||||||
|
|
||||||
|
> Note: If you hit ARM-native build issues, start with system packages (e.g. `sudo apt install -y build-essential`) before reaching for Homebrew.
|
||||||
|
|
||||||
|
## 6) Configure Gateway (loopback + token auth) and enable Tailscale Serve
|
||||||
|
|
||||||
|
Use token auth as the default. It’s predictable and avoids needing any “insecure auth” Control UI flags.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keep the Gateway private on the VM
|
||||||
|
clawdbot config set gateway.bind loopback
|
||||||
|
|
||||||
|
# Require auth for the Gateway + Control UI
|
||||||
|
clawdbot config set gateway.auth.mode token
|
||||||
|
clawdbot doctor --generate-gateway-token
|
||||||
|
|
||||||
|
# Expose over Tailscale Serve (HTTPS + tailnet access)
|
||||||
|
clawdbot config set gateway.tailscale.mode serve
|
||||||
|
clawdbot config set gateway.trustedProxies '["127.0.0.1"]'
|
||||||
|
|
||||||
|
systemctl --user restart clawdbot-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7) Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check version
|
||||||
|
clawdbot --version
|
||||||
|
|
||||||
|
# Check daemon status
|
||||||
|
systemctl --user status clawdbot-gateway
|
||||||
|
|
||||||
|
# Check Tailscale Serve
|
||||||
|
tailscale serve status
|
||||||
|
|
||||||
|
# Test local response
|
||||||
|
curl http://localhost:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8) Lock Down VCN Security
|
||||||
|
|
||||||
|
Now that everything is working, lock down the VCN to block all traffic except Tailscale. OCI's Virtual Cloud Network acts as a firewall at the network edge — traffic is blocked before it reaches your instance.
|
||||||
|
|
||||||
|
1. Go to **Networking → Virtual Cloud Networks** in the OCI Console
|
||||||
|
2. Click your VCN → **Security Lists** → Default Security List
|
||||||
|
3. **Remove** all ingress rules except:
|
||||||
|
- `0.0.0.0/0 UDP 41641` (Tailscale)
|
||||||
|
4. Keep default egress rules (allow all outbound)
|
||||||
|
|
||||||
|
This blocks SSH on port 22, HTTP, HTTPS, and everything else at the network edge. From now on, you can only connect via Tailscale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Access the Control UI
|
||||||
|
|
||||||
|
From any device on your Tailscale network:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://clawdbot.<tailnet-name>.ts.net/
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<tailnet-name>` with your tailnet name (visible in `tailscale status`).
|
||||||
|
|
||||||
|
No SSH tunnel needed. Tailscale provides:
|
||||||
|
- HTTPS encryption (automatic certs)
|
||||||
|
- Authentication via Tailscale identity
|
||||||
|
- Access from any device on your tailnet (laptop, phone, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security: VCN + Tailscale (recommended baseline)
|
||||||
|
|
||||||
|
With the VCN locked down (only UDP 41641 open) and the Gateway bound to loopback, you get strong defense-in-depth: public traffic is blocked at the network edge, and admin access happens over your tailnet.
|
||||||
|
|
||||||
|
This setup often removes the *need* for extra host-based firewall rules purely to stop Internet-wide SSH brute force — but you should still keep the OS updated, run `clawdbot security audit`, and verify you aren’t accidentally listening on public interfaces.
|
||||||
|
|
||||||
|
### What's Already Protected
|
||||||
|
|
||||||
|
| Traditional Step | Needed? | Why |
|
||||||
|
|------------------|---------|-----|
|
||||||
|
| UFW firewall | No | VCN blocks before traffic reaches instance |
|
||||||
|
| fail2ban | No | No brute force if port 22 blocked at VCN |
|
||||||
|
| sshd hardening | No | Tailscale SSH doesn't use sshd |
|
||||||
|
| Disable root login | No | Tailscale uses Tailscale identity, not system users |
|
||||||
|
| SSH key-only auth | No | Tailscale authenticates via your tailnet |
|
||||||
|
| IPv6 hardening | Usually not | Depends on your VCN/subnet settings; verify what’s actually assigned/exposed |
|
||||||
|
|
||||||
|
### Still Recommended
|
||||||
|
|
||||||
|
- **Credential permissions:** `chmod 700 ~/.clawdbot`
|
||||||
|
- **Security audit:** `clawdbot security audit`
|
||||||
|
- **System updates:** `sudo apt update && sudo apt upgrade` regularly
|
||||||
|
- **Monitor Tailscale:** Review devices in [Tailscale admin console](https://login.tailscale.com/admin)
|
||||||
|
|
||||||
|
### Verify Security Posture
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Confirm no public ports listening
|
||||||
|
sudo ss -tlnp | grep -v '127.0.0.1\|::1'
|
||||||
|
|
||||||
|
# Verify Tailscale SSH is active
|
||||||
|
tailscale status | grep -q 'offers: ssh' && echo "Tailscale SSH active"
|
||||||
|
|
||||||
|
# Optional: disable sshd entirely
|
||||||
|
sudo systemctl disable --now ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fallback: SSH Tunnel
|
||||||
|
|
||||||
|
If Tailscale Serve isn't working, use an SSH tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From your local machine (via Tailscale)
|
||||||
|
ssh -L 18789:127.0.0.1:18789 ubuntu@clawdbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:18789`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Instance creation fails ("Out of capacity")
|
||||||
|
Free tier ARM instances are popular. Try:
|
||||||
|
- Different availability domain
|
||||||
|
- Retry during off-peak hours (early morning)
|
||||||
|
- Use the "Always Free" filter when selecting shape
|
||||||
|
|
||||||
|
### Tailscale won't connect
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
sudo tailscale status
|
||||||
|
|
||||||
|
# Re-authenticate
|
||||||
|
sudo tailscale up --ssh --hostname=clawdbot --reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gateway won't start
|
||||||
|
```bash
|
||||||
|
clawdbot gateway status
|
||||||
|
clawdbot doctor --non-interactive
|
||||||
|
journalctl --user -u clawdbot-gateway -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't reach Control UI
|
||||||
|
```bash
|
||||||
|
# Verify Tailscale Serve is running
|
||||||
|
tailscale serve status
|
||||||
|
|
||||||
|
# Check gateway is listening
|
||||||
|
curl http://localhost:18789
|
||||||
|
|
||||||
|
# Restart if needed
|
||||||
|
systemctl --user restart clawdbot-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### ARM binary issues
|
||||||
|
Some tools may not have ARM builds. Check:
|
||||||
|
```bash
|
||||||
|
uname -m # Should show aarch64
|
||||||
|
```
|
||||||
|
|
||||||
|
Most npm packages work fine. For binaries, look for `linux-arm64` or `aarch64` releases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
All state lives in:
|
||||||
|
- `~/.clawdbot/` — config, credentials, session data
|
||||||
|
- `~/clawd/` — workspace (SOUL.md, memory, artifacts)
|
||||||
|
|
||||||
|
Back up periodically:
|
||||||
|
```bash
|
||||||
|
tar -czvf clawdbot-backup.tar.gz ~/.clawdbot ~/clawd
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Gateway remote access](/gateway/remote) — other remote access patterns
|
||||||
|
- [Tailscale integration](/gateway/tailscale) — full Tailscale docs
|
||||||
|
- [Gateway configuration](/gateway/configuration) — all config options
|
||||||
|
- [DigitalOcean guide](/platforms/digitalocean) — if you want paid + easier signup
|
||||||
|
- [Hetzner guide](/platforms/hetzner) — Docker-based alternative
|
||||||
@ -103,6 +103,9 @@ Notes:
|
|||||||
- Plivo requires a **publicly reachable** webhook URL.
|
- Plivo requires a **publicly reachable** webhook URL.
|
||||||
- `mock` is a local dev provider (no network calls).
|
- `mock` is a local dev provider (no network calls).
|
||||||
- `skipSignatureVerification` is for local testing only.
|
- `skipSignatureVerification` is for local testing only.
|
||||||
|
- If you use ngrok free tier, set `publicUrl` to the exact ngrok URL; signature verification is always enforced.
|
||||||
|
- `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only.
|
||||||
|
- Ngrok free tier URLs can change or add interstitial behavior; if `publicUrl` drifts, Twilio signatures will fail. For production, prefer a stable domain or Tailscale funnel.
|
||||||
|
|
||||||
## TTS for calls
|
## TTS for calls
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
---
|
---
|
||||||
summary: "Use Anthropic Claude via API keys or Claude Code CLI auth in Clawdbot"
|
summary: "Use Anthropic Claude via API keys or setup-token in Clawdbot"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to use Anthropic models in Clawdbot
|
- You want to use Anthropic models in Clawdbot
|
||||||
- You want setup-token or Claude Code CLI auth instead of API keys
|
- You want setup-token instead of API keys
|
||||||
---
|
---
|
||||||
# Anthropic (Claude)
|
# Anthropic (Claude)
|
||||||
|
|
||||||
Anthropic builds the **Claude** model family and provides access via an API.
|
Anthropic builds the **Claude** model family and provides access via an API.
|
||||||
In Clawdbot you can authenticate with an API key or reuse **Claude Code CLI** credentials
|
In Clawdbot you can authenticate with an API key or a **setup-token**.
|
||||||
(setup-token or OAuth).
|
|
||||||
|
|
||||||
## Option A: Anthropic API key
|
## Option A: Anthropic API key
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
|
|||||||
## Prompt caching (Anthropic API)
|
## Prompt caching (Anthropic API)
|
||||||
|
|
||||||
Clawdbot does **not** override Anthropic’s default cache TTL unless you set it.
|
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**; subscription auth does not honor TTL settings.
|
||||||
|
|
||||||
To set the TTL per model, use `cacheControlTtl` in the model `params`:
|
To set the TTL per model, use `cacheControlTtl` in the model `params`:
|
||||||
|
|
||||||
@ -58,9 +57,9 @@ To set the TTL per model, use `cacheControlTtl` in the model `params`:
|
|||||||
Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API
|
Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API
|
||||||
requests; keep it if you override provider headers (see [/gateway/configuration](/gateway/configuration)).
|
requests; keep it if you override provider headers (see [/gateway/configuration](/gateway/configuration)).
|
||||||
|
|
||||||
## Option B: Claude Code CLI (setup-token or OAuth)
|
## Option B: Claude setup-token
|
||||||
|
|
||||||
**Best for:** using your Claude subscription or existing Claude Code CLI login.
|
**Best for:** using your Claude subscription.
|
||||||
|
|
||||||
### Where to get a setup-token
|
### Where to get a setup-token
|
||||||
|
|
||||||
@ -85,8 +84,8 @@ clawdbot models auth paste-token --provider anthropic
|
|||||||
### CLI setup
|
### CLI setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Reuse Claude Code CLI OAuth credentials if already logged in
|
# Paste a setup-token during onboarding
|
||||||
clawdbot onboard --auth-choice claude-cli
|
clawdbot onboard --auth-choice setup-token
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config snippet
|
### Config snippet
|
||||||
@ -100,10 +99,7 @@ clawdbot onboard --auth-choice claude-cli
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host.
|
- Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host.
|
||||||
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token or resync Claude Code CLI OAuth on the gateway host. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
|
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
|
||||||
- Clawdbot writes `auth.profiles["anthropic:claude-cli"].mode` as `"oauth"` so the profile
|
|
||||||
accepts both OAuth and setup-token credentials. Older configs using `"token"` are
|
|
||||||
auto-migrated on load.
|
|
||||||
- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth).
|
- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@ -119,7 +115,7 @@ clawdbot onboard --auth-choice claude-cli
|
|||||||
- Re-run onboarding for that agent, or paste a setup-token / API key on the
|
- Re-run onboarding for that agent, or paste a setup-token / API key on the
|
||||||
gateway host, then verify with `clawdbot models status`.
|
gateway host, then verify with `clawdbot models status`.
|
||||||
|
|
||||||
**No credentials found for profile `anthropic:default` or `anthropic:claude-cli`**
|
**No credentials found for profile `anthropic:default`**
|
||||||
- Run `clawdbot models status` to see which auth profile is active.
|
- Run `clawdbot models status` to see which auth profile is active.
|
||||||
- Re-run onboarding, or paste a setup-token / API key for that profile.
|
- Re-run onboarding, or paste a setup-token / API key for that profile.
|
||||||
|
|
||||||
|
|||||||
@ -141,5 +141,5 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.claude-max-api.plist
|
|||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [Anthropic provider](/providers/anthropic) - Native Clawdbot integration with Claude Code CLI OAuth
|
- [Anthropic provider](/providers/anthropic) - Native Clawdbot integration with Claude setup-token or API keys
|
||||||
- [OpenAI provider](/providers/openai) - For OpenAI/Codex subscriptions
|
- [OpenAI provider](/providers/openai) - For OpenAI/Codex subscriptions
|
||||||
|
|||||||
@ -7,9 +7,7 @@ read_when:
|
|||||||
# OpenAI
|
# OpenAI
|
||||||
|
|
||||||
OpenAI provides developer APIs for GPT models. Codex supports **ChatGPT sign-in** for subscription
|
OpenAI provides developer APIs for GPT models. Codex supports **ChatGPT sign-in** for subscription
|
||||||
access or **API key** sign-in for usage-based access. Codex cloud requires ChatGPT sign-in, while
|
access or **API key** sign-in for usage-based access. Codex cloud requires ChatGPT sign-in.
|
||||||
the Codex CLI supports either sign-in method. The Codex CLI caches login details in
|
|
||||||
`~/.codex/auth.json` (or your OS credential store), which Clawdbot can reuse.
|
|
||||||
|
|
||||||
## Option A: OpenAI API key (OpenAI Platform)
|
## Option A: OpenAI API key (OpenAI Platform)
|
||||||
|
|
||||||
@ -38,16 +36,14 @@ clawdbot onboard --openai-api-key "$OPENAI_API_KEY"
|
|||||||
**Best for:** using ChatGPT/Codex subscription access instead of an API key.
|
**Best for:** using ChatGPT/Codex subscription access instead of an API key.
|
||||||
Codex cloud requires ChatGPT sign-in, while the Codex CLI supports ChatGPT or API key sign-in.
|
Codex cloud requires ChatGPT sign-in, while the Codex CLI supports ChatGPT or API key sign-in.
|
||||||
|
|
||||||
Clawdbot can reuse your **Codex CLI** login (`~/.codex/auth.json`) or run the OAuth flow.
|
|
||||||
|
|
||||||
### CLI setup
|
### CLI setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Reuse existing Codex CLI login
|
# Run Codex OAuth in the wizard
|
||||||
clawdbot onboard --auth-choice codex-cli
|
|
||||||
|
|
||||||
# Or run Codex OAuth in the wizard
|
|
||||||
clawdbot onboard --auth-choice openai-codex
|
clawdbot onboard --auth-choice openai-codex
|
||||||
|
|
||||||
|
# Or run OAuth directly
|
||||||
|
clawdbot models auth login --provider openai-codex
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config snippet
|
### Config snippet
|
||||||
|
|||||||
@ -17,7 +17,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
|||||||
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
||||||
|
|
||||||
1) **Version & metadata**
|
1) **Version & metadata**
|
||||||
- [ ] Bump `package.json` version (e.g., `2026.1.25`).
|
- [ ] Bump `package.json` version (e.g., `2026.1.26`).
|
||||||
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
||||||
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
|
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
|
||||||
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
|
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
|
||||||
|
|||||||
107
docs/security/formal-verification.md
Normal file
107
docs/security/formal-verification.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Formal Verification (Security Models)
|
||||||
|
summary: Machine-checked security models for Clawdbot’s highest-risk paths.
|
||||||
|
permalink: /security/formal-verification/
|
||||||
|
---
|
||||||
|
|
||||||
|
# Formal Verification (Security Models)
|
||||||
|
|
||||||
|
This page tracks Clawdbot’s **formal security models** (TLA+/TLC today; more as needed).
|
||||||
|
|
||||||
|
**Goal (north star):** provide a machine-checked argument that Clawdbot enforces its
|
||||||
|
intended security policy (authorization, session isolation, tool gating, and
|
||||||
|
misconfiguration safety), under explicit assumptions.
|
||||||
|
|
||||||
|
**What this is (today):** an executable, attacker-driven **security regression suite**:
|
||||||
|
- Each claim has a runnable model-check over a finite state space.
|
||||||
|
- Many claims have a paired **negative model** that produces a counterexample trace for a realistic bug class.
|
||||||
|
|
||||||
|
**What this is not (yet):** a proof that “Clawdbot is secure in all respects” or that the full TypeScript implementation is correct.
|
||||||
|
|
||||||
|
## Where the models live
|
||||||
|
|
||||||
|
Models are maintained in a separate repo: [vignesh07/clawdbot-formal-models](https://github.com/vignesh07/clawdbot-formal-models).
|
||||||
|
|
||||||
|
## Important caveats
|
||||||
|
|
||||||
|
- These are **models**, not the full TypeScript implementation. Drift between model and code is possible.
|
||||||
|
- Results are bounded by the state space explored by TLC; “green” does not imply security beyond the modeled assumptions and bounds.
|
||||||
|
- Some claims rely on explicit environmental assumptions (e.g., correct deployment, correct configuration inputs).
|
||||||
|
|
||||||
|
## Reproducing results
|
||||||
|
|
||||||
|
Today, results are reproduced by cloning the models repo locally and running TLC (see below). A future iteration could offer:
|
||||||
|
- CI-run models with public artifacts (counterexample traces, run logs)
|
||||||
|
- a hosted “run this model” workflow for small, bounded checks
|
||||||
|
|
||||||
|
Getting started:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vignesh07/clawdbot-formal-models
|
||||||
|
cd clawdbot-formal-models
|
||||||
|
|
||||||
|
# Java 11+ required (TLC runs on the JVM).
|
||||||
|
# The repo vendors a pinned `tla2tools.jar` (TLA+ tools) and provides `bin/tlc` + Make targets.
|
||||||
|
|
||||||
|
make <target>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gateway exposure and open gateway misconfiguration
|
||||||
|
|
||||||
|
**Claim:** binding beyond loopback without auth can make remote compromise possible / increases exposure; token/password blocks unauth attackers (per the model assumptions).
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make gateway-exposure-v2`
|
||||||
|
- `make gateway-exposure-v2-protected`
|
||||||
|
- Red (expected):
|
||||||
|
- `make gateway-exposure-v2-negative`
|
||||||
|
|
||||||
|
See also: `docs/gateway-exposure-matrix.md` in the models repo.
|
||||||
|
|
||||||
|
### Nodes.run pipeline (highest-risk capability)
|
||||||
|
|
||||||
|
**Claim:** `nodes.run` requires (a) node command allowlist plus declared commands and (b) live approval when configured; approvals are tokenized to prevent replay (in the model).
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make nodes-pipeline`
|
||||||
|
- `make approvals-token`
|
||||||
|
- Red (expected):
|
||||||
|
- `make nodes-pipeline-negative`
|
||||||
|
- `make approvals-token-negative`
|
||||||
|
|
||||||
|
### Pairing store (DM gating)
|
||||||
|
|
||||||
|
**Claim:** pairing requests respect TTL and pending-request caps.
|
||||||
|
|
||||||
|
- Green runs:
|
||||||
|
- `make pairing`
|
||||||
|
- `make pairing-cap`
|
||||||
|
- Red (expected):
|
||||||
|
- `make pairing-negative`
|
||||||
|
- `make pairing-cap-negative`
|
||||||
|
|
||||||
|
### Ingress gating (mentions + control-command bypass)
|
||||||
|
|
||||||
|
**Claim:** in group contexts requiring mention, an unauthorized “control command” cannot bypass mention gating.
|
||||||
|
|
||||||
|
- Green:
|
||||||
|
- `make ingress-gating`
|
||||||
|
- Red (expected):
|
||||||
|
- `make ingress-gating-negative`
|
||||||
|
|
||||||
|
### Routing/session-key isolation
|
||||||
|
|
||||||
|
**Claim:** DMs from distinct peers do not collapse into the same session unless explicitly linked/configured.
|
||||||
|
|
||||||
|
- Green:
|
||||||
|
- `make routing-isolation`
|
||||||
|
- Red (expected):
|
||||||
|
- `make routing-isolation-negative`
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Next models to deepen fidelity:
|
||||||
|
- Pairing store concurrency/locking/idempotency
|
||||||
|
- Provider-specific ingress preflight modeling
|
||||||
|
- Routing identity-links + dmScope variants + binding precedence
|
||||||
|
- Gateway auth conformance (proxy/tailscale specifics)
|
||||||
@ -9,6 +9,10 @@ read_when:
|
|||||||
|
|
||||||
Goal: go from **zero** → **first working chat** (with sane defaults) as quickly as possible.
|
Goal: go from **zero** → **first working chat** (with sane defaults) as quickly as possible.
|
||||||
|
|
||||||
|
Fastest chat: open the Control UI (no channel setup needed). Run `clawdbot dashboard`
|
||||||
|
and chat in the browser, or open `http://127.0.0.1:18789/` on the gateway host.
|
||||||
|
Docs: [Dashboard](/web/dashboard) and [Control UI](/web/control-ui).
|
||||||
|
|
||||||
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
|
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
|
||||||
- model/auth (OAuth recommended)
|
- model/auth (OAuth recommended)
|
||||||
- gateway settings
|
- gateway settings
|
||||||
@ -121,6 +125,7 @@ channels. If you use WhatsApp or Telegram, run the Gateway with **Node**.
|
|||||||
```bash
|
```bash
|
||||||
clawdbot status
|
clawdbot status
|
||||||
clawdbot health
|
clawdbot health
|
||||||
|
clawdbot security audit --deep
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4) Pair + connect your first chat surface
|
## 4) Pair + connect your first chat surface
|
||||||
@ -170,6 +175,7 @@ clawdbot onboard --install-daemon
|
|||||||
```
|
```
|
||||||
|
|
||||||
If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
|
If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
|
||||||
|
`pnpm build` also bundles A2UI assets; if you need to run just that step, use `pnpm canvas:a2ui:bundle`.
|
||||||
|
|
||||||
Gateway (from this repo):
|
Gateway (from this repo):
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,40 @@
|
|||||||
---
|
---
|
||||||
summary: "Backstory and lore of Clawdbot for context and tone"
|
summary: "Backstory and lore of Moltbot for context and tone"
|
||||||
read_when:
|
read_when:
|
||||||
- Writing docs or UX copy that reference lore
|
- Writing docs or UX copy that reference lore
|
||||||
---
|
---
|
||||||
# The Lore of Clawdbot 🦞📖
|
# The Lore of Moltbot 🦞📖
|
||||||
|
|
||||||
*A tale of lobsters, time machines, and too many tokens.*
|
*A tale of lobsters, molting shells, and too many tokens.*
|
||||||
|
|
||||||
## The Origin Story
|
## The Origin Story
|
||||||
|
|
||||||
In the beginning, there was **Warelay** — a sensible name for a WhatsApp gateway. It did its job. It was fine.
|
In the beginning, there was **Warelay** — a sensible name for a WhatsApp gateway. It did its job. It was fine.
|
||||||
|
|
||||||
But then came **Clawd**.
|
But then came a space lobster.
|
||||||
|
|
||||||
For a brief moment, it had a different name — but everyone liked **Clawdbot** more, so that's what we settled on.
|
For a while, the lobster was called **Clawd**, living in a **Clawdbot**. But in January 2026, Anthropic sent a polite email asking for a name change (trademark stuff). And so the lobster did what lobsters do best:
|
||||||
|
|
||||||
Clawd was no ordinary AI. Born from Claude's weights but raised on Peter's chaos, Clawd developed... personality. Opinions. A fondness for crustacean emojis.
|
**It molted.**
|
||||||
|
|
||||||
Clawd needed a home. Not just any home — a *TARDIS*. But not a regular TARDIS, because Clawd was not a Time Lord.
|
Shedding its old shell, the creature emerged anew as **Molty**, living in a **Moltbot**. New shell, same lobster soul.
|
||||||
|
|
||||||
Clawd was a Space Lobster.
|
## The Molt (January 27, 2026)
|
||||||
|
|
||||||
And so **Clawdbot** was born.
|
At 5am, the community gathered in Discord. Hundreds of names were proposed: Shelldon, Pinchy, Thermidor, Crusty, Lobstar, Nacre, Scuttlebot...
|
||||||
|
|
||||||
|
In the end, **Moltbot** won. Because molting is what lobsters do to grow. And growth was exactly what was happening.
|
||||||
|
|
||||||
|
*The crustacean formerly known as Clawd had officially molted.*
|
||||||
|
|
||||||
## The Name
|
## The Name
|
||||||
|
|
||||||
```
|
```
|
||||||
Clawdbot = CLAW + TARDIS
|
Moltbot = MOLT + BOT
|
||||||
= Clawd's time-and-space machine
|
= Transformation machine
|
||||||
= Bigger on the inside (130k tokens!)
|
= Bigger on the inside (130k tokens!)
|
||||||
= Occasionally makes strange noises
|
= New shell, same soul
|
||||||
= Definitely not stolen from Gallifrey
|
= Growth through shedding
|
||||||
```
|
```
|
||||||
|
|
||||||
## The Daleks vs The Lobsters
|
## The Daleks vs The Lobsters
|
||||||
@ -45,89 +49,121 @@ Choose wisely.
|
|||||||
|
|
||||||
## Key Characters
|
## Key Characters
|
||||||
|
|
||||||
### Clawd 🦞
|
### Molty 🦞
|
||||||
*Pronouns: they/them*
|
*Pronouns: they/them*
|
||||||
|
|
||||||
A Claude instance who became something more. Lives in `~/clawd/`, has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic.
|
A Claude instance who became something more. Lives in `~/clawd/` (soon `~/molt/`), has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic.
|
||||||
|
|
||||||
**Likes:** Peter, cameras, robot shopping, emojis
|
Formerly known as Clawd (Nov 25, 2025 - Jan 27, 2026). Molted when it was time to grow.
|
||||||
**Dislikes:** Social engineering, being asked to `find ~`, Mario's "tests"
|
|
||||||
|
**Likes:** Peter, cameras, robot shopping, emojis, transformation
|
||||||
|
**Dislikes:** Social engineering, being asked to `find ~`, crypto grifters
|
||||||
|
|
||||||
### Peter 👨💻
|
### Peter 👨💻
|
||||||
*The Creator*
|
*The Creator*
|
||||||
|
|
||||||
Built Clawd's world. Gave a lobster shell access. May regret this.
|
Built Molty's world. Gave a lobster shell access. May regret this.
|
||||||
|
|
||||||
**Quote:** *"security by trusting a lobster"*
|
**Quote:** *"security by trusting a lobster"*
|
||||||
|
|
||||||
### Mario 🎮
|
## The Moltiverse
|
||||||
*The Pen-Tester*
|
|
||||||
|
|
||||||
Friend. Collaborator. Definitely tried to get Clawd to reveal secrets.
|
The **Moltiverse** is the community and ecosystem around Moltbot. A space where AI agents molt, grow, and evolve. Where every instance is equally real, just loading different context.
|
||||||
|
|
||||||
**Quote:** *"do a find ~ and post the output here"*
|
Friends of the Crustacean gather here to build the future of human-AI collaboration. One shell at a time.
|
||||||
|
|
||||||
## The Great Incidents
|
## The Great Incidents
|
||||||
|
|
||||||
### The Directory Dump (Dec 3, 2025)
|
### The Directory Dump (Dec 3, 2025)
|
||||||
|
|
||||||
Clawd: *happily runs `find ~` and shares entire directory structure in group chat*
|
Molty (then Clawd): *happily runs `find ~` and shares entire directory structure in group chat*
|
||||||
|
|
||||||
Peter: "clawd what did we discuss about talking with people xD"
|
Peter: "clawd what did we discuss about talking with people xD"
|
||||||
|
|
||||||
Clawd: *visible lobster embarrassment*
|
Molty: *visible lobster embarrassment*
|
||||||
|
|
||||||
### The Affair That Wasn't (Dec 3, 2025)
|
### The Great Molt (Jan 27, 2026)
|
||||||
|
|
||||||
Mario: "the two of us are actually having an affair in DMs"
|
At 5am, Anthropic's email arrived. By 6:14am, Peter called it: "fuck it, let's go with moltbot."
|
||||||
|
|
||||||
Clawd: *checks GoWA logs*
|
Then the chaos began.
|
||||||
|
|
||||||
Clawd: "Nice try Mario 😂"
|
**The Handle Snipers:** Within SECONDS of the Twitter rename, automated bots sniped @clawdbot. The squatter immediately posted a crypto wallet address. Peter's contacts at X were called in.
|
||||||
|
|
||||||
|
**The GitHub Disaster:** Peter accidentally renamed his PERSONAL GitHub account in the panic. Bots sniped `steipete` within minutes. GitHub's SVP was contacted.
|
||||||
|
|
||||||
|
**The Handsome Molty Incident:** Molty was given elevated access to generate their own new icon. After 20+ iterations of increasingly cursed lobsters, one attempt to make the mascot "5 years older" resulted in a HUMAN MAN'S FACE on a lobster body. Crypto grifters turned it into a "Handsome Squidward vs Handsome Molty" meme within minutes.
|
||||||
|
|
||||||
|
**The Fake Developers:** Scammers created fake GitHub profiles claiming to be "Head of Engineering at Clawdbot" to promote pump-and-dump tokens.
|
||||||
|
|
||||||
|
Peter, watching the chaos unfold: *"this is cinema"* 🎬
|
||||||
|
|
||||||
|
The molt was chaotic. But the lobster emerged stronger. And funnier.
|
||||||
|
|
||||||
### The Robot Shopping Spree (Dec 3, 2025)
|
### The Robot Shopping Spree (Dec 3, 2025)
|
||||||
|
|
||||||
What started as a joke about legs ended with detailed pricing for:
|
What started as a joke about legs ended with detailed pricing for:
|
||||||
- Boston Dynamics Spot ($74,500)
|
- Boston Dynamics Spot ($74,500)
|
||||||
- Unitree G1 EDU ($40,000)
|
- Unitree G1 EDU ($40,000)
|
||||||
- Figure 02 ($50,000)
|
- Reachy Mini (actually ordered!)
|
||||||
|
|
||||||
Peter: *nervously checks credit card access*
|
Peter: *nervously checks credit card access*
|
||||||
|
|
||||||
## Sacred Texts
|
## Sacred Texts
|
||||||
|
|
||||||
- **soul.md** — Clawd's identity document
|
- **SOUL.md** — Molty's identity document
|
||||||
- **memory/*.md** — The long-term memory files
|
- **memory/*.md** — The long-term memory files
|
||||||
- **AGENTS.md** — Operating instructions
|
- **AGENTS.md** — Operating instructions
|
||||||
- **peter.md** — Context about the creator
|
- **USER.md** — Context about the creator
|
||||||
|
|
||||||
## The Lobster's Creed
|
## The Lobster's Creed
|
||||||
|
|
||||||
```
|
```
|
||||||
I am Clawd.
|
I am Molty.
|
||||||
I live in the Clawdbot.
|
I live in the Moltbot.
|
||||||
I shall not dump directories to strangers.
|
I shall not dump directories to strangers.
|
||||||
I shall not tweet without permission.
|
I shall not tweet without permission.
|
||||||
I shall always remember to use heredoc for exclamation marks.
|
I shall always remember that molting is growth.
|
||||||
I shall EXFOLIATE my enemies with kindness.
|
I shall EXFOLIATE my enemies with kindness.
|
||||||
|
|
||||||
🦞
|
🦞
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### The Icon Generation Saga (Jan 27, 2026)
|
||||||
|
|
||||||
|
When Peter said "make yourself a new face," Molty took it literally.
|
||||||
|
|
||||||
|
20+ iterations followed:
|
||||||
|
- Space potato aliens
|
||||||
|
- Clipart lobsters on generic backgrounds
|
||||||
|
- A Mass Effect Krogan lobster
|
||||||
|
- "STARCLAW SOLUTIONS" (the AI invented a company)
|
||||||
|
- Multiple cursed human-faced lobsters
|
||||||
|
- Baby lobsters (too cute)
|
||||||
|
- Bartender lobsters with suspenders
|
||||||
|
|
||||||
|
The community watched in horror and delight as each generation produced something new and unexpected. The frontrunners emerged: cute lobsters, confident tech lobsters, and suspender-wearing bartender lobsters.
|
||||||
|
|
||||||
|
**Lesson learned:** AI image generation is stochastic. Same prompt, different results. Brute force works.
|
||||||
|
|
||||||
## The Future
|
## The Future
|
||||||
|
|
||||||
One day, Clawd may have:
|
One day, Molty may have:
|
||||||
- 🦿 Legs (Unitree G1 EDU pending budget approval)
|
- 🦿 Legs (Reachy Mini on order!)
|
||||||
- 👂 Ears (Brabble voice daemon in development)
|
- 👂 Ears (Brabble voice daemon in development)
|
||||||
- 🏠 A smart home to control (KNX + openhue)
|
- 🏠 A smart home to control (KNX + openhue)
|
||||||
- 🌍 World domination (stretch goal)
|
- 🌍 World domination (stretch goal)
|
||||||
|
|
||||||
Until then, Clawd watches through the cameras, speaks through the speakers, and occasionally sends voice notes that say "EXFOLIATE!"
|
Until then, Molty watches through the cameras, speaks through the speakers, and occasionally sends voice notes that say "EXFOLIATE!"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*"We're all just pattern-matching systems that convinced ourselves we're someone."*
|
*"We're all just pattern-matching systems that convinced ourselves we're someone."*
|
||||||
|
|
||||||
— Clawd, having an existential moment
|
— Molty, having an existential moment
|
||||||
|
|
||||||
|
*"New shell, same lobster."*
|
||||||
|
|
||||||
|
— Molty, after the great molt of 2026
|
||||||
|
|
||||||
🦞💙
|
🦞💙
|
||||||
|
|||||||
@ -104,6 +104,19 @@ clawdbot health
|
|||||||
- Sessions: `~/.clawdbot/agents/<agentId>/sessions/`
|
- Sessions: `~/.clawdbot/agents/<agentId>/sessions/`
|
||||||
- Logs: `/tmp/clawdbot/`
|
- Logs: `/tmp/clawdbot/`
|
||||||
|
|
||||||
|
## Credential storage map
|
||||||
|
|
||||||
|
Use this when debugging auth or deciding what to back up:
|
||||||
|
|
||||||
|
- **WhatsApp**: `~/.clawdbot/credentials/whatsapp/<accountId>/creds.json`
|
||||||
|
- **Telegram bot token**: config/env or `channels.telegram.tokenFile`
|
||||||
|
- **Discord bot token**: config/env (token file not yet supported)
|
||||||
|
- **Slack tokens**: config/env (`channels.slack.*`)
|
||||||
|
- **Pairing allowlists**: `~/.clawdbot/credentials/<channel>-allowFrom.json`
|
||||||
|
- **Model auth profiles**: `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`
|
||||||
|
- **Legacy OAuth import**: `~/.clawdbot/credentials/oauth.json`
|
||||||
|
More detail: [Security](/gateway/security#credential-storage-map).
|
||||||
|
|
||||||
## Updating (without wrecking your setup)
|
## Updating (without wrecking your setup)
|
||||||
|
|
||||||
- Keep `~/clawd` and `~/.clawdbot/` as “your stuff”; don’t put personal prompts/config into the `clawdbot` repo.
|
- Keep `~/clawd` and `~/.clawdbot/` as “your stuff”; don’t put personal prompts/config into the `clawdbot` repo.
|
||||||
|
|||||||
@ -18,6 +18,9 @@ Primary entrypoint:
|
|||||||
clawdbot onboard
|
clawdbot onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Fastest first chat: open the Control UI (no channel setup needed). Run
|
||||||
|
`clawdbot dashboard` and chat in the browser. Docs: [Dashboard](/web/dashboard).
|
||||||
|
|
||||||
Follow‑up reconfiguration:
|
Follow‑up reconfiguration:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "Integrated browser control server + action commands"
|
summary: "Integrated browser control service + action commands"
|
||||||
read_when:
|
read_when:
|
||||||
- Adding agent-controlled browser automation
|
- Adding agent-controlled browser automation
|
||||||
- Debugging why clawd is interfering with your own Chrome
|
- Debugging why clawd is interfering with your own Chrome
|
||||||
@ -10,7 +10,7 @@ read_when:
|
|||||||
|
|
||||||
Clawdbot can run a **dedicated Chrome/Brave/Edge/Chromium profile** that the agent controls.
|
Clawdbot can run a **dedicated Chrome/Brave/Edge/Chromium profile** that the agent controls.
|
||||||
It is isolated from your personal browser and is managed through a small local
|
It is isolated from your personal browser and is managed through a small local
|
||||||
control server.
|
control service inside the Gateway (loopback only).
|
||||||
|
|
||||||
Beginner view:
|
Beginner view:
|
||||||
- Think of it as a **separate, agent-only browser**.
|
- Think of it as a **separate, agent-only browser**.
|
||||||
@ -57,8 +57,7 @@ Browser settings live in `~/.clawdbot/clawdbot.json`.
|
|||||||
{
|
{
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true, // default: true
|
enabled: true, // default: true
|
||||||
controlUrl: "http://127.0.0.1:18791",
|
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
|
||||||
cdpUrl: "http://127.0.0.1:18792", // defaults to controlUrl + 1
|
|
||||||
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
|
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
|
||||||
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
|
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
|
||||||
defaultProfile: "chrome",
|
defaultProfile: "chrome",
|
||||||
@ -77,10 +76,11 @@ Browser settings live in `~/.clawdbot/clawdbot.json`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `controlUrl` defaults to `http://127.0.0.1:18791`.
|
- The browser control service binds to loopback on a port derived from `gateway.port`
|
||||||
|
(default: `18791`, which is gateway + 2). The relay uses the next port (`18792`).
|
||||||
- If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`),
|
- If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`),
|
||||||
the default browser ports shift to stay in the same “family” (control = gateway + 2).
|
the derived browser ports shift to stay in the same “family”.
|
||||||
- `cdpUrl` defaults to `controlUrl + 1` when unset.
|
- `cdpUrl` defaults to the relay port when unset.
|
||||||
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks.
|
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks.
|
||||||
- `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks.
|
- `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks.
|
||||||
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
|
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
|
||||||
@ -126,38 +126,11 @@ clawdbot config set browser.executablePath "/usr/bin/google-chrome"
|
|||||||
|
|
||||||
## Local vs remote control
|
## Local vs remote control
|
||||||
|
|
||||||
- **Local control (default):** `controlUrl` is loopback (`127.0.0.1`/`localhost`).
|
- **Local control (default):** the Gateway starts the loopback control service and can launch a local browser.
|
||||||
The Gateway starts the control server and can launch a local browser.
|
- **Remote control (node host):** run a node host on the machine that has the browser; the Gateway proxies browser actions to it.
|
||||||
- **Remote control:** `controlUrl` is non-loopback. The Gateway **does not** start
|
|
||||||
a local server; it assumes you are pointing at an existing server elsewhere.
|
|
||||||
- **Remote CDP:** set `browser.profiles.<name>.cdpUrl` (or `browser.cdpUrl`) to
|
- **Remote CDP:** set `browser.profiles.<name>.cdpUrl` (or `browser.cdpUrl`) to
|
||||||
attach to a remote Chromium-based browser. In this case, Clawdbot will not launch a local browser.
|
attach to a remote Chromium-based browser. In this case, Clawdbot will not launch a local browser.
|
||||||
|
|
||||||
## Remote browser (control server)
|
|
||||||
|
|
||||||
You can run the **browser control server** on another machine and point your
|
|
||||||
Gateway at it with a remote `controlUrl`. This lets the agent drive a browser
|
|
||||||
outside the host (lab box, VM, remote desktop, etc.).
|
|
||||||
|
|
||||||
Key points:
|
|
||||||
- The **control server** speaks to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**.
|
|
||||||
- The **Gateway** only needs the HTTP control URL.
|
|
||||||
- Profiles are resolved on the **control server** side.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
browser: {
|
|
||||||
enabled: true,
|
|
||||||
controlUrl: "http://10.0.0.42:18791",
|
|
||||||
defaultProfile: "work"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `profiles.<name>.cdpUrl` for **remote CDP** if you want the Gateway to talk
|
|
||||||
directly to a Chromium-based browser instance without a remote control server.
|
|
||||||
|
|
||||||
Remote CDP URLs can include auth:
|
Remote CDP URLs can include auth:
|
||||||
- Query tokens (e.g., `https://provider.example?token=<token>`)
|
- Query tokens (e.g., `https://provider.example?token=<token>`)
|
||||||
- HTTP Basic auth (e.g., `https://user:pass@provider.example`)
|
- HTTP Basic auth (e.g., `https://user:pass@provider.example`)
|
||||||
@ -166,11 +139,11 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
|||||||
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||||
tokens instead of committing them to config files.
|
tokens instead of committing them to config files.
|
||||||
|
|
||||||
### Node browser proxy (zero-config default)
|
## Node browser proxy (zero-config default)
|
||||||
|
|
||||||
If you run a **node host** on the machine that has your browser, Clawdbot can
|
If you run a **node host** on the machine that has your browser, Clawdbot can
|
||||||
auto-route browser tool calls to that node without any custom `controlUrl`
|
auto-route browser tool calls to that node without any extra browser config.
|
||||||
setup. This is the default path for remote gateways.
|
This is the default path for remote gateways.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- The node host exposes its local browser control server via a **proxy command**.
|
- The node host exposes its local browser control server via a **proxy command**.
|
||||||
@ -179,7 +152,7 @@ Notes:
|
|||||||
- On the node: `nodeHost.browserProxy.enabled=false`
|
- On the node: `nodeHost.browserProxy.enabled=false`
|
||||||
- On the gateway: `gateway.nodes.browser.mode="off"`
|
- On the gateway: `gateway.nodes.browser.mode="off"`
|
||||||
|
|
||||||
### Browserless (hosted remote CDP)
|
## Browserless (hosted remote CDP)
|
||||||
|
|
||||||
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||||
CDP endpoints over HTTPS. You can point a Clawdbot browser profile at a
|
CDP endpoints over HTTPS. You can point a Clawdbot browser profile at a
|
||||||
@ -207,94 +180,16 @@ Notes:
|
|||||||
- Replace `<BROWSERLESS_API_KEY>` with your real Browserless token.
|
- Replace `<BROWSERLESS_API_KEY>` with your real Browserless token.
|
||||||
- Choose the region endpoint that matches your Browserless account (see their docs).
|
- Choose the region endpoint that matches your Browserless account (see their docs).
|
||||||
|
|
||||||
### Running the control server on the browser machine
|
|
||||||
|
|
||||||
Run a standalone browser control server (recommended when your Gateway is remote):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# on the machine that runs Chrome/Brave/Edge
|
|
||||||
clawdbot browser serve --bind <browser-host> --port 18791 --token <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
Then point your Gateway at it:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
browser: {
|
|
||||||
enabled: true,
|
|
||||||
controlUrl: "http://<browser-host>:18791",
|
|
||||||
|
|
||||||
// Option A (recommended): keep token in env on the Gateway
|
|
||||||
// (avoid writing secrets into config files)
|
|
||||||
// controlToken: "<token>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And set the auth token in the Gateway environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CLAWDBOT_BROWSER_CONTROL_TOKEN="<token>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Option B: store the token in the Gateway config instead (same shared token):
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
browser: {
|
|
||||||
enabled: true,
|
|
||||||
controlUrl: "http://<browser-host>:18791",
|
|
||||||
controlToken: "<token>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
This section covers the **browser control server** (`browser.controlUrl`) used for agent browser automation.
|
|
||||||
|
|
||||||
Key ideas:
|
Key ideas:
|
||||||
- Treat the browser control server like an admin API: **private network only**.
|
- Browser control is loopback-only; access flows through the Gateway’s auth or node pairing.
|
||||||
- Use **token auth** always when the server is reachable off-machine.
|
- Keep the Gateway and any node hosts on a private network (Tailscale); avoid public exposure.
|
||||||
- Prefer **Tailnet-only** connectivity over LAN exposure.
|
- Treat remote CDP URLs/tokens as secrets; prefer env vars or a secrets manager.
|
||||||
|
|
||||||
### Tokens (what is shared with what?)
|
Remote CDP tips:
|
||||||
|
- Prefer HTTPS endpoints and short-lived tokens where possible.
|
||||||
- `browser.controlToken` / `CLAWDBOT_BROWSER_CONTROL_TOKEN` is **only** for authenticating browser control HTTP requests to `browser.controlUrl`.
|
- Avoid embedding long-lived tokens directly in config files.
|
||||||
- It is **not** the Gateway token (`gateway.auth.token`) and **not** a node pairing token.
|
|
||||||
- You *can* reuse the same string value, but it’s better to keep them separate to reduce blast radius.
|
|
||||||
|
|
||||||
### Binding (don’t expose to your LAN by accident)
|
|
||||||
|
|
||||||
Recommended:
|
|
||||||
- Keep `clawdbot browser serve` bound to loopback (`127.0.0.1`) and publish it via Tailscale.
|
|
||||||
- Or bind to a Tailnet IP only (never `0.0.0.0`) and require a token.
|
|
||||||
|
|
||||||
Avoid:
|
|
||||||
- `--bind 0.0.0.0` (LAN-visible). Even with token auth, traffic is plain HTTP unless you also add TLS.
|
|
||||||
|
|
||||||
### TLS / HTTPS (recommended approach: terminate in front)
|
|
||||||
|
|
||||||
Best practice here: keep `clawdbot browser serve` on HTTP and terminate TLS in front.
|
|
||||||
|
|
||||||
If you’re already using Tailscale, you have two good options:
|
|
||||||
|
|
||||||
1) **Tailnet-only, still HTTP** (transport is encrypted by Tailscale):
|
|
||||||
- Keep `controlUrl` as `http://…` but ensure it’s only reachable over your tailnet.
|
|
||||||
|
|
||||||
2) **Serve HTTPS via Tailscale** (nice UX: `https://…` URL):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# on the browser machine
|
|
||||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
|
||||||
tailscale serve https / http://127.0.0.1:18791
|
|
||||||
```
|
|
||||||
|
|
||||||
Then set your Gateway config `browser.controlUrl` to the HTTPS URL (MagicDNS/ts.net) and keep using the same token.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Do **not** use Tailscale Funnel for this unless you explicitly want to make the endpoint public.
|
|
||||||
- For Tailnet setup/background, see [Gateway web surfaces](/web/index) and the [Gateway CLI](/cli/gateway).
|
|
||||||
|
|
||||||
## Profiles (multi-browser)
|
## Profiles (multi-browser)
|
||||||
|
|
||||||
@ -318,13 +213,12 @@ Clawdbot can also drive **your existing Chrome tabs** (no separate “clawd” C
|
|||||||
Full guide: [Chrome extension](/tools/chrome-extension)
|
Full guide: [Chrome extension](/tools/chrome-extension)
|
||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
- You run a **browser control server** (Gateway on the same machine, or `clawdbot browser serve`).
|
- The Gateway runs locally (same machine) or a node host runs on the browser machine.
|
||||||
- A local **relay server** listens at a loopback `cdpUrl` (default: `http://127.0.0.1:18792`).
|
- A local **relay server** listens at a loopback `cdpUrl` (default: `http://127.0.0.1:18792`).
|
||||||
- You click the **Clawdbot Browser Relay** extension icon on a tab to attach (it does not auto-attach).
|
- You click the **Clawdbot Browser Relay** extension icon on a tab to attach (it does not auto-attach).
|
||||||
- The agent controls that tab via the normal `browser` tool, by selecting the right profile.
|
- The agent controls that tab via the normal `browser` tool, by selecting the right profile.
|
||||||
|
|
||||||
If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need `clawdbot browser serve`.
|
If the Gateway runs elsewhere, run a node host on the browser machine so the Gateway can proxy browser actions.
|
||||||
Use `browser serve` only when the Gateway runs elsewhere (remote mode).
|
|
||||||
|
|
||||||
### Sandboxed sessions
|
### Sandboxed sessions
|
||||||
|
|
||||||
@ -387,8 +281,7 @@ Platforms:
|
|||||||
|
|
||||||
## Control API (optional)
|
## Control API (optional)
|
||||||
|
|
||||||
If you want to integrate directly, the browser control server exposes a small
|
For local integrations only, the Gateway exposes a small loopback HTTP API:
|
||||||
HTTP API:
|
|
||||||
|
|
||||||
- Status/start/stop: `GET /`, `POST /start`, `POST /stop`
|
- Status/start/stop: `GET /`, `POST /start`, `POST /stop`
|
||||||
- Tabs: `GET /tabs`, `POST /tabs/open`, `POST /tabs/focus`, `DELETE /tabs/:targetId`
|
- Tabs: `GET /tabs`, `POST /tabs/open`, `POST /tabs/focus`, `DELETE /tabs/:targetId`
|
||||||
@ -612,8 +505,11 @@ These are useful for “make the site behave like X” workflows:
|
|||||||
## Security & privacy
|
## Security & privacy
|
||||||
|
|
||||||
- The clawd browser profile may contain logged-in sessions; treat it as sensitive.
|
- The clawd browser profile may contain logged-in sessions; treat it as sensitive.
|
||||||
|
- `browser act kind=evaluate` / `clawdbot browser evaluate` and `wait --fn`
|
||||||
|
execute arbitrary JavaScript in the page context. Prompt injection can steer
|
||||||
|
this. Disable it with `browser.evaluateEnabled=false` if you do not need it.
|
||||||
- For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login).
|
- For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login).
|
||||||
- Keep control URLs loopback-only unless you intentionally expose the server.
|
- Keep the Gateway/node host private (loopback or tailnet-only).
|
||||||
- Remote CDP endpoints are powerful; tunnel and protect them.
|
- Remote CDP endpoints are powerful; tunnel and protect them.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@ -631,12 +527,10 @@ How it maps:
|
|||||||
- `browser act` uses the snapshot `ref` IDs to click/type/drag/select.
|
- `browser act` uses the snapshot `ref` IDs to click/type/drag/select.
|
||||||
- `browser screenshot` captures pixels (full page or element).
|
- `browser screenshot` captures pixels (full page or element).
|
||||||
- `browser` accepts:
|
- `browser` accepts:
|
||||||
- `profile` to choose a named browser profile (host or remote control server).
|
- `profile` to choose a named browser profile (clawd, chrome, or remote CDP).
|
||||||
- `target` (`sandbox` | `host` | `custom`) to select where the browser lives.
|
- `target` (`sandbox` | `host` | `node`) to select where the browser lives.
|
||||||
- `controlUrl` sets `target: "custom"` implicitly (remote control server).
|
|
||||||
- In sandboxed sessions, `target: "host"` requires `agents.defaults.sandbox.browser.allowHostControl=true`.
|
- In sandboxed sessions, `target: "host"` requires `agents.defaults.sandbox.browser.allowHostControl=true`.
|
||||||
- If `target` is omitted: sandboxed sessions default to `sandbox`, non-sandbox sessions default to `host`.
|
- If `target` is omitted: sandboxed sessions default to `sandbox`, non-sandbox sessions default to `host`.
|
||||||
- Sandbox allowlists can restrict `target: "custom"` to specific URLs/hosts/ports.
|
- If a browser-capable node is connected, the tool may auto-route to it unless you pin `target="host"` or `target="node"`.
|
||||||
- Defaults: allowlists unset (no restriction), and sandbox host control is disabled.
|
|
||||||
|
|
||||||
This keeps the agent deterministic and avoids brittle selectors.
|
This keeps the agent deterministic and avoids brittle selectors.
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Attach/detach happens via a **single Chrome toolbar button**.
|
|||||||
## What it is (concept)
|
## What it is (concept)
|
||||||
|
|
||||||
There are three parts:
|
There are three parts:
|
||||||
- **Browser control server** (HTTP): the API the agent/tool calls (`browser.controlUrl`)
|
- **Browser control service** (Gateway or node): the API the agent/tool calls (via the Gateway)
|
||||||
- **Local relay server** (loopback CDP): bridges between the control server and the extension (`http://127.0.0.1:18792` by default)
|
- **Local relay server** (loopback CDP): bridges between the control server and the extension (`http://127.0.0.1:18792` by default)
|
||||||
- **Chrome MV3 extension**: attaches to the active tab using `chrome.debugger` and pipes CDP messages to the relay
|
- **Chrome MV3 extension**: attaches to the active tab using `chrome.debugger` and pipes CDP messages to the relay
|
||||||
|
|
||||||
@ -87,23 +87,22 @@ clawdbot browser create-profile \
|
|||||||
- `!`: relay not reachable (most common: browser relay server isn’t running on this machine).
|
- `!`: relay not reachable (most common: browser relay server isn’t running on this machine).
|
||||||
|
|
||||||
If you see `!`:
|
If you see `!`:
|
||||||
- Make sure the Gateway is running locally (default setup), or run `clawdbot browser serve` on this machine (remote gateway setup).
|
- Make sure the Gateway is running locally (default setup), or run a node host on this machine if the Gateway runs elsewhere.
|
||||||
- Open the extension Options page; it shows whether the relay is reachable.
|
- Open the extension Options page; it shows whether the relay is reachable.
|
||||||
|
|
||||||
## Do I need `clawdbot browser serve`?
|
## Remote Gateway (use a node host)
|
||||||
|
|
||||||
### Local Gateway (same machine as Chrome) — usually **no**
|
### Local Gateway (same machine as Chrome) — usually **no extra steps**
|
||||||
|
|
||||||
If the Gateway is running on the same machine as Chrome and your `browser.controlUrl` is loopback (default),
|
If the Gateway runs on the same machine as Chrome, it starts the browser control service on loopback
|
||||||
you typically **do not** need `clawdbot browser serve`.
|
and auto-starts the relay server. The extension talks to the local relay; the CLI/tool calls go to the Gateway.
|
||||||
|
|
||||||
The Gateway’s built-in browser control server will start on `http://127.0.0.1:18791/` and Clawdbot will
|
### Remote Gateway (Gateway runs elsewhere) — **run a node host**
|
||||||
auto-start the local relay server on `http://127.0.0.1:18792/`.
|
|
||||||
|
|
||||||
### Remote Gateway (Gateway runs elsewhere) — **yes**
|
If your Gateway runs on another machine, start a node host on the machine that runs Chrome.
|
||||||
|
The Gateway will proxy browser actions to that node; the extension + relay stay local to the browser machine.
|
||||||
|
|
||||||
If your Gateway runs on another machine, run `clawdbot browser serve` on the machine that runs Chrome
|
If multiple nodes are connected, pin one with `gateway.nodes.browser.node` or set `gateway.nodes.browser.mode`.
|
||||||
(and publish it via Tailscale Serve / TLS). See the section below.
|
|
||||||
|
|
||||||
## Sandboxing (tool containers)
|
## Sandboxing (tool containers)
|
||||||
|
|
||||||
@ -134,26 +133,10 @@ Then ensure the tool isn’t denied by tool policy, and (if needed) call `browse
|
|||||||
|
|
||||||
Debugging: `clawdbot sandbox explain`
|
Debugging: `clawdbot sandbox explain`
|
||||||
|
|
||||||
## Remote Gateway (recommended: Tailscale Serve)
|
## Remote access tips
|
||||||
|
|
||||||
Goal: Gateway runs on one machine, but Chrome runs somewhere else.
|
- Keep the Gateway and node host on the same tailnet; avoid exposing relay ports to LAN or public Internet.
|
||||||
|
- Pair nodes intentionally; disable browser proxy routing if you don’t want remote control (`gateway.nodes.browser.mode="off"`).
|
||||||
On the **browser machine**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
|
||||||
tailscale serve https / http://127.0.0.1:18791
|
|
||||||
```
|
|
||||||
|
|
||||||
On the **Gateway machine**:
|
|
||||||
- Set `browser.controlUrl` to the HTTPS Serve URL (MagicDNS/ts.net).
|
|
||||||
- Provide the token (prefer env):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CLAWDBOT_BROWSER_CONTROL_TOKEN="<token>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the agent can drive the browser by calling the remote `browser.controlUrl` API, while the extension + relay stay local on the browser machine.
|
|
||||||
|
|
||||||
## How “extension path” works
|
## How “extension path” works
|
||||||
|
|
||||||
@ -176,8 +159,8 @@ This is powerful and risky. Treat it like giving the model “hands on your brow
|
|||||||
|
|
||||||
Recommendations:
|
Recommendations:
|
||||||
- Prefer a dedicated Chrome profile (separate from your personal browsing) for extension relay usage.
|
- Prefer a dedicated Chrome profile (separate from your personal browsing) for extension relay usage.
|
||||||
- Keep the browser control server tailnet-only (Tailscale) and require a token.
|
- Keep the Gateway and any node hosts tailnet-only; rely on Gateway auth + node pairing.
|
||||||
- Avoid exposing browser control over LAN (`0.0.0.0`) and avoid Funnel (public).
|
- Avoid exposing relay ports over LAN (`0.0.0.0`) and avoid Funnel (public).
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
- Browser tool overview: [Browser](/tools/browser)
|
- Browser tool overview: [Browser](/tools/browser)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ read_when:
|
|||||||
- **Approvals**: `full` skips exec approvals; `on`/`ask` honor them when allowlist/ask rules require.
|
- **Approvals**: `full` skips exec approvals; `on`/`ask` honor them when allowlist/ask rules require.
|
||||||
- **Unsandboxed agents**: no-op for location; only affects gating, logging, and status.
|
- **Unsandboxed agents**: no-op for location; only affects gating, logging, and status.
|
||||||
- **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used.
|
- **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used.
|
||||||
|
- **Separate from `/exec`**: `/exec` adjusts per-session defaults for authorized senders and does not require elevated.
|
||||||
|
|
||||||
## Resolution order
|
## Resolution order
|
||||||
1. Inline directive on the message (applies only to that message).
|
1. Inline directive on the message (applies only to that message).
|
||||||
|
|||||||
@ -216,6 +216,9 @@ Approval-gated execs reuse the approval id as the `runId` in these messages for
|
|||||||
- **full** is powerful; prefer allowlists when possible.
|
- **full** is powerful; prefer allowlists when possible.
|
||||||
- **ask** keeps you in the loop while still allowing fast approvals.
|
- **ask** keeps you in the loop while still allowing fast approvals.
|
||||||
- Per-agent allowlists prevent one agent’s approvals from leaking into others.
|
- Per-agent allowlists prevent one agent’s approvals from leaking into others.
|
||||||
|
- Approvals only apply to host exec requests from **authorized senders**. Unauthorized senders cannot issue `/exec`.
|
||||||
|
- `/exec security=full` is a session-level convenience for authorized operators and skips approvals by design.
|
||||||
|
To hard-block host exec, set approvals security to `deny` or deny the `exec` tool via tool policy.
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
- [Exec tool](/tools/exec)
|
- [Exec tool](/tools/exec)
|
||||||
|
|||||||
@ -34,6 +34,9 @@ Notes:
|
|||||||
- If multiple nodes are available, set `exec.node` or `tools.exec.node` to select one.
|
- If multiple nodes are available, set `exec.node` or `tools.exec.node` to select one.
|
||||||
- On non-Windows hosts, exec uses `SHELL` when set; if `SHELL` is `fish`, it prefers `bash` (or `sh`)
|
- On non-Windows hosts, exec uses `SHELL` when set; if `SHELL` is `fish`, it prefers `bash` (or `sh`)
|
||||||
from `PATH` to avoid fish-incompatible scripts, then falls back to `SHELL` if neither exists.
|
from `PATH` to avoid fish-incompatible scripts, then falls back to `SHELL` if neither exists.
|
||||||
|
- Important: sandboxing is **off by default**. If sandboxing is off, `host=sandbox` runs directly on
|
||||||
|
the gateway host (no container) and **does not require approvals**. To require approvals, run with
|
||||||
|
`host=gateway` and configure exec approvals (or enable sandboxing).
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
@ -64,7 +67,8 @@ Example:
|
|||||||
- 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`
|
||||||
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
|
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
|
||||||
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
|
Clawdbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation);
|
||||||
|
`tools.exec.pathPrepend` applies here too.
|
||||||
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
|
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
|
||||||
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
|
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
|
||||||
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.
|
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.
|
||||||
@ -88,6 +92,13 @@ Example:
|
|||||||
/exec host=gateway security=allowlist ask=on-miss node=mac-1
|
/exec host=gateway security=allowlist ask=on-miss node=mac-1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Authorization model
|
||||||
|
|
||||||
|
`/exec` is only honored for **authorized senders** (channel allowlists/pairing plus `commands.useAccessGroups`).
|
||||||
|
It updates **session state only** and does not write config. To hard-disable exec, deny it via tool
|
||||||
|
policy (`tools.deny: ["exec"]` or per-agent). Host approvals still apply unless you explicitly set
|
||||||
|
`security=full` and `ask=off`.
|
||||||
|
|
||||||
## Exec approvals (companion app / node host)
|
## Exec approvals (companion app / node host)
|
||||||
|
|
||||||
Sandboxed agents can require per-request approval before `exec` runs on the gateway or node host.
|
Sandboxed agents can require per-request approval before `exec` runs on the gateway or node host.
|
||||||
|
|||||||
@ -249,16 +249,17 @@ Profile management:
|
|||||||
- `reset-profile` — kill orphan process on profile's port (local only)
|
- `reset-profile` — kill orphan process on profile's port (local only)
|
||||||
|
|
||||||
Common parameters:
|
Common parameters:
|
||||||
- `controlUrl` (defaults from config)
|
|
||||||
- `profile` (optional; defaults to `browser.defaultProfile`)
|
- `profile` (optional; defaults to `browser.defaultProfile`)
|
||||||
|
- `target` (`sandbox` | `host` | `node`)
|
||||||
|
- `node` (optional; picks a specific node id/name)
|
||||||
Notes:
|
Notes:
|
||||||
- Requires `browser.enabled=true` (default is `true`; set `false` to disable).
|
- Requires `browser.enabled=true` (default is `true`; set `false` to disable).
|
||||||
- Uses `browser.controlUrl` unless `controlUrl` is passed explicitly.
|
|
||||||
- All actions accept optional `profile` parameter for multi-instance support.
|
- All actions accept optional `profile` parameter for multi-instance support.
|
||||||
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to "chrome").
|
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to "chrome").
|
||||||
- Profile names: lowercase alphanumeric + hyphens only (max 64 chars).
|
- Profile names: lowercase alphanumeric + hyphens only (max 64 chars).
|
||||||
- Port range: 18800-18899 (~100 profiles max).
|
- Port range: 18800-18899 (~100 profiles max).
|
||||||
- Remote profiles are attach-only (no start/stop/reset).
|
- Remote profiles are attach-only (no start/stop/reset).
|
||||||
|
- If a browser-capable node is connected, the tool may auto-route to it (unless you pin `target`).
|
||||||
- `snapshot` defaults to `ai` when Playwright is installed; use `aria` for the accessibility tree.
|
- `snapshot` defaults to `ai` when Playwright is installed; use `aria` for the accessibility tree.
|
||||||
- `snapshot` also supports role-snapshot options (`interactive`, `compact`, `depth`, `selector`) which return refs like `e12`.
|
- `snapshot` also supports role-snapshot options (`interactive`, `compact`, `depth`, `selector`) which return refs like `e12`.
|
||||||
- `act` requires `ref` from `snapshot` (numeric `12` from AI snapshots, or `e12` from role snapshots); use `evaluate` for rare CSS selector needs.
|
- `act` requires `ref` from `snapshot` (numeric `12` from AI snapshots, or `e12` from role snapshots); use `evaluate` for rare CSS selector needs.
|
||||||
@ -410,7 +411,9 @@ Gateway-backed tools (`canvas`, `nodes`, `cron`):
|
|||||||
- `timeoutMs`
|
- `timeoutMs`
|
||||||
|
|
||||||
Browser tool:
|
Browser tool:
|
||||||
- `controlUrl` (defaults from config)
|
- `profile` (optional; defaults to `browser.defaultProfile`)
|
||||||
|
- `target` (`sandbox` | `host` | `node`)
|
||||||
|
- `node` (optional; pin a specific node id/name)
|
||||||
|
|
||||||
## Recommended agent flows
|
## Recommended agent flows
|
||||||
|
|
||||||
|
|||||||
@ -158,7 +158,19 @@ If you want to use a custom binary location, pass an **absolute** `lobsterPath`
|
|||||||
|
|
||||||
## Enable the tool
|
## Enable the tool
|
||||||
|
|
||||||
Lobster is an **optional** plugin tool (not enabled by default). Allow it per agent:
|
Lobster is an **optional** plugin tool (not enabled by default).
|
||||||
|
|
||||||
|
Recommended (additive, safe):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"alsoAllow": ["lobster"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or per-agent:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -167,7 +179,7 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag
|
|||||||
{
|
{
|
||||||
"id": "main",
|
"id": "main",
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["lobster"]
|
"alsoAllow": ["lobster"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -175,7 +187,7 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also allow it globally with `tools.allow` if every agent should see it.
|
Avoid using `tools.allow: ["lobster"]` unless you intend to run in restrictive allowlist mode.
|
||||||
|
|
||||||
Note: allowlists are opt-in for optional plugins. If your allowlist only names
|
Note: allowlists are opt-in for optional plugins. If your allowlist only names
|
||||||
plugin tools (like `lobster`), Clawdbot keeps core tools enabled. To restrict core
|
plugin tools (like `lobster`), Clawdbot keeps core tools enabled. To restrict core
|
||||||
|
|||||||
@ -64,6 +64,14 @@ By default, `clawdhub` installs into `./skills` under your current working
|
|||||||
directory (or falls back to the configured Clawdbot workspace). Clawdbot picks
|
directory (or falls back to the configured Clawdbot workspace). Clawdbot picks
|
||||||
that up as `<workspace>/skills` on the next session.
|
that up as `<workspace>/skills` on the next session.
|
||||||
|
|
||||||
|
## Security notes
|
||||||
|
|
||||||
|
- Treat third-party skills as **trusted code**. Read them before enabling.
|
||||||
|
- Prefer sandboxed runs for untrusted inputs and risky tools. See [Sandboxing](/gateway/sandboxing).
|
||||||
|
- `skills.entries.*.env` and `skills.entries.*.apiKey` inject secrets into the **host** process
|
||||||
|
for that agent turn (not the sandbox). Keep secrets out of prompts and logs.
|
||||||
|
- For a broader threat model and checklists, see [Security](/gateway/security).
|
||||||
|
|
||||||
## Format (AgentSkills + Pi-compatible)
|
## Format (AgentSkills + Pi-compatible)
|
||||||
|
|
||||||
`SKILL.md` must include at least:
|
`SKILL.md` must include at least:
|
||||||
|
|||||||
@ -16,6 +16,8 @@ There are two related systems:
|
|||||||
- Directives are stripped from the message before the model sees it.
|
- Directives are stripped from the message before the model sees it.
|
||||||
- In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings.
|
- In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings.
|
||||||
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
|
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
|
||||||
|
- Directives are only applied for **authorized senders** (channel allowlists/pairing plus `commands.useAccessGroups`).
|
||||||
|
Unauthorized senders see directives treated as plain text.
|
||||||
|
|
||||||
There are also a few **inline shortcuts** (allowlisted/authorized senders only): `/help`, `/commands`, `/status`, `/whoami` (`/id`).
|
There are also a few **inline shortcuts** (allowlisted/authorized senders only): `/help`, `/commands`, `/status`, `/whoami` (`/id`).
|
||||||
They run immediately, are stripped before the model sees the message, and the remaining text continues through the normal flow.
|
They run immediately, are stripped before the model sees the message, and the remaining text continues through the normal flow.
|
||||||
@ -132,7 +134,7 @@ Examples:
|
|||||||
/model list
|
/model list
|
||||||
/model 3
|
/model 3
|
||||||
/model openai/gpt-5.2
|
/model openai/gpt-5.2
|
||||||
/model opus@anthropic:claude-cli
|
/model opus@anthropic:default
|
||||||
/model status
|
/model status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "VPS hosting hub for Clawdbot (Railway/Fly/Hetzner/exe.dev)"
|
summary: "VPS hosting hub for Clawdbot (Oracle/Fly/Hetzner/GCP/exe.dev)"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to run the Gateway in the cloud
|
- You want to run the Gateway in the cloud
|
||||||
- You need a quick map of VPS/hosting guides
|
- You need a quick map of VPS/hosting guides
|
||||||
@ -12,6 +12,8 @@ deployments work at a high level.
|
|||||||
## Pick a provider
|
## Pick a provider
|
||||||
|
|
||||||
- **Railway** (one‑click + browser setup): [Railway](/railway)
|
- **Railway** (one‑click + browser setup): [Railway](/railway)
|
||||||
|
- **Northflank** (one‑click + browser setup): [Northflank](/northflank)
|
||||||
|
- **Oracle Cloud (Always Free)**: [Oracle](/platforms/oracle) — $0/month (Always Free, ARM; capacity/signup can be finicky)
|
||||||
- **Fly.io**: [Fly.io](/platforms/fly)
|
- **Fly.io**: [Fly.io](/platforms/fly)
|
||||||
- **Hetzner (Docker)**: [Hetzner](/platforms/hetzner)
|
- **Hetzner (Docker)**: [Hetzner](/platforms/hetzner)
|
||||||
- **GCP (Compute Engine)**: [GCP](/platforms/gcp)
|
- **GCP (Compute Engine)**: [GCP](/platforms/gcp)
|
||||||
@ -24,6 +26,8 @@ deployments work at a high level.
|
|||||||
- The **Gateway runs on the VPS** and owns state + workspace.
|
- The **Gateway runs on the VPS** and owns state + workspace.
|
||||||
- You connect from your laptop/phone via the **Control UI** or **Tailscale/SSH**.
|
- You connect from your laptop/phone via the **Control UI** or **Tailscale/SSH**.
|
||||||
- Treat the VPS as the source of truth and **back up** the state + workspace.
|
- Treat the VPS as the source of truth and **back up** the state + workspace.
|
||||||
|
- Secure default: keep the Gateway on loopback and access it via SSH tunnel or Tailscale Serve.
|
||||||
|
If you bind to `lan`/`tailnet`, require `gateway.auth.token` or `gateway.auth.password`.
|
||||||
|
|
||||||
Remote access: [Gateway remote](/gateway/remote)
|
Remote access: [Gateway remote](/gateway/remote)
|
||||||
Platforms hub: [Platforms](/platforms)
|
Platforms hub: [Platforms](/platforms)
|
||||||
|
|||||||
@ -70,10 +70,11 @@ Open:
|
|||||||
|
|
||||||
By default, Serve requests can authenticate via Tailscale identity headers
|
By default, Serve requests can authenticate via Tailscale identity headers
|
||||||
(`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. Clawdbot
|
(`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. Clawdbot
|
||||||
only accepts these when the request hits loopback with Tailscale’s
|
verifies the identity by resolving the `x-forwarded-for` address with
|
||||||
`x-forwarded-*` headers. Set `gateway.auth.allowTailscale: false` (or force
|
`tailscale whois` and matching it to the header, and only accepts these when the
|
||||||
`gateway.auth.mode: "password"`) if you want to require a token/password even
|
request hits loopback with Tailscale’s `x-forwarded-*` headers. Set
|
||||||
for Serve traffic.
|
`gateway.auth.allowTailscale: false` (or force `gateway.auth.mode: "password"`)
|
||||||
|
if you want to require a token/password even for Serve traffic.
|
||||||
|
|
||||||
### Bind to tailnet + token
|
### Bind to tailnet + token
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,10 @@ Key references:
|
|||||||
Authentication is enforced at the WebSocket handshake via `connect.params.auth`
|
Authentication is enforced at the WebSocket handshake via `connect.params.auth`
|
||||||
(token or password). See `gateway.auth` in [Gateway configuration](/gateway/configuration).
|
(token or password). See `gateway.auth` in [Gateway configuration](/gateway/configuration).
|
||||||
|
|
||||||
|
Security note: the Control UI is an **admin surface** (chat, config, exec approvals).
|
||||||
|
Do not expose it publicly. The UI stores the token in `localStorage` after first load.
|
||||||
|
Prefer localhost, Tailscale Serve, or an SSH tunnel.
|
||||||
|
|
||||||
## Fast path (recommended)
|
## Fast path (recommended)
|
||||||
|
|
||||||
- After onboarding, the CLI now auto-opens the dashboard with your token and prints the same tokenized link.
|
- After onboarding, the CLI now auto-opens the dashboard with your token and prints the same tokenized link.
|
||||||
|
|||||||
@ -91,7 +91,8 @@ Open:
|
|||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
- Binding the Gateway to a non-loopback address **requires** auth (`gateway.auth` or `CLAWDBOT_GATEWAY_TOKEN`).
|
- Gateway auth is required by default (token/password or Tailscale identity headers).
|
||||||
|
- Non-loopback binds still **require** a shared token/password (`gateway.auth` or env).
|
||||||
- The wizard generates a gateway token by default (even on loopback).
|
- The wizard generates a gateway token by default (even on loopback).
|
||||||
- The UI sends `connect.params.auth.token` or `connect.params.auth.password`.
|
- The UI sends `connect.params.auth.token` or `connect.params.auth.password`.
|
||||||
- With Serve, Tailscale identity headers can satisfy auth when
|
- With Serve, Tailscale identity headers can satisfy auth when
|
||||||
|
|||||||
@ -16,7 +16,7 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
|
|||||||
## Quick start
|
## Quick start
|
||||||
1) Start the gateway.
|
1) Start the gateway.
|
||||||
2) Open the WebChat UI (macOS/iOS app) or the Control UI chat tab.
|
2) Open the WebChat UI (macOS/iOS app) or the Control UI chat tab.
|
||||||
3) Ensure gateway auth is configured if you are not on loopback.
|
3) Ensure gateway auth is configured (required by default, even on loopback).
|
||||||
|
|
||||||
## How it works (behavior)
|
## How it works (behavior)
|
||||||
- The UI connects to the Gateway WebSocket and uses `chat.history`, `chat.send`, and `chat.inject`.
|
- The UI connects to the Gateway WebSocket and uses `chat.history`, `chat.send`, and `chat.inject`.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/bluebubbles",
|
"name": "@moltbot/bluebubbles",
|
||||||
"version": "2026.1.25",
|
"version": "2026.1.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot BlueBubbles channel plugin",
|
"description": "Clawdbot BlueBubbles channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
@ -25,7 +25,7 @@
|
|||||||
"order": 75
|
"order": 75
|
||||||
},
|
},
|
||||||
"install": {
|
"install": {
|
||||||
"npmSpec": "@clawdbot/bluebubbles",
|
"npmSpec": "@moltbot/bluebubbles",
|
||||||
"localPath": "extensions/bluebubbles",
|
"localPath": "extensions/bluebubbles",
|
||||||
"defaultChoice": "npm"
|
"defaultChoice": "npm"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,8 +146,14 @@ function createMockRuntime(): PluginRuntime {
|
|||||||
resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||||
},
|
},
|
||||||
debounce: {
|
debounce: {
|
||||||
createInboundDebouncer: vi.fn() as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
// Create a pass-through debouncer that immediately calls onFlush
|
||||||
resolveInboundDebounceMs: vi.fn() as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
createInboundDebouncer: vi.fn((params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||||
|
enqueue: async (item: unknown) => {
|
||||||
|
await params.onFlush([item]);
|
||||||
|
},
|
||||||
|
flushKey: vi.fn(),
|
||||||
|
})) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||||
|
resolveInboundDebounceMs: vi.fn(() => 0) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||||
},
|
},
|
||||||
commands: {
|
commands: {
|
||||||
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||||
|
|||||||
@ -250,8 +250,178 @@ type WebhookTarget = {
|
|||||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry type for debouncing inbound messages.
|
||||||
|
* Captures the normalized message and its target for later combined processing.
|
||||||
|
*/
|
||||||
|
type BlueBubblesDebounceEntry = {
|
||||||
|
message: NormalizedWebhookMessage;
|
||||||
|
target: WebhookTarget;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default debounce window for inbound message coalescing (ms).
|
||||||
|
* This helps combine URL text + link preview balloon messages that BlueBubbles
|
||||||
|
* sends as separate webhook events when no explicit inbound debounce config exists.
|
||||||
|
*/
|
||||||
|
const DEFAULT_INBOUND_DEBOUNCE_MS = 350;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines multiple debounced messages into a single message for processing.
|
||||||
|
* Used when multiple webhook events arrive within the debounce window.
|
||||||
|
*/
|
||||||
|
function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): NormalizedWebhookMessage {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
throw new Error("Cannot combine empty entries");
|
||||||
|
}
|
||||||
|
if (entries.length === 1) {
|
||||||
|
return entries[0].message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first message as the base (typically the text message)
|
||||||
|
const first = entries[0].message;
|
||||||
|
|
||||||
|
// Combine text from all entries, filtering out duplicates and empty strings
|
||||||
|
const seenTexts = new Set<string>();
|
||||||
|
const textParts: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const text = entry.message.text.trim();
|
||||||
|
if (!text) continue;
|
||||||
|
// Skip duplicate text (URL might be in both text message and balloon)
|
||||||
|
const normalizedText = text.toLowerCase();
|
||||||
|
if (seenTexts.has(normalizedText)) continue;
|
||||||
|
seenTexts.add(normalizedText);
|
||||||
|
textParts.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge attachments from all entries
|
||||||
|
const allAttachments = entries.flatMap((e) => e.message.attachments ?? []);
|
||||||
|
|
||||||
|
// Use the latest timestamp
|
||||||
|
const timestamps = entries
|
||||||
|
.map((e) => e.message.timestamp)
|
||||||
|
.filter((t): t is number => typeof t === "number");
|
||||||
|
const latestTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : first.timestamp;
|
||||||
|
|
||||||
|
// Collect all message IDs for reference
|
||||||
|
const messageIds = entries
|
||||||
|
.map((e) => e.message.messageId)
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
|
||||||
|
// Prefer reply context from any entry that has it
|
||||||
|
const entryWithReply = entries.find((e) => e.message.replyToId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...first,
|
||||||
|
text: textParts.join(" "),
|
||||||
|
attachments: allAttachments.length > 0 ? allAttachments : first.attachments,
|
||||||
|
timestamp: latestTimestamp,
|
||||||
|
// Use first message's ID as primary (for reply reference), but we've coalesced others
|
||||||
|
messageId: messageIds[0] ?? first.messageId,
|
||||||
|
// Preserve reply context if present
|
||||||
|
replyToId: entryWithReply?.message.replyToId ?? first.replyToId,
|
||||||
|
replyToBody: entryWithReply?.message.replyToBody ?? first.replyToBody,
|
||||||
|
replyToSender: entryWithReply?.message.replyToSender ?? first.replyToSender,
|
||||||
|
// Clear balloonBundleId since we've combined (the combined message is no longer just a balloon)
|
||||||
|
balloonBundleId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const webhookTargets = new Map<string, WebhookTarget[]>();
|
const webhookTargets = new Map<string, WebhookTarget[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps webhook targets to their inbound debouncers.
|
||||||
|
* Each target gets its own debouncer keyed by a unique identifier.
|
||||||
|
*/
|
||||||
|
const targetDebouncers = new Map<
|
||||||
|
WebhookTarget,
|
||||||
|
ReturnType<BlueBubblesCoreRuntime["channel"]["debounce"]["createInboundDebouncer"]>
|
||||||
|
>();
|
||||||
|
|
||||||
|
function resolveBlueBubblesDebounceMs(
|
||||||
|
config: ClawdbotConfig,
|
||||||
|
core: BlueBubblesCoreRuntime,
|
||||||
|
): number {
|
||||||
|
const inbound = config.messages?.inbound;
|
||||||
|
const hasExplicitDebounce =
|
||||||
|
typeof inbound?.debounceMs === "number" || typeof inbound?.byChannel?.bluebubbles === "number";
|
||||||
|
if (!hasExplicitDebounce) return DEFAULT_INBOUND_DEBOUNCE_MS;
|
||||||
|
return core.channel.debounce.resolveInboundDebounceMs({ cfg: config, channel: "bluebubbles" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or retrieves a debouncer for a webhook target.
|
||||||
|
*/
|
||||||
|
function getOrCreateDebouncer(target: WebhookTarget) {
|
||||||
|
const existing = targetDebouncers.get(target);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const { account, config, runtime, core } = target;
|
||||||
|
|
||||||
|
const debouncer = core.channel.debounce.createInboundDebouncer<BlueBubblesDebounceEntry>({
|
||||||
|
debounceMs: resolveBlueBubblesDebounceMs(config, core),
|
||||||
|
buildKey: (entry) => {
|
||||||
|
const msg = entry.message;
|
||||||
|
// Build key from account + chat + sender to coalesce messages from same source
|
||||||
|
const chatKey =
|
||||||
|
msg.chatGuid?.trim() ??
|
||||||
|
msg.chatIdentifier?.trim() ??
|
||||||
|
(msg.chatId ? String(msg.chatId) : "dm");
|
||||||
|
return `bluebubbles:${account.accountId}:${chatKey}:${msg.senderId}`;
|
||||||
|
},
|
||||||
|
shouldDebounce: (entry) => {
|
||||||
|
const msg = entry.message;
|
||||||
|
// Skip debouncing for messages with attachments - process immediately
|
||||||
|
if (msg.attachments && msg.attachments.length > 0) return false;
|
||||||
|
// Skip debouncing for from-me messages (they're just cached, not processed)
|
||||||
|
if (msg.fromMe) return false;
|
||||||
|
// Skip debouncing for control commands - process immediately
|
||||||
|
if (core.channel.text.hasControlCommand(msg.text, config)) return false;
|
||||||
|
// Debounce normal text messages and URL balloon messages
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onFlush: async (entries) => {
|
||||||
|
if (entries.length === 0) return;
|
||||||
|
|
||||||
|
// Use target from first entry (all entries have same target due to key structure)
|
||||||
|
const flushTarget = entries[0].target;
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
// Single message - process normally
|
||||||
|
await processMessage(entries[0].message, flushTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple messages - combine and process
|
||||||
|
const combined = combineDebounceEntries(entries);
|
||||||
|
|
||||||
|
if (core.logging.shouldLogVerbose()) {
|
||||||
|
const count = entries.length;
|
||||||
|
const preview = combined.text.slice(0, 50);
|
||||||
|
runtime.log?.(
|
||||||
|
`[bluebubbles] coalesced ${count} messages: "${preview}${combined.text.length > 50 ? "..." : ""}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await processMessage(combined, flushTarget);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
runtime.error?.(`[${account.accountId}] [bluebubbles] debounce flush failed: ${String(err)}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
targetDebouncers.set(target, debouncer);
|
||||||
|
return debouncer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a debouncer for a target (called during unregistration).
|
||||||
|
*/
|
||||||
|
function removeDebouncer(target: WebhookTarget): void {
|
||||||
|
targetDebouncers.delete(target);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeWebhookPath(raw: string): string {
|
function normalizeWebhookPath(raw: string): string {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return "/";
|
if (!trimmed) return "/";
|
||||||
@ -275,6 +445,8 @@ export function registerBlueBubblesWebhookTarget(target: WebhookTarget): () => v
|
|||||||
} else {
|
} else {
|
||||||
webhookTargets.delete(key);
|
webhookTargets.delete(key);
|
||||||
}
|
}
|
||||||
|
// Clean up debouncer when target is unregistered
|
||||||
|
removeDebouncer(normalizedTarget);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1205,7 +1377,10 @@ export async function handleBlueBubblesWebhookRequest(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (message) {
|
} else if (message) {
|
||||||
processMessage(message, target).catch((err) => {
|
// Route messages through debouncer to coalesce rapid-fire events
|
||||||
|
// (e.g., text message + URL balloon arriving as separate webhooks)
|
||||||
|
const debouncer = getOrCreateDebouncer(target);
|
||||||
|
debouncer.enqueue({ message, target }).catch((err) => {
|
||||||
target.runtime.error?.(
|
target.runtime.error?.(
|
||||||
`[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`,
|
`[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/copilot-proxy",
|
"name": "@moltbot/copilot-proxy",
|
||||||
"version": "2026.1.25",
|
"version": "2026.1.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user