This protocol is authority separation and robust local IPC, not an OS sandbox. Plugins are trusted same-UID executables installed by the Worker operator. One fresh process handles one operation.
Each frame is one compact UTF-8 JSON object followed by LF. Stdout contains frames only; stderr is private diagnostics. Duplicate or unknown fields, invalid UTF-8, blank lines, CRLF, malformed JSON, frames over 1 MiB, and a final frame without LF are invalid.
The Worker generates a lowercase 32-hex id. Every frame has exactly version, id, type, and the fields shown below. version is exactly v1; IDs must match.
- Worker sends
initialize, plugin sendsinitialized. - Worker sends one
execute. - Plugin may send negotiated
progressframes, then exactly oneresultorfailure. - Worker closes stdin and requires clean stdout EOF and exit 0 within 250 ms. Any trailing byte/frame or nonzero exit is a protocol failure.
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"initialize","capabilities":["text"],"limits":{"frame_bytes":1048576,"progress_frames":128,"text_bytes":65536,"progress_text_bytes":1024,"commit_subject_bytes":256}}
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"initialized","capabilities":["text"]}Capabilities are closed: text, workspace_edit, progress, cancel, commit_subject, agent_report. initialized.capabilities is a duplicate-free subset of the offer and must include the operation capability. Unknown or unoffered selections fail.
Text execution and success:
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"execute","operation":"text","input":"hello"}
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"result","output":"HELLO"}input and output are at most 65,536 UTF-8 bytes.
Workspace execution and success:
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"execute","operation":"workspace_edit","workspace":"/operator/path","instruction":"edit the files","timeout_ms":60000}
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"result","commit_subject":"feat: describe the edit"}agent_report is an optional v1 capability. Workers offer it; plugins select it only when offered. Workspace results may contain summary and changes only when selected. Otherwise plugins omit both fields and receivers strictly reject either field, including null. Legacy commit_subject results remain valid. The v1 limits and version are unchanged.
The workspace result has the common fields, optional commit_subject, and an optional report pair: summary and changes. Legacy results without the report remain valid. A report has a non-empty summary of at most 1024 UTF-8 bytes and 1–12 non-empty change strings of at most 256 UTF-8 bytes each. Report strings must be trimmed and contain no Unicode control/format characters or U+2028/U+2029. Unknown, duplicate, malformed Unicode, or partially supplied report fields fail closed. The report is advisory and self-reported, never check evidence. Worker encodes it canonically into the existing candidate result only after successful checks and candidate creation; Gate persists it with the producing attempt. Progress frames remain transient and are not stored. If present, commit_subject requires negotiation, is 1..256 UTF-8 bytes, has no leading/trailing Unicode whitespace, Unicode control or format characters, U+2028/U+2029, or logical second line. When absent, Worker uses chore: apply coding task. Worker passes it as one argv element.
Progress requires negotiation, is limited to 128 frames, and has monotonically consecutive sequence numbers starting at 1. stage is one of started, working, finalizing; text is at most 1,024 UTF-8 bytes:
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"progress","sequence":1,"stage":"started","text":"editing"}Failure has category equal to invalid_request, incompatible, execution_failed, or cancelled. It describes only the plugin operation; Worker owns task disposition:
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"failure","category":"execution_failed"}If local context expires after negotiated execution, Worker sends exactly one cancel and allows 250 ms to drain. On Linux, each invocation runs under a child-subreaper wrapper that performs bounded, PID-identity-safe termination and reaping of invocation descendants after successful, failed, or cancelled completion, including ordinary and setsid descendants that remain attached or are reparented to the wrapper. This is best-effort cleanup for trusted local same-UID plugins, not hostile-plugin containment; without a cgroup or PID namespace, a deliberately adversarial process may escape tracking or interfere with the Worker. Other supported systems retain bounded ordinary process-group cleanup on cancellation. Cancellation is authoritative once observed before Run returns, including over concurrent success, exchange errors, or cleanup-induced wrapper exit. A hard output-limit violation wins only when Worker selects it while the context is still live; an already-ready cancellation wins instead of relying on random select choice.
{"version":"v1","id":"0123456789abcdef0123456789abcdef","type":"cancel"}Progress and stderr stay Worker-local. Plugins cannot lease, retry, complete attempts, create candidate refs, or declare authoritative checks/state.
Build forge-plugin-conformance, then pass the plugin executable after flags. It runs one valid session plus fresh-process malformed-request rejection scenarios and exits nonzero on any failure:
Invalid initialization must produce no stdout before a bounded nonzero exit. Invalid execution may produce exactly the deterministic initialized frame, then no other stdout, before a bounded nonzero exit. The suite rejects blank, malformed, invalid UTF-8, CRLF, non-compact, oversized, and unterminated frames; invalid IDs, fields, capabilities, limits, versions, operations, payloads, and frame order; and contradictory or duplicate execute fields.
go build -o /tmp/forge-plugin-conformance ./cmd/forge-plugin-conformance
/tmp/forge-plugin-conformance ./examples/forge-plugin-v1.py
/tmp/forge-plugin-conformance ./bin/forge-ref-plugin
CODEX_BIN=/path/to/fake-codex /tmp/forge-plugin-conformance -operation workspace_edit ./bin/forge-codex-pluginscripts/plugin-conformance-e2e.sh builds and runs that suite against the shipped reference plugin, the dependency-free Python example, and the Codex workspace plugin with a local fake Codex binary.
The progress capability under plugin protocol v1 retains exactly started,
working, and finalizing. Old strict peers remain supported. ServeWithProgress
serializes concurrent callbacks and closes progress before the terminal frame.
The Codex plugin emits fixed lifecycle text only; executor stdout/stderr is never
forwarded. Receivers validate negotiated capability, UTF-8, sequence, frame count
and byte limits before invoking the progress callback.
A Worker separately offers activity_version=live_activity/v1 on its Gate
connection. Only a Gate that recognizes that offer selects activity_version in
its lease. An absent selection disables activity. Unknown lease versions and
leases carrying an activity payload are rejected before execution. Activity
payloads belong only to activity messages, with exact job, attempt and Worker
identity and the selected version.
The Worker maps started to analyzing and working to editing, discarding
plugin text. Legacy finalizing does not advance the operator stage before
Worker checks. The Worker emits testing once before a nonempty declared-check
batch and preparing_result once after the batch ends, including its existing
stop-on-failure path. Empty check lists emit no testing stage. Stages advance
strictly, with at most four events per attempt; no command names, output, prompts,
JSON payloads or plugin timestamps enter activity.
Gate stamps receive time and projects the exact Run ID, Attempt ID/ordinal and Worker on each event. Storage is transient, bounded to 1,024 attempt entries and four closed-stage events per entry; terminal, disconnected and expired activity is removed. Current lease deadlines are consulted because heartbeats extend them. The open drawer refreshes every five seconds independently of backlog loading, uses text-only rendering, and keeps activity separate from Agent report, Forge verification and delivery evidence. There is no subagent telemetry. This activity is neither scoped-check evidence nor delivery acceptance evidence.