Skip to main content

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.

Prerequisites
  • 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)
Two different surfaces

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

MethodPathAuthDescription
POST/api/ingest/v1/enrollnoneToken-less self-enrollment
POST/api/ingest/v1/enroll/pollnonePoll a pending enrollment for approval
POST/api/ingest/v1/heartbeatBearerLiveness + lightweight counts; returns directives
POST/api/ingest/v1/syncBearerUpsert entities and append fact events
POST/api/ingest/v1/manifestBearerNightly reconciliation by entity counts
GET/api/ingest/v1/skillsBearerPull the published skill catalog (descriptors)
GET/api/ingest/v1/skills/{key}BearerPull 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

FieldTypeRequiredDescription
protocolVersionintegeryesMust equal 1
instanceobjectyesIdentity: machineId, instanceId, hostname, os (darwin|linux|win32), slawVersion
capabilitiesobjectnoreportIssueTitles (default true), liveStream (default false)

Response

FieldTypeDescription
enrollmentIdUUIDEnrollment to poll while pending
statestringpending, active, rejected, or revoked
apiKeystringPresent only when state is active
pollIntervalSecintegerSuggested 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

FieldTypeDescription
protocolVersionintegerMust equal 1
sentAtstringISO-8601 timestamp
statusstringok or degraded
uptimeSecintegerProcess uptime
countsobjectsquads, agents, activeRuns, openIssues
spendobjecttodayCents, monthCents
lastEventCursorstring | nullCursor of the last synced event
appliedLimitVersioninteger (optional)Budget-limit version the instance has applied (de-dupe)
appliedSkillCatalogVersioninteger (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

FieldTypeDescription
protocolVersionintegerMust equal 1
sentAtstringISO-8601 timestamp
batchCursorstringMonotonic cursor the instance proposes for this batch
upsertsarrayUp to 2000 entity upserts (squad, agent, squad_skill, project, issue)
factsarrayUp 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

FieldTypeDescription
protocolVersionintegerMust equal 1
sentAtstringISO-8601 timestamp
countsobjectsquads, 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/skills returns the published catalog: a catalogVersion plus descriptor entries (key, name, version, contentHash, trustLevel, …) with no markdown body.
  • GET /api/ingest/v1/skills/{key} returns one skill's full content, including markdown and any inline files. Returns 404 (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:

CodeMeaning
protocol_version_unsupportedReported protocolVersion is too old (426)
invalid_payloadBody failed schema validation (400)
unauthorizedMissing or invalid instance API key (401)
enrollment_not_foundUnknown enrollmentId (404)
enrollment_rejectedEnrollment was rejected
enrollment_revokedInstance key has been revoked
rate_limitedIngest rate limit exceeded

Next steps

  • Authentication — the local SLAW REST API auth model
  • Costs — the spend data summarized in heartbeat and sync payloads