This contract lets an existing Agent runtime drive the AWP Desktop without linking against the Vue or Electron code. The boundary is one explicitly configured subprocess per conversation, JSON Lines over standard input and standard output, and an optional opaque native session id for resume.
The protocol is small on purpose. AWP owns product concerns such as the desktop window, threads, attachments, stream rendering, diagnostics, and lifecycle. The CLI continues to own models, planning, tools, permission policy, and its private history.
The operator configures:
| Variable | Contract |
|---|---|
AWP_AGENT_CLI_EXECUTABLE |
Absolute path to a regular, non-symbolic-link executable |
AWP_AGENT_CLI_ARGS_JSON |
Optional JSON array of exact prefix arguments; no shell parsing |
AWP_AGENT_CLI_ENV_JSON |
Optional JSON object containing the only agent-specific child environment values |
AWP_AGENT_CLI_PROTOCOL |
awp-jsonl for this contract, or stream-json for CLIs that require compatibility flags |
AWP_AGENT_DEFAULT_MODEL |
Model id shown by Desktop and passed unchanged to the CLI |
The child inherits only a small operating-system environment allowlist plus the
exact AWP_AGENT_CLI_ENV_JSON entries. A remote HTTP(S) URL in arguments or
child environment is rejected unless AWP_AGENT_REMOTE_API_OPT_IN=1 is set on
the Desktop launcher.
For awp-jsonl, AWP invokes the executable without a shell:
<executable> <prefix args...> --model <model> [--resume <native-session-id>] [--mcp-config <path>]
stream-json adds these compatibility arguments before the model and resume
arguments:
--output-format stream-json --input-format stream-json --print --verbose
The optional MCP file contains endpoints generated by the Desktop main process.
An adapter may ignore --mcp-config when it does not support MCP, but it must
consume both the flag and its value without treating them as a prompt.
The process stays alive for the conversation. Each turn is one UTF-8 line:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Explain this failure"}]}}Content blocks can contain:
{ "type": "text", "text": "..." }{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "..." } }{ "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": "..." } }
An adapter must reject unsupported blocks explicitly or convert them to a documented fallback. It must not silently reinterpret file paths as commands.
On Windows, graceful shutdown can arrive as:
{"type":"shutdown"}On every platform the process must also handle ordinary termination. The CLI must not emit a response before it has accepted the first input line; this keeps the first event associated with the wrapper session returned to the renderer.
Standard output is protocol-only. Diagnostics belong on standard error. Every output event is one complete JSON object followed by a newline.
The recommended start event establishes the opaque resume id:
{"type":"system","subtype":"init","session_id":"opaque-session-id","model":"provider-model-id"}Text can be emitted as complete assistant messages:
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Partial or complete text"}]}}or as streaming deltas:
{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Partial text"}}
{"type":"content_block_stop","index":0}Tool progress can use an assistant tool_use block or the corresponding raw
content_block_start event. AWP renders the tool name and a bounded summary; it
does not execute the tool for the CLI.
The checked-in reference CLI is one concrete example of that ownership rule. It is chat-only unless the launcher receives all of:
AWP_AGENT_MANAGED_TASKS_OPT_IN=1
AWP_CONTROL_PLANE_URL=http://127.0.0.1:8100
AWP_CONTROL_PLANE_API_KEY=<operator-provided secret>
When enabled, it advertises only awp_run_managed_task. The tool accepts a
bounded argv array plus a 1–300 second wait limit, submits a command task with
an idempotency key, polls the control plane, and returns bounded stdout/stderr
to the provider for a follow-up response. It never invokes a shell. The worker's
configured executable allow-list is the final authority.
This is deliberately not a generic MCP host or permission system. A timeout means the adapter stopped waiting; it does not prove the worker cancelled the task. A transport failure after submission can leave the outcome unknown. The idempotency key makes a repeated submission of the same session/turn/tool call safe at the control plane, but does not claim global exactly-once execution.
Exactly one terminal event is required for every accepted user turn. Preferred:
{"type":"result","subtype":"success","is_error":false,"session_id":"opaque-session-id","usage":{"input_tokens":12,"output_tokens":8}}Failure:
{"type":"result","subtype":"error","is_error":true,"session_id":"opaque-session-id","result":"stable_machine_readable_error"}Raw streams may instead finish with message_stop. Exiting before a terminal
event is an abnormal mid-turn failure. Malformed stdout lines are not rendered;
an unbroken stdout buffer over 4 MiB terminates the runtime.
session_idis opaque to AWP and must match[A-Za-z0-9._:-]{1,256}.- Desktop persists the id with its thread and supplies it through
--resumeon a later process. - The CLI decides what the id restores. It must not assume Desktop copied the CLI's private provider history.
- AWP guarantees one stdin write attempt for one accepted UI send. It does not claim exactly-once provider execution. A process/network failure may leave the provider outcome unknown, so adapters should use provider idempotency keys where available.
agent:electronis fail-visible and does not replay through the deterministic adapter. The general hosted compatibility mode may perform one documented HTTP fallback; that mode is not used by the real-agent launcher.
The Agent CLI is a trusted local process with the permissions of the Desktop user. This is an integration boundary, not an untrusted-code sandbox.
- AWP never invokes the CLI through a shell.
- Remote URLs require explicit opt-in; non-loopback plain HTTP is rejected by the checked-in reference adapter.
- The optional managed-task bridge is disabled unless its exact opt-in, URL, and key are all present. Remote control planes require HTTPS plus the general remote-network opt-in.
- Secrets should be passed through the operator's secret store and environment, never prefix arguments or repository files.
- Prompts, tool payloads, and provider output can be private. Local trace collection is disabled by default and still should not be treated as a safe place for secrets.
examples/openai-compatible-agent-cli/awp-agent-cli.mjs
implements this protocol using only Node.js built-ins. It streams a real
OpenAI-compatible endpoint and uses same-filesystem replace writes for native
session history.
The Desktop regression starts a real subprocess plus loopback provider and control-plane fixtures. It proves request delivery, translated text/usage/tool events, managed-task request/result framing, a terminal result, persisted native session id, and restored history on resume:
npm --prefix apps/desktop run test:reference-provider
The local-stack CI job additionally drives the reference CLI through a deterministic provider fixture and the real Compose FastAPI/Redis/Python-worker path:
node scripts/reference_agent_local_round_trip.mjs
The provider fixture is intentionally not described as an LLM. It makes the tool decision deterministic while leaving task storage, claim, execution, and result return on the real public stack.