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
experimentalin 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
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
A command's
typeiscontrol.{rest}with an imperative verb (control.cancel,control.attention.respond).Addressing: a command carries
session(and optionallyrun) naming the target;agentnames the sender (an ordinary emitter identity, e.g.telegram-sink). Commands MUST omitseq: 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-emittedattention.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-
sessionaddressing presumes a unique live claimant at the routing point. §4.5 says what a router does when that presumption fails.Idempotency: the envelope
idis the idempotency key. A retried command MUST reuse the sameid(and rides the samesource, 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.Commands are advisory: the agent stays sovereign and MAY refuse (
refusednack); 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
Control Events flow only on authenticated duplex bindings (WebSocket in 0.1; AEP-0003 §8: authentication is REQUIRED unconditionally, localhost included).
A target's
hello.capabilities.control.acceptsis authoritative: commands whosetypeis not listed MUST be nackedunsupported(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
helloon one of its connections. A consumer learns the live claim set and each claim's declared acceptance through therosterframe (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
errorframe (AEP-0003 §2), never an Event; it carries noseqand never enters session history (§8):{ "type": "error", "cause": <the command's id>, "code": <string>, "detail"?: <string> }. The relay MUST setcauseto the command'sidso 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-acceptsrefusal on the target's behalf),ambiguous(more than one live claimant, item 5), andno-route(no connected emitter owns the addressedsession).A sender treats any
cause-matchederrorframe as that command's refusal, whatever thecode. The §2.3 retry rules are unchanged: a refused command MAY be retried when conditions change, e.g. after anambiguousclaim resolves to a single claimant.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-acceptstype 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.A relay forwarding a command MUST preserve it byte-identically (AEP-0001 §4.2) and SHOULD persist it in the session history it serves.
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 bysessionalone), the relay MUST refuse the command (the connection-scopederrorframe of the §4.2 relay-behalf mechanism, codeambiguous); 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.
causeSHOULD reference the Event that prompted it (forcontrol.attention.respond: theattention.requestedid); this keeps the causal DAG connected across the control boundary - ack.
cause= command.id - outcome.
cause= command.id(or the ack'sid; consumers MUST follow either one hop to the command) control.attention.respond.subject=attention.requested.id, and the resultingattention.*Events keep that samesubject(AEP-0002 §7.1); a consumer can reconstruct the full attention loop fromsubjectalone.
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
experimentaluntil 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 asunauthorizednacks (§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-privilegeacceptslists, 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
errorframe 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'scontrol-behalf-refusal).Sender identity in the history is asserted identity. A command's
agentnames 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
sessionstring (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 seesambiguousand 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
idandsource, 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
idis 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;
refusedis a non-error outcome, §3). Target-side policy (refusing acontrol.attention.respondthat arrives suspiciously fast, or acontrol.cancelfrom 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).