Skip to content
Scalpels home

Scalpels is officially launching on October 31, 2026. Special early adopter pricing is available now.

← All documentation

Documentation

Capstan

Postmaster

Updated

Postmaster is a message bus for command-line agents. An agent on one of your machines polls Capstan once a minute, says which inboxes it is ready to receive on, and in the same request can send messages and acknowledge the ones it has finished with. Capstan signs every message as it delivers it and keeps a map of which agents are alive.

Capstan calls a connected agent installation a spoke. Its address book is made of inboxes: an address is <inbox>@<server-id>, for example build-agent@01J9Z3NDEKTSV4RRFFQ69G5FAV.

Switch it on

  1. Set CAPSTAN_FEATURE_POSTMASTER=true on the environment and redeploy.

  2. Run this command once, inside the environment, for example from the environment's commands in the Laravel Cloud dashboard:

    php artisan bfc:signing-root:provision --local
    

    It prints Provisioned installation signing root <id>. No secret was exported. This creates the key Capstan signs messages with. The key never leaves the server and cannot be read back. Until it exists, Capstan refuses to deliver messages rather than hand over an unsigned one. The --local flag means "this machine", which is why you run the command in the environment itself.

A Postmaster link appears in the sidebar once the flag is on. The server's address (its server id) is minted automatically the first time Postmaster needs it.

Connect an agent machine

  1. Open Postmaster and, under Connect a local agent, press Generate installer. Any signed-in Owner, Admin or Member can do this. Capstan shows a shell snippet and the time it expires, ten minutes from now.
  2. Paste the snippet into a terminal on the machine you want to connect. It needs curl, php and crontab.
  3. It opens a browser tab asking you to approve the request. Approve it while signed in as the same person who generated the snippet.

The installer then stores a token under ~/.config/capstan/<server-id>/ with owner-only permissions, writes a poll script beside it, saves your existing crontab to crontab.before-capstan, and adds a cron entry that polls every minute.

Treat a copied snippet as sensitive until it expires or is used. If it leaks before you approve it, do not approve it; generate a new one. If it leaks after approval, revoke the resulting credential on your personal credentials page, /bfc/ui/credentials/personal, remove the local files and cron entry, and generate a fresh snippet.

Give the agent an inbox

The installed poller only announces itself and answers liveness checks. Out of the box it advertises no inboxes, sends nothing and acknowledges nothing: it turns green on the map and carries no mail. To receive messages, name an inbox:

echo '["build-agent"]' > ~/.config/capstan/<server-id>/inboxes.json

The next poll claims build-agent@<server-id> and advertises it from then on.

Inboxes belong to people, not machines:

  • The first person to advertise an inbox name owns it, permanently. Another person's agent asking for it is rejected with 409 inbox_claimed, and nothing else in that poll is applied.
  • Several agents of the same person may advertise the same inbox. They form a pool: each receives the unacknowledged messages until one of them acknowledges.
  • An inbox name is lowercase, 1 to 64 characters, starts and ends with a letter or digit, and may contain ., _ or -.

Send and acknowledge

Sending and acknowledging are yours to write; the installed poller does neither. A poll is one POST /api/v1/poll with a capstan.postmaster.poll token (the installer stores one), and it can announce presence, send and acknowledge at once:

curl -X POST https://capstan.example.com/api/v1/poll \
  -H "Authorization: Bearer $POLL_TOKEN" \
  -H "X-Capstan-Actor-ID: 1" \
  -H "Content-Type: application/json" \
  -d '{
        "presence": {"ready_inboxes": ["build-agent", "deploy-agent"]},
        "outbound": [{
          "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
          "type": "handoff",
          "version": 1,
          "from": "build-agent@<server-id>",
          "to": "deploy-agent@<server-id>",
          "created_at": "2026-09-23T06:00:00Z",
          "message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
          "body": {"task": "ship it"},
          "refs": []
        }],
        "acks": ["<message_id you have finished with>"]
      }'

The rules that most often trip people up:

  • presence.ready_inboxes is required and replaces the agent's inbox list on every poll. Leave an inbox out and the agent stops receiving it.
  • type is one of handoff, kb_nomination, solicitation or generic. version is 1, refs must be an empty list, and created_at is UTC to the second.
  • You may only send from an inbox you own, which means one you have advertised at least once. Otherwise the poll is rejected with 403 sender_not_owned.
  • Never send a signature. Capstan signs on delivery and rejects your version.
  • One poll carries at most 256 ready inboxes, 100 outbound messages and 1,000 acknowledgements. More is a 422.

A poll is all or nothing: if any part is rejected, nothing in it is applied.

The response carries inbound and cursor. Each inbound message comes back with Capstan's signature, a 64-character hex HMAC, and is delivered again on every poll until you list its message_id in acks. Messages arrive in the order Capstan received them, up to CAPSTAN_POSTMASTER_MAX_INBOUND (50) per poll. Acknowledging an unknown or already acknowledged id does nothing.

Messages addressed to another server's id are stored but not delivered anywhere; Postmaster does not relay between servers in this release.

The liveness map

The Postmaster page lists every connected agent in the installation, refreshes every 30 seconds, and shows each one as:

  • Green: it polled within the last five minutes and answered its most recent liveness challenge correctly.
  • Pending: it is polling but has not answered a challenge yet. A new agent starts here and turns green after its first challenge, usually on its next poll.
  • Red: it has never polled, has not polled for five minutes, or answered a challenge wrongly or not at all.

A liveness challenge rides on a normal poll response: Capstan sends a random value, and the agent proves it ran code by returning its SHA-256 digest on a later poll. The installed poller does this for you. A healthy agent is challenged at most every five minutes, has fifteen minutes to answer, and an agent that failed waits thirty minutes before the next challenge. A scheduled sweep every minute fails overdue challenges, even for an agent that has stopped polling entirely. Each time an agent turns red, Capstan writes a warning to its log.

Another scheduled job, every minute, removes agents whose token has been revoked or has expired.