// XEM Guides

Slack Approval Bot

Route approval gates to a Slack channel with one-click approve and reject buttons. The router emits exec.approval_requested; a small bot posts an interactive message and calls back to the approve or reject endpoint on click. Expected time: 45 minutes including Slack app setup.

Prerequisites

  • An API key with the execution scope.
  • A Slack workspace with permission to create an app + bot.
  • A public HTTPS endpoint for the bot (e.g. ngrok for dev).
  • The webhook family enabled for your project (see Webhooks).

Environment

Five environment variables drive the bot. Set them once; the snippets below reference them by name.

Variable Purpose
SLACK_BOT_TOKEN Bot user OAuth token (xoxb-...); needs chat:write + chat:update.
XEROTIER_WEBHOOK_SECRET Hex secret returned by xeroctl webhooks --create. Used to verify HMAC.
XEROTIER_API_KEY Service-account key with the execution scope.
XEROTIER_BASE Router base URL, e.g. https://xerotier.ai.
XEROTIER_PROJECT_ID Project external id (prj_...); read with xeroctl auth whoami.

Step 1: Register the Webhook

Point the Xerotier webhook at your bot and subscribe to the four terminal approval states:

  • exec.approval_requested -- new pending approval, post a Slack message.
  • exec.approved -- decision recorded; mark the Slack message resolved.
  • exec.rejected -- decision recorded; mark the Slack message resolved.
  • exec.approval_timed_out -- no decision in window; mark the Slack message stale.
bash
xeroctl webhooks --create \ --url https://your-bot.example.com/xerotier \ --events exec.approval_requested,exec.approved,exec.rejected,exec.approval_timed_out \ --secret "$(openssl rand -hex 32)"

Save the secret. The bot verifies every inbound request via HMAC using this value.

Step 2: Write the Slack Bot

A minimal Python + slack_sdk implementation. Only the webhook-receive path is shown; Slack app setup is standard.

python
import os import hmac import hashlib from flask import Flask, request, abort from slack_sdk import WebClient app = Flask(__name__) slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) XEROTIER_SECRET = os.environ["XEROTIER_WEBHOOK_SECRET"] APPROVALS_CHANNEL = "#xem-approvals" def verify_signature(body, signature, timestamp): # Router emits "sha256=<hex>" where hex is # HMAC-SHA256(secret, "<timestamp>.<body>"). if not signature.startswith("sha256="): return False expected = signature.split("=", 1)[1] signed_payload = f"{timestamp}.".encode() + body mac = hmac.new(XEROTIER_SECRET.encode(), signed_payload, hashlib.sha256) return hmac.compare_digest(mac.hexdigest(), expected) @app.route("/xerotier", methods=["POST"]) def receive(): sig = request.headers.get("X-Webhook-Signature", "") ts = request.headers.get("X-Webhook-Timestamp", "") if not verify_signature(request.data, sig, ts): abort(401) event = request.get_json() # Only act on the requested event; other terminal events # (approved/rejected/timed_out) are handled separately. if event.get("event_type") != "exec.approval_requested": return "", 204 slack.chat_postMessage( channel=APPROVALS_CHANNEL, text=f"Approval requested: {event['tool_name']}", blocks=[ {"type": "section", "text": {"type": "mrkdwn", "text": f"*Tool:* `{event['tool_name']}`\n" f"*Workspace:* {event['workspace_id']}\n" f"*Risk:* {event['risk']}\n" f"*Invocation:* `{event['invocation_id']}`"}}, {"type": "actions", "elements": [ {"type": "button", "text": {"type": "plain_text", "text": "Approve"}, "style": "primary", "value": event["approval_id"], "action_id": "approve"}, {"type": "button", "text": {"type": "plain_text", "text": "Reject"}, "style": "danger", "value": event["approval_id"], "action_id": "reject"}, ]} ]) return "", 204

Step 3: Interactive Buttons

Slack posts button clicks to the bot's /slack/actions endpoint. Resolve them by calling back to the Xerotier router:

python
import json import os import requests XEROTIER_API_KEY = os.environ["XEROTIER_API_KEY"] XEROTIER_BASE = os.environ["XEROTIER_BASE"] XEROTIER_PROJECT_ID = os.environ["XEROTIER_PROJECT_ID"] @app.route("/slack/actions", methods=["POST"]) def slack_action(): payload = json.loads(request.form["payload"]) action = payload["actions"][0] approval_id = action["value"] verb = action["action_id"] # "approve" or "reject" # Approve/reject routes are mounted under the project prefix. resp = requests.post( f"{XEROTIER_BASE}/{XEROTIER_PROJECT_ID}" f"/v1/exec/approvals/{approval_id}/{verb}", headers={"Authorization": f"Bearer {XEROTIER_API_KEY}"}) resp.raise_for_status() return "", 200

The approve/reject handler reads no request body; the approver recorded in the audit log is the API- key user id bound to the bot's service-account key. Provision a dedicated service-account key for the bot so the audit trail clearly attributes decisions to the Slack channel. The router callback path is mounted under /:project_id/v1/exec/approvals/...; obtain the project id from xeroctl auth whoami (the key's bound project) and pass it to the bot as XEROTIER_PROJECT_ID.

Step 4: Verify End-to-End

  1. Invoke a gated tool (hypothetical example): xeroctl exec invoke-blocking --tool kubectl_drain .... Use invoke-blocking so the operator's shell waits for the terminal outcome; xeroctl exec invoke returns as soon as the invocation is persisted in pending_approval and does not surface the post-approval tool result in the same shell.
  2. A message appears in #xem-approvals within a few seconds.
  3. Click Approve.
  4. The pending invocation proceeds. The invoke-blocking caller receives the tool result. If you used the non-blocking form, retrieve the result with xeroctl exec poll <invocation_id>.

Productionizing

  • Run the bot behind a proper HTTPS endpoint with a certificate. Do not leave ngrok in place.
  • Use a dedicated service-account API key with the minimum scope (execution only).
  • Log every approval decision to your audit system; the Xerotier audit log is authoritative, but a parallel record simplifies Slack-side forensics.
  • Wire the exec.approval_timed_out event to update the Slack message so stale approvals do not remain clickable. For complete terminal coverage also subscribe to exec.approved and exec.rejected and update the message in the receive handler.

Troubleshooting

401 at /xerotier
HMAC mismatch. Verify XEROTIER_WEBHOOK_SECRET matches the value returned by xeroctl webhooks --create, and that the canonical string is <timestamp>.<raw-body> (no JSON re-encoding before signing).
Slack message posted, Approve click does nothing
Either XEROTIER_PROJECT_ID is wrong (callback returns 404) or the API key lacks the execution scope (callback returns 403). Confirm with xeroctl auth whoami and xeroctl api-keys show.
Approval shown as already resolved
Either the Slack retry hit the callback twice (see the note in Step 3) or the approval window expired and an exec.approval_timed_out event raced ahead. Mark the Slack message stale on the timed-out event so the buttons read as resolved before the user clicks.