Public Launch API
August 21, 2026 ยท View on GitHub
AMQ exposes a versioned launch contract for tools that must plan and apply a
session without parsing human output. The Go package is launchapi. The JSON
contract is schemas/launch-api-v1.schema.json.
The current contract is 0.61.1. A caller must negotiate its
required contract range, intent version, result version, and features before it
depends on them:
negotiated, err := launchapi.Negotiate(launchapi.RequirementV1{
ContractSemver: ">=0.61.0 <0.62.0",
IntentVersion: launchapi.IntentVersionV1,
ResultVersion: launchapi.ResultVersionV1,
Features: []string{"prepare_apply_v1"},
})
if err != nil {
return err
}
_ = negotiated
A 0.61.1 binary advertises placement, initial_input, base_root,
on_live, caller_context, executable_identity, and wrapper. A caller must
require every feature it uses. Contract semver alone does not claim that an
unadvertised feature is available.
The advertised feature set is derived from the callable platform implementation at build time:
| Build platform | Advertised launch features |
|---|---|
| macOS, Linux (including WSL) | The full v1 set: launch_intent_v1, prepare_apply_v1, lifecycle_v1, managed_tmux_v1, plan_only_commands_v1, plus initial_input, placement, base_root, on_live, caller_context, executable_identity, and wrapper. |
| Native Windows | launch_intent_v1 and plan_only_commands_v1 only. prepare_apply_v1, lifecycle_v1, and managed_tmux_v1 are not callable and are rejected by negotiation. |
WSL uses the Linux binary and therefore gets the Linux row. Negotiation fails closed for a feature that is not in the current build's advertised set.
PreviewV1.capabilities reports the selected providers' static adapter grammar
without executing a caller-supplied provider. grammar_version is the
adapter-owned version that consumers compare. It changes when the allowed
argument forms, configuration overrides, or carrier support changes.
verified_provider_version is informational and names the provider release on
which that grammar was verified; it does not claim the installed version.
Runtime provider identity is bound into subject schema 2 before Apply executes
it. Prepare records each runnable provider's consulted path (PATH lookup or
absolute) plus the on-disk identity tuple, including symlink hops. Schema 1
omits that binding. A same-path inode, mtime, symlink-hop, or PATH retarget
changes subject_digest; Apply then returns subject_changed with no launch
mutation. If the provider, wrapper, or cwd changes after Apply authorization,
Reconcile refuses before capability probing or ticket creation with the typed
authorized_identity_changed result. Plan and trust digests stay stable across
those replacements.
The initial-input carrier is typed. Claude, Codex, and Grok currently
advertise argument; AMQ appends its exact text as the final provider argv
element.
stdin and file remain typed but unadvertised and return
initial_input_unsupported until real stdin/file seams exist. Content is
limited to 262,144 UTF-8 bytes without C0 controls, DEL, CR, or LF, and must
not begin with -; invalid text returns a typed initial_input_control or
initial_input_leading_dash validation code before Prepare inspects the
target. Content changes the plan and subject digests but not the trust digest.
Initial input is a one-time bootstrap carrier: a resume plan never appends it
again. The normalized execution and wake policy enters both the plan and trust
digests, so any non-default execution, wake, or injector policy is
trust-bearing and requires fresh trust for the exact policy.
Claude admits one --allowedTools <comma-separated-list> pair. Grok Build uses
the canonical --tools <list> and --disallowed-tools <list> flags; their
values are opaque Grok tool names and are not compiled through Claude's
--allowedTools validator. Codex admits
ordered -c model_reasoning_effort=<value> pairs with values minimal, low,
medium, high, or xhigh; duplicate keys and unknown keys or values reject.
Every env_overlay key uses the POSIX environment grammar
[A-Za-z_][A-Za-z0-9_]*; shell syntax such as X;touch /tmp/pwn is refused.
Public JSON decoders perform a bounded structural pass before contract
decoding. A structural refusal is returned as *launchapi.StrictJSONError;
inspect its typed Code instead of parsing the message. The validation codes
are launchapi.StrictJSONDuplicateKey (duplicate_json_key) for an exact
duplicate object key and launchapi.StrictJSONDepthExceeded (depth_exceeded)
when nesting exceeds launchapi.StrictJSONMaxDepth (256). Object keys are
compared after JSON string decoding, so case- or whitespace-distinct keys are
different keys. The error also carries the containing Path and duplicate
Key where applicable.
An optional participant wrapper contains a clean absolute executable path
and static args. The path must resolve to one regular file; resolvable
symlinks are accepted. AMQ executes the exact argv wrapper executable + wrapper args + resolved provider executable + provider args, without a shell. The
declared wrapper path and arguments enter the plan, trust, subject, ticket, and
command preview. An argument initial input stays the final inner-provider
argument. Unsupported stdin and file carriers remain typed refusals when a
wrapper is present. The wrapper feature is advertised, and a strict decode
accepts the field.
Executable containment refusals use stable typed codes: provider_project_contained
for a provider path presented from the project, wrapper_project_contained for
a wrapper path presented from the project, and amq_launcher_project_contained for the
AMQ launcher path. Both the raw path and its resolved target are checked before
the executable can be planned or ticketed.
Intent
The intent owns the desired participants. Discovery owns the project root, default session, session root, and local launcher preference. A public intent does not replace committed project configuration.
{
"intent_version": 1,
"participants": [
{
"handle": "claude",
"runnable": true,
"executable": "claude",
"wrapper": {"executable": "/opt/company/bin/seat-wrapper", "args": ["--profile", "lead"]},
"cwd": {"kind": "relative", "path": "."},
"resume_policy": "resume",
"execution": {
"require_wake": true,
"no_gitignore": false,
"wake": {"mode": "enabled"}
}
},
{"handle": "operator", "runnable": false}
]
}
resume_policy accepts exactly resume, fresh, or disabled.
The v1 adapter set supports Claude Code, Codex CLI, Cursor CLI, and Grok Build.
Claude Code and Grok Build mint their session IDs from the launch nonce. Codex CLI 0.147.0 reports its
provider-owned thread ID after a completed turn through its legacy notify
hook. AMQ adds one static hook override that is bound to the session root,
handle, launch nonce, and AMQ executable. The private hook validates the exact
ticket and payload, persists immutable evidence, and only then publishes the
conversation identity. It forwards the identical payload to the operator's
configured Codex notify command after publication; a missing command is a
no-op, and a forwarding failure cannot undo or fail the evidence path. An
unused Codex launch remains pending and cannot be resumed. Cursor CLI acquires
its provider-owned chat ID before process start through create-chat at exact
version 2026.08.11-e8db854. Cursor's provider identity is cursor-agent.
The current executable is agent; cursor-agent remains a supported legacy
alias, including .exe variants.
The registered launcher backends are commands, tmux, cmux, and
ghostty. --launcher auto walks the local preference; an explicit
--launcher <name> wins. When CMUX_SURFACE_ID is set, auto prepends cmux
ahead of ghostty; otherwise TERM_PROGRAM=ghostty prepends ghostty.
Selection still requires Detect Available. Cmux Create uses --focus false
and restores prior selection; see Managed launch recovery.
The tier-1 provider smokes are opt-in and skip unless the env is 1. The Codex
smoke first proves that an unused launch stays pending and returns AMQ's typed
stale-conversation action. It then sends one fixed prompt through the managed
pane, waits for notify-backed identity publication, and performs one exact
headless resume:
AMQ_CLAUDE_LIVE=1 go test ./internal/launch -run TestClaudeLiveManagedMintResumeAndCrashReuse -count=1 -v
AMQ_CODEX_LIVE=1 go test ./internal/launch -run TestCodexLiveManagedAcquireResumeAndCrashReuse -count=1 -v
AMQ_CURSOR_LIVE=1 go test ./internal/launch -run TestCursorLiveResumeManagedExecutionAndCrashReuse -count=1 -v
AMQ_GROK_LIVE=1 go test ./internal/launch -run TestGrokLiveMintExitAndExactResume -count=1 -v
Managed launcher live proofs skip unless the env is 1. Run them from a shell
inside the matching surface:
AMQ_CMUX_LIVE=1 go test ./internal/launch -run TestCmuxLive -count=1 -v
AMQ_GHOSTTY_LIVE=1 go test ./internal/launch -run TestGhosttyLive -count=1 -v
The smoke harness disables Claude tools with the CLI-equivalent single argument
--tools= and uses
--permission-mode plan; it runs Codex with --sandbox read-only. These are
harness controls, not additions to the adapter's committed option contract.
The smokes record the exact managed process arguments and provider IDs, require
headless resume to return the requested ID, and stop at the first failure.
Claude persists a minted --session-id only after its first turn. The live
smoke proves that an unused mint returns AMQ's typed stale-conversation action,
then bootstraps the same ID with a no-tools turn before it proves exact resume.
It uses an already-trusted checkout as the Claude cwd and keeps its AMQ session
root in a temporary directory; it never changes Claude trust state.
Pi is excluded because it has no provider CLI; Gemini CLI and OpenCode also remain outside this adapter set.
Prepare and Apply
Prepare is read-only. It returns the exact subject schema, subject, plan, and trust digests, a preview, current observations, and required actions. Apply accepts the original Prepare request, the returned subject schema and digest, and one explicit decision for each required action. It recomputes the subject under that schema and fails closed if state changed.
Omitted placement keeps the selected backend's v0.61 layout and still appears
on PreviewV1.placement.effective. An explicit tuple the backend cannot
realize returns outcome: unsupported and reason: placement_unsupported
with zero planned backend mutation. Subject schema 2 binds that preview into
subject_digest; schema 1 rejects an explicit placement field.
Omitted TargetV1.base_root preserves the v0.61 exact-root behavior. When it
is present, only the exact project_root/.amqrc authorizes it: the value must
be the configured root or one direct child, and session_root must be the
direct child named by session. Prepare does not create directories. A missing
authorized base appears as a deterministic create_base_root entry in
planned_writes; Apply revalidates the config and parent identity, then creates
the base and session exclusively with mode 0700. Siblings, nested roots,
symlinks, alternate spellings, or changed authority return a typed refusal with
no launch mutation.
on_live is keep or refuse. Omission and explicit refuse keep the v0.61
whole-binding refusal. Explicit keep on a proven-owned live seat keeps that
process and lets Apply create missing seats in the same tmux omitted-placement
session. Hostile live resources refuse even with keep. Schema 1 rejects
on_live: keep; omit and refuse stay digest-stable.
caller_context is an opaque correlation map. It is limited to 32 entries;
keys are 1 through 64 UTF-8 bytes, values are at most 1,024 UTF-8 bytes, and
all keys plus values are at most 16 KiB. NUL, invalid UTF-8, and duplicate JSON
keys reject. Canonical key ordering enters subject schema 2 and immutable
evidence, but never the plan or trust digest. Apply echoes the request map.
Inspect, Focus, and Close load it from the proven-owned binding; lifecycle
requests cannot replace it.
amq-squad compiles only its public request: it sends target.base_root as the
one profile child of the .amqrc root; sends on_live: keep only for a seat
it already knows to be proven live; maps placement spellings current-window to
current_window, vertical to columns, and horizontal to rows; and sends
the generated bootstrap prompt through initial_input. Those are compiler
mappings, not contract aliases. The public decoder accepts only the underscore
target enum and the columns|rows|tiled layout enum. One refused seat
cohort-blocks creation of every missing seat. When a wrapper is present, stdin
initial input is delivered to the wrapper, which owns forwarding it to the
provider.
request := launchapi.PrepareRequestV1{
RequestVersion: launchapi.RequestVersionV1,
Target: launchapi.TargetV1{
ProjectRoot: "/workspace/project",
BaseRoot: "/workspace/project/.agent-mail/profile-a",
SessionRoot: "/workspace/project/.agent-mail/profile-a/collab",
Session: "collab",
},
Launcher: "tmux",
Placement: &launchapi.PlacementV1{
Target: launchapi.PlacementCurrentWindow, Layout: launchapi.PlacementColumns, LauncherPane: "%17",
},
CallerContext: map[string]string{
"run_id": "run-42", "task_generation": "3",
},
Intent: intent,
}
prepared, err := launchapi.Prepare(ctx, request)
if err != nil {
return err
}
decisions := make([]launchapi.DecisionV1, 0, len(prepared.RequiredActions))
for _, action := range prepared.RequiredActions {
choice, ok := reviewedChoiceFor(action)
if !ok {
return fmt.Errorf("no reviewed decision for %s", action.ActionID)
}
decisions = append(decisions, launchapi.DecisionV1{
ActionID: action.ActionID,
Choice: choice,
})
}
result, err := launchapi.Apply(ctx, launchapi.ApplyRequestV1{
RequestVersion: launchapi.RequestVersionV1,
Prepare: request,
SubjectSchema: prepared.SubjectSchema,
SubjectDigest: prepared.SubjectDigest,
Decisions: decisions,
})
New Prepare calls use subject schema 2. Apply input serialized by a 0.61.0
caller has no subject_schema; 0.61.1 interprets that omission as schema 1
and reports reprepare_recommended in the result hints. A new caller must copy
the returned schema. It must not omit the field to select legacy behavior.
Apply with schema 1 remains compatible for participant-only requests; if any participant is runnable, it returns action_required with reason_code reprepare_required and performs no launch mutation, so the caller must re-Prepare and Apply with schema 2.
reviewedChoiceFor is intentionally caller-owned. AMQ does not choose trust,
stale-conversation, rebind, or degraded-capability decisions for the caller.
Apply and lifecycle results include a typed mutation disposition (not_applied,
committed, or uncertain) and, after a launch commit, its binding generation;
these fields describe the launch backend binding only, not session or roster
durability codes. Post-commit failures remain action-required results with exit
code 6, not bare errors.
Apply with schema 1 remains compatible for participant-only requests. If any
participant is runnable, it returns action_required with
reason_code=reprepare_required before launch mutation; re-Prepare and Apply
with schema 2. A participant-only schema-1 Apply remains provisionable.
The equivalent CLI split is:
amq launch --plan intent.json --prepare --json --launcher commands
amq launch --apply apply-request.json --json
Both commands accept - for standard input. The Apply document contains the
complete target and launcher, so --session, --root, and --launcher are not
valid with --apply. Exit code 6 means the JSON result requires an operator
action. JSON on stdout remains the machine contract; stderr is for people and
must not be parsed.