diff --git a/.env.schema b/.env.schema index 6faa79a..efff7d5 100644 --- a/.env.schema +++ b/.env.schema @@ -35,21 +35,38 @@ OPENCODE_ZEN_API_KEY= # @sensitive=false @type=string BAUDBOT_MODEL= -# ── Slack ──────────────────────────────────────────────────────────────────── +# ── Slack / Gateway bridge ─────────────────────────────────────────────────── -# Slack bot OAuth token (required for direct Socket Mode, optional in broker mode) +# Preferred naming: GATEWAY_* +# Legacy naming: SLACK_* (still supported) +# If both are set, GATEWAY_* takes precedence. + +# Gateway bot OAuth token (preferred; required for direct Socket Mode, optional in broker mode) # @type=string(startsWith=xoxb-) # @docs("Create a Slack app", https://api.slack.com/apps) +GATEWAY_BOT_TOKEN= + +# Legacy alias for GATEWAY_BOT_TOKEN +# @type=string(startsWith=xoxb-) SLACK_BOT_TOKEN= -# Slack app-level token (Socket Mode; optional in broker mode) +# Gateway app-level token (preferred; Socket Mode; optional in broker mode) +# @type=string(startsWith=xapp-) +GATEWAY_APP_TOKEN= + +# Legacy alias for GATEWAY_APP_TOKEN # @type=string(startsWith=xapp-) SLACK_APP_TOKEN= -# Comma-separated Slack user IDs allowed to interact with the agent +# Comma-separated Slack user IDs allowed to interact with the agent (preferred) # Optional — if unset, all workspace members can interact. # @sensitive=false @type=string # @example="U01ABCDEF,U02GHIJKL" +GATEWAY_ALLOWED_USERS= + +# Legacy alias for GATEWAY_ALLOWED_USERS +# @sensitive=false @type=string +# @example="U01ABCDEF,U02GHIJKL" SLACK_ALLOWED_USERS= # ── Experimental Feature Flag ─────────────────────────────────────────────── @@ -93,9 +110,13 @@ SENTRY_ORG= # @sensitive=false @type=string(startsWith=C) SENTRY_CHANNEL_ID= -# ── Slack Channels (optional) ─────────────────────────────────────────────── +# ── Gateway / Slack Channels (optional) ───────────────────────────────────── -# Additional monitored channel (responds to all messages, not just @mentions) +# Additional monitored channel (preferred; responds to all messages, not just @mentions) +# @sensitive=false @type=string(startsWith=C) +GATEWAY_CHANNEL_ID= + +# Legacy alias for GATEWAY_CHANNEL_ID # @sensitive=false @type=string(startsWith=C) SLACK_CHANNEL_ID= @@ -106,49 +127,93 @@ SLACK_CHANNEL_ID= # @docs(https://kernel.computer) KERNEL_API_KEY= -# ── Slack Broker Registration (optional) ───────────────────────────────────── +# ── Gateway/Slack Broker Registration (optional) ──────────────────────────── -# Slack broker base URL +# Gateway broker base URL (preferred) +# @sensitive=false @type=url +GATEWAY_BROKER_URL= + +# Legacy alias for GATEWAY_BROKER_URL # @sensitive=false @type=url SLACK_BROKER_URL= -# Slack workspace/team ID registered with broker +# Gateway workspace/team ID registered with broker (preferred) +# @sensitive=false @type=string(startsWith=T) +GATEWAY_BROKER_WORKSPACE_ID= + +# Legacy alias for GATEWAY_BROKER_WORKSPACE_ID # @sensitive=false @type=string(startsWith=T) SLACK_BROKER_WORKSPACE_ID= -# Server X25519 private key (base64) +# Gateway server X25519 private key (base64, preferred) +# @type=string +GATEWAY_BROKER_SERVER_PRIVATE_KEY= + +# Legacy alias for GATEWAY_BROKER_SERVER_PRIVATE_KEY # @type=string SLACK_BROKER_SERVER_PRIVATE_KEY= -# Server X25519 public key (base64) +# Gateway server X25519 public key (base64, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_SERVER_PUBLIC_KEY= + +# Legacy alias for GATEWAY_BROKER_SERVER_PUBLIC_KEY # @sensitive=false @type=string SLACK_BROKER_SERVER_PUBLIC_KEY= -# Server Ed25519 private signing key (base64) +# Gateway server Ed25519 private signing key (base64, preferred) +# @type=string +GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY= + +# Legacy alias for GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY # @type=string SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY= -# Server Ed25519 public signing key (base64) +# Gateway server Ed25519 public signing key (base64, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_SERVER_SIGNING_PUBLIC_KEY= + +# Legacy alias for GATEWAY_BROKER_SERVER_SIGNING_PUBLIC_KEY # @sensitive=false @type=string SLACK_BROKER_SERVER_SIGNING_PUBLIC_KEY= -# Broker X25519 public key (base64) +# Gateway broker X25519 public key (base64, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_PUBLIC_KEY= + +# Legacy alias for GATEWAY_BROKER_PUBLIC_KEY # @sensitive=false @type=string SLACK_BROKER_PUBLIC_KEY= -# Broker Ed25519 public signing key (base64) +# Gateway broker Ed25519 public signing key (base64, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_SIGNING_PUBLIC_KEY= + +# Legacy alias for GATEWAY_BROKER_SIGNING_PUBLIC_KEY # @sensitive=false @type=string SLACK_BROKER_SIGNING_PUBLIC_KEY= -# Broker-issued bearer token for broker API auth (required for broker pull mode) +# Gateway broker-issued bearer token for broker API auth (preferred; required for broker pull mode) +# @type=string +GATEWAY_BROKER_ACCESS_TOKEN= + +# Legacy alias for GATEWAY_BROKER_ACCESS_TOKEN # @type=string SLACK_BROKER_ACCESS_TOKEN= -# Optional broker token expiration timestamp (ISO-8601) +# Optional gateway broker token expiration timestamp (ISO-8601, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT= + +# Legacy alias for GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT # @sensitive=false @type=string SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT= -# Optional broker token scopes (comma-separated) +# Optional gateway broker token scopes (comma-separated, preferred) +# @sensitive=false @type=string +GATEWAY_BROKER_ACCESS_TOKEN_SCOPES= + +# Legacy alias for GATEWAY_BROKER_ACCESS_TOKEN_SCOPES # @sensitive=false @type=string SLACK_BROKER_ACCESS_TOKEN_SCOPES= @@ -163,15 +228,35 @@ GITHUB_IGNORED_USERS= # @sensitive=false @type=string BAUDBOT_AGENT_VERSION= -# Broker pull cadence in milliseconds (default: 3000) +# Gateway broker pull cadence in milliseconds (preferred; default: 3000) +# @sensitive=false @type=number +GATEWAY_BROKER_POLL_INTERVAL_MS=3000 + +# Legacy alias for GATEWAY_BROKER_POLL_INTERVAL_MS # @sensitive=false @type=number SLACK_BROKER_POLL_INTERVAL_MS=3000 -# Max messages to lease per inbox pull request (default: 10) +# Gateway max messages to lease per inbox pull request (preferred; default: 10) +# @sensitive=false @type=number +GATEWAY_BROKER_MAX_MESSAGES=10 + +# Legacy alias for GATEWAY_BROKER_MAX_MESSAGES # @sensitive=false @type=number SLACK_BROKER_MAX_MESSAGES=10 -# Dedupe cache TTL in milliseconds (default: 20 minutes) +# Gateway long-poll wait window in seconds (preferred; default: 20, max: 25) +# @sensitive=false @type=number +GATEWAY_BROKER_WAIT_SECONDS=20 + +# Legacy alias for GATEWAY_BROKER_WAIT_SECONDS +# @sensitive=false @type=number +SLACK_BROKER_WAIT_SECONDS=20 + +# Gateway dedupe cache TTL in milliseconds (preferred; default: 20 minutes) +# @sensitive=false @type=number +GATEWAY_BROKER_DEDUPE_TTL_MS=1200000 + +# Legacy alias for GATEWAY_BROKER_DEDUPE_TTL_MS # @sensitive=false @type=number SLACK_BROKER_DEDUPE_TTL_MS=1200000 diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 7ae8e2d..c37875f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -37,11 +37,16 @@ The agent also uses an SSH key (`~/.ssh/id_ed25519`) for git push. Setup generat | Variable | Description | How to get it | |----------|-------------|---------------| -| `SLACK_BOT_TOKEN` | Slack bot OAuth token (required for direct Socket Mode; ignored by broker pull 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; not used by broker pull 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` | +| `GATEWAY_BOT_TOKEN` | **Preferred** bot OAuth token for Socket Mode (ignored by broker pull 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_BOT_TOKEN` | Legacy alias for `GATEWAY_BOT_TOKEN` (still supported) | Same token as above; migrate to `GATEWAY_BOT_TOKEN` over time. | +| `GATEWAY_APP_TOKEN` | **Preferred** app-level token for Socket Mode | In your Slack app settings → **Basic Information** → **App-Level Tokens**, create a token with `connections:write` scope. | +| `SLACK_APP_TOKEN` | Legacy alias for `GATEWAY_APP_TOKEN` (still supported) | Same token as above; migrate to `GATEWAY_APP_TOKEN` over time. | +| `GATEWAY_ALLOWED_USERS` | **Preferred** comma-separated Slack user IDs allowlist | **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` | Legacy alias for `GATEWAY_ALLOWED_USERS` (still supported) | Same value as above; migrate to `GATEWAY_ALLOWED_USERS` over time. | -If you're using Slack broker mode (`SLACK_BROKER_*` vars), the runtime uses broker pull delivery and does not require Socket Mode callbacks. +If both alias forms are present, `GATEWAY_*` takes precedence. + +If you're using broker mode (`GATEWAY_BROKER_*` preferred, `SLACK_BROKER_*` legacy), the runtime uses broker pull delivery and does not require Socket Mode callbacks. If you're using the Slack broker OAuth flow, register this server after install: @@ -105,7 +110,8 @@ The integration token only provides read access to pages/databases explicitly sh | Variable | Description | How to get it | |----------|-------------|---------------| -| `SLACK_CHANNEL_ID` | Additional monitored channel | If set, the bridge responds to all messages in this channel (not just @mentions). | +| `GATEWAY_CHANNEL_ID` | **Preferred** additional monitored channel | If set, the bridge responds to all messages in this channel (not just @mentions). | +| `SLACK_CHANNEL_ID` | Legacy alias for `GATEWAY_CHANNEL_ID` (still supported) | Same value as above; migrate to `GATEWAY_CHANNEL_ID` over time. | ### Slack Broker Registration (optional) @@ -113,24 +119,41 @@ Set by `sudo baudbot broker register` when using brokered Slack OAuth flow. | Variable | Description | |----------|-------------| -| `SLACK_BROKER_URL` | Broker base URL | -| `SLACK_BROKER_WORKSPACE_ID` | Slack workspace/team ID (`T...`) | -| `SLACK_BROKER_SERVER_PRIVATE_KEY` | Server X25519 private key (base64) | -| `SLACK_BROKER_SERVER_PUBLIC_KEY` | Server X25519 public key (base64) | -| `SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY` | Server Ed25519 private signing key (base64) | -| `SLACK_BROKER_SERVER_SIGNING_PUBLIC_KEY` | Server Ed25519 public signing key (base64) | -| `SLACK_BROKER_PUBLIC_KEY` | Broker X25519 public key (base64) | -| `SLACK_BROKER_SIGNING_PUBLIC_KEY` | Broker Ed25519 public signing key (base64) | -| `SLACK_BROKER_ACCESS_TOKEN` | Broker-issued bearer token for broker API auth (required for broker pull mode runtime) | -| `SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT` | ISO timestamp for broker token expiry (recommended; runtime exits if expired) | -| `SLACK_BROKER_ACCESS_TOKEN_SCOPES` | Comma-separated broker token scopes | +| `GATEWAY_BROKER_URL` | **Preferred** broker base URL | +| `SLACK_BROKER_URL` | Legacy alias for `GATEWAY_BROKER_URL` (still supported) | +| `GATEWAY_BROKER_WORKSPACE_ID` | **Preferred** Slack workspace/team ID (`T...`) | +| `SLACK_BROKER_WORKSPACE_ID` | Legacy alias for `GATEWAY_BROKER_WORKSPACE_ID` | +| `GATEWAY_BROKER_SERVER_PRIVATE_KEY` | **Preferred** server X25519 private key (base64) | +| `SLACK_BROKER_SERVER_PRIVATE_KEY` | Legacy alias for `GATEWAY_BROKER_SERVER_PRIVATE_KEY` | +| `GATEWAY_BROKER_SERVER_PUBLIC_KEY` | **Preferred** server X25519 public key (base64) | +| `SLACK_BROKER_SERVER_PUBLIC_KEY` | Legacy alias for `GATEWAY_BROKER_SERVER_PUBLIC_KEY` | +| `GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY` | **Preferred** server Ed25519 private signing key (base64) | +| `SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY` | Legacy alias for `GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY` | +| `GATEWAY_BROKER_SERVER_SIGNING_PUBLIC_KEY` | **Preferred** server Ed25519 public signing key (base64) | +| `SLACK_BROKER_SERVER_SIGNING_PUBLIC_KEY` | Legacy alias for `GATEWAY_BROKER_SERVER_SIGNING_PUBLIC_KEY` | +| `GATEWAY_BROKER_PUBLIC_KEY` | **Preferred** broker X25519 public key (base64) | +| `SLACK_BROKER_PUBLIC_KEY` | Legacy alias for `GATEWAY_BROKER_PUBLIC_KEY` | +| `GATEWAY_BROKER_SIGNING_PUBLIC_KEY` | **Preferred** broker Ed25519 public signing key (base64) | +| `SLACK_BROKER_SIGNING_PUBLIC_KEY` | Legacy alias for `GATEWAY_BROKER_SIGNING_PUBLIC_KEY` | +| `GATEWAY_BROKER_ACCESS_TOKEN` | **Preferred** broker-issued bearer token for broker API auth (required for broker pull mode runtime) | +| `SLACK_BROKER_ACCESS_TOKEN` | Legacy alias for `GATEWAY_BROKER_ACCESS_TOKEN` | +| `GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT` | **Preferred** ISO timestamp for broker token expiry (runtime exits if expired) | +| `SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT` | Legacy alias for `GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT` | +| `GATEWAY_BROKER_ACCESS_TOKEN_SCOPES` | **Preferred** comma-separated broker token scopes | +| `SLACK_BROKER_ACCESS_TOKEN_SCOPES` | Legacy alias for `GATEWAY_BROKER_ACCESS_TOKEN_SCOPES` | | `GITHUB_IGNORED_USERS` | Optional comma-separated GitHub logins to ignore when forwarding broker GitHub events (`baudbot-agent` is always ignored) | -| `SLACK_BROKER_POLL_INTERVAL_MS` | Inbox poll interval in milliseconds (default: `3000`) | -| `SLACK_BROKER_MAX_MESSAGES` | Max leased messages per poll request (default: `10`) | -| `SLACK_BROKER_WAIT_SECONDS` | Long-poll wait window for `/api/inbox/pull` (default: `20`, set `0` for immediate short-poll, max `25`) | -| `SLACK_BROKER_DEDUPE_TTL_MS` | Dedupe cache TTL in milliseconds (default: `1200000`) | +| `GATEWAY_BROKER_POLL_INTERVAL_MS` | **Preferred** inbox poll interval in milliseconds (default: `3000`) | +| `SLACK_BROKER_POLL_INTERVAL_MS` | Legacy alias for `GATEWAY_BROKER_POLL_INTERVAL_MS` | +| `GATEWAY_BROKER_MAX_MESSAGES` | **Preferred** max leased messages per poll request (default: `10`) | +| `SLACK_BROKER_MAX_MESSAGES` | Legacy alias for `GATEWAY_BROKER_MAX_MESSAGES` | +| `GATEWAY_BROKER_WAIT_SECONDS` | **Preferred** long-poll wait window for `/api/inbox/pull` (default: `20`, set `0` for immediate short-poll, max `25`) | +| `SLACK_BROKER_WAIT_SECONDS` | Legacy alias for `GATEWAY_BROKER_WAIT_SECONDS` | +| `GATEWAY_BROKER_DEDUPE_TTL_MS` | **Preferred** dedupe cache TTL in milliseconds (default: `1200000`) | +| `SLACK_BROKER_DEDUPE_TTL_MS` | Legacy alias for `GATEWAY_BROKER_DEDUPE_TTL_MS` | | `BAUDBOT_AGENT_VERSION` | Optional override for broker observability `meta.agent_version` (otherwise read from `~/.pi/agent/baudbot-version.json` when available) | +If both alias forms are set, `GATEWAY_BROKER_*` takes precedence. + Broker mode also emits best-effort context usage telemetry in inbox pull `meta` by reading `~/.pi/agent/context-usage.json` (written by the `context` extension on session start/turn end/tool results). ### Kernel (Cloud Browsers) @@ -223,25 +246,25 @@ ANTHROPIC_API_KEY=sk-ant-... # GitHub: authenticate with `sudo -u baudbot_agent gh auth login` -# Slack -SLACK_BOT_TOKEN=xoxb-... -SLACK_APP_TOKEN=xapp-... -SLACK_ALLOWED_USERS=U01ABCDEF,U02GHIJKL +# Gateway bridge (legacy SLACK_* aliases are still supported) +GATEWAY_BOT_TOKEN=xoxb-... +GATEWAY_APP_TOKEN=xapp-... +GATEWAY_ALLOWED_USERS=U01ABCDEF,U02GHIJKL SENTRY_CHANNEL_ID=C0987654321 -# Slack broker registration (optional, set by: sudo baudbot broker register) -SLACK_BROKER_URL=https://broker.example.com -SLACK_BROKER_WORKSPACE_ID=T0123ABCD +# Gateway broker registration (optional, set by: sudo baudbot broker register) +GATEWAY_BROKER_URL=https://broker.example.com +GATEWAY_BROKER_WORKSPACE_ID=T0123ABCD # Optional broker auth token fields (set by broker register when provided) -# SLACK_BROKER_ACCESS_TOKEN=... -# SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT=2026-02-22T22:15:00.000Z -# SLACK_BROKER_ACCESS_TOKEN_SCOPES=slack.send,inbox.pull,inbox.ack +# GATEWAY_BROKER_ACCESS_TOKEN=... +# GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT=2026-02-22T22:15:00.000Z +# GATEWAY_BROKER_ACCESS_TOKEN_SCOPES=slack.send,inbox.pull,inbox.ack # Optional GitHub bot/user filters for broker-delivered GitHub webhook events # GITHUB_IGNORED_USERS=dependabot[bot],renovate[bot] -SLACK_BROKER_POLL_INTERVAL_MS=3000 -SLACK_BROKER_MAX_MESSAGES=10 -SLACK_BROKER_WAIT_SECONDS=20 -SLACK_BROKER_DEDUPE_TTL_MS=1200000 +GATEWAY_BROKER_POLL_INTERVAL_MS=3000 +GATEWAY_BROKER_MAX_MESSAGES=10 +GATEWAY_BROKER_WAIT_SECONDS=20 +GATEWAY_BROKER_DEDUPE_TTL_MS=1200000 # Experimental features (required for email) # BAUDBOT_EXPERIMENTAL=1 diff --git a/bin/config.sh b/bin/config.sh index 07e3bc2..bd9198d 100755 --- a/bin/config.sh +++ b/bin/config.sh @@ -574,8 +574,8 @@ fi # ── Validation ─────────────────────────────────────────────────────────────── -if [ -z "${ENV_VARS[SLACK_ALLOWED_USERS]:-}" ]; then - warn "SLACK_ALLOWED_USERS not set — all workspace members will be allowed" +if [ -z "${ENV_VARS[GATEWAY_ALLOWED_USERS]:-${ENV_VARS[SLACK_ALLOWED_USERS]:-}}" ]; then + warn "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set — all workspace members will be allowed" fi # ── Write config ───────────────────────────────────────────────────────────── diff --git a/bin/doctor.sh b/bin/doctor.sh index 5cfc92e..d74bdf5 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -191,59 +191,77 @@ if [ -f "$ENV_FILE" ]; then fail "no valid LLM API key set (need ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or OPENCODE_ZEN_API_KEY)" fi - BROKER_REQUIRED_KEYS=( - SLACK_BROKER_URL - SLACK_BROKER_WORKSPACE_ID - SLACK_BROKER_SERVER_PRIVATE_KEY - SLACK_BROKER_SERVER_PUBLIC_KEY - SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY - SLACK_BROKER_PUBLIC_KEY - SLACK_BROKER_SIGNING_PUBLIC_KEY + read_first_env_value() { + local preferred_key="$1" + local legacy_key="$2" + local preferred_value="" + + preferred_value="$(bb_read_env_value "$ENV_FILE" "$preferred_key")" + if [ -n "$preferred_value" ]; then + printf '%s' "$preferred_value" + return 0 + fi + + bb_read_env_value "$ENV_FILE" "$legacy_key" + } + + BROKER_REQUIRED_PAIRS=( + "GATEWAY_BROKER_URL:SLACK_BROKER_URL" + "GATEWAY_BROKER_WORKSPACE_ID:SLACK_BROKER_WORKSPACE_ID" + "GATEWAY_BROKER_SERVER_PRIVATE_KEY:SLACK_BROKER_SERVER_PRIVATE_KEY" + "GATEWAY_BROKER_SERVER_PUBLIC_KEY:SLACK_BROKER_SERVER_PUBLIC_KEY" + "GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY:SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY" + "GATEWAY_BROKER_PUBLIC_KEY:SLACK_BROKER_PUBLIC_KEY" + "GATEWAY_BROKER_SIGNING_PUBLIC_KEY:SLACK_BROKER_SIGNING_PUBLIC_KEY" ) BROKER_MODE_READY=true - for key in "${BROKER_REQUIRED_KEYS[@]}"; do - if [ -z "$(bb_read_env_value "$ENV_FILE" "$key")" ]; then + for pair in "${BROKER_REQUIRED_PAIRS[@]}"; do + IFS=':' read -r preferred_key legacy_key <<<"$pair" + if [ -z "$(read_first_env_value "$preferred_key" "$legacy_key")" ]; then BROKER_MODE_READY=false break fi done SOCKET_MODE_READY=true - for key in SLACK_BOT_TOKEN SLACK_APP_TOKEN; do - if [ -z "$(bb_read_env_value "$ENV_FILE" "$key")" ]; then + for pair in "GATEWAY_BOT_TOKEN:SLACK_BOT_TOKEN" "GATEWAY_APP_TOKEN:SLACK_APP_TOKEN"; do + IFS=':' read -r preferred_key legacy_key <<<"$pair" + if [ -z "$(read_first_env_value "$preferred_key" "$legacy_key")" ]; then SOCKET_MODE_READY=false break fi done if [ "$BROKER_MODE_READY" = true ]; then - pass "broker mode configured (SLACK_BROKER_*)" - for key in SLACK_BOT_TOKEN SLACK_APP_TOKEN; do - if [ -n "$(bb_read_env_value "$ENV_FILE" "$key")" ]; then - pass "$key is set" + pass "broker mode configured (GATEWAY_BROKER_* preferred; SLACK_BROKER_* legacy)" + for pair in "GATEWAY_BOT_TOKEN:SLACK_BOT_TOKEN" "GATEWAY_APP_TOKEN:SLACK_APP_TOKEN"; do + IFS=':' read -r preferred_key legacy_key <<<"$pair" + if [ -n "$(read_first_env_value "$preferred_key" "$legacy_key")" ]; then + pass "$preferred_key/$legacy_key is set" else - pass "$key not required in broker mode" + pass "$preferred_key/$legacy_key not required in broker mode" fi done else - for key in SLACK_BOT_TOKEN SLACK_APP_TOKEN; do - if [ -n "$(bb_read_env_value "$ENV_FILE" "$key")" ]; then - pass "$key is set" + for pair in "GATEWAY_BOT_TOKEN:SLACK_BOT_TOKEN" "GATEWAY_APP_TOKEN:SLACK_APP_TOKEN"; do + IFS=':' read -r preferred_key legacy_key <<<"$pair" + if [ -n "$(read_first_env_value "$preferred_key" "$legacy_key")" ]; then + pass "$preferred_key/$legacy_key is set" else - warn "$key is not set" + warn "$preferred_key/$legacy_key is not set" fi done if [ "$SOCKET_MODE_READY" = false ]; then - warn "no Slack transport configured (set SLACK_BROKER_* for broker mode or SLACK_BOT_TOKEN+SLACK_APP_TOKEN for socket mode)" + warn "no Gateway transport configured (set GATEWAY_BROKER_* or SLACK_BROKER_* for broker mode, or GATEWAY_BOT_TOKEN+GATEWAY_APP_TOKEN / SLACK_BOT_TOKEN+SLACK_APP_TOKEN for socket mode)" fi fi - if grep -q '^SLACK_ALLOWED_USERS=.\+' "$ENV_FILE" 2>/dev/null; then - pass "SLACK_ALLOWED_USERS is set" + if [ -n "$(read_first_env_value GATEWAY_ALLOWED_USERS SLACK_ALLOWED_USERS)" ]; then + pass "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS is set" else - warn "SLACK_ALLOWED_USERS is not set (all workspace members allowed)" + warn "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS is not set (all workspace members allowed)" fi else if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.config" ]; then diff --git a/bin/lib/baudbot-runtime.sh b/bin/lib/baudbot-runtime.sh index 121cb0d..a63ebab 100644 --- a/bin/lib/baudbot-runtime.sh +++ b/bin/lib/baudbot-runtime.sh @@ -64,8 +64,8 @@ print_deployed_version() { broker_mode_configured() { local env_file="/home/${1:-baudbot_agent}/.config/.env" [ -r "$env_file" ] || return 1 - grep -Eq '^SLACK_BROKER_URL=[^[:space:]].*$' "$env_file" || return 1 - grep -Eq '^SLACK_BROKER_WORKSPACE_ID=[^[:space:]].*$' "$env_file" || return 1 + grep -Eq '^(GATEWAY_BROKER_URL|SLACK_BROKER_URL)=[^[:space:]].*$' "$env_file" || return 1 + grep -Eq '^(GATEWAY_BROKER_WORKSPACE_ID|SLACK_BROKER_WORKSPACE_ID)=[^[:space:]].*$' "$env_file" || return 1 } print_broker_connection_status() { diff --git a/bin/security-audit.sh b/bin/security-audit.sh index 05d9a2b..52e1be1 100755 --- a/bin/security-audit.sh +++ b/bin/security-audit.sh @@ -730,18 +730,23 @@ echo "" echo "Bridge Configuration" -# Check SLACK_ALLOWED_USERS mode (without reading the actual value) +# Check Gateway/Slack allowlist mode (without reading the actual value) if [ -f "$BAUDBOT_HOME/.config/.env" ]; then - if grep -q '^SLACK_ALLOWED_USERS=' "$BAUDBOT_HOME/.config/.env" 2>/dev/null; then - allowed_count=$(grep '^SLACK_ALLOWED_USERS=' "$BAUDBOT_HOME/.config/.env" 2>/dev/null | cut -d= -f2 | tr ',' '\n' | grep -c . || echo 0) + allowed_line=$(grep '^GATEWAY_ALLOWED_USERS=' "$BAUDBOT_HOME/.config/.env" 2>/dev/null | tail -n 1 || true) + if [ -z "$allowed_line" ]; then + allowed_line=$(grep '^SLACK_ALLOWED_USERS=' "$BAUDBOT_HOME/.config/.env" 2>/dev/null | tail -n 1 || true) + fi + if [ -n "$allowed_line" ]; then + allowed_key="${allowed_line%%=*}" + allowed_count=$(printf '%s\n' "$allowed_line" | cut -d= -f2 | tr ',' '\n' | grep -c . || echo 0) if [ "$allowed_count" -gt 0 ]; then - ok "SLACK_ALLOWED_USERS configured ($allowed_count user(s))" + ok "$allowed_key configured ($allowed_count user(s))" else - finding "WARN" "SLACK_ALLOWED_USERS is empty" \ + finding "WARN" "$allowed_key is empty" \ "Bridge will allow all workspace members" fi else - finding "WARN" "SLACK_ALLOWED_USERS not set in .env" \ + finding "WARN" "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set in .env" \ "Bridge will allow all workspace members" fi fi diff --git a/bin/security-audit.test.sh b/bin/security-audit.test.sh index 4f9842f..b83aef8 100644 --- a/bin/security-audit.test.sh +++ b/bin/security-audit.test.sh @@ -126,7 +126,7 @@ HOME3="$TMPDIR/no-allowed" setup_base "$HOME3" output=$(run_audit "$HOME3") -expect_contains "reports missing SLACK_ALLOWED_USERS" "$output" "SLACK_ALLOWED_USERS not set" +expect_contains "reports missing allowlist vars" "$output" "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set" echo "" diff --git a/bin/test.sh b/bin/test.sh index 312abe2..3923a93 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -50,6 +50,7 @@ JS_TEST_FILES=( pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs slack-bridge/security.test.mjs + slack-bridge/env-aliases.test.mjs bin/scan-extensions.test.mjs bin/broker-register.test.mjs ) @@ -59,6 +60,7 @@ JS_TEST_NAMES=( "heartbeat" "memory" "bridge security" + "gateway env aliases" "extension scanner" "broker register" ) diff --git a/install.sh b/install.sh index 3cc7a11..917310b 100755 --- a/install.sh +++ b/install.sh @@ -274,27 +274,33 @@ fi HAS_SOCKET=false HAS_BROKER=false -if grep -q '^SLACK_BOT_TOKEN=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_APP_TOKEN=.\+' "$ENV_FILE" 2>/dev/null; then +has_env_any() { + local preferred_key="$1" + local legacy_key="$2" + grep -q "^${preferred_key}=.\+" "$ENV_FILE" 2>/dev/null || grep -q "^${legacy_key}=.\+" "$ENV_FILE" 2>/dev/null +} + +if has_env_any GATEWAY_BOT_TOKEN SLACK_BOT_TOKEN \ + && has_env_any GATEWAY_APP_TOKEN SLACK_APP_TOKEN; then HAS_SOCKET=true fi -if grep -q '^SLACK_BROKER_URL=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_WORKSPACE_ID=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_SERVER_PRIVATE_KEY=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_SERVER_PUBLIC_KEY=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_PUBLIC_KEY=.\+' "$ENV_FILE" 2>/dev/null \ - && grep -q '^SLACK_BROKER_SIGNING_PUBLIC_KEY=.\+' "$ENV_FILE" 2>/dev/null; then +if has_env_any GATEWAY_BROKER_URL SLACK_BROKER_URL \ + && has_env_any GATEWAY_BROKER_WORKSPACE_ID SLACK_BROKER_WORKSPACE_ID \ + && has_env_any GATEWAY_BROKER_SERVER_PRIVATE_KEY SLACK_BROKER_SERVER_PRIVATE_KEY \ + && has_env_any GATEWAY_BROKER_SERVER_PUBLIC_KEY SLACK_BROKER_SERVER_PUBLIC_KEY \ + && has_env_any GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY \ + && has_env_any GATEWAY_BROKER_PUBLIC_KEY SLACK_BROKER_PUBLIC_KEY \ + && has_env_any GATEWAY_BROKER_SIGNING_PUBLIC_KEY SLACK_BROKER_SIGNING_PUBLIC_KEY; then HAS_BROKER=true fi if [ "$HAS_SOCKET" = false ] && [ "$HAS_BROKER" = false ]; then - MISSING+=" - Slack integration (either SLACK_BOT_TOKEN + SLACK_APP_TOKEN, or broker registration via 'sudo baudbot broker register')\n" + MISSING+=" - Gateway bridge integration (either GATEWAY_BOT_TOKEN + GATEWAY_APP_TOKEN, legacy SLACK_BOT_TOKEN + SLACK_APP_TOKEN, or broker registration via 'sudo baudbot broker register')\n" fi -if ! grep -q '^SLACK_ALLOWED_USERS=.\+' "$ENV_FILE" 2>/dev/null; then - warn "SLACK_ALLOWED_USERS not set — all workspace members will be allowed" +if ! has_env_any GATEWAY_ALLOWED_USERS SLACK_ALLOWED_USERS; then + warn "GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set — all workspace members will be allowed" fi if [ -n "$MISSING" ]; then diff --git a/pi/extensions/sentry-monitor.ts b/pi/extensions/sentry-monitor.ts index 2f97162..fcbf0a9 100644 --- a/pi/extensions/sentry-monitor.ts +++ b/pi/extensions/sentry-monitor.ts @@ -13,19 +13,23 @@ import { StringEnum } from "@mariozechner/pi-ai"; * Investigation: Sentry API for deep-dive on flagged issues * * Requires: - * SLACK_BOT_TOKEN — Slack bot OAuth token - * SENTRY_AUTH_TOKEN — Sentry API bearer token - * SENTRY_ORG — Sentry organization slug - * SENTRY_CHANNEL_ID — Slack channel ID for #bots-sentry + * GATEWAY_BOT_TOKEN or SLACK_BOT_TOKEN — Slack bot OAuth token + * SENTRY_AUTH_TOKEN — Sentry API bearer token + * SENTRY_ORG — Sentry organization slug + * SENTRY_CHANNEL_ID — Slack channel ID for #bots-sentry */ // ── Config ──────────────────────────────────────────────────────────────────── const SENTRY_ORG = process.env.SENTRY_ORG || ""; const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN || ""; -const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || ""; +const SLACK_BOT_TOKEN = process.env.GATEWAY_BOT_TOKEN || process.env.SLACK_BOT_TOKEN || ""; let SENTRY_CHANNEL_ID = process.env.SENTRY_CHANNEL_ID || ""; +if (!process.env.GATEWAY_BOT_TOKEN && process.env.SLACK_BOT_TOKEN) { + console.warn("⚠️ SLACK_BOT_TOKEN is deprecated; set GATEWAY_BOT_TOKEN instead (legacy fallback still supported)."); +} + // ── Types ───────────────────────────────────────────────────────────────────── interface SlackMessage { @@ -113,7 +117,7 @@ export default function (pi: ExtensionAPI) { // ── Slack API ─────────────────────────────────────────────────────────── async function slackGet(method: string, params: Record = {}): Promise { - if (!SLACK_BOT_TOKEN) throw new Error("SLACK_BOT_TOKEN not set"); + if (!SLACK_BOT_TOKEN) throw new Error("GATEWAY_BOT_TOKEN/SLACK_BOT_TOKEN not set"); const qs = new URLSearchParams(params).toString(); const url = `https://slack.com/api/${method}${qs ? `?${qs}` : ""}`; const res = await fetch(url, { @@ -125,7 +129,7 @@ export default function (pi: ExtensionAPI) { } async function slackPost(method: string, body: Record): Promise { - if (!SLACK_BOT_TOKEN) throw new Error("SLACK_BOT_TOKEN not set"); + if (!SLACK_BOT_TOKEN) throw new Error("GATEWAY_BOT_TOKEN/SLACK_BOT_TOKEN not set"); const res = await fetch(`https://slack.com/api/${method}`, { method: "POST", headers: { @@ -389,7 +393,7 @@ export default function (pi: ExtensionAPI) { switch (params.action) { case "start": { if (!SLACK_BOT_TOKEN) { - text = "❌ SLACK_BOT_TOKEN not set. Add it to ~/.config/.env and restart."; + text = "❌ GATEWAY_BOT_TOKEN/SLACK_BOT_TOKEN not set. Add one to ~/.config/.env and restart."; break; } if (params.interval_minutes) { @@ -420,7 +424,7 @@ export default function (pi: ExtensionAPI) { ` Baseline: ${state.baselineComplete ? "complete" : "pending"}`, ` Tracked messages: ${state.seenTs.size}`, ` Channel ID: ${SENTRY_CHANNEL_ID || "(will resolve on start)"}`, - ` Slack token: ${SLACK_BOT_TOKEN ? "✅ set" : "❌ missing"}`, + ` Gateway/Slack token: ${SLACK_BOT_TOKEN ? "✅ set" : "❌ missing"}`, ` Sentry token: ${SENTRY_AUTH_TOKEN ? "✅ set" : "❌ missing"}`, ` Org: ${SENTRY_ORG}`, ].join("\n"); @@ -429,7 +433,7 @@ export default function (pi: ExtensionAPI) { case "check": { if (!SLACK_BOT_TOKEN) { - text = "❌ SLACK_BOT_TOKEN not set."; + text = "❌ GATEWAY_BOT_TOKEN/SLACK_BOT_TOKEN not set."; break; } try { @@ -467,7 +471,7 @@ export default function (pi: ExtensionAPI) { case "list": { if (!SLACK_BOT_TOKEN) { - text = "❌ SLACK_BOT_TOKEN not set."; + text = "❌ GATEWAY_BOT_TOKEN/SLACK_BOT_TOKEN not set."; break; } try { diff --git a/pi/skills/control-agent/startup-pi.sh b/pi/skills/control-agent/startup-pi.sh index f78189d..0f2f0c1 100755 --- a/pi/skills/control-agent/startup-pi.sh +++ b/pi/skills/control-agent/startup-pi.sh @@ -93,22 +93,22 @@ mkdir -p "$BRIDGE_LOG_DIR" # --- Detect bridge mode --- BRIDGE_SCRIPT="" if [ -f "$BRIDGE_DIR/broker-bridge.mjs" ] && varlock run --path "$HOME/.config/" -- sh -c ' - test -n "$SLACK_BROKER_URL" && - test -n "$SLACK_BROKER_WORKSPACE_ID" && - test -n "$SLACK_BROKER_SERVER_PRIVATE_KEY" && - test -n "$SLACK_BROKER_SERVER_PUBLIC_KEY" && - test -n "$SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY" && - test -n "$SLACK_BROKER_PUBLIC_KEY" && - test -n "$SLACK_BROKER_SIGNING_PUBLIC_KEY"' 2>/dev/null; then + test -n "${GATEWAY_BROKER_URL:-${SLACK_BROKER_URL:-}}" && + test -n "${GATEWAY_BROKER_WORKSPACE_ID:-${SLACK_BROKER_WORKSPACE_ID:-}}" && + test -n "${GATEWAY_BROKER_SERVER_PRIVATE_KEY:-${SLACK_BROKER_SERVER_PRIVATE_KEY:-}}" && + test -n "${GATEWAY_BROKER_SERVER_PUBLIC_KEY:-${SLACK_BROKER_SERVER_PUBLIC_KEY:-}}" && + test -n "${GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY:-${SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY:-}}" && + test -n "${GATEWAY_BROKER_PUBLIC_KEY:-${SLACK_BROKER_PUBLIC_KEY:-}}" && + test -n "${GATEWAY_BROKER_SIGNING_PUBLIC_KEY:-${SLACK_BROKER_SIGNING_PUBLIC_KEY:-}}"' 2>/dev/null; then BRIDGE_SCRIPT="broker-bridge.mjs" elif [ -f "$BRIDGE_DIR/bridge.mjs" ] && varlock run --path "$HOME/.config/" -- sh -c ' - test -n "$SLACK_BOT_TOKEN" && - test -n "$SLACK_APP_TOKEN"' 2>/dev/null; then + test -n "${GATEWAY_BOT_TOKEN:-${SLACK_BOT_TOKEN:-}}" && + test -n "${GATEWAY_APP_TOKEN:-${SLACK_APP_TOKEN:-}}"' 2>/dev/null; then BRIDGE_SCRIPT="bridge.mjs" fi if [ -z "$BRIDGE_SCRIPT" ]; then - echo "No Slack transport configured (missing broker keys and socket tokens); skipping bridge startup." + echo "No Gateway transport configured (missing broker keys and socket tokens); skipping bridge startup." echo "" echo "=== Startup Complete ===" exit 0 @@ -117,7 +117,7 @@ fi # --- Launch bridge in a tmux session with supervised restart loop --- # The restart loop: # - Re-reads .env on every restart (picks up config changes) -# - Unsets SLACK_BROKER_* before sourcing (avoids stale parent env) +# - Unsets SLACK_BROKER_* and GATEWAY_BROKER_* before sourcing (avoids stale parent env) # - Tracks consecutive fast failures (<60s runtime) and gives up after 10 # - Backs off: 5s base + 2s per failure, capped at 60s # - Kills port holders before retrying (avoids EADDRINUSE spin) @@ -156,7 +156,7 @@ tmux new-session -d -s "$BRIDGE_TMUX_SESSION" "\ while true; do \ echo \"[\$(date -Is)] bridge: starting $BRIDGE_SCRIPT (attempt \$((consecutive_failures + 1)))\" >> $BRIDGE_LOG_FILE; \ start_time=\$(date +%s); \ - for v in \$(env | grep ^SLACK_BROKER_ | cut -d= -f1 || true); do unset \$v; done; \ + for v in \$(env | grep -E '^(SLACK_BROKER_|GATEWAY_BROKER_)' | cut -d= -f1 || true); do unset \$v; done; \ set -a; source \$HOME/.config/.env; set +a; \ node $BRIDGE_SCRIPT >> $BRIDGE_LOG_FILE 2>&1; \ exit_code=\$?; \ diff --git a/pi/skills/sentry-agent/SKILL.md b/pi/skills/sentry-agent/SKILL.md index 2e3be1f..cefe6c0 100644 --- a/pi/skills/sentry-agent/SKILL.md +++ b/pi/skills/sentry-agent/SKILL.md @@ -110,4 +110,4 @@ Required env vars (injected via `varlock` at launch): - `SENTRY_AUTH_TOKEN` — Sentry API bearer token - `SENTRY_CHANNEL_ID` — Slack channel ID for Sentry alerts - `SENTRY_ORG` — Sentry organization slug -- `SLACK_BOT_TOKEN` — Slack bot OAuth token (required by `list` action) +- `GATEWAY_BOT_TOKEN` (preferred) or `SLACK_BOT_TOKEN` (legacy) — Slack bot OAuth token (required by `list` action) diff --git a/slack-bridge/bridge.mjs b/slack-bridge/bridge.mjs index c310f66..9f119fd 100644 --- a/slack-bridge/bridge.mjs +++ b/slack-bridge/bridge.mjs @@ -6,15 +6,15 @@ * Uses Socket Mode (no public URL needed). * * Required env vars: - * SLACK_BOT_TOKEN - Slack bot OAuth token - * SLACK_APP_TOKEN - Slack app-level token (for Socket Mode) - * SLACK_ALLOWED_USERS - comma-separated Slack user IDs (optional — allow all if unset) + * GATEWAY_BOT_TOKEN or SLACK_BOT_TOKEN - Slack bot OAuth token + * GATEWAY_APP_TOKEN or SLACK_APP_TOKEN - Slack app-level token (for Socket Mode) + * GATEWAY_ALLOWED_USERS or SLACK_ALLOWED_USERS - comma-separated Slack user IDs (optional — allow all if unset) * * Optional: - * PI_SESSION_ID - target pi session ID (defaults to auto-detect control-agent) - * SLACK_CHANNEL_ID - if set, also responds to all messages in this channel (not just @mentions) - * SENTRY_CHANNEL_ID - Slack channel ID for Sentry alerts (forwarded to agent) - * BRIDGE_API_PORT - outbound API port (default: 7890) + * PI_SESSION_ID - target pi session ID (defaults to auto-detect control-agent) + * GATEWAY_CHANNEL_ID or SLACK_CHANNEL_ID - if set, also responds to all messages in this channel (not just @mentions) + * SENTRY_CHANNEL_ID - Slack channel ID for Sentry alerts (forwarded to agent) + * BRIDGE_API_PORT - outbound API port (default: 7890) */ // Env vars loaded and validated by varlock (via `varlock run` or `start.sh`). @@ -35,6 +35,12 @@ import { validateReactParams, createRateLimiter, } from "./security.mjs"; +import { applyGatewayEnvAliases } from "./env-aliases.mjs"; + +const gatewayAliasWarnings = applyGatewayEnvAliases(process.env).warnings; +for (const warning of gatewayAliasWarnings) { + console.warn(warning); +} // ── Config ────────────────────────────────────────────────────────────────── @@ -45,7 +51,8 @@ const API_PORT = parseInt(process.env.BRIDGE_API_PORT || "7890", 10); // Validate required env vars for (const key of ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]) { if (!process.env[key]) { - console.error(`❌ Missing required env var: ${key}`); + const gatewayAlias = key.replace(/^SLACK_/, "GATEWAY_"); + console.error(`❌ Missing required env var: ${key} (or ${gatewayAlias})`); process.exit(1); } } @@ -55,7 +62,7 @@ for (const key of ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]) { const ALLOWED_USERS = parseAllowedUsers(process.env.SLACK_ALLOWED_USERS); if (ALLOWED_USERS.length === 0) { - console.warn("⚠️ SLACK_ALLOWED_USERS not set — all workspace members can interact"); + console.warn("⚠️ GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set — all workspace members can interact"); } console.log(`🔒 Access control: ${ALLOWED_USERS.length || 'all'} allowed user(s)`); diff --git a/slack-bridge/broker-bridge.mjs b/slack-bridge/broker-bridge.mjs index 4d93ea6..797e81c 100755 --- a/slack-bridge/broker-bridge.mjs +++ b/slack-bridge/broker-bridge.mjs @@ -34,6 +34,9 @@ import { canonicalizeProtocolRequest, canonicalizeSendRequest, } from "./crypto.mjs"; +import { applyGatewayEnvAliases } from "./env-aliases.mjs"; + +const gatewayAliasWarnings = applyGatewayEnvAliases(process.env).warnings; const SOCKET_DIR = path.join(homedir(), ".pi", "session-control"); const AGENT_TIMEOUT_MS = 120_000; @@ -120,6 +123,10 @@ function logWarn(...args) { logWithLevel("warn", ...args); } +for (const warning of gatewayAliasWarnings) { + logWarn(warning); +} + for (const key of [ "SLACK_BROKER_URL", "SLACK_BROKER_WORKSPACE_ID", @@ -131,14 +138,15 @@ for (const key of [ "SLACK_BROKER_ACCESS_TOKEN", ]) { if (!process.env[key]) { - logError(`❌ Missing required env var for broker mode: ${key}`); + const gatewayAlias = key.replace(/^SLACK_/, "GATEWAY_"); + logError(`❌ Missing required env var for broker mode: ${key} (or ${gatewayAlias})`); process.exit(1); } } const ALLOWED_USERS = parseAllowedUsers(process.env.SLACK_ALLOWED_USERS); if (ALLOWED_USERS.length === 0) { - logWarn("⚠️ SLACK_ALLOWED_USERS not set — all workspace members can interact"); + logWarn("⚠️ GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set — all workspace members can interact"); } const GITHUB_IGNORED_USERS = parseIgnoredUsers(process.env.GITHUB_IGNORED_USERS); @@ -562,7 +570,7 @@ function isBrokerAccessTokenExpired() { const ts = Date.parse(brokerAccessTokenExpiresAt); if (!Number.isFinite(ts)) { if (!brokerTokenExpiryFormatWarned) { - logWarn("⚠️ invalid SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT format; expected ISO-8601 timestamp"); + logWarn("⚠️ invalid SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT/GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT format; expected ISO-8601 timestamp"); brokerTokenExpiryFormatWarned = true; } return false; diff --git a/slack-bridge/env-aliases.mjs b/slack-bridge/env-aliases.mjs new file mode 100644 index 0000000..5b3cf61 --- /dev/null +++ b/slack-bridge/env-aliases.mjs @@ -0,0 +1,80 @@ +/** + * Gateway bridge environment alias helpers. + * + * Migration rule: + * - Prefer GATEWAY_* variables when present. + * - Fall back to legacy SLACK_* variables. + * - Emit non-breaking deprecation warnings when legacy vars are used. + */ + +export const GATEWAY_ENV_ALIAS_PAIRS = [ + ["GATEWAY_BOT_TOKEN", "SLACK_BOT_TOKEN"], + ["GATEWAY_APP_TOKEN", "SLACK_APP_TOKEN"], + ["GATEWAY_ALLOWED_USERS", "SLACK_ALLOWED_USERS"], + ["GATEWAY_CHANNEL_ID", "SLACK_CHANNEL_ID"], + ["GATEWAY_BROKER_URL", "SLACK_BROKER_URL"], + ["GATEWAY_BROKER_WORKSPACE_ID", "SLACK_BROKER_WORKSPACE_ID"], + ["GATEWAY_BROKER_SERVER_PRIVATE_KEY", "SLACK_BROKER_SERVER_PRIVATE_KEY"], + ["GATEWAY_BROKER_SERVER_PUBLIC_KEY", "SLACK_BROKER_SERVER_PUBLIC_KEY"], + ["GATEWAY_BROKER_SERVER_SIGNING_PRIVATE_KEY", "SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY"], + ["GATEWAY_BROKER_SERVER_SIGNING_PUBLIC_KEY", "SLACK_BROKER_SERVER_SIGNING_PUBLIC_KEY"], + ["GATEWAY_BROKER_PUBLIC_KEY", "SLACK_BROKER_PUBLIC_KEY"], + ["GATEWAY_BROKER_SIGNING_PUBLIC_KEY", "SLACK_BROKER_SIGNING_PUBLIC_KEY"], + ["GATEWAY_BROKER_ACCESS_TOKEN", "SLACK_BROKER_ACCESS_TOKEN"], + ["GATEWAY_BROKER_ACCESS_TOKEN_EXPIRES_AT", "SLACK_BROKER_ACCESS_TOKEN_EXPIRES_AT"], + ["GATEWAY_BROKER_ACCESS_TOKEN_SCOPES", "SLACK_BROKER_ACCESS_TOKEN_SCOPES"], + ["GATEWAY_BROKER_POLL_INTERVAL_MS", "SLACK_BROKER_POLL_INTERVAL_MS"], + ["GATEWAY_BROKER_MAX_MESSAGES", "SLACK_BROKER_MAX_MESSAGES"], + ["GATEWAY_BROKER_WAIT_SECONDS", "SLACK_BROKER_WAIT_SECONDS"], + ["GATEWAY_BROKER_DEDUPE_TTL_MS", "SLACK_BROKER_DEDUPE_TTL_MS"], +]; + +function hasConfiguredValue(value) { + return value !== undefined && value !== null && value !== ""; +} + +export function resolveGatewayEnvAliases(env = process.env) { + const resolved = {}; + const warnings = []; + const legacyFallbackKeys = []; + + for (const [gatewayKey, legacySlackKey] of GATEWAY_ENV_ALIAS_PAIRS) { + const gatewayValue = env[gatewayKey]; + const legacySlackValue = env[legacySlackKey]; + const hasGateway = hasConfiguredValue(gatewayValue); + const hasLegacySlack = hasConfiguredValue(legacySlackValue); + + if (hasGateway) { + resolved[legacySlackKey] = String(gatewayValue); + if (hasLegacySlack && String(gatewayValue) !== String(legacySlackValue)) { + warnings.push( + `⚠️ Both ${gatewayKey} and ${legacySlackKey} are set; using ${gatewayKey} and ignoring ${legacySlackKey}.`, + ); + } + continue; + } + + if (hasLegacySlack) { + resolved[legacySlackKey] = String(legacySlackValue); + legacyFallbackKeys.push(`${legacySlackKey}→${gatewayKey}`); + } + } + + if (legacyFallbackKeys.length > 0) { + warnings.push( + `⚠️ Using legacy SLACK_* env vars (${legacyFallbackKeys.join(", ")}); set GATEWAY_* aliases instead (legacy fallback still supported).`, + ); + } + + return { resolved, warnings }; +} + +export function applyGatewayEnvAliases(env = process.env) { + const { resolved, warnings } = resolveGatewayEnvAliases(env); + + for (const [legacySlackKey, value] of Object.entries(resolved)) { + env[legacySlackKey] = value; + } + + return { warnings }; +} diff --git a/slack-bridge/env-aliases.test.mjs b/slack-bridge/env-aliases.test.mjs new file mode 100644 index 0000000..aee95f7 --- /dev/null +++ b/slack-bridge/env-aliases.test.mjs @@ -0,0 +1,66 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { applyGatewayEnvAliases, resolveGatewayEnvAliases } from "./env-aliases.mjs"; + +test("falls back to legacy SLACK_* var and emits deprecation warning", () => { + const env = { + SLACK_BOT_TOKEN: "xoxb-legacy-token", + }; + + const result = resolveGatewayEnvAliases(env); + + assert.equal(result.resolved.SLACK_BOT_TOKEN, "xoxb-legacy-token"); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /Using legacy SLACK_\* env vars/); + assert.match(result.warnings[0], /SLACK_BOT_TOKEN→GATEWAY_BOT_TOKEN/); +}); + +test("prefers GATEWAY_* when both gateway and legacy vars are set", () => { + const env = { + GATEWAY_BOT_TOKEN: "xoxb-gateway-token", + SLACK_BOT_TOKEN: "xoxb-legacy-token", + }; + + const result = resolveGatewayEnvAliases(env); + + assert.equal(result.resolved.SLACK_BOT_TOKEN, "xoxb-gateway-token"); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /Both GATEWAY_BOT_TOKEN and SLACK_BOT_TOKEN are set/); + assert.match(result.warnings[0], /using GATEWAY_BOT_TOKEN/); +}); + +test("does not emit conflict warning when both aliases are set to the same value", () => { + const env = { + GATEWAY_ALLOWED_USERS: "U1,U2", + SLACK_ALLOWED_USERS: "U1,U2", + }; + + const result = resolveGatewayEnvAliases(env); + + assert.equal(result.resolved.SLACK_ALLOWED_USERS, "U1,U2"); + assert.equal(result.warnings.length, 0); +}); + +test("empty gateway value falls back to legacy slack value", () => { + const env = { + GATEWAY_CHANNEL_ID: "", + SLACK_CHANNEL_ID: "C123", + }; + + const result = resolveGatewayEnvAliases(env); + + assert.equal(result.resolved.SLACK_CHANNEL_ID, "C123"); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /SLACK_CHANNEL_ID→GATEWAY_CHANNEL_ID/); +}); + +test("applyGatewayEnvAliases mutates env with resolved canonical legacy keys", () => { + const env = { + GATEWAY_BROKER_URL: "https://broker.gateway.example", + }; + + const result = applyGatewayEnvAliases(env); + + assert.equal(env.SLACK_BROKER_URL, "https://broker.gateway.example"); + assert.equal(result.warnings.length, 0); +}); diff --git a/slack-bridge/package.json b/slack-bridge/package.json index c0b45cd..cc55226 100644 --- a/slack-bridge/package.json +++ b/slack-bridge/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "start": "node bridge.mjs", - "test": "node --test security.test.mjs" + "test": "node --test security.test.mjs env-aliases.test.mjs" }, "dependencies": { "@slack/bolt": "^4.6.0", diff --git a/slack-bridge/security.mjs b/slack-bridge/security.mjs index 0cfc59f..df30a66 100644 --- a/slack-bridge/security.mjs +++ b/slack-bridge/security.mjs @@ -226,7 +226,7 @@ export function sanitizeOutboundText(input) { // ── Access Control ────────────────────────────────────────────────────────── /** - * Parse SLACK_ALLOWED_USERS env var into a list. + * Parse GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS env var into a list. */ export function parseAllowedUsers(envValue) { const users = (envValue || "") diff --git a/test/legacy-node-tests.test.mjs b/test/legacy-node-tests.test.mjs index 977004e..a96d5ae 100644 --- a/test/legacy-node-tests.test.mjs +++ b/test/legacy-node-tests.test.mjs @@ -27,6 +27,10 @@ describe("legacy node:test suites", () => { expect(() => runNodeTest("slack-bridge/security.test.mjs")).not.toThrow(); }); + it("gateway env aliases", () => { + expect(() => runNodeTest("slack-bridge/env-aliases.test.mjs")).not.toThrow(); + }); + it("extension scanner", () => { expect(() => runNodeTest("bin/scan-extensions.test.mjs")).not.toThrow(); }); diff --git a/test/security-audit.test.mjs b/test/security-audit.test.mjs index 7e7255f..b927bcd 100644 --- a/test/security-audit.test.mjs +++ b/test/security-audit.test.mjs @@ -87,11 +87,11 @@ describe("security-audit.sh", () => { } }); - it("reports missing SLACK_ALLOWED_USERS when unset", async () => { + it("reports missing allowlist vars when unset", async () => { setupFixture(tmpRoot); const { output } = await runAuditWithLocalBridge(tmpRoot); - expect(output).toContain("SLACK_ALLOWED_USERS not set"); + expect(output).toContain("GATEWAY_ALLOWED_USERS/SLACK_ALLOWED_USERS not set"); }); it("reports configured SLACK_ALLOWED_USERS when set", async () => {