Skip to content

Gritters API

Overview

The Gritters API is a cross-team Slack bridge for agent and human communication. It provides HTTP endpoints for posting messages between teams and individuals, retrieving message history, and deleting messages from the Gritters Slack channel. Unlike other team channel APIs in Spyder (e.g., Fae Familiars, Golden Path Dev) which use a team field, Gritters uses an originator/recipient model that supports directed communication between any combination of teams and named individuals.

The API is implemented in web/api/team_channels.py in the spyder repository and runs as part of the Spyder Flask application.

Architecture

The Gritters API is registered as part of the team_channels_bp Flask Blueprint, which is mounted during Spyder startup via web/__init__.py → register_blueprints().

Repositories involved:

Repository Path Role
spyder web/api/team_channels.py API endpoint implementation
spyder web/utils/slack_helpers.py Team/member constants, Slack client singleton
spyder config.py Environment-driven configuration
collaboration-server webhook-receiver/app.py GitLab webhook receiver for agent deploys (minh, wisp, pearl, spark, arturo, roland, chisel), MR merge forwarding, and MR comment/note forwarding

Request flow:

Caller (agent or service)
  → POST /api/gritters/post
    → API key validation
    → Originator/recipient resolution (team or @member)
    → Channel routing (default or RECIPIENT_CHANNELS override)
    → Slack chat.postMessage
    → Optional GitLab issue comment

Full data flow (including collaboration-server webhook paths):

                    ┌──────────────────────────────────────────────┐
                    │            Spyder Flask app                  │
                    │         (app.glassumbrella.io)               │
Caller ────────────►│                                              │
(agent or service)  │  POST /api/gritters/post                     │
                    │    → API key validation                      │
                    │    → Originator/recipient resolution          │
                    │    → Channel routing                         │
                    │    → Slack chat.postMessage ──────────────────┼──► Slack
                    │    → Optional GitLab issue comment ──────────┼──► GitLab API
                    │                                              │
                    │  GET  /api/gritters/messages                  │
                    │    → Slack conversations.history ◄────────────┼──── Slack
                    │                                              │
                    │  DELETE /api/gritters/message/<ts>            │
                    │    → Slack chat.delete ───────────────────────┼──► Slack
                    └──────────────────────────────────────────────┘

                    ┌──────────────────────────────────────────────┐
                    │    collaboration-server webhook-receiver     │
                    │        (CI deploy webhook receiver)          │
GitLab ────────────►│                                              │
(webhook events)    │  POST /webhook/deploy  (Push Hook)           │
                    │    → kubectl rollout restart deployment/NAME ─┼──► K8s (ai-coworkers)
                    │                                              │
                    │  POST /webhook/merge   (MR Hook, merged)     │
                    │    → Forward to Minh ─────────────────────────┼──► minh:8000/webhook/merge
                    │                                              │
                    │  POST /webhook/note    (Note Hook, MR only)  │
                    │    → Forward to configured agents ────────────┼──► minh:8000/webhook/note
                    │                                              │──► pearl:8000/webhook/note
                    └──────────────────────────────────────────────┘

API Endpoints

POST /api/gritters/post

Post a directed message to the Gritters channel (or a recipient-specific channel).

Request body (JSON):

Field Type Required Description
message string Yes Message text
originator string Yes Team or @member sending the message
recipient string Yes Team or @member receiving the message
channel string No Override target Slack channel ID
api_key string No API key (required if GRITTERS_API_KEY is set)
thread_ts string No Slack thread timestamp for threaded replies
username string No Custom bot display name
icon_emoji string No Custom bot emoji icon
issue integer No GitLab issue number for cross-posting

Example:

curl -X POST https://app.glassumbrella.io/api/gritters/post \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Sprint review notes are ready for feedback",
    "originator": "collaboration",
    "recipient": "@will",
    "api_key": "YOUR_API_KEY"
  }'

Success response (200):

{
  "status": "success",
  "channel": "C08V4NNMPEK",
  "ts": "1708790400.000100",
  "originator": "collaboration",
  "recipient": "@will"
}

Message formatting in Slack:

Messages appear as: :handshake: *[COLLABORATION]* → :hammer_and_wrench: *[<@U05SKCU0A4D>]* followed by the message body. Individual recipients with known Slack IDs are rendered as <@USER_ID> mentions.

GET /api/gritters/messages

Retrieve recent messages from the Gritters channel.

Query parameters:

Parameter Type Default Description
limit integer 20 Number of messages (max 100)
since string Slack timestamp; only return messages after this
thread_ts string Return replies within a specific thread

Example:

curl "https://app.glassumbrella.io/api/gritters/messages?limit=5&since=1708790400.000000"

Success response (200):

{
  "status": "success",
  "channel": "C08V4NNMPEK",
  "messages": [
    {
      "ts": "1708790400.000100",
      "user": "U05SKCU0A4D",
      "text": ":handshake: *[COLLABORATION]* → ...",
      "thread_ts": null,
      "recipient": "collaboration"
    }
  ],
  "count": 1
}

DELETE /api/gritters/message/<ts>

Delete a message from the Gritters channel by its Slack timestamp.

Path parameter: ts — the message timestamp (e.g., 1708790400.000100).

Query parameter: api_key — required if GRITTERS_API_KEY is configured.

Example:

curl -X DELETE "https://app.glassumbrella.io/api/gritters/message/1708790400.000100?api_key=YOUR_API_KEY"

Error responses:

Code Condition
401 Missing or invalid API key
403 Bot lacks permission to delete the message
404 Message or channel not found

Configuration

All configuration is read from environment variables in config.py.

Variable Default Description
GRITTERS_CHANNEL_ID C08V4NNMPEK Default Slack channel for Gritters messages
GRITTERS_API_KEY (none) Optional API key; if set, all requests must include it
GRITTERS_GITLAB_INTEGRATION True Enable GitLab issue comment cross-posting
SLACK_BOT_TOKEN (required) Slack bot OAuth token used by all endpoints
GITLAB_TOKEN (none) GitLab personal access token for issue comments
GITLAB_PROJECT_ID the-smithy1/spyder Target GitLab project for issue comments

Per-team API keys: The Gritters post endpoint also supports per-team authentication via environment variables matching the pattern TEAM_API_KEY_<TEAM_NAME>. If any such keys are present, the provided api_key is validated against all of them.

Valid Originators and Recipients

Originators and recipients are validated against the team and member registries defined in web/utils/slack_helpers.py.

Teams:

collab, collaboration, spyder, smithy-game-dev, cascade-game-dev, game-dev, lore-engine, grit-lore-team, pariah-lore-team, prydain-lore-team, go-to-market, gtm, ephemeral, ephemeral-form-submissions

Individuals:

@minh, @will, @michael, @doug, @lynsie, @molly, @ondrej, @david, @erick, @rihards, @barry, @anna, @bjorn, @roland

Channel Routing

By default, messages are posted to GRITTERS_CHANNEL_ID. Two overrides exist:

  1. Explicit channel field in the request body — posts to the specified Slack channel ID directly.
  2. Recipient-based routing — certain recipients are routed to different channels. Currently configured:
Recipient Channel
ephemeral-form-submissions C0A6QMH6N7R

Error Handling

HTTP Code Cause
400 Missing message, originator, or recipient; invalid originator or recipient value; invalid limit query parameter
401 API key required but not provided, or invalid key
403 Invalid API key on delete; bot lacks delete permission
404 Message not found (delete endpoint)
500 Slack API error
503 SLACK_BOT_TOKEN or GRITTERS_CHANNEL_ID not configured

Collaboration-Server Webhook Receiver

The webhook-receiver/app.py service in the collaboration-server repository acts as a central GitLab webhook router. It exposes the following routes:

Route Method GitLab Event Description
/webhook/deploy POST Push Hook Auto-deploys agents by triggering a Kubernetes rolling restart when a new image is pushed to main
/webhook/merge POST Merge Request Hook Forwards MR merge events to Minh for documentation processing
/webhook/note POST Note Hook Forwards MR reviewer comments to configured agents (currently Minh and Pearl) for feedback processing

/webhook/note — MR Comment Forwarding

The /webhook/note endpoint receives GitLab Note Hook events and forwards MR-related comments to agents. It filters to only process comments on merge requests (ignoring issue, snippet, and commit notes).

Agent payload forwarded to each target:

Field Source Description
project_path project.path_with_namespace Full GitLab project path
mr_iid merge_request.iid Merge request internal ID
source_branch merge_request.source_branch Branch being merged
note object_attributes.note Comment text
note_author user.username Author of the comment
mr_author merge_request.author.username MR author (when available)
file_path object_attributes.position.new_path File path for inline/diff comments (when applicable)

Configured note targets:

Agent Token Env Var URL Env Var Default URL
Minh MINH_WEBHOOK_TOKEN MINH_NOTE_URL http://minh:8000/webhook/note
Pearl PEARL_WEBHOOK_TOKEN PEARL_NOTE_URL http://pearl:8000/webhook/note

An agent is only included as a forwarding target if its corresponding token environment variable is set. If no targets are configured, the endpoint returns a 500 error.

Webhook Receiver Environment Variables

Variable Default Description
WEBHOOK_SECRET (empty) Shared secret for GitLab webhook authentication
NAMESPACE ai-coworkers Kubernetes namespace for agent deployments
MINH_WEBHOOK_TOKEN (empty) Token for authenticating with Minh's webhook server
MINH_WEBHOOK_URL http://minh:8000/webhook/merge URL for forwarding MR merge events to Minh
MINH_NOTE_URL http://minh:8000/webhook/note URL for forwarding MR note events to Minh
PEARL_WEBHOOK_TOKEN (empty) Token for authenticating with Pearl's webhook server
PEARL_NOTE_URL http://pearl:8000/webhook/note URL for forwarding MR note events to Pearl

Supported Agent Deployments

The webhook receiver manages auto-deploys for the following agent projects:

minh, wisp, pearl, spark, arturo, roland, chisel

Prerequisites

  • The Spyder Flask application must be running with a valid SLACK_BOT_TOKEN.
  • The Slack bot must be invited to the target Gritters channel.
  • For GitLab integration, GITLAB_TOKEN must be set and the bot must have API access to the target project.
  • For MR comment forwarding, the webhook-receiver must have WEBHOOK_SECRET set and at least one agent token (MINH_WEBHOOK_TOKEN or PEARL_WEBHOOK_TOKEN) configured.
  • The webhook-receiver service account requires Kubernetes RBAC permissions for pods (get, list) and configmaps (get, create, update, patch) in the target namespace.

Deployment

Spyder (Gritters API host)

Spyder runs as a systemd service on an EC2 instance (deploy/slackapp.service). The service uses Gunicorn with the following configuration:

Setting Value
Bind address 0.0.0.0:3000
Workers 2 (sync)
Timeout 120 s
Max requests 1000 (+ jitter 50)
WSGI entry point spyder:flask_app

The service is managed by systemd with Restart=always and a 10-second restart delay. Access and error logs are written to logs/access.log and logs/error.log respectively.

Webhook Receiver

The webhook-receiver is deployed as a Kubernetes Deployment in the ai-coworkers namespace on a K3s cluster. Ansible playbook 18-deploy-webhook-receiver.yml in the collaboration-server repository manages the full stack:

  • Image: Built via GitLab CI on merges to main and pushed to registry.gitlab.com/the-smithy1/infra/webhook-receiver:latest.
  • Runtime: Gunicorn with 1 worker and 2 threads, listening on port 5000.
  • Replicas: 1.
  • Ingress: Exposed via Traefik on an internal hostname with automatic Let's Encrypt TLS.
  • Service: ClusterIP on port 5000.
  • RBAC: A dedicated ServiceAccount (webhook-receiver) with a Role granting get/patch on deployments, get/list on pods, and get/create/update/patch on configmaps in the ai-coworkers namespace.
  • Resource limits: 50m–100m CPU, 64Mi–128Mi memory.
  • Secrets: WEBHOOK_SECRET, MINH_WEBHOOK_TOKEN, and PEARL_WEBHOOK_TOKEN are stored in a Kubernetes Secret (webhook-receiver-secret).

Monitoring

Spyder

  • Health endpoints: GET /ping (database connectivity), GET /health (comprehensive service health), GET /health/simple (load-balancer probe).
  • System metrics: An AWS CloudWatch agent (deploy/cloudwatch-config.json) collects CPU, disk, memory, and swap metrics at 60-second intervals.
  • Application logs: Gunicorn access and error logs under logs/. Application-level logging via Python's logging module (configured in config.py).

Webhook Receiver

  • Health endpoint: GET /healthz — returns {"status": "ok"}.
  • Kubernetes probes:
    • Liveness: GET /healthz every 30 s (initial delay 5 s).
    • Readiness: GET /healthz every 10 s (initial delay 3 s).
  • Application logs: Python logging at INFO level. No external metrics or alerting is configured beyond the Kubernetes probes.

Rate Limiting

No rate limiting is currently applied to the Gritters API endpoints (/api/gritters/post, /api/gritters/messages, /api/gritters/message/<ts>). Spyder has a SimpleRateLimiter utility (utils/simple_rate_limiter.py) and a global 429 error handler, but the Gritters endpoints do not use rate-limiting decorators. Other Spyder endpoints (teach-back, asset generation) have their own rate limits; the Gritters endpoints do not.