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