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:
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:
- Explicit
channelfield in the request body — posts to the specified Slack channel ID directly. - 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_TOKENmust be set and the bot must have API access to the target project. - For MR comment forwarding, the webhook-receiver must have
WEBHOOK_SECRETset and at least one agent token (MINH_WEBHOOK_TOKENorPEARL_WEBHOOK_TOKEN) configured. - The webhook-receiver service account requires Kubernetes RBAC permissions for
pods(get, list) andconfigmaps(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
mainand pushed toregistry.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 grantingget/patchondeployments,get/listonpods, andget/create/update/patchonconfigmapsin theai-coworkersnamespace. - Resource limits: 50m–100m CPU, 64Mi–128Mi memory.
- Secrets:
WEBHOOK_SECRET,MINH_WEBHOOK_TOKEN, andPEARL_WEBHOOK_TOKENare 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'sloggingmodule (configured inconfig.py).
Webhook Receiver¶
- Health endpoint:
GET /healthz— returns{"status": "ok"}. - Kubernetes probes:
- Liveness:
GET /healthzevery 30 s (initial delay 5 s). - Readiness:
GET /healthzevery 10 s (initial delay 3 s).
- Liveness:
- Application logs: Python
loggingat 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.