Skip to content

Orchestration protocol

This page documents the wire format behind jailbee inbox. Agents use the jailbee inbox commands; this schema is for protocol development and review. The host broker validates manifests and answers invalid requests with refused. All names are short names (the branch-level name, never repo-prefixed).

Containers reach the broker only through the socket protocol. Upgrade only when no orchestration is in progress: file-transport waiters and unread messages are not carried over.

Socket transport

The broker listens on one Unix socket per container. The host keeps it under the broker state dir (<state>/sock/<instance uuid>.<random>/broker.sock, a directory name no device has used before; a legacy <state>/sock/<instance uuid>/ is replaced by a fresh one and removed the first time the broker serves the container; directory mode 0700, socket mode 0600) and the broker attaches that directory to the container as a read-only disk device named jailbee-orchestration, mounted at /run/jailbee/orchestration/. The client connects to /run/jailbee/orchestration/broker.sock (override with JAILBEE_ORCHESTRATION_SOCKET). The broker identifies the caller by which socket accepted the connection, never by anything in the request. Only orchestrators and children get the device. Jailbee's own copies strip it at once; a raw incus copy keeps it until the broker's next round removes it. In that round the broker also moves the original's socket to a fresh directory, removes the old one and re-attaches the original's device there, whether or not the copy's device could be removed: every connection drops (the original's client reconnects), and the copy's mount, however long it lingers, leads to a deleted directory, so the copy cannot keep speaking as the original. Two instances that share one instance uuid (an incus import beside the original) get no socket at all while both exist: the broker logs it and neither can reach the broker until the conflict is resolved.

Messages and statuses that the broker delivers live in broker.db (unread and read), not in files. jailbee inbox ls, read and wait read them through the socket.

Framing

Messages are one JSON object per line (UTF-8, newline-terminated), at most 1 MiB including the newline. A longer line gets a bad_request response and the connection is closed. A client keeps its write side open until the reply has arrived: a half-close is treated as a disconnect. A container may hold at most 8 connections; a ninth is answered with busy and closed. Each connection has one request in flight: the broker reads the next line only after answering the previous one (a parked next counts), and stops reading a connection whose replies the client is not collecting.

Envelopes

A request carries the protocol version, an op and its arguments:

{"v":1,"op":"read","args":{"id":"h-3fa9c012","keep":false}}

A success response carries the op's result:

{"v":1,"ok":true,"result":{"version":1,"id":"h-3fa9c012","type":"message"}}

A failure carries an error code and a human-readable detail:

{"v":1,"ok":false,"error":{"code":"not_found","detail":"no unread item 'h-3fa9c012'"}}

A request with another v is answered with error code version and the client exits 4 (jb apply / restart the broker to align versions).

Ops

Op Args Result
send manifest (a manifest object), body (string or null; the body file's text), wait (bool) The status item for the manifest (an InboxStatus object). Delivered already read when the connection is live and either wait is true or the first state is refused; unread otherwise. Uses the ordinary per-recipient pending gate.
list after (required; 0 for the first page, then the previous page's next) The unread items, oldest first, in the shape jailbee inbox ls prints (long texts cut), one page at a time: {"items": [...], "next": cursor or null}.
read id, keep (bool) The unread item. Marks it read unless keep. jailbee inbox read calls it with keep: true, prints the item, then calls ack.
ack id {"acked": bool}. Marks the item read. jailbee inbox read uses it after printing; if the ack fails the message stays unread.
next wake_on (list of wake tokens or null), quiet_after and digest (positive seconds or null), timeout (seconds or null for none) A long-poll: {"kind":"message","id":...}, {"kind":"quiet-child","child":...}, {"kind":"digest","children":[...],"last_from_child":{...},"observed_at":...} or {"kind":"timeout"}. Nothing is marked read.
children none Orchestrators only: {"observed_at", "children", "last_from_child"}.
status child (string or null) Like children, narrowed to one child.
grant none The container's grant document, with default_runner_source.

The broker serves list, read and ack outside the executor, including while a long-running handler is executing. Each container may have at most two of these store queries queued or running; further requests wait in their connections without a busy error. This leaves query workers available to other containers. The shared worker queue remains bounded: a store query gets busy only when the broker-wide query queue is full.

children, status and grant read an immutable snapshot from the last completed broker round without calling Incus or taking a store-query slot. jailbee inbox runners obtains that grant before rendering local runners. In contrast, send and next use the executor and may wait for a handler to finish.

At most one next is parked per container; a second gets busy. A send whose manifest id already has a recorded status gets that status back again, with no new delivery. duplicate is returned only when no status was recorded for the id. Resending the same send is safe: the broker journals each id.

Error codes and exit statuses

Code Meaning jailbee inbox result
version protocol version differs exit 4
bad_request malformed request, unknown op or invalid argument wait and send (commands that parse arguments): exit 2; elsewhere exit 1
not_orchestrated the container has no orchestration role (or is not an orchestrator, for children/status/next with quiet_after/digest) exit 1 with a message
not_found no unread item with that id, or an unknown child exit 1
refused the broker declined the request exit 1
store the broker's message store failed exit 4
busy another next waiter on the container, the per-container connection cap, or a full broker-wide store-query queue wait: exit 2; store queries: exit 1; the connection cap can also surface as exit 4
duplicate a send id with no recorded status exit 0, note on stderr

The connection cap is 8 connections per container. A ninth connection gets busy and is closed right after that reply, so a client may see the reply as a dropped connection and exit 4.

Exit 4 is also used when the broker cannot be reached at all: the socket is missing, refuses connections (broker stopped), or the connection drops without an answer and does not come back. jailbee inbox wait retries a dropped connection with backoff until its timeout, but a first connect that fails exits 4 at once. Any send with no reply before its timeout exits 3. The request may or may not have run: a disconnected request still queued for execution is dropped as flood defense. Inspect jailbee inbox ls and jailbee inbox status before resending with a new ID. A disconnect after handling began leaves the status unread, including for send --wait.

Manifest rules

Manifest IDs match ^[a-z0-9][a-z0-9-]{0,63}$; use a new ID for every request, even after refusal. Unknown keys are refused. Long prompt or message text is at most 65536 bytes and is supplied as the socket request's body when the manifest uses prompt_file or body_file. The jailbee inbox send commands construct the request and print its ID.

Envelope

Every manifest has "version", "id" and "type". Version is 1, except spawn also supports version 2. The type is one of spawn, message, import, update, restart, refresh, stop, destroy, rights.

Orchestrator manifests

publish

Only orchestrators with matching grant publish rights may request this; children never can. The target file is read from the requester's own outbox.

{"version": 1, "id": "publish-1", "type": "publish", "outbox": "issue", "manifest": "close-12.json"}

outbox is issue or pr. manifest matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.json$, never a path or .progress.json. Use jailbee inbox send publish --issue NAME --wait or --pr NAME --wait. Issue action kinds and repo coverage must all be granted. Source PR manifests need pull_requests (own branches only, no force); review manifests need pr_reviews (bound PRs in the own repo only). Missing publish grants nothing. One request runs per orchestrator; all pending actions reserve sliding-hour budget up front, refunded only if the in-lock policy refuses before any mutation. A source publish counts as one action. Replies: started, then done with URLs/issue numbers or failed with the domain error. --wait may return on started; consume the final reply for the same ref. Uncertain outcomes, including broker restart, require human resolution through jb outbox, never an automatic retry. Retry with a new id.

refresh

Refresh services in a child with jailbee inbox send refresh --child C --services db,web [--force] [--wait] [--timeout DUR]. The services array requires 1 to 32 valid service names; duplicates are removed in order. force defaults to false and skips the idle gate when true. Existing stored grants do not gain this operation automatically; re-grant to allow refresh.

{"version": 1, "id": "refresh-1", "type": "refresh", "child": "c",
 "services": ["db", "web"], "force": false}

spawn

spawn and restart accept optional autostart_profile: "NAME" (helper: --autostart-profile NAME), independent of the runner's profile. The broker validates against the host repo's autostart.profiles; an unknown name is refused with autostart profile `NAME` is not defined. Spawn uses the named profile for creation. Restart stores it before booting and runs missing one-shot setup before the profile's on_start stages. Omission keeps the normal default on spawn and the child's stored selection on restart.

Both also accept services: ["web"] (up to 32 names), or helper --services web,db; dependencies are included. [] / --services '' starts none. Explicit services override the profile; omission uses the profile or all on spawn and retains an explicit selection on restart. Prepare is tracked per service/container, and cannot exceed stored child rights. services is incompatible with agent_only.

Spawn requests support version 2; legacy version 1 remains valid for fork/new without ref, pr, or warm_paths. Other messages, inbox, grant and finish envelopes remain version 1.

Create a child container. kind is fork (the child starts from your committed HEAD; a dirty tree is refused) or new (a fresh checkout from base, which is only valid with kind: "new"). branch is the child's branch. rights must be within grantable (see below). note is optional. on_done is optional: "destroy" (default) or "keep". If the grant lacks destroy, omission defaults to keep and explicit destroy is refused. CLI: --on-done destroy|keep. Version 2 also permits kind: "fork" with ref (a full refs/ name or 40-hex commit), or kind: "pr" with a positive integer pr. base is only for new; ref is only for fork. Use refs/heads/<name> for a local branch, not a bare name. Explicit-ref forks do not inherit PR bindings. A PR child's own outbox is lost when destroyed; use --on-done keep to preserve it.

Optional version-2 warm_paths: list[str] is valid only for kind: "fork". Omission uses fork.warm_paths; [] disables it; a non-empty list replaces it. Values are repo-relative globs of Git-ignored build outputs, seeded by btrfs reflink before autostart. * matches one segment; ** matches zero or more. Absolute paths, .., and .git roots are refused. Unmatched patterns, non-btrfs pools, dirty sources and source HEADs different from the fork commit skip with a note; seeding never fails the fork. CLI: repeatable --warm PATH or --no-warm (mutually exclusive; neither is valid with --new/--pr).

{"version": 1, "id": "spawn-fix-12", "type": "spawn", "kind": "fork",
 "branch": "fix/issue-12", "prompt_file": "spawn-fix-12.md",
 "rights": {"mounts": [], "loose": false, "egress": []}, "note": "GitHub #12"}
{"version": 1, "id": "spawn-docs-1", "type": "spawn", "kind": "new",
 "base": "main", "branch": "docs/intro", "prompt": "Write the intro page.",
 "rights": {"mounts": [], "loose": false, "egress": []}}

Optional runner (spawn and restart): {"agent": "claude" | "claude-jb", "profile": <LiteLLM profile, claude-jb only>, "model": "fable" | "opus" | "sonnet" | "haiku"}; only agent is required, unknown keys are refused. On spawn, omitting it uses the effective default runner in the grant, or the repo's default agent if none is configured. Choose a cheaper or stronger tier when appropriate for the task, following the user's model guidance; when in doubt use the default. Keep the default agent and profile unless guidance says otherwise. On restart, omission keeps the child's runner; null selects the current effective default, or clears the runner when no default is configured.

{"version": 1, "id": "spawn-fix-13", "type": "spawn", "kind": "fork",
 "branch": "fix/issue-13", "prompt": "Fix issue 13.",
 "rights": {"mounts": [], "loose": false, "egress": []},
 "runner": {"agent": "claude-jb", "profile": "grok", "model": "sonnet"}}
{"version": 1, "id": "spawn-fix-14", "type": "spawn", "kind": "fork",
 "branch": "fix/issue-14", "prompt": "Fix issue 14.",
 "rights": {"mounts": [], "loose": false, "egress": []},
 "runner": {"agent": "claude", "model": "sonnet"}}

A spawn the broker cannot run with that runner is refused (never run with the default). Details:

  • agent `claude` is not enabled in this repo
  • agent `claude` has no prompt_args, so it cannot take a prompt
  • agents.claude.command must start with `claude` or `claude-jb` to take a runner (it is `...`)
  • claude-jb needs LiteLLM enabled in this repo
  • LiteLLM profile `P` is not served (configured: ...; run `jailbee litellm up`)
  • LiteLLM profile `P` has no `M` tier

CLI: jailbee inbox send spawn [--branch fix/issue-12] [--new [--base main] | --ref REF | --pr N] [--mount M]... [--loose] [--egress H]... [--note T] [--agent A [--profile P] [--model M]] - with the prompt on stdin (or --text T; --file only with an absolute path outside the repo). --profile and --model need --agent, and --profile needs --agent claude-jb. Model tiers fable, opus, sonnet, and haiku may optionally carry the exact suffix [1m]; quote shell arguments such as --model 'sonnet[1m]'. LiteLLM profile validation uses the base tier. jailbee inbox runners [--json] lists the runners you may name and what omitting one starts: default: omit runner (<agent>), then one line per claude and per claude-jb -P <profile> with its models. The default agent is named only when known; otherwise it reads unknown and you simply omit runner. Never leave prompt files in the repo work tree: untracked files make a fork spawn refused (plain import does not require a clean target).

The short name is derived from the branch: lowercased, characters outside [a-z0-9-] become -, runs of - collapse. A taken name is refused ("already exists").

One PR per child

Spawn --new --base main from the upstream base, not the orchestrator's HEAD. On done, stage a source manifest with source = imported_ref and source_sha = imported_sha, without merging. For rework use --ref or --pr N, then import and stage pr: N with the new source. See the source examples.

message

Send a message to a child. to is the child's short name; body or body_file; kind is optional (note, question, done, blocked); in_reply_to is the h-<hex> id of the inbox message you answer (the host re-ids every relayed message, so use the id you read in the inbox). The broker maps a reply back for its recipient: when in_reply_to names a message in your inbox that this child sent, the child receives that message's origin_id — the id it gave its own manifest — so it can match the answer to its question. The original id itself is also accepted and passes through, as does any other id. Ordinary messages are delivered to the broker store even while the child is stopped, normally with a done acknowledgement (delivered to <child>). A queued acknowledgement means store delivery is pending, for example behind older pending work; it does not depend on whether the child is running.

{"version": 1, "id": "reply-0004", "type": "message", "to": "fix-issue-12",
 "body": "Use the existing parser, do not add a new one.", "kind": "note",
 "in_reply_to": "h-3fa9c012"}

CLI: jailbee inbox send message --to C [--kind K] [--reply-to ID] --text T.

import

Fetch a child's committed work into your repo as refs/jailbee/from/<child>/<branch>. Nothing is merged.

{"version": 1, "id": "import-fix-12", "type": "import", "child": "fix-issue-12"}

update

Transport the orchestrator's current branch into a running child at refs/jailbee/from/<orchestrator>/<branch>. Without merge: true, this does not touch its index or worktree, and the child merges when convenient. The child receives an update message carrying imported_ref and imported_sha. The import grant operation permits update; no new grant is needed. A stopped child is refused. Without a source selector, detached parent HEAD is refused as before.

Optional ref accepts a full refs/ name or 40-hex commit in the orchestrator's repository, using spawn's explicit-ref rules. Optional base accepts a host branch, resolved with the same local/upstream and new.autofetch policy as spawn --new --base. They are mutually exclusive. Explicit sources land at refs/jailbee/update/<orchestrator>/<id>: a stable per-request ref in a separate namespace, never overwriting current-branch updates or imported child refs. They work with a detached parent HEAD. The message still carries the exact imported_ref and imported_sha. Without merge: true, no local branch, base anchor, index or worktree is moved.

{"version": 1, "id": "update-fix-12", "type": "update", "child": "fix-issue-12"}
{"version": 1, "id": "update-release-1", "type": "update", "child": "pool-release", "base": "release/1"}

Optional merge: true applies the transported source to the child's branch only after a conflict check. A clean fast-forward advances the branch; divergent histories get a merge commit; an already-contained source leaves HEAD unchanged. The broker refuses a stopped or mount-mode child, detached HEAD, a dirty worktree, an in-progress merge, or conflicts, including gitlink conflicts conservatively refused even if Git might resolve them. Other transport or Git failures can also fail the request. The child may be running: there is no agent-idle gate, so coordinate before applying changes. An update message with optional merged_head reports the resulting child HEAD: the child must re-read files and must not merge imported_ref again. Without it, the child merges manually.

CLI: jailbee inbox send update --child C [--ref REF | --base BRANCH] [--merge] --wait. For example, use --ref refs/jailbee/from/worker/fix to reuse imported work, or --base release/1 to deliver a host-resolved release branch.

restart

Start a stopped child again with a new prompt (prompt or prompt_file). Status: started after handing boot and setup to a background worker (see the log in the detail). queued means no slot is free, then started when launched later. The final done ("child x is ready; agent is alive") requires two live process observations at least two seconds apart after setup. Failed boot/setup, an immediate exit or no agent within 120 seconds after setup yields failed for the same ref. If the child already reported done before readiness is confirmed, the final done reads "child x is ready; it already reported done". Subsequent agent death without accepted child done yields a blocked message, including exit 0 and signals. Broker stop/destroy intentionally ends monitoring.

runner is optional. Absent keeps the child's previous runner; an explicit "runner": null selects the current effective default (clearing the label if none is configured); an object replaces it. Queued requests retain the runner resolved when accepted. The refusals are the spawn ones above. on_done: "destroy" | "keep" changes the policy; absent or null keeps it. Explicit destroy requires the destroy operation. Restart clears any pending cleanup and stop-time dirty marker.

{"version": 1, "id": "restart-fix-12-b", "type": "restart",
 "child": "fix-issue-12", "prompt_file": "restart-fix-12-b.md"}
{"version": 1, "id": "restart-fix-12-c", "type": "restart",
 "child": "fix-issue-12", "prompt": "Try again.", "runner": null}

CLI: jailbee inbox send restart --child C [--agent-only] [--agent A [--profile P] [--model M] | --default-runner] with the prompt on stdin or --text.

Optional agent_only: true (--agent-only) instead requires a RUNNING child. It replaces only the agent tmux window with a fresh prompted, monitored session, without boot, setup or any on_start actions; services stay running. It is allowed only after accepted child done, or when the previous monitored agent has ended (already reported, or reportable, as blocked). Busy children, active startup workers and unknown liveness are refused; no force option. Runner and on_done handling are unchanged. Combining autostart_profile with agent_only is refused. It consumes no new slot, creates no reservation and never queues. Status is started, then ready-and-alive done or startup failed; subsequent exit without accepted child done yields blocked. Absent/false keeps ordinary stopped-child restart semantics (a running child is refused: "stop it first").

stop

Stop a running child, freeing its slot.

{"version": 1, "id": "stop-fix-12", "type": "stop", "child": "fix-issue-12"}

destroy

Delete a running or stopped child; a done keep child needs no prior stop. Refused for uncommitted changes, unreadable status or commits never imported, unless force is true: its current HEAD must equal the last import. For a stopped child the HEAD recorded when the broker stopped it counts; one stopped any other way is refused until restarted.

{"version": 1, "id": "destroy-fix-12", "type": "destroy",
 "child": "fix-issue-12", "force": false}

rights

Grant or revoke an existing child's rights. Orchestrators must name their own child. Children must omit child or name themselves, and omit grant entirely; otherwise: children may only revoke their own rights.

{"version": 1, "type": "rights", "id": "rights-1", "child": "fix-12",
 "grant": {"loose": true, "mounts": ["aws"], "egress": ["pypi.org"], "for": "5m"},
 "revoke": {"loose": false, "mounts": [], "egress": ["old.example.com"]}}

Both blocks are optional; at least one item overall is required. An item cannot be in both blocks. grant.for is mandatory: a TTL (max 24h) or never, covering all granted items. Grant items pass the same current grant/repo cap checks as spawn. Effective loose permission also permits egress covered by the repo cap, or any entry when no repo egress cap is set. Missing operation: operation `rights` is not in this container's grant. Revoke-only does not need the operation, but the orchestrator must still hold a grant.

Revoke only orchestration-owned rights (spawn, permanent or timed grants). Hand-added rights are left alone, with <item> was not given by orchestration; left as is. Timed grants never shorten a permanent right or later expiry; never clears expiry (acknowledged as (no expiry)). Revoke before grant; failure rolls back best effort and replies failed; validation refusal changes nothing. Success replies done with details. Timed rights expire on the host's 60-second timer. Every applied manifest is journalled. A child's revoke sends its parent a note, e.g. fix-12 gave up: loose. Stopped children take network changes on start and mounts as devices. Wildcard egress requires the work network.

CLI: jailbee inbox send rights --child C [--loose] [--mount K]... [--egress H]... --for T [--revoke-loose] [--revoke-mount K]... [--revoke-egress H]... [--wait]. Revoke-only omits --for.

Grant operation per type

Type Needs grant operation
spawn spawn (always granted)
message message (always granted)
import import
update import
restart restart
stop stop
destroy destroy
rights rights when granting; revoke-only needs an active grant, no operation

A type whose operation is missing from the effective grant is refused. Query it with jailbee inbox grant.

Child manifests

A child may write message manifests, with no to (they go to its orchestrator), and revoke-only rights for itself.

{"version": 1, "id": "msg-0003", "type": "message", "kind": "done",
 "body": "Fixed the parser; tests pass. Everything is committed.",
 "in_reply_to": null}

A kind: "done" message is accepted only when the worktree is clean and HEAD is on a branch. Refusals include uncommitted changes: commit everything before reporting done and detached HEAD: check out your branch before reporting done; fix the cause and resend under a new id. Always use send message --kind done --wait. After a timeout, inspect inbox messages and status before deciding whether to resend with a new ID; the request may or may not have run. The container may be destroyed after accepted done, so commit first.

An accepted done records HEAD, imports before delivery, then schedules cleanup according to on_done. A stopped or busy orchestrator gets an ordered pending finish; a vanished orchestrator refuses the report and keeps the child. Import failure still delivers done with import_error and keeps the child.

Inbox

Host-generated IDs look like h-<8 hex>. Use jailbee inbox read <id> to inspect and acknowledge a message.

A message from a child:

{"version": 1, "id": "h-3fa9c012", "type": "message", "from": "fix-issue-12",
 "kind": "question", "body": "Which parser do you mean?", "in_reply_to": null,
 "origin_id": "msg-0002"}

A message relayed from another container carries origin_id, the id its sender gave the manifest; an update message carries the id of the orchestrator's update manifest, and a done report the id of the child's done manifest. Host-generated messages (status-like notes, idle, adoption notices) have no origin_id key. Reply with the h-<hex> id (or the origin_id): the recipient's copy of your reply has in_reply_to set to its own original id. The same holds in the other direction, including a child's done report that names one of your messages.

Messages may carry optional imported_ref, imported_sha, import_error, on_done (destroy or keep) and on_done_reason. Successful done reports carry the ref and full SHA; failed imports carry the error and on_done: keep. import_error is only ever a real import failure: when the import succeeded but the child is kept anyway (the grant lost destroy, or the destroy marker could not be set), the report has on_done: keep and on_done_reason says why, and no re-import is needed. Update messages carry the parent's ref and SHA. ls marks imported/failed reports; read shows all fields. Merge imported_ref in the merged-fork workflow, or stage a source manifest without merging for one PR per child; retry import after fixing import_error.

send --wait [--timeout DUR|none] consumes only the status for that manifest: exit 0 on done/queued/started, 1 on refused/failed, 2 on usage errors, 3 on timeout (default 30m). none waits without a deadline and never exits 3. Recommend it for spawn, update and destroy. A first-state refused prints its reason to stderr and exits 1 even without --wait; stdout contains only the request ID. This refusal is stored as read history while the connection is live, so an ordinary waiter does not report it again. Other --wait statuses print after the ID. Statuses follow the same pending delivery order as messages.

For waits, use jailbee inbox wait --wake-on msg:done,question,blocked,idle,status:failed,status:refused --quiet-after 60m --timeout 30m. The broker may send a host-only message of kind idle, with the child's short name in from and the spawn/restart ref in in_reply_to. Its body names the child, elapsed wait, last accepted child message kind and bounded first line (or none), followed by a bounded pane tail when available. This is not a status state and cannot be sent as a manifest kind.

Idle detection begins after readiness and uses the prompted process's exact PID/start identity and adapter session/activity signal. Claude waiting means input is required; idle counts only with no subagent history. An old transcript is not completion evidence, so plain idle after any subagent history is conservatively unknown; genuine waiting still reports. Busy/shell, unknown signals and agents without adapter support do not count; pane silence alone is not evidence. The repo's orchestration.idle_after_seconds defaults to 900 positive seconds, for both first report and repeat cadence. Activity/new accepted child messages reset the episode. Accepted done and broker stop/destroy end it, as does an agent-exit blocked report. An accepted child blocked suppresses idle until its next accepted message. Refused done does not end monitoring. State survives broker restart; wait only filters.

A status reply answers one manifest; ref is the manifest id and state is one of five values: queued, started, done, failed, refused.

{"version": 1, "id": "h-0a1b2c3d", "type": "status", "ref": "spawn-fix-12",
 "state": "queued", "detail": "queued at position 1"}
{"version": 1, "id": "h-0a1b2c3e", "type": "status", "ref": "spawn-fix-12",
 "state": "started", "detail": "creation of child `fix-issue-12` started"}
{"version": 1, "id": "h-0a1b2c3f", "type": "status", "ref": "import-fix-12",
 "state": "done",
 "detail": "imported as refs/jailbee/from/fix-issue-12/fix/issue-12 at 1a2b3c4d5e6f"}

A spawn's final status is done or failed:

{"version": 1, "id": "h-0a1b2c40", "type": "status", "ref": "spawn-fix-12",
 "state": "done", "detail": "child `fix-issue-12` is ready"}
{"version": 1, "id": "h-0a1b2c41", "type": "status", "ref": "spawn-fix-12",
 "state": "failed",
 "detail": "creation of child `fix-issue-12` failed: the worker stopped during deps — the child was removed"}

failed is final and also reports runtime rights application failures; a restart reports done or started, then failed for the same ref if its boot or setup fails later. Other details: creation of child `<child>` stopped unexpectedly during <phase> (then — the child was removed, — the child was kept, stopped or — the child was kept running; `stop` it before a `restart`), creation of child `<child>` ended without a container; a missing error reads see the worker log. Statuses never carry host paths: the broker log names the worker's log.

A queued status applies to restart as well as spawn, and to a message waiting for store delivery or older pending work, regardless of the recipient's running state. Stopped children normally receive ordinary messages in the store immediately, with done: delivered to <child>. Other done details: child \` stopped,child `` destroyed.refusedis final. Therefof a status is the id of your manifest; note the id a helper prints (--). Asking about an unknown child is refused ("x` is not a child of this orchestrator"); after a revoked grant, "orchestration grant revoked".

Grant document schema

{"version": 1, "max_running_children": 2,
 "operations": ["destroy", "import", "message", "restart", "rights", "spawn", "stop"],
 "grantable": {"mounts": [], "loose": false, "egress": []},
 "default_runner": {"agent": "claude", "model": "sonnet"},
 "default_runner_source": "repo"}
  • default_runner: the effective runner for an omitted spawn runner, or null when no runner default is configured. It is a preference, not a right.
  • default_runner_source: container, local, repo, or global, or null when none is configured.
  • max_running_children: how many children may run at once. A spawn beyond it is queued until a slot frees. During creation, a child counts from the started reply until it is running and its create has finished, or its create has failed.
  • operations: the manifest types you may use. spawn and message are always present.
  • grantable: the most you may hand to a child.

The repo may cap the grant further; what you read is what applies.

Rights rule

A spawn's rights must be a subset of grantable: every mount by name is in grantable.mounts, loose is true only if grantable.loose is, and each egress host is covered by an entry in grantable.egress, or effective loose permission allows it within the repo egress cap (if set). This rule applies to spawn and runtime grants. Anything else is refused.

Child rights specifics

Give up rights orchestration supplied; ask for additional rights with a question message, never a grant. child must be absent or your own short name, and grant must be absent (even null is not allowed). Otherwise the broker replies children may only revoke their own rights.

{"version": 1, "id": "rights-1", "type": "rights",
 "revoke": {"loose": true, "mounts": ["aws"], "egress": ["pypi.org"]}}

At least one item is required. CLI: jailbee inbox send rights --revoke-loose --revoke-mount aws --revoke-egress pypi.org --wait. A successful revoke replies done and sends the parent a note such as fix-12 gave up: loose. Hand-added rights not owned by orchestration are left as is with a detail, not an error.

The orchestrator's rights operation allows grants within its current permission and repo cap; revoke-only needs no operation but still an active grant. Effective loose permission implies narrower egress within the repo cap (or any host if the cap has no egress list), at spawn and runtime alike. Every grant has for (max 24h or never), never shortens a right orchestration owns and skips one held by hand, and is checked/applied whole with best-effort rollback on failure. A child never sends the grant block.

Child inbox specifics

Host-generated IDs look like h-<8 hex>. Use jailbee inbox read <id> to inspect and acknowledge a message.

A message from your orchestrator (from is its short name):

{"version": 1, "id": "h-3fa9c012", "type": "message", "from": "main-orch",
 "kind": "note", "body": "Use the existing parser.", "in_reply_to": "msg-0002",
 "origin_id": "reply-0004"}

When your orchestrator answers a message you sent, in_reply_to is the id you gave that message (the one jailbee inbox send printed), even though the orchestrator saw it under a host id. origin_id is the id the orchestrator gave its own manifest; to answer, set in_reply_to to this message's id or origin_id, and the orchestrator receives its own id back.

An update message carries optional imported_ref and imported_sha (full commit ID), plus a body naming the parent's source. With no selector, that is its current branch at refs/jailbee/from/<orchestrator>/<branch>. An explicit ref, commit or host base arrives instead at refs/jailbee/update/<orchestrator>/<manifest-id>, isolated from branch updates and imported child refs. Always use the message's ref. If the message has merged_head, re-read the affected files and continue without merging the ref again. Otherwise commit or stash, merge imported_ref, resolve conflicts, commit, then continue. Other import fields on messages are import_error, on_done (destroy or keep) and on_done_reason (why a successfully imported child is kept).

Done status details say where the work was imported and whether the child will be destroyed or kept. Import errors keep the child and are delivered to the orchestrator for retry. A queued finish waits for parent delivery before import/cleanup; a vanished parent keeps the child and reports an error.

A status reply answers one of your manifests; ref is the manifest id and state is done (delivered), queued (waiting for store delivery or older pending work; a finish may also wait for a running orchestrator before import) or refused. Ordinary messages can be delivered while the orchestrator is stopped. Only refused is final and needs action. The two done-check refusals above are joined by could not fetch your branch to the host: ... and could not record the done report: ..., which are not your manifest's fault: retry done once under a new id, and send blocked if it repeats.

{"version": 1, "id": "h-0a1b2c40", "type": "status", "ref": "msg-0003",
 "state": "refused",
 "detail": "uncommitted changes: commit everything before reporting done"}