AEP-0004: Control Profile (experimental)

Defines the L3 control profile: how a consumer (human surface, automation, or another agent) sends commands to an emitter, how commands are acknowledged, gated, deduplicated, and how outcomes are observed.

Draft: the AEP suite is pre-v0.1; each document's status lives in its header table. This page is rendered from the canonical spec/AEP-0004-control-profile.md, which is the normative text.

Field Value
AEP 0004
Title Control Profile
Type Standards Track — Core
Status Draft (profile status: experimental in 0.1)
Sponsor AEP maintainers
Created 2026-07-03
Requires AEP-0001, AEP-0002, AEP-0003
Supersedes / Superseded-by n/a

Provenance (non-normative): control is modeled as events (not JSON-RPC) on traffic-shape and topology grounds; connection-level rules are defined in AEP-0003. The profile is experimental in 0.1 and graduates only after being proven end-to-end on a reference agent adapter.

Abstract

Defines the L3 control profile: how a consumer (human surface, automation, or another agent) sends commands to an emitter, how commands are acknowledged, gated, deduplicated, and how outcomes are observed. Commands, acknowledgements, and outcomes are all ordinary AEP Events (same envelope, same storage, same filters), so a session's replay includes who commanded what, when, with what result: the audit trail is structural, not bolted on.

1. Model

text
sender                    target (emitter)
  │  control.{verb}  ──▶    │        (1) command
  │  ◀── control.accepted / │        (2) ack | nack   (within the ack window)
  │        control.rejected │
  │  ◀── domain Event(s)    │        (3) outcome      (may arrive much later)

Ack semantics ("I will try") are explicitly divorced from outcome semantics ("done"): an agent may take minutes to honor pause. The outcome is an ordinary domain Event (run.cancelled, attention.resolved, and so on) whose cause references the command.

2. Command Events

  1. A command's type is control.{rest} with an imperative verb (control.cancel, control.attention.respond).

  2. Addressing: a command carries session (and optionally run) naming the target; agent names the sender (an ordinary emitter identity, e.g. telegram-sink). Commands MUST omit seq: the sender does not own the target session's order. This is an instance of AEP-0001 §5.2's foreign-emitter exception, alongside consumer-emitted attention.routed; the target's resulting ack and outcome Events are ordinary sequenced session Events, which is how control lands in the session's ordered history.

    Session identity is emitter-scoped (AEP-0001 §5.2), so bare-session addressing presumes a unique live claimant at the routing point. §4.5 says what a router does when that presumption fails.

  3. Idempotency: the envelope id is the idempotency key. A retried command MUST reuse the same id (and rides the same source, the sender's identity); targets MUST dedupe on (source, id) (AEP-0001 §7.4) and MUST NOT re-execute. Retry policy is the sender's timeout policy: safe by construction. Redelivery is also the recovery path for a lost acknowledgement: a target that still holds the command's dedupe state answers a retry by re-emitting its recorded ack (§3), so a retry is always safe and always answered.

  4. Commands are advisory: the agent stays sovereign and MAY refuse (refused nack); commands are requests, not orders.

3. Acknowledgement Events

Type Meaning Payload
control.accepted target will attempt the command { }
control.rejected target will not { "reason": enum REQ, "detail"?: string [redacted] }

Rules: the target makes exactly one ack decision per deduplicated command, embodied in exactly one distinct ack Event with cause = the command's id, emitted within the negotiated ack window (hello.capabilities.control.ack_window_ms, default 10 000 ms).

On redelivery of a command it has already acknowledged, a target MUST re-emit the recorded ack Event byte-identically (same id, same position: legal at-least-once redelivery that consumer dedupe, AEP-0001 §7.4, collapses); never a second decision, never a fresh Event. This is how a lost ack is recovered: the sender's retry (§2.3) elicits the recorded answer.

A duplicate arriving while the decision is still pending needs no additional ack: the one forthcoming ack answers every delivery of the command. The recorded ack is retained exactly as long as the command's dedupe state (§2.3), and a target that no longer remembers the command treats its redelivery as a new command.

Sender-side, silence past the window is treated as a nack with reason timeout (synthesized locally, never emitted on the wire). reason enum: unsupported, unauthorized, busy, refused, invalid. refused is a first-class, non-error outcome.

4. Gating and transport

  1. Control Events flow only on authenticated duplex bindings (WebSocket in 0.1; AEP-0003 §8: authentication is REQUIRED unconditionally, localhost included).

  2. A target's hello.capabilities.control.accepts is authoritative: commands whose type is not listed MUST be nacked unsupported (by the target; a relay that knows the target's declaration MAY nack on its behalf).

    A relay learns a declaration by having terminated that target's own hello on one of its connections. A consumer learns the live claim set and each claim's declared acceptance through the roster frame (AEP-0003 §4.1), a snapshot, never a promise: this gate stays authoritative at command time regardless of what a roster reported. Discovery across chained relays remains each endpoint's own knowledge (AEP-0003 §4.1 scope rule; §8 states the audit boundary relay-behalf rejection creates, a decided design, not a gap).

    A relay refusing a command on a target's behalf answers the sender's connection with a connection-scoped error frame (AEP-0003 §2), never an Event; it carries no seq and never enters session history (§8): { "type": "error", "cause": <the command's id>, "code": <string>, "detail"?: <string> }. The relay MUST set cause to the command's id so the sender can settle the pending command.

    Command-path codes defined in 0.1: unauthorized (unauthenticated binding, or a policy denial per item 3), unsupported (declared-accepts refusal on the target's behalf), ambiguous (more than one live claimant, item 5), and no-route (no connected emitter owns the addressed session).

    A sender treats any cause-matched error frame as that command's refusal, whatever the code. The §2.3 retry rules are unchanged: a refused command MAY be retried when conditions change, e.g. after an ambiguous claim resolves to a single claimant.

  3. Who may command whom is deployment-owned authorization, by decision: ACLs over sender identity, command type, and target, enforced at the relay/endpoint, surfaced to the sender as unauthorized (item 2's frame when a relay refuses; an in-stream nack when a target does, §3). The protocol's own gates are authentication (item 1) and the declared-accepts type gate (item 2): both wire-verifiable. A cross-deployment policy vocabulary is deliberately not part of this profile: deployments differ in identity models and policy engines, and this profile does not standardize a least common denominator the wire cannot verify. A future policy surface, if one proves warranted, arrives as a separate capability-advertised extension and does not change this default.

  4. A relay forwarding a command MUST preserve it byte-identically (AEP-0001 §4.2) and SHOULD persist it in the session history it serves.

  5. A relay MUST deliver a command to at most one target. When more than one live connection claims the addressed session (session identity is emitter-scoped, AEP-0001 §5.2: colliding names are distinct sessions the relay cannot tell apart by session alone), the relay MUST refuse the command (the connection-scoped error frame of the §4.2 relay-behalf mechanism, code ambiguous); never guess a claimant, never broadcast. One command MUST NOT become N executions; a sender that receives acks from a target it did not address has been routed wrong, not answered twice.

5. Commands defined in 0.1 (all experimental)

Type Target scope Payload Expected outcome Event
control.attention.respond session { "answer": { "option"?: string, "text"?: string [redacted], "values"?: object [metadata*] } REQ } (values answers a kind: "form" request, carried identically to attention.answered.answer.values; AEP-0002 §5.3, AEP-0006); subject = id of the attention.requested Event attention.answered then attention.resolved (both cause-linked)
control.cancel run or session { "scope": "run"|"session" REQ, "reason"?: string [redacted] } run.cancelled (and/or session.ended)
control.pause session { } vendor-defined pause state, reported via the §7 outcome Event cause-linked to the command (e.g. progress.status)
control.resume session { } resumption reported via the §7 outcome Event (subsequent Events cause-linked to the command)

Additions to the core command set require the AEP process. Vendors extend via the standard extension namespace (x.{vendor}.control.{verb}); vendor commands are gated by control.accepts identically to core commands.

6. Correlation summary

  • command.cause SHOULD reference the Event that prompted it (for control.attention.respond: the attention.requested id); this keeps the causal DAG connected across the control boundary
  • ack.cause = command.id
  • outcome.cause = command.id (or the ack's id; consumers MUST follow either one hop to the command)
  • control.attention.respond.subject = attention.requested.id, and the resulting attention.* Events keep that same subject (AEP-0002 §7.1); a consumer can reconstruct the full attention loop from subject alone.

7. Conformance

A control-capable target MUST: declare control.accepts at hello; dedupe commands on (source, id); ack or nack every command within the window; retain the recorded ack alongside its dedupe state and re-emit it byte-identically on redelivery (§3); nack unknown (unsupported) and unauthorized (unauthorized) commands; emit outcome Events cause-linked to the command.

A control sender MUST: reuse id on retries; treat window silence as timeout; never depend on outcomes without observing them as Events.

A relay that routes commands MUST: refuse per §4.2/§4.5 with cause-correlated, connection-scoped error frames that never enter the addressed session's history (§8); deliver each command to at most one target (§4.5).

Fixture classes: dedupe-on-retry (one decision, re-acked byte-identically), window timeout, capability nack, attention round-trip (request → respond → answered → resolved), relay-behalf refusal (the deterministic cause-correlated frame with the addressed session's replay untouched; the live checker's control-behalf-refusal).

8. Security considerations

This section is informative; every rule it mentions is defined where cited.

  • State the blast radius plainly. A control-capable sender can cancel and pause running sessions and, through control.attention.respond, answer permission and input requests on a human's behalf (§5). Compromise of a control sender is compromise of the human approval loop.

    That is why control rides only authenticated duplex bindings with authentication required unconditionally, localhost included (§4.1, AEP-0003 §8.1), and why this profile is experimental until the surrounding practice hardens.

  • Authorization is deployment-owned: by decision, not omission. The protocol authenticates the connection and gates command types via control.accepts (§4.2); who may command whom is ACLs at the relay or endpoint, surfaced as unauthorized nacks (§4.3, which also names the extension path for any future policy surface). A deployment that enables control has exactly the authorization it configures: least-privilege accepts lists, per-sender scoping at the relay, and separate tokens for control-capable connections are the operator's tools.

  • The audit trail is structural, with one stated boundary at the relay. Commands, acks, and outcomes are ordinary Events in the session's replayable history (Abstract; §4.4), so "who commanded what, with what result" is reconstructable from the stream itself.

    The boundary, decided: a relay rejecting on a disconnected or non-accepting target's behalf (§4.2) answers the sender with a connection-scoped error frame that never enters the addressed session's history (by design, not omission). Session identity is emitter-scoped (AEP-0001 §5.2), so a refusal Event in that session's history would be an Event the relay authored under an identity that is not its own: precisely the class of forgery the asserted-identity bullets below warn about.

    The durable record lives with the honest authors instead. The sender holds the cause-correlated frame and MAY emit its own outcome record; a relay that needs a durable audit trail logs behalf-rejections operationally and MAY publish them as ordinary Events in a session it emits under its own identity (x.* kinds on the ordinary envelope, a composition that already exists and needs no new protocol surface). Conformance pins the boundary: the addressed session's replay never contains the refusal (the live checker's control-behalf-refusal).

  • Sender identity in the history is asserted identity. A command's agent names the sender under the same self-asserted identity model as every Event (AEP-0003 §8.2); the connection is authenticated, the claimed name is not proven. Audit conclusions drawn from stored command history inherit that trust level until signed identity (AEP-0003 §8.3) hardens it.

  • Target identity is asserted too, and the router never resolves a tie. Any authenticated emitter can claim any session string (AEP-0001 §12), so a spoofed claim of a session under command is possible; §4.5's refusal rule turns that attack into denial of control (the sender sees ambiguous and no one receives the command), never diversion of it. Deployments that need control under collision resolve it operationally (disconnect the impostor, scope tokens per emitter); the protocol refuses to pick a side it cannot verify.

  • Idempotency bounds replay. Retries reuse the command id and source, and targets dedupe on the pair (§2.3), so a captured command replayed byte-identically is inert against a target that has seen it. It does elicit the recorded ack again (§3): a byte-identical re-emission of an Event that was already public on the wire, so the replayer learns nothing new and nothing re-executes.

    Re-issuing the command under a fresh id is indistinguishable from a legitimate new command. The boundary is authenticated emit access to the duplex binding, not the dedupe.

  • Agent sovereignty is a safety property. Commands are advisory and the agent may refuse (§2.4; refused is a non-error outcome, §3). Target-side policy (refusing a control.attention.respond that arrives suspiciously fast, or a control.cancel from an unfamiliar sender) is a legitimate and encouraged defense layer on top of deployment ACLs.

References

  • AEP-0001 §5.2/§7, AEP-0002 §7, AEP-0003 §4/§8.
  • MCP timeout-and-cancel guidance (pattern source).
  • OpenClaw accepted/streaming/final pattern (pattern source).
On this page