Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ BAUDBOT_AGENT_HOME=/home/baudbot_agent
# @sensitive=false @type=string
BAUDBOT_SOURCE_DIR=

# ── Startup Integrity ────────────────────────────────────────────────────────

# Startup deploy-manifest integrity mode: off | warn | strict
# @sensitive=false @type=string
BAUDBOT_STARTUP_INTEGRITY_MODE=warn

# ── Bridge ───────────────────────────────────────────────────────────────────

# Local HTTP API port for outbound Slack messages
Expand Down
12 changes: 12 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ Set during `setup.sh` / `baudbot install` via env vars:
| `IDLE_COMPACT_THRESHOLD_PCT` | Context usage % to trigger compaction (10–90) | `25` |
| `IDLE_COMPACT_ENABLED` | Set to `0`, `false`, or `no` to disable idle compaction | enabled |

### Startup Integrity

| Variable | Description | Default |
|----------|-------------|---------|
| `BAUDBOT_STARTUP_INTEGRITY_MODE` | Startup manifest verification mode: `off`, `warn`, `strict` | `warn` |

On startup, Baudbot verifies deployed runtime files against `~/.pi/agent/baudbot-manifest.json` and records the result in `~/.pi/agent/manifest-integrity-status.json`.

- `warn`: log high-severity warnings but continue startup
- `strict`: fail startup on missing/mismatched files or unreadable manifest
- `off`: skip verification (not recommended)

### Bridge

| Variable | Description | Default |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,12 @@ Baudbot is built for utility **and** containment:
- isolated `baudbot_agent` Unix user (no general sudo)
- per-UID firewall controls + process isolation
- source/runtime separation with deploy manifests
- startup deploy-manifest integrity verification (`warn`/`strict` modes)
- read-only protection for security-critical files
- session log hygiene (startup redaction + retention pruning)
- layered tool and shell guardrails (policy/guidance layer, not sole containment)

See [SECURITY.md](SECURITY.md) for full threat model, trust boundaries, and known risks. In particular: tool/shell guards are defense-in-depth policy layers; hard containment comes from OS/runtime boundaries.
See [SECURITY.md](SECURITY.md) for full threat model, trust boundaries, and known risks. In particular: many controls here are defense-in-depth (helpful for drift prevention/detection and keeping healthy agents on-task), while hard containment comes from OS/runtime boundaries.

## Documentation

Expand Down
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Live execution runs from release snapshots under `/opt/baudbot`.

Primary hard boundaries are runtime permissions, user isolation, and release-based deployment. If local source isolation is also enforced, admin can re-deploy from source to restore expected state.

> **Important scope note:** Many controls in this document are defense-in-depth and can be bypassed by a sufficiently capable or compromised rogue agent operating within its allowed user permissions. They are still valuable because they reduce accidental drift, surface tampering quickly, and help a non-compromised agent stay aligned with intended workflows.

## User Model

| User | Role | Sudo | Groups |
Expand Down
4 changes: 2 additions & 2 deletions bin/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
cp --no-preserve=ownership "$BAUDBOT_SRC/start.sh" "$STAGE_DIR/start.sh"
mkdir -p "$STAGE_DIR/bin"
mkdir -p "$STAGE_DIR/bin/lib"
for script in harden-permissions.sh redact-logs.sh prune-session-logs.sh; do
for script in harden-permissions.sh redact-logs.sh prune-session-logs.sh verify-manifest.sh; do
[ -f "$BAUDBOT_SRC/bin/$script" ] && cp --no-preserve=ownership "$BAUDBOT_SRC/bin/$script" "$STAGE_DIR/bin/$script"
done
[ -f "$BAUDBOT_SRC/bin/lib/runtime-node.sh" ] && cp --no-preserve=ownership "$BAUDBOT_SRC/bin/lib/runtime-node.sh" "$STAGE_DIR/bin/lib/runtime-node.sh"
Expand Down Expand Up @@ -249,7 +249,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
as_agent mkdir -p "$BAUDBOT_HOME/runtime/bin"
as_agent mkdir -p "$BAUDBOT_HOME/runtime/bin/lib"

for script in harden-permissions.sh redact-logs.sh prune-session-logs.sh; do
for script in harden-permissions.sh redact-logs.sh prune-session-logs.sh verify-manifest.sh; do
if [ -f "$STAGE_DIR/bin/$script" ]; then
as_agent cp "$STAGE_DIR/bin/$script" "$BAUDBOT_HOME/runtime/bin/$script"
as_agent chmod u+x "$BAUDBOT_HOME/runtime/bin/$script"
Expand Down
32 changes: 32 additions & 0 deletions bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,38 @@ else
fi
fi

INTEGRITY_STATUS_FILE="$BAUDBOT_INTEGRITY_STATUS_FILE"
if [ -f "$INTEGRITY_STATUS_FILE" ]; then
integrity_status="$(jq -r '.status // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
integrity_checked_at="$(jq -r '.checked_at // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
integrity_missing="$(jq -r '.missing_files // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
integrity_mismatches="$(jq -r '.hash_mismatches // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"

case "$integrity_status" in
pass)
pass "startup manifest integrity passed ($integrity_checked_at)"
;;
warn)
warn "startup manifest integrity reported issues at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
;;
fail)
fail "startup manifest integrity failed at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
;;
skipped)
warn "startup manifest integrity check is disabled/skipped"
;;
*)
warn "startup manifest integrity status unknown (file: $INTEGRITY_STATUS_FILE)"
;;
esac
else
if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then
warn "cannot verify startup manifest integrity status as non-root (run: sudo baudbot doctor)"
else
warn "startup manifest integrity status file missing ($INTEGRITY_STATUS_FILE)"
fi
fi

# ── Security ─────────────────────────────────────────────────────────────────

echo ""
Expand Down
3 changes: 2 additions & 1 deletion bin/lib/paths-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ bb_init_paths() {
: "${BAUDBOT_AGENT_SETTINGS_FILE:=$BAUDBOT_AGENT_DIR/settings.json}"
: "${BAUDBOT_VERSION_FILE:=$BAUDBOT_AGENT_DIR/baudbot-version.json}"
: "${BAUDBOT_MANIFEST_FILE:=$BAUDBOT_AGENT_DIR/baudbot-manifest.json}"
: "${BAUDBOT_INTEGRITY_STATUS_FILE:=$BAUDBOT_AGENT_DIR/manifest-integrity-status.json}"
: "${BAUDBOT_ENV_FILE:=$BAUDBOT_AGENT_HOME/.config/.env}"

bb_refresh_release_paths

export BAUDBOT_AGENT_USER BAUDBOT_AGENT_HOME BAUDBOT_HOME
export BAUDBOT_RUNTIME_DIR BAUDBOT_PI_DIR BAUDBOT_AGENT_DIR
export BAUDBOT_AGENT_EXT_DIR BAUDBOT_AGENT_SKILLS_DIR BAUDBOT_AGENT_SETTINGS_FILE
export BAUDBOT_VERSION_FILE BAUDBOT_MANIFEST_FILE BAUDBOT_ENV_FILE
export BAUDBOT_VERSION_FILE BAUDBOT_MANIFEST_FILE BAUDBOT_INTEGRITY_STATUS_FILE BAUDBOT_ENV_FILE
export BAUDBOT_RELEASE_ROOT BAUDBOT_RELEASES_DIR BAUDBOT_CURRENT_LINK BAUDBOT_PREVIOUS_LINK
export BAUDBOT_SOURCE_URL_FILE BAUDBOT_SOURCE_BRANCH_FILE
}
30 changes: 30 additions & 0 deletions bin/security-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,36 @@ else
finding "WARN" "No deploy manifest found — cannot verify integrity" \
"Run deploy.sh to generate"
fi

if [ -f "$BAUDBOT_INTEGRITY_STATUS_FILE" ]; then
status_value="$(jq -r '.status // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
status_checked_at="$(jq -r '.checked_at // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
status_missing="$(jq -r '.missing_files // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
status_mismatches="$(jq -r '.hash_mismatches // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"

case "$status_value" in
pass)
ok "Last startup integrity check passed ($status_checked_at)"
;;
warn)
finding "WARN" "Last startup integrity check reported issues" \
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
;;
fail)
finding "CRITICAL" "Last startup integrity check failed" \
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
;;
skipped)
finding "WARN" "Last startup integrity check was skipped/disabled" "$status_checked_at"
;;
*)
finding "INFO" "Startup integrity status unknown" "$BAUDBOT_INTEGRITY_STATUS_FILE"
;;
esac
else
finding "WARN" "No startup integrity status found" \
"Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)"
fi
echo ""

# ── Secrets in readable files ────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions bin/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ run_shell_tests() {
run "safe-bash wrapper" bash bin/baudbot-safe-bash.test.sh
run "log redaction" bash bin/redact-logs.test.sh
run "log pruning" bash bin/prune-session-logs.test.sh
run "manifest integrity" bash bin/verify-manifest.test.sh
run "config flow" bash bin/config.test.sh
run "deploy lib helpers" bash bin/lib/deploy-common.test.sh
run "doctor lib helpers" bash bin/lib/doctor-common.test.sh
Expand Down
138 changes: 138 additions & 0 deletions bin/verify-manifest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/bin/bash
# Verify deployed runtime files against ~/.pi/agent/baudbot-manifest.json.
#
# Modes:
# off - skip verification (exit 0)
# warn - log warnings on mismatch (exit 0)
# strict - fail on mismatch (exit 1)

set -euo pipefail

MODE="${BAUDBOT_STARTUP_INTEGRITY_MODE:-warn}"
MANIFEST_FILE="${BAUDBOT_MANIFEST_FILE:-$HOME/.pi/agent/baudbot-manifest.json}"
STATUS_FILE="${BAUDBOT_INTEGRITY_STATUS_FILE:-$HOME/.pi/agent/manifest-integrity-status.json}"
AGENT_HOME="${BAUDBOT_HOME:-$HOME}"
RELEASE_ROOT="${BAUDBOT_CURRENT_LINK:-/opt/baudbot/current}"

# Expected mutable content that should not block startup if present in a manifest.
EXCLUDE_REGEX='^\.pi/agent/(sessions|memory|logs)/|\.log$'

mkdir -p "$(dirname "$STATUS_FILE")"

write_status() {
local status="$1"
local checked_files="$2"
local skipped_files="$3"
local missing_files="$4"
local hash_mismatches="$5"

cat >"$STATUS_FILE" <<EOF
{
"checked_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"mode": "$MODE",
"status": "$status",
"manifest": "$MANIFEST_FILE",
"checked_files": $checked_files,
"skipped_files": $skipped_files,
"missing_files": $missing_files,
"hash_mismatches": $hash_mismatches
}
EOF
chmod 600 "$STATUS_FILE" 2>/dev/null || true
}

case "$MODE" in
off|warn|strict) ;;
*)
echo "⚠️ Unknown BAUDBOT_STARTUP_INTEGRITY_MODE='$MODE' (expected: off|warn|strict). Falling back to warn." >&2
MODE="warn"
;;
esac

if [ "$MODE" = "off" ]; then
echo "Startup integrity check disabled (BAUDBOT_STARTUP_INTEGRITY_MODE=off)."
write_status "skipped" 0 0 0 0
exit 0
fi

if [ ! -f "$MANIFEST_FILE" ]; then
echo "⚠️ Deploy manifest not found: $MANIFEST_FILE" >&2
write_status "warn" 0 0 0 0
if [ "$MODE" = "strict" ]; then
echo "❌ Startup integrity verification failed (missing manifest, strict mode)." >&2
exit 1
fi
exit 0
fi

if ! command -v jq >/dev/null 2>&1; then
echo "⚠️ jq not found; cannot parse deploy manifest for startup integrity check." >&2
write_status "warn" 0 0 0 0
if [ "$MODE" = "strict" ]; then
echo "❌ Startup integrity verification failed (jq missing, strict mode)." >&2
exit 1
fi
exit 0
fi

if ! jq -e '.files and (.files | type == "object")' "$MANIFEST_FILE" >/dev/null 2>&1; then
echo "⚠️ Invalid manifest format (missing .files object): $MANIFEST_FILE" >&2
write_status "warn" 0 0 0 0
if [ "$MODE" = "strict" ]; then
echo "❌ Startup integrity verification failed (invalid manifest, strict mode)." >&2
exit 1
fi
exit 0
fi

checked_files=0
skipped_files=0
missing_files=0
hash_mismatches=0

while IFS=$'\t' read -r rel_path expected_hash; do
[ -n "$rel_path" ] || continue

if [[ "$rel_path" =~ $EXCLUDE_REGEX ]]; then
skipped_files=$((skipped_files + 1))
continue
fi

if [[ "$rel_path" == release/* ]]; then
full_path="$RELEASE_ROOT/${rel_path#release/}"
else
full_path="$AGENT_HOME/$rel_path"
fi

checked_files=$((checked_files + 1))

if [ ! -f "$full_path" ]; then
echo "⚠️ Missing file from manifest: $rel_path ($full_path)" >&2
missing_files=$((missing_files + 1))
continue
fi

actual_hash=$(sha256sum "$full_path" | awk '{print $1}')
if [ "$actual_hash" != "$expected_hash" ]; then
echo "⚠️ Hash mismatch: $rel_path" >&2
hash_mismatches=$((hash_mismatches + 1))
fi
done < <(jq -r '.files | to_entries[] | [.key, .value] | @tsv' "$MANIFEST_FILE")

if [ "$missing_files" -eq 0 ] && [ "$hash_mismatches" -eq 0 ]; then
echo "✅ Startup integrity check passed ($checked_files files, $skipped_files skipped)."
write_status "pass" "$checked_files" "$skipped_files" 0 0
exit 0
fi

total_issues=$((missing_files + hash_mismatches))
echo "⚠️ Startup integrity check found $total_issues issue(s) ($missing_files missing, $hash_mismatches hash mismatch)." >&2

if [ "$MODE" = "strict" ]; then
write_status "fail" "$checked_files" "$skipped_files" "$missing_files" "$hash_mismatches"
echo "❌ Strict mode enabled; refusing to start." >&2
exit 1
fi

write_status "warn" "$checked_files" "$skipped_files" "$missing_files" "$hash_mismatches"
exit 0
Loading