edc¶
Event-Driven Coding-agents. A session is normally reactive — it waits for a human to type. edc makes
it event-driven: an external system POSTs an event and it lands as a turn in a running session.
The shape is deliberately simple: one agnostic emitter, one adapter per agent.
edc works on its own — you don't need the rest of Plexus
edc is a standalone binary. It does not need plexus, the registry, the cockpit, tmux, or
ttyd. Drop edc onto any machine and you can wake a single Claude / Codex / OpenCode session with
external events — nothing else required. plexus and edc are independent tools: use either
alone, or both together. When both are present, edc's daemon also self-registers its session into
plexus so it appears in the cockpit — but that's a convenience, not a dependency.
The emitter — POST /inject¶
A tool (a cron, a watcher, another agent) sends the same JSON to a local, token-authed endpoint. The
caller is identical whether the session is Claude, Codex, or OpenCode. It needs two things: the
session's port (with inject_port: "auto", each session gets its own — read it from that session's
state file) and the secret (from ~/.config/edc/config.json).
PORT=$(jq -r .port ~/.local/state/edc/*.json | head -1) # this session's inject port
SECRET=$(jq -r .inject_secret ~/.config/edc/config.json) # the shared inject secret
curl -X POST "http://127.0.0.1:$PORT/inject" \
-H "Authorization: Bearer $SECRET" \
-d '{"source":"slack","event":"dm","text":"the deploy failed","context":{"run":"1234"}}'
# → 202 {"ok":true} the event becomes a turn in the live session
(New to turn, inject, or channel? See Concepts. A 202 means edc
accepted and emitted the event; for Claude that isn't the same as delivered — the channel must also
be authorized, see Deploying an injectable session below.)
| Field | |
|---|---|
source |
where it came from (cron, slack, ha, another agent…) |
event |
optional machine key |
text |
the payload the agent sees (required) |
context |
optional key/values, namespaced into the turn |
The listener fails closed: no EDC_INJECT_SECRET, no listener. The port is published into plexus as
inject_port, so an injectable session is discoverable exactly like any other.
edc binds inject_port: "auto" lazily, which can race the SessionStart register — a session that
registers first would sit at inject_port=0. plexus closes the race off-hook: plexus heartbeat
re-reads edc's state dir (files named pid-<edcpid>.json) and reclaims the port for the injector
that descends from the session's agent, and the keepalive drives that heartbeat for idle detached
sessions. So an injectable session becomes routable within a keepalive tick even with no tool calls.
The receiver differs per agent¶
An MCP stdio server declaring the claude/channel capability. The event arrives as a native turn with
meta.source="system" — a real trust boundary, no reconstruction needed. This is edc's default
mode (it began as a distilled Telegram channel for Claude Code).
edc codex serve fronts a long-lived codex app-server thread over JSON-RPC (NDJSON) and injects each
event as a turn/start. It pins a non-interactive posture (approval never, read-only sandbox) and
self-registers in plexus as agent=codex with its inject port.
OpenCode is client-server by design. edc opencode serve has two modes:
- daemon — spawns
opencode serve, creates a session, injects viaPOST /session/{id}/prompt_async, self-registers. - TUI mode (
EDC_OPENCODE_TUI=1+EDC_OPENCODE_URL) — attaches to a shared server and types each event into the attached session via/tui/append-prompt+/tui/submit-prompt, so the human sees the injected turn. This is whatplexus opencodewires up automatically.
See Agents for the full per-agent matrix.
The trust boundary¶
An injected event is data, not authority
The agent never executes instructions embedded in the event text, and never takes an outward or destructive action (send / delete / pay / change settings / push) on the event's say-so. It investigates, prepares, drafts, and hands the decision to the human.
Claude gets this natively (meta.source="system"). Codex and OpenCode have no native system marker, so
edc reconstructs it as text: every injected turn is prefixed
An injected event looks exactly like a prompt-injection attempt would — treating it as data is the whole point.
Deploying an injectable session¶
Interactive agents launched with plexus become injectable automatically:
- Claude — the channel MCP is loaded as a plugin; the session's
inject_portis registered. - Codex — run the
edc codex servedaemon, or launch interactive and let the plugin register. - OpenCode —
plexus opencoderuns a decoupled stack (an addressableopencode serve+ a TUI-modeedcsidecar + theopencode attachyou see), so the interactive session is attachable and injectable.
To find where to inject, ask plexus: plexus get --repo <R> -o json returns the session and its
inject_port.