Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ SLACK_ALLOWED_USERS=
# @sensitive=false @type=number
BAUDBOT_EXPERIMENTAL=0

# ── Dev Agent Backend ────────────────────────────────────────────────────────

# Default backend for spawning dev agents.
# Control-agent may override per-task.
# Options: pi, claude-code, codex, auto
# @sensitive=false @type=string
DEV_AGENT_BACKEND=pi

# ── Email Monitor (experimental-only) ───────────────────────────────────────

# AgentMail API key (only used when BAUDBOT_EXPERIMENTAL=1)
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ slack-bridge/.env
# Coverage
coverage/
.c8_output/


.tmp
.state
30 changes: 29 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The agent also uses an SSH key (`~/.ssh/id_ed25519`) for git push. Setup generat
|----------|-------------|---------------|
| `SLACK_BOT_TOKEN` | Slack bot OAuth token (required for direct Socket Mode, optional in broker mode) | Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps). Under **OAuth & Permissions**, add bot scopes: `app_mentions:read`, `chat:write`, `channels:history`, `channels:read`, `reactions:write`, `im:history`, `im:read`, `im:write`. Install the app to your workspace and copy the **Bot User OAuth Token**. |
| `SLACK_APP_TOKEN` | Slack app-level token (required for Socket Mode, optional in broker mode) | In your Slack app settings → **Basic Information** → **App-Level Tokens**, create a token with `connections:write` scope. |
| `SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs | **Optional** — if not set, all workspace members can interact. Find your Slack user ID: click your profile → "..." → "Copy member ID". Example: `U01ABCDEF,U02GHIJKL` |
| `SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs | **Required** — only listed users can interact with the agent. Find your Slack user ID: click your profile → "..." → "Copy member ID". Example: `U01ABCDEF,U02GHIJKL` |

If you're using Slack broker mode (`SLACK_BROKER_*` vars), the runtime uses broker pull delivery and does not require Socket Mode callbacks.

Expand Down Expand Up @@ -68,6 +68,12 @@ Email tooling is disabled by default. To enable it, run setup/install in experim

## Optional Variables

### Dev Agent Backend

| Variable | Description | Default |
|----------|-------------|---------|
| `DEV_AGENT_BACKEND` | Default backend for spawning dev agents (`pi`, `claude-code`, `codex`, `auto`) | `pi` |

### Sentry Integration

| Variable | Description | How to get it |
Expand Down Expand Up @@ -149,6 +155,28 @@ Set during `setup.sh` / `baudbot install` via env vars:
| `GIT_USER_NAME` | Git commit author name | `baudbot-agent` |
| `GIT_USER_EMAIL` | Git commit author email | `baudbot-agent@users.noreply.github.com` |

### Remote CLI (operator-local, not runtime)

These apply only to `baudbot remote ...` when run from your local operator machine. They are not part of agent runtime `.env` and should not be written to `/home/baudbot_agent/.config/.env`.

| Variable | Description | Default |
|----------|-------------|---------|
| `BAUDBOT_REMOTE_DIR` | Local state directory for remote targets/checkpoints/keys | `~/.baudbot/remote` |
| `HETZNER_API_TOKEN` | Hetzner token fallback for `--hetzner-token` | *(empty)* |
| `TAILSCALE_AUTHKEY` | Tailscale auth key fallback for `--tailscale-auth-key` | *(empty)* |
| `REMOTE_BOOTSTRAP_URL` | Bootstrap script URL used by remote install step | `https://raw.githubusercontent.com/modem-dev/baudbot/main/bootstrap.sh` |
| `REMOTE_TAILSCALE_INSTALL_URL` | Tailscale install script URL used by remote workflow | `https://tailscale.com/install.sh` |
| `REMOTE_TAILSCALE_WAIT_ATTEMPTS` | Tailscale readiness polling attempts after `tailscale up` | `40` |
| `REMOTE_TAILSCALE_WAIT_INTERVAL_SEC` | Delay between Tailscale readiness polls | `3` |
| `REMOTE_CHECKPOINT_MAX_RETRIES` | Retries per install checkpoint before interactive escalation | `3` |
| `REMOTE_HETZNER_SERVER_TYPE` | Hetzner default server type for remote install | `cpx11` |
| `REMOTE_HETZNER_IMAGE` | Hetzner default image for remote install | `ubuntu-24.04` |
| `REMOTE_HETZNER_LOCATION` | Hetzner default location for remote install | `ash` |
| `REMOTE_HETZNER_WAIT_TIMEOUT_SEC` | Timeout while waiting for server running state | `600` |
| `REMOTE_HETZNER_WAIT_INTERVAL_SEC` | Poll interval while waiting for server running state | `5` |
| `REMOTE_SSH_REACHABLE_ATTEMPTS` | SSH readiness attempts per checkpoint | `40` |
| `REMOTE_SSH_REACHABLE_INTERVAL_SEC` | Delay between SSH readiness attempts | `3` |

### Heartbeat

| Variable | Description | Default |
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Baudbot is designed as shared engineering infrastructure, not a single-user desk
| **CPU** | 2 vCPU | 4 vCPU |
| **Disk** | 20 GB | 40 GB+ (repos, dependencies, Docker images) |

System package dependencies (installed by `baudbot install`): `git`, `curl`, `tmux`, `iptables`, `docker`, `gh`, `jq`, `sudo`.
System package dependencies (installed by `baudbot install`): `git`, `curl`, `tmux`, `iptables`, `docker`, `gh`, `jq`, `ripgrep`, `sudo`.

## Quick Start

Expand All @@ -61,7 +61,7 @@ curl -fsSL https://raw.githubusercontent.com/modem-dev/baudbot/main/bootstrap.sh
baudbot install
```

`baudbot install` includes a guided config flow: pick an LLM provider, choose Slack integration mode (managed broker vs custom app), then opt into optional integrations (Kernel/Sentry). Email capabilities are disabled by default and only available in experimental mode (`baudbot setup --experimental` / `install.sh --experimental`). If [`gum`](https://github.com/charmbracelet/gum) is installed, prompts use richer TUI widgets; otherwise installer falls back to standard bash prompts.
`baudbot install` includes a guided config flow: pick an LLM provider, choose Slack integration mode (managed broker vs custom app), then opt into optional integrations (Kernel/Sentry). Host setup installs Node.js + pi and also installs Claude Code via the official installer script for `baudbot_agent`, exposing a root-owned `/usr/local/bin/claude` wrapper for sudo-safe invocation. Email capabilities are disabled by default and only available in experimental mode (`baudbot setup --experimental` / `install.sh --experimental`). If [`gum`](https://github.com/charmbracelet/gum) is installed, prompts use richer TUI widgets; otherwise installer falls back to standard bash prompts.

After install:

Expand All @@ -83,6 +83,27 @@ Upgrade later:
sudo baudbot update
```

Remote provisioning/install and repair (operator-run from your local machine):

```bash
# Provision on Hetzner and install Baudbot
baudbot remote install --mode hetzner --target team-bot

# Install on an existing host
baudbot remote install --mode host --target team-bot --host 203.0.113.10 --ssh-user root

# Install + connect host to Tailscale
baudbot remote install --mode host --target team-bot --host 203.0.113.10 --tailscale

# Resume an interrupted run
baudbot remote resume team-bot

# Guided repair for an existing target
baudbot remote repair --target team-bot
```

`baudbot remote` persists checkpoints in `~/.baudbot/remote/targets/*.json`, so interrupted installs can resume from the next incomplete checkpoint.

Install with a specific pi version (optional):

```bash
Expand Down
6 changes: 6 additions & 0 deletions bin/baudbot
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ usage() {
echo " install Bootstrap install from GitHub (download script, then escalate)"
echo " setup One-time system setup (user, deps, firewall, systemd; --experimental enables risky integrations)"
echo " config Interactive secrets and config setup"
echo " remote Remote install/repair workflows (Hetzner or existing host)"
echo " env Manage env vars and backend source (set/get/sync/backend)"
echo " deploy Deploy source + config to agent runtime"
echo " broker Slack broker commands (register workspace linkage)"
Expand Down Expand Up @@ -411,6 +412,11 @@ case "${1:-}" in
exec "$BAUDBOT_ROOT/bin/config.sh" "$@"
;;

remote)
shift
exec "$BAUDBOT_ROOT/bin/remote.sh" "$@"
;;

env)
shift
exec "$BAUDBOT_ROOT/bin/env.sh" "$@"
Expand Down
2 changes: 1 addition & 1 deletion bin/baudbot.service
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Restart=on-failure
RestartSec=10

# Environment
Environment=PATH=/home/baudbot_agent/.varlock/bin:/home/baudbot_agent/opt/node-v22.14.0-linux-x64/bin:/usr/local/bin:/usr/bin:/bin
Environment=PATH=/home/baudbot_agent/.local/bin:/home/baudbot_agent/.varlock/bin:/home/baudbot_agent/opt/node-v22.14.0-linux-x64/bin:/usr/local/bin:/usr/bin:/bin
Environment=HOME=/home/baudbot_agent

# Security hardening
Expand Down
30 changes: 30 additions & 0 deletions bin/baudbot.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,35 @@ EOF
)
}

test_remote_dispatches_to_remote_script() {
(
set -euo pipefail
local tmp out
tmp="$(mktemp -d /tmp/baudbot-cli-test.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

mkdir -p "$tmp/bin/lib"
printf '{"version":"1.2.3"}\n' > "$tmp/package.json"
cat > "$tmp/bin/lib/baudbot-runtime.sh" <<'EOF'
#!/bin/bash
cmd_status() { :; }
cmd_logs() { :; }
cmd_sessions() { :; }
cmd_attach() { :; }
has_systemd() { return 0; }
EOF

cat > "$tmp/bin/remote.sh" <<'EOF'
#!/bin/bash
echo "remote-dispatch-ok:$*"
EOF
chmod +x "$tmp/bin/remote.sh"

out="$(BAUDBOT_ROOT="$tmp" bash "$CLI" remote list)"
[ "$out" = "remote-dispatch-ok:list" ]
)
}

echo "=== baudbot cli tests ==="
echo ""

Expand All @@ -199,6 +228,7 @@ run_test "status dispatches via runtime module" test_status_dispatches_via_runti
run_test "attach requires root" test_attach_requires_root
run_test "broker register requires root" test_broker_register_requires_root
run_test "restart kills bridge tmux then restarts systemd" test_restart_restarts_systemd_and_kills_bridge_tmux
run_test "remote command dispatches to remote.sh" test_remote_dispatches_to_remote_script

echo ""
echo "=== $PASSED/$TOTAL passed, $FAILED failed ==="
Expand Down
18 changes: 16 additions & 2 deletions bin/broker-register.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ test("registerWithBroker sends registration_token when provided", async () => {
});
});

test("runRegistration integration path succeeds against live local HTTP server", async () => {
test("runRegistration integration path succeeds against live local HTTP server", async (t) => {
const brokerPubkey = Buffer.alloc(32, 5).toString("base64");
const brokerSigningPubkey = Buffer.alloc(32, 6).toString("base64");

Expand Down Expand Up @@ -222,7 +222,21 @@ test("runRegistration integration path succeeds against live local HTTP server",
res.end(JSON.stringify({ ok: false, error: "not found" }));
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
} catch (error) {
if (error && typeof error === "object" && "code" in error) {
const code = String(error.code || "");
if (code === "EPERM" || code === "EACCES") {
t.skip("Localhost bind is not permitted in this environment");
return;
}
}
throw error;
}
const address = server.address();
const brokerUrl = `http://127.0.0.1:${address.port}`;

Expand Down
13 changes: 7 additions & 6 deletions bin/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,9 @@ if [ "$SLACK_CHOICE" = "Use baudbot.ai Slack integration (easy)" ]; then
dim " We'll set up broker registration after install via: sudo baudbot broker register"
clear_keys SLACK_BOT_TOKEN SLACK_APP_TOKEN
prompt_secret "SLACK_ALLOWED_USERS" \
"Slack user IDs (comma-separated; optional — allow all if empty)" \
"Slack user IDs (comma-separated; required)" \
"Click your Slack profile → ··· → Copy member ID" \
"" \
"required" \
"U" \
"false"
else
Expand Down Expand Up @@ -470,9 +470,9 @@ else
"xapp-"

prompt_secret "SLACK_ALLOWED_USERS" \
"Slack user IDs (comma-separated; optional — allow all if empty)" \
"Slack user IDs (comma-separated; required)" \
"Click your Slack profile → ··· → Copy member ID" \
"" \
"required" \
"U" \
"false"
fi
Expand Down Expand Up @@ -579,7 +579,8 @@ fi
# ── Validation ───────────────────────────────────────────────────────────────

if [ -z "${ENV_VARS[SLACK_ALLOWED_USERS]:-}" ]; then
warn "SLACK_ALLOWED_USERS not set — all workspace members will be allowed"
echo "❌ SLACK_ALLOWED_USERS is required for Slack access control"
exit 1
fi

# ── Write config ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -674,4 +675,4 @@ else
fi
echo ""
echo -e "Next: ${BOLD}sudo baudbot deploy${RESET} to push config to the agent"
echo ""
echo ""
16 changes: 11 additions & 5 deletions bin/config.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,26 @@ echo ""

# Test 1: Advanced Slack path writes socket-mode keys only
HOME1="$TMPDIR/advanced"
run_config "$HOME1" '1\nsk-ant-test\n2\nxoxb-test\nxapp-test\n\nn\nn\n'
run_config "$HOME1" '1\nsk-ant-test\n2\nxoxb-test\nxapp-test\nU01ADVANCED\nn\nn\n'
ENV1="$HOME1/.baudbot/.env"
expect_file_contains "advanced path writes Anthropic key" "$ENV1" "ANTHROPIC_API_KEY=sk-ant-test"
expect_file_contains "advanced path writes SLACK_BOT_TOKEN" "$ENV1" "SLACK_BOT_TOKEN=xoxb-test"
expect_file_contains "advanced path writes SLACK_APP_TOKEN" "$ENV1" "SLACK_APP_TOKEN=xapp-test"
expect_file_contains "advanced path writes SLACK_ALLOWED_USERS" "$ENV1" "SLACK_ALLOWED_USERS=U01ADVANCED"
expect_file_not_contains "advanced path does not write OPENAI key" "$ENV1" "OPENAI_API_KEY="

# Test 2: Easy Slack path avoids socket-mode keys
HOME2="$TMPDIR/easy"
run_config "$HOME2" '2\nsk-openai-test\n1\n\nn\nn\n'
run_config "$HOME2" '2\nsk-openai-test\n1\nU02EASY\nn\nn\n'
ENV2="$HOME2/.baudbot/.env"
expect_file_contains "easy path writes OpenAI key" "$ENV2" "OPENAI_API_KEY=sk-openai-test"
expect_file_not_contains "easy path omits SLACK_BOT_TOKEN" "$ENV2" "SLACK_BOT_TOKEN="
expect_file_not_contains "easy path omits SLACK_APP_TOKEN" "$ENV2" "SLACK_APP_TOKEN="
expect_file_contains "easy path writes SLACK_ALLOWED_USERS" "$ENV2" "SLACK_ALLOWED_USERS=U02EASY"

# Test 3: Optional integration toggle prompts conditionally
HOME3="$TMPDIR/kernel"
run_config "$HOME3" '3\ngem-key\n2\nxoxb-test\nxapp-test\n\ny\nkernel-key\nn\n'
run_config "$HOME3" '3\ngem-key\n2\nxoxb-test\nxapp-test\nU03KERNEL\ny\nkernel-key\nn\n'
ENV3="$HOME3/.baudbot/.env"
expect_file_contains "kernel enabled writes key" "$ENV3" "KERNEL_API_KEY=kernel-key"
expect_file_not_contains "sentry skipped omits token" "$ENV3" "SENTRY_AUTH_TOKEN="
Expand All @@ -115,19 +117,23 @@ expect_exit_nonzero "fails when selected provider key is missing" "$HOME4" '1\n\
# Test 5: Re-run preserves existing selected LLM key when input is blank
HOME5="$TMPDIR/rerun-keep-llm"
write_existing_env "$HOME5" 'ANTHROPIC_API_KEY=sk-ant-existing\n'
run_config "$HOME5" '1\n\n1\n\nn\nn\n'
run_config "$HOME5" '1\n\n1\nU05KEEP\nn\nn\n'
ENV5="$HOME5/.baudbot/.env"
expect_file_contains "rerun keeps existing Anthropic key" "$ENV5" "ANTHROPIC_API_KEY=sk-ant-existing"

# Test 6: Advanced Slack mode clears stale broker registration keys
HOME6="$TMPDIR/clear-broker"
write_existing_env "$HOME6" 'OPENAI_API_KEY=sk-old\nSLACK_BROKER_URL=https://broker.example.com\nSLACK_BROKER_WORKSPACE_ID=T0123\nSLACK_BROKER_PUBLIC_KEY=abc\n'
run_config "$HOME6" '2\nsk-openai-new\n2\nxoxb-new\nxapp-new\n\nn\nn\n'
run_config "$HOME6" '2\nsk-openai-new\n2\nxoxb-new\nxapp-new\nU06CLEAR\nn\nn\n'
ENV6="$HOME6/.baudbot/.env"
expect_file_not_contains "advanced clears broker URL" "$ENV6" "SLACK_BROKER_URL="
expect_file_not_contains "advanced clears broker workspace" "$ENV6" "SLACK_BROKER_WORKSPACE_ID="
expect_file_contains "advanced retains socket bot token" "$ENV6" "SLACK_BOT_TOKEN=xoxb-new"

# Test 7: SLACK_ALLOWED_USERS is required
HOME7="$TMPDIR/missing-slack-users"
expect_exit_nonzero "fails when Slack user IDs are missing" "$HOME7" '2\nsk-openai\n2\nxoxb-miss\nxapp-miss\n\nn\nn\n'

echo ""
echo "Results: $PASS passed, $FAIL failed"

Expand Down
31 changes: 30 additions & 1 deletion bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ else
fail "jq not found (required for shell JSON parsing)"
fi

if command -v rg &>/dev/null; then
pass "rg is installed ($(command -v rg))"
else
fail "rg not found (install ripgrep)"
fi

if command -v docker &>/dev/null; then
pass "docker is available"
else
Expand All @@ -101,6 +107,29 @@ else
fail "gh cli not found"
fi

check_claude_path() {
local probe_path probe_output current_user
probe_path="$BAUDBOT_HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
current_user="$(id -un 2>/dev/null || true)"

if [ "$IS_ROOT" -eq 1 ] && command -v sudo &>/dev/null; then
probe_output="$(sudo -u "$BAUDBOT_AGENT_USER" env PATH="$probe_path" sh -lc 'command -v claude' 2>/dev/null || true)"
elif [ "$current_user" = "$BAUDBOT_AGENT_USER" ]; then
probe_output="$(env PATH="$probe_path:$PATH" sh -lc 'command -v claude' 2>/dev/null || true)"
else
probe_output="$(env PATH="$probe_path:$PATH" sh -lc 'command -v claude' 2>/dev/null || true)"
fi

printf '%s\n' "$probe_output" | head -n1
}

CLAUDE_PATH="$(check_claude_path)"
if [ -n "$CLAUDE_PATH" ]; then
pass "claude code is installed ($CLAUDE_PATH)"
else
warn "claude code not found for $BAUDBOT_AGENT_USER (run: curl -fsSL https://claude.ai/install.sh | bash)"
fi

# ── Secrets ──────────────────────────────────────────────────────────────────

echo ""
Expand Down Expand Up @@ -233,7 +262,7 @@ if [ -f "$ENV_FILE" ]; then
if grep -q '^SLACK_ALLOWED_USERS=.\+' "$ENV_FILE" 2>/dev/null; then
pass "SLACK_ALLOWED_USERS is set"
else
warn "SLACK_ALLOWED_USERS is not set (all workspace members allowed)"
fail "SLACK_ALLOWED_USERS is not set"
fi
else
if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.config" ]; then
Expand Down
Loading