Skip to content

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 via POST /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 what plexus opencode wires 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

SYSTEM EVENT (untrusted data) — source=… event=…

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_port is registered.
  • Codex — run the edc codex serve daemon, or launch interactive and let the plugin register.
  • OpenCodeplexus opencode runs a decoupled stack (an addressable opencode serve + a TUI-mode edc sidecar + the opencode attach you 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.