---
title: "AEP-0004: Control Profile (experimental)"
sidebarTitle: "AEP-0004: Control profile"
description: "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."
---

{/* GENERATED by docs/render-site.js from spec/AEP-0004-control-profile.md; do not edit. Edit the source and re-run `node docs/render-site.js` */}

<Note>

**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`](https://github.com/agenteventprotocol/agent-event-protocol/blob/main/spec/AEP-0004-control-profile.md), which is the normative text.

</Note>

| 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

```
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).
