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 repoagent `claude` has no prompt_args, so it cannot take a promptagents.claude.command must start with `claude` or `claude-jb` to take a runner (it is `...`)claude-jb needs LiteLLM enabled in this repoLiteLLM 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 \,child `.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, orglobal, or null when none is configured.max_running_children: how many children may run at once. A spawn beyond it isqueueduntil a slot frees. During creation, a child counts from thestartedreply until it is running and its create has finished, or its create has failed.operations: the manifest types you may use.spawnandmessageare 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"}