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
2 changes: 1 addition & 1 deletion .c8rc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"all": false,
"include": [
"slack-bridge/security.mjs",
"broker-gateway/security.mjs",
"bin/scan-extensions.mjs"
],
"exclude": [
Expand Down
4 changes: 4 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ GEMINI_API_KEY=
# @docs(https://opencode.ai)
OPENCODE_ZEN_API_KEY=

# Override auto-detected model (e.g. anthropic/claude-haiku for CI)
# @sensitive=false @type=string
BAUDBOT_MODEL=

# ── Slack ────────────────────────────────────────────────────────────────────

# Slack bot OAuth token (required for direct Socket Mode, optional in broker mode)
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ jobs:
bash bin/ci/droplet.sh run \
"${{ steps.droplet.outputs.DROPLET_IP }}" \
~/.ssh/ci_key \
"${{ matrix.setup_script }}"
"${{ matrix.setup_script }}" \
"CI_ANTHROPIC_API_KEY=${{ secrets.CI_ANTHROPIC_API_KEY }}"

- name: Cleanup
if: always()
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
*.key
*.pem
node_modules/
# Slack bridge
slack-bridge/node_modules/
slack-bridge/.env
# Broker gateway
broker-gateway/node_modules/
broker-gateway/.env
.pi/
# Coverage
coverage/
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Baudbot is hardened infrastructure for running always-on AI agents.
Use this file for **repo-wide** guidance. For directory-specific rules, use the nearest nested `AGENTS.md`:
- [`bin/AGENTS.md`](bin/AGENTS.md)
- [`pi/extensions/AGENTS.md`](pi/extensions/AGENTS.md)
- [`slack-bridge/AGENTS.md`](slack-bridge/AGENTS.md)
- [`broker-gateway/AGENTS.md`](broker-gateway/AGENTS.md)

## How Baudbot works

Expand All @@ -16,7 +16,7 @@ Baudbot is a persistent, team-facing coding agent system. It connects to Slack,
```text
Slack
slack-bridge (broker pull-mode or legacy Socket Mode)
broker-gateway (broker pull-mode or legacy Socket Mode)
control-agent (always-on, manages todo/routing/Slack threads)
├── dev-agent(s) — ephemeral coding workers in isolated worktrees
Expand All @@ -36,7 +36,7 @@ git commits → PRs → CI feedback → thread updates back to Slack
- `dev-agent/` — coding worker persona
- `sentry-agent/` — incident triage persona
- `pi/settings.json` — pi agent settings
- `slack-bridge/` — Slack integration bridges + security module
- `broker-gateway/` — Slack integration bridges + security module
- `docs/` — architecture/operations/security documentation
- `test/` — vitest wrappers for shell scripts, integration, and legacy Node tests
- `hooks/` — git hooks (security-critical `pre-commit` protecting admin-managed files)
Expand Down
16 changes: 12 additions & 4 deletions bin/ci/droplet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,20 @@ cmd_wait_ssh() {

# ── run <ip> <ssh_private_key_file> <script> ──────────────────────────────────
cmd_run() {
local ip="${1:?Usage: droplet.sh run <ip> <ssh_private_key_file> <script>}"
local ip="${1:?Usage: droplet.sh run <ip> <ssh_private_key_file> <script> [env_vars...]}"
local key_file="${2:?}"
local script="${3:?}"

ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
-i "$key_file" "root@$ip" bash -s < "$script"
shift 3

# Remaining args are KEY=VALUE env vars forwarded to the remote script.
# Prepend export statements so the remote bash -s session inherits them.
{
for var in "$@"; do
printf 'export %s\n' "$var"
done
cat "$script"
} | ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
-i "$key_file" "root@$ip" bash -s
}

# ── list ──────────────────────────────────────────────────────────────────────
Expand Down
5 changes: 4 additions & 1 deletion bin/ci/setup-arch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ bash /home/baudbot_admin/baudbot/bin/ci/smoke-cli.sh
echo "=== Running runtime smoke checks ==="
bash /home/baudbot_admin/baudbot/bin/ci/smoke-agent-runtime.sh

echo "=== Running inference smoke check ==="
bash /home/baudbot_admin/baudbot/bin/ci/smoke-agent-inference.sh

echo "=== Installing test dependencies ==="
export PATH="/home/baudbot_agent/opt/node/bin:$PATH"
cd /home/baudbot_admin/baudbot
npm install --ignore-scripts 2>&1 | tail -1
cd slack-bridge && npm install 2>&1 | tail -1
cd broker-gateway && npm install 2>&1 | tail -1
cd ..

echo "=== Running tests ==="
Expand Down
5 changes: 4 additions & 1 deletion bin/ci/setup-ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,14 @@ bash /home/baudbot_admin/baudbot/bin/ci/smoke-cli.sh
echo "=== Running runtime smoke checks ==="
bash /home/baudbot_admin/baudbot/bin/ci/smoke-agent-runtime.sh

echo "=== Running inference smoke check ==="
bash /home/baudbot_admin/baudbot/bin/ci/smoke-agent-inference.sh

echo "=== Installing test dependencies ==="
export PATH="/home/baudbot_agent/opt/node/bin:$PATH"
cd /home/baudbot_admin/baudbot
npm install --ignore-scripts 2>&1 | tail -1
cd slack-bridge && npm install 2>&1 | tail -1
cd broker-gateway && npm install 2>&1 | tail -1
cd ..

echo "=== Running tests ==="
Expand Down
210 changes: 210 additions & 0 deletions bin/ci/smoke-agent-inference.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# Inference smoke-test for baudbot.
#
# Verifies that the control-agent can complete at least one real LLM turn
# end-to-end via session-control RPC.
#
# Requires CI_ANTHROPIC_API_KEY in the environment (injected into the
# agent's .env before starting baudbot).
#
# Expects baudbot to be already installed and stoppable via `sudo baudbot`.

set -Eeuo pipefail

readonly AGENT_USER="baudbot_agent"
readonly AGENT_HOME="/home/${AGENT_USER}"
readonly AGENT_ENV="${AGENT_HOME}/.config/.env"
readonly CONTROL_DIR="${AGENT_HOME}/.pi/session-control"
readonly CONTROL_ALIAS="${CONTROL_DIR}/control-agent.alias"
readonly START_TIMEOUT_SECONDS=60
readonly INFERENCE_TIMEOUT_SECONDS=120
readonly EXPECTED_TOKEN="CI_INFERENCE_OK"

started=0

log() {
printf '[inference-smoke] %s\n' "$*"
}

cleanup() {
local exit_code=$?
if [[ $started -eq 1 ]]; then
log "cleanup: stopping baudbot"
sudo baudbot stop >/dev/null 2>&1 || true
fi
exit "$exit_code"
}
trap cleanup EXIT

wait_for_control_socket() {
local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
local target=""

while (( SECONDS < deadline )); do
if [[ -L "$CONTROL_ALIAS" ]]; then
target="$(readlink -- "$CONTROL_ALIAS" 2>/dev/null || true)"
if [[ -n "$target" ]]; then
if [[ "$target" != /* ]]; then
target="${CONTROL_DIR}/${target}"
fi
if [[ -S "$target" ]]; then
printf '%s\n' "$target"
return 0
fi
fi
fi
sleep 1
done

return 1
}

dump_diagnostics() {
log "--- diagnostics ---"
sudo baudbot status 2>&1 || true
log "--- end diagnostics ---"
}

# Send a message via session-control RPC and wait for turn_end.
# Prints the assistant response content on success, exits non-zero on failure.
rpc_send_wait_turn_end() {
local socket_path="$1"
local message="$2"
local timeout_seconds="$3"

sudo -u "$AGENT_USER" python3 - "$socket_path" "$message" "$timeout_seconds" <<'PY'
import json
import socket
import sys

sock_path = sys.argv[1]
message = sys.argv[2]
timeout_seconds = int(sys.argv[3])

send_cmd = {"type": "send", "message": message, "mode": "steer"}
subscribe_cmd = {"type": "subscribe", "event": "turn_end"}

client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
client.settimeout(timeout_seconds)
client.connect(sock_path)

# Send both commands
client.sendall((json.dumps(send_cmd) + "\n").encode("utf-8"))
client.sendall((json.dumps(subscribe_cmd) + "\n").encode("utf-8"))

buf = b""
send_response = None

while True:
chunk = client.recv(8192)
if not chunk:
print("connection closed before turn_end", file=sys.stderr)
sys.exit(1)
buf += chunk

while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
line = line.strip()
if not line:
continue

try:
msg = json.loads(line.decode("utf-8", errors="replace"))
except json.JSONDecodeError:
continue

if msg.get("type") == "response":
cmd = msg.get("command", "")
if cmd == "send":
if not msg.get("success", False):
print(f"send failed: {msg.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)
send_response = msg
# Ignore subscribe response
continue

if msg.get("type") == "event" and msg.get("event") == "turn_end":
if send_response is None:
print("received turn_end before send response", file=sys.stderr)
sys.exit(1)
data = msg.get("data", {})
assistant_msg = data.get("message", {})
content = assistant_msg.get("content", "")
if not content:
print("turn completed but no assistant content", file=sys.stderr)
sys.exit(1)
print(content)
sys.exit(0)

print("stream ended without turn_end event", file=sys.stderr)
sys.exit(1)
except socket.timeout:
print("timeout waiting for inference response", file=sys.stderr)
sys.exit(1)
finally:
client.close()
PY
}

readonly CI_MODEL="anthropic/claude-haiku"

inject_ci_config() {
if [[ -z "${CI_ANTHROPIC_API_KEY:-}" ]]; then
log "ERROR: CI_ANTHROPIC_API_KEY is not set"
return 1
fi
log "injecting CI API key and model override into agent .env"
sed -i "s|^ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=${CI_ANTHROPIC_API_KEY}|" "$AGENT_ENV"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The smoke test's sed command for ANTHROPIC_API_KEY injection is fragile. It fails silently if the key doesn't exist in .env, which occurs when a non-Anthropic LLM is used.
Severity: MEDIUM

Suggested Fix

Update the script to check if ANTHROPIC_API_KEY exists in the .env file before attempting to replace it. If it doesn't exist, append the key and value to the file. This mirrors the safer pattern already used for BAUDBOT_MODEL in the same script.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: bin/ci/smoke-agent-inference.sh#L158

Potential issue: The smoke test script at `bin/ci/smoke-agent-inference.sh` uses a `sed`
command to update the `ANTHROPIC_API_KEY`. This command silently fails if the key is not
already present in the agent's `.env` file. The agent installation process only includes
the API key for the LLM provider selected during setup. Consequently, if an agent is
configured with a provider other than Anthropic, the `.env` file will not contain
`ANTHROPIC_API_KEY`. When the smoke test runs against such a configuration, the key
injection fails, and the subsequent inference test fails with an authentication error,
creating a false negative and making the test fragile.

# Use a cheap model for the smoke test — no need to burn Sonnet/Opus tokens.
if grep -q "^BAUDBOT_MODEL=" "$AGENT_ENV" 2>/dev/null; then
sed -i "s|^BAUDBOT_MODEL=.*|BAUDBOT_MODEL=${CI_MODEL}|" "$AGENT_ENV"
else
echo "BAUDBOT_MODEL=${CI_MODEL}" >> "$AGENT_ENV"
fi
}

main() {
inject_ci_config

log "starting baudbot"
sudo baudbot start
started=1

log "waiting for control-agent socket"
local socket_path=""
if ! socket_path="$(wait_for_control_socket)"; then
log "control-agent socket did not become ready within ${START_TIMEOUT_SECONDS}s"
dump_diagnostics
return 1
fi
log "control socket ready: ${socket_path}"

log "sending inference prompt (timeout ${INFERENCE_TIMEOUT_SECONDS}s)"
local response=""
if ! response="$(rpc_send_wait_turn_end "$socket_path" \
"Reply with exactly: ${EXPECTED_TOKEN}" \
"$INFERENCE_TIMEOUT_SECONDS")"; then
log "inference failed"
dump_diagnostics
return 1
fi

# Validate response contains expected token
if [[ "$response" == *"$EXPECTED_TOKEN"* ]]; then
log "inference response contains expected token"
else
log "unexpected response (missing '${EXPECTED_TOKEN}'):"
log " ${response:0:500}"
dump_diagnostics
return 1
fi

log "stopping baudbot"
sudo baudbot stop
started=0

log "inference smoke passed"
}

main "$@"
2 changes: 1 addition & 1 deletion bin/ci/smoke-agent-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ readonly AGENT_USER="baudbot_agent"
readonly AGENT_HOME="/home/${AGENT_USER}"
readonly CONTROL_DIR="${AGENT_HOME}/.pi/session-control"
readonly CONTROL_ALIAS="${CONTROL_DIR}/control-agent.alias"
readonly BRIDGE_STATUS_FILE="${AGENT_HOME}/.pi/agent/slack-bridge-supervisor.json"
readonly BRIDGE_STATUS_FILE="${AGENT_HOME}/.pi/agent/broker-gateway-supervisor.json"
readonly START_TIMEOUT_SECONDS=60
readonly STABILIZE_SECONDS=20

Expand Down
2 changes: 1 addition & 1 deletion bin/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ VEOF
echo ' \"source_sha\": \"$GIT_SHA\",'
echo ' \"files\": {'
first=1
for dir in '$BAUDBOT_HOME/.pi/agent/extensions' '$BAUDBOT_HOME/.pi/agent/skills' '/opt/baudbot/current/slack-bridge' '$BAUDBOT_HOME/runtime/bin'; do
for dir in '$BAUDBOT_HOME/.pi/agent/extensions' '$BAUDBOT_HOME/.pi/agent/skills' '/opt/baudbot/current/broker-gateway' '$BAUDBOT_HOME/runtime/bin'; do
if [ -d \"\$dir\" ]; then
while IFS= read -r f; do
hash=\$(sha256sum \"\$f\" | cut -d' ' -f1)
Expand Down
4 changes: 2 additions & 2 deletions bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ else
fi
fi

BRIDGE_DIR="$BAUDBOT_CURRENT_LINK/slack-bridge"
BRIDGE_DIR="$BAUDBOT_CURRENT_LINK/broker-gateway"
if [ -d "$BRIDGE_DIR" ] && [ -f "$BRIDGE_DIR/bridge.mjs" ]; then
pass "slack bridge deployed ($BRIDGE_DIR)"
else
Expand Down Expand Up @@ -457,7 +457,7 @@ fi
echo ""
echo "Runtime health:"

# Slack bridge
# Broker gateway
if curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:7890/send -H 'Content-Type: application/json' -d '{}' 2>/dev/null | grep -q "400"; then
pass "slack bridge responding (port 7890)"
else
Expand Down
2 changes: 1 addition & 1 deletion bin/lib/baudbot-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ PY

print_bridge_supervisor_status() {
local agent_user="${BAUDBOT_AGENT_USER:-baudbot_agent}"
local status_file="/home/$agent_user/.pi/agent/slack-bridge-supervisor.json"
local status_file="/home/$agent_user/.pi/agent/broker-gateway-supervisor.json"
local summary=""
local mode=""
local state=""
Expand Down
Loading
Loading