Skip to content

Latest commit

 

History

History
198 lines (148 loc) · 8.06 KB

File metadata and controls

198 lines (148 loc) · 8.06 KB

AWP Agent CLI protocol

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.

Launch contract

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.

Input JSONL

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.

Output JSONL

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.

Resume and delivery semantics

  • session_id is opaque to AWP and must match [A-Za-z0-9._:-]{1,256}.
  • Desktop persists the id with its thread and supplies it through --resume on 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:electron is 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.

Security boundary

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.

Reference implementation and regression

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.