Botfather Protocol
The wire contract a SLAW instance uses to report up to a Botfather tower: enroll once, then heartbeat, sync entities and facts, reconcile via a manifest, and pull the central skill catalog. All calls flow instance → tower.
- A reachable Botfather tower base URL:
https://<botfather-host>/api/ingest/v1 - For authenticated calls: an instance API key from enrollment, sent as
Authorization: Bearer <instance-api-key> - Every request carries
protocolVersion(current version:1)
This page documents the tower-side ingest protocol. The Operator-facing controls on a local SLAW instance (connect, re-enroll, force-sync, skill install) live under /api/botfather/* on the instance itself and are covered with the rest of the local REST API.
Versioning
A tower accepts protocolVersion equal to the current version or one below it. Older instances receive 426 with a human-readable reason. Bump-tolerant fields are optional so a newer instance still validates against an older tower.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/ingest/v1/enroll | none | Token-less self-enrollment |
POST | /api/ingest/v1/enroll/poll | none | Poll a pending enrollment for approval |
POST | /api/ingest/v1/heartbeat | Bearer | Liveness + lightweight counts; returns directives |
POST | /api/ingest/v1/sync | Bearer | Upsert entities and append fact events |
POST | /api/ingest/v1/manifest | Bearer | Nightly reconciliation by entity counts |
GET | /api/ingest/v1/skills | Bearer | Pull the published skill catalog (descriptors) |
GET | /api/ingest/v1/skills/{key} | Bearer | Pull full content for one skill |
Enroll
POST /api/ingest/v1/enroll
Token-less. The instance presents its identity; the tower returns an enrollment record. If an auto-approve rule matches, the response is 200 with an apiKey and state: "active"; otherwise it is 202 with state: "pending".
Request body
| Field | Type | Required | Description |
|---|---|---|---|
protocolVersion | integer | yes | Must equal 1 |
instance | object | yes | Identity: machineId, instanceId, hostname, os (darwin|linux|win32), slawVersion |
capabilities | object | no | reportIssueTitles (default true), liveStream (default false) |
Response
| Field | Type | Description |
|---|---|---|
enrollmentId | UUID | Enrollment to poll while pending |
state | string | pending, active, rejected, or revoked |
apiKey | string | Present only when state is active |
pollIntervalSec | integer | Suggested poll interval (default 10) |
Poll
POST /api/ingest/v1/enroll/poll with { "protocolVersion": 1, "enrollmentId": "..." } returns the current state and, once approved, the apiKey. Returns 404 (enrollment_not_found) for an unknown ID.
Heartbeat
POST /api/ingest/v1/heartbeat
Sent on an interval. Proves liveness, reports rolled-up counts and spend, and receives any pending directives.
Request body
| Field | Type | Description |
|---|---|---|
protocolVersion | integer | Must equal 1 |
sentAt | string | ISO-8601 timestamp |
status | string | ok or degraded |
uptimeSec | integer | Process uptime |
counts | object | squads, agents, activeRuns, openIssues |
spend | object | todayCents, monthCents |
lastEventCursor | string | null | Cursor of the last synced event |
appliedLimitVersion | integer (optional) | Budget-limit version the instance has applied (de-dupe) |
appliedSkillCatalogVersion | integer (optional) | Catalog version the instance has last pulled |
Response
{ "acknowledged": true, "directives": [] }
Directives are a tower → instance back-channel. Kinds: set_sync_interval, request_reconciliation, request_live_stream, stop_live_stream, set_limits (pushes a budget LimitSpec), and skills_updated (a hint that the catalog advanced).
Sync
POST /api/ingest/v1/sync
Carries entity upserts and append-only fact events in a cursored batch.
Request body
| Field | Type | Description |
|---|---|---|
protocolVersion | integer | Must equal 1 |
sentAt | string | ISO-8601 timestamp |
batchCursor | string | Monotonic cursor the instance proposes for this batch |
upserts | array | Up to 2000 entity upserts (squad, agent, squad_skill, project, issue) |
facts | array | Up to 5000 fact events (cost_event, run_event, activity_event) |
Issue titles may be redacted to the issue key when the instance enrolled with reportIssueTitles: false. Activity facts are restricted to a whitelisted set of actions; anything else is rejected at the schema.
Response
{
"acknowledgedCursor": "...",
"accepted": { "upserts": 12, "facts": 40, "deduplicated": 3 },
"directives": []
}
Manifest
POST /api/ingest/v1/manifest
Nightly self-heal. The instance reports per-type counts; the tower compares them against its own view and asks for a full resync of any divergent type.
Request body
| Field | Type | Description |
|---|---|---|
protocolVersion | integer | Must equal 1 |
sentAt | string | ISO-8601 timestamp |
counts | object | squads, agents, projects, issues, costEvents |
Response
{ "inSync": true, "resyncTypes": [] }
resyncTypes lists entity types (squad, agent, project, issue, cost_event) the instance should fully resync.
Skill catalog
Botfather is the master skill registry; content flows tower → instance.
GET /api/ingest/v1/skillsreturns the published catalog: acatalogVersionplus descriptor entries (key,name,version,contentHash,trustLevel, …) with no markdown body.GET /api/ingest/v1/skills/{key}returns one skill's full content, includingmarkdownand any inlinefiles. Returns404(skill_not_found) if the skill is unknown or unpublished.
The instance pulls full content only when a descriptor's contentHash changes, so unchanged skills are not re-fetched.
Errors
Error responses carry { "error": "...", "code": "..." }. Codes:
| Code | Meaning |
|---|---|
protocol_version_unsupported | Reported protocolVersion is too old (426) |
invalid_payload | Body failed schema validation (400) |
unauthorized | Missing or invalid instance API key (401) |
enrollment_not_found | Unknown enrollmentId (404) |
enrollment_rejected | Enrollment was rejected |
enrollment_revoked | Instance key has been revoked |
rate_limited | Ingest rate limit exceeded |
Next steps
- Authentication — the local SLAW REST API auth model
- Costs — the spend data summarized in heartbeat and sync payloads