AEP-0003: Transport Bindings, Connection Lifecycle, and Subscriptions
Defines the four AEP 0.1 transport bindings (JSONL (stdio/file), HTTP POST (emit), SSE (consume), WebSocket (duplex)) and the connection lifecycle each requires: the hello exchange and capability negotiation, producer identity and authentication, the attr-match subscription filter dialect, resum
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-0003-bindings-and-lifecycle.md, which is the normative text.
| Field | Value |
|---|---|
| AEP | 0003 |
| Title | Transport Bindings, Connection Lifecycle, and Subscriptions |
| Type | Standards Track — Binding |
| Status | Draft |
| Sponsor | AEP maintainers |
| Created | 2026-07-03 |
| Requires | AEP-0001, AEP-0002 |
| Supersedes / Superseded-by | n/a |
Provenance (non-normative): one wire unit in four framings (no JSON-RPC), per-binding lifecycle classes, the
hellohandshake, attr-match, and(epoch, seq)resume. Filter keys use native envelope spelling.
Abstract
Defines the four AEP 0.1 transport bindings (JSONL (stdio/file), HTTP POST (emit),
SSE (consume), WebSocket (duplex)) and the connection lifecycle each requires: the
hello exchange and capability negotiation, producer identity and authentication,
the attr-match subscription filter dialect, resume via (session, epoch, seq),
backpressure rules, and adapter conformance guidance including capture redaction.
The wire unit on every binding is the identical AEP-0001 envelope; no binding wraps
or rewrites Events.
1. Binding classes and lifecycle surface
Events are valid without any connection (AEP-0001 §4.2). Lifecycle obligations apply only to live bindings, graduated by class:
| Binding | Class | Lifecycle surface |
|---|---|---|
| JSONL (stdio/file) | none | no handshake; the envelope alone carries version and identity |
| HTTP POST (emit) | stateless-lite | version in a request header; identity via auth; discovery via GET (§3.4); no negotiation round-trip |
| SSE (consume) | half | subscription expressed in the request; resume via Last-Event-ID |
| WebSocket (duplex) | full | hello exchange, capability negotiation, multiplexed subscriptions, control (AEP-0004) |
An implementation claims L2 conformance per binding it supports; no binding is
mandatory for emitters (the cheapest conformant emitter writes JSONL to stdout).
Consumers claiming L2 MUST implement attr-match (§6) and resume (§7) on the
consuming bindings they support.
2. Frames versus Events (WebSocket vocabulary)
On the WebSocket binding, each text frame carries one JSON object that is either an
Event (AEP-0001 envelope; its type always contains a dot) or a frame: a
connection-scoped message whose type contains no dot. Frame types in 0.1:
hello, subscribe, subscribed, unsubscribe, unsubscribed, roster,
error, replay-exhausted, closing, ping, pong. Unknown frame types MUST be
ignored (forward compatibility). Frames are never persisted, never carry seq, and
never appear on non-WS bindings.
3. Binding specifications
3.1 JSONL (stdio / file)
- One Event per line,
\n-delimited, UTF-8, no blank lines; writers MUST write each line atomically (singlewrite) so concurrent tailing never observes a torn Event. - An append-only session log file is simultaneously a durable history format and a
transport:
tail -f x.jsonlis a conformant consume path. RECOMMENDED layout:{root}/{agent}/{session}.jsonl. - No handshake, no version negotiation: the
aepattribute on each line is the version marker; a reader encountering an unsupported major version MUST skip the line and SHOULD report it once. - Trust model: filesystem and process trust (§8.1).
3.2 HTTP POST (emit)
POST {base}/eventswith either a single Event (Content-Type: application/json) or a batch, one Event per line (Content-Type: application/x-ndjson). Batch order within one request is preserved.- Request header
AEP-Version: 0.1is REQUIRED. Servers MUST answer an unsupported major version with 426 Upgrade Required and body{ "versions": ["0.1"] }. - Responses:
202accepted (body optional);400with{ "error": "invalid-event", "detail": ... }when any Event fails envelope validation (batch:207-style per-line report MAY be used, or reject the batch whole with the first failing line index);401/403per §8;413oversize;415wrong content type. - Delivery is at-least-once from the emitter's perspective: emitters SHOULD retry
on network failure and 5xx with the same Event
ids (dedupe is the consumer's duty, AEP-0001 §7.4). - Structured body only; there is no header-mapped ("binary mode") encoding.
3.3 SSE (consume)
GET {base}/streamwithAccept: text/event-stream. Subscription parameters:filter(URL-encoded attr-match JSON, §6),capture(ceiling, §6.4),from(URL-encoded JSON array of{session, epoch, seq}triples for multi-session exact resume, or the literal stringallfor replay-all, §5).- Each SSE message:
event:= the AEPtype;data:= the Event JSON on one line;id:={session}:{epoch}:{seq}for session-scoped Events. Agent-scoped Events carry noid:field (they are not resumable positions). - Resume: the
Last-Event-IDrequest header, when present, is the resume position for the session it names; servers MUST replay buffered Events after that position for that session before live delivery. For exact multi-session resume, consumers SHOULD use thefromparameter (which takes precedence overLast-Event-ID). When a requested position predates the buffer, the server MUST emit an SSE comment line: replay-exhausted {session} {earliest-epoch}:{earliest-seq}before delivering what it holds. - Heartbeat: servers SHOULD send a comment line (
: ping) at least every 30 s.
3.4 HTTP discovery document
GET {base}/events (the same URL emitters POST to) MUST return the discovery
document, Content-Type: application/json:
{
"aep": ["0.1"],
"role": "collector",
"endpoints": { "events": "/events", "stream": "/stream", "socket": "/socket" },
"capabilities": {
"filters": ["attr-match"],
"replay": { "buffer": 10000, "all": true },
"capture": ["none", "metadata", "redacted"],
"control": { "accepts": [] },
"roster": {},
"experimental": {}
}
}capabilities reuses the hello capability shape (§4) so stateless emitters learn
the same facts a WS party would negotiate. Origin-root deployments SHOULD also serve
it at /.well-known/aep. Fields other than aep and endpoints are OPTIONAL;
unknown fields MUST be ignored.
3.5 WebSocket (duplex)
- URL:
{base}/socket. After the WS handshake, the first frame in each direction MUST behello(§4). Any other first frame gets anerrorframe (code: "hello-required") and close. - One Event or frame per text frame; binary frames are reserved and MUST be rejected in 0.1.
- Emitters send Events after
hellocompletes; consumers manage subscriptions withsubscribe/unsubscribeframes (§6.5); control Events flow only on this binding (AEP-0004). - Orderly close: a party closing deliberately SHOULD first send a
closingframe:{ "type": "closing", "reason": string, "delivered": [{ "sub": id, "session": s, "epoch": e, "seq": n }...] }carrying last-delivered positions per subscription, then perform the WS close handshake. Consumers MUST NOT depend on receiving it (abnormal loss happens); they track their own positions. - Liveness:
ping/pongframes ({ "type": "ping", "t": ... }echoed aspong); RECOMMENDED interval 30 s, timeout 90 s.
4. The hello exchange (WS)
{
"type": "hello",
"versions": ["0.1"],
"role": "emitter",
"identity": {
"agent": "claude-code",
"instance": "host-a/7c1f",
"card": null
},
"capabilities": {
"emits": ["session", "run", "tool", "attention", "error"],
"capture": ["none", "metadata", "redacted"],
"control": { "accepts": ["control.attention.respond"], "ack_window_ms": 10000 },
"replay": { "buffer": 10000, "all": true },
"filters": ["attr-match"],
"experimental": {}
}
}Normative rules:
versionsis ordered preference, newest first. The responder'shelloselects exactly one version from the intersection (itsversionsarray has length 1); no intersection gets anerrorframe (code: "version-unsupported",versionslisting supported ones) and close. One version governs the whole connection.roleisemitter,consumer, orboth.identity.agentis REQUIRED for emitters (matches the envelopeagentof the Events it will send);instancedisambiguates processes;cardoptionally carries the JWS identity document (§8.3).- Unknown capability keys MUST be ignored: capabilities are a discovery surface, never a validation gate.
- Capabilities are declarations, not promises of exclusivity: an emitter MAY emit
categories it did not declare (declarations are routing hints), but a party MUST
NOT depend on a feature the peer did not declare. Exception (control):
control.acceptsis authoritative; commands outside it MUST be nacked (unsupported), per AEP-0004. capabilities.experimentalis the graduation namespace: features live there before standardization.
4.1 The roster frame (WS)
Declarations exist per connection (§4), but a consumer watching a fleet through a
relay could not see any other party's declarations: it learned a target's
control.accepts only by sending a command and reading the nack. The roster
frame closes that gap: a consumer asks the endpoint it is connected to for a
snapshot of the live emitter claims that endpoint terminates, and the declared
capability facts attached to each claim.
{ "type": "roster", "id": "r-1" }The endpoint replies with the complete snapshot:
{ "type": "roster", "id": "r-1",
"entries": [
{ "session": "s_8f2c", "agent": "claude-code", "instance": "host-a/7c1f",
"control": { "accepts": ["control.attention.respond"], "ack_window_ms": 10000 },
"emits": ["session", "run", "tool", "attention"] }
] }Normative rules:
- Advertisement. Endpoints implementing this frame declare
"roster": {}in their capabilities (hello§4, and the discovery document §3.4). A consumer MUST NOT expect an answer from an endpoint that did not declare it: unknown frame types are ignored (§2), so an undeclared request would wait on silence. - Request/reply. The request carries
id(REQUIRED); the reply echoes it. The frame is watch-class: requests are honored onconsumer/bothconnections under the same §8 authentication posture assubscribe, never looser. Anemitter-role request is refused in-band with the §6.5 pattern: anerrorframe (code: "unauthorized", sameid), and the connection survives. - Scope.
entriescovers exactly the live (connection, claimed session) pairs the answering endpoint currently terminates, the same knowledge boundary AEP-0004 §4 states for relay-behalf refusals. Chained relays answer for themselves; no cross-relay aggregation is defined in this revision. Sessions with history but no live duplex claimant are absent: absence means no live route (the command would nackno-route), which is exactly the affordance fact a consumer needs. - Entries relay declarations verbatim. Per entry:
session(REQUIRED, the claim as declared),agentandinstancefrom the claiming connection'shelloidentity,control(that connection's declaredcapabilities.control, verbatim, absent when none was declared) andemitswhen declared. Endpoints MUST NOT synthesize, merge, or infer members it was never sent. - Ambiguity is visible, never resolved. One entry per live claim: a
sessionvalue repeating across entries reports the multi-claimant state (AEP-0004 §4). Endpoints MUST NOT merge claims or elect one. - A roster is a snapshot, never a promise. Entries state declarations as of
the reply. The authoritative gate for commands remains the target's declared
acceptsat command time (AEP-0004 §4); nack semantics are identical with or without this frame. Consumers MAY re-request at any time; this revision defines no change-push (livesession.*Events already signal the moments worth re-asking about). - Metadata only. Entries carry identity fields and declared verb/category lists: no event content, and no interaction with capture ceilings (§6.4).
5. Replay buffers
An endpoint that fans out Events MAY keep a bounded per-session replay buffer
and, if it does, MUST advertise its depth in hello and the discovery document
(replay.buffer, count of Events per session or a global count; implementations
document which).
Requested resume positions that predate the buffer MUST produce
replay-exhausted (WS frame: { "type": "replay-exhausted", "sub": id, "session": s, "earliest": { "epoch": e, "seq": n } }; SSE comment, §3.3.3),
followed by delivery from the earliest held position. The consumer then knows to
consult a history store.
Replay-all. In place of the position array, from MAY carry the literal
string all: replay every session buffer the endpoint currently holds, each
from its earliest held position (filter and capture ceiling applied as for any
replay), then live delivery; live=0 composes unchanged. Endpoints
implementing it advertise "all": true inside replay (hello §4 and the
discovery document §3.4).
Three honesty rules:
- Replay-all is bounded by what the endpoint holds. It is never a completeness claim, and sessions wholly evicted are absent.
- No
replay-exhaustedfires for it: the request names no position that could predate a buffer, and "earliest held" is the ask itself. - A consumer that needs evicted history consults a history store, exactly as for any exhausted resume.
An endpoint that does not implement the value refuses it deterministically
through the closed refusal classes that already govern malformed from
(SSE §3.3, WS §6.5); advertisement therefore serves planning, not
determinism, and a consumer MAY equally try first and fall back to enumerated
positions on the refusal.
A buffering fan-out endpoint is not a terminal consumer: buffer admission
dedupes on (source, id) (AEP-0001 §7.4), but live fan-out forwards Events as
received: at-least-once redelivery MUST be able to traverse the hop, or
recovery built on redelivery (AEP-0004 §3's re-acknowledgement of a retried
command) breaks at it. Dedupe is the terminal consumer's duty.
6. The attr-match filter dialect
attr-match is the REQUIRED filter dialect; richer dialects (e.g. cesql) are
optional, declared via capabilities.filters. Design bound: implementable in an
afternoon in any language.
6.1 Grammar
A filter is a JSON object. Each member is a predicate on one envelope context
attribute (native spelling) or extension attribute. A filter
matches an Event iff every member matches (conjunction). An empty filter {}
matches every Event.
Filter = { (AttrName : Predicate)* }
Predicate = Scalar ; equality
| [ Scalar+ ] ; inclusion: any-of
| TypePattern | [ TypePattern+ ] ; only on "type"
| SeverityCmp ; only on "severity"
Scalar = JSON string / integer / boolean
TypePattern = exact type
| prefix ".*" ; e.g. "attention.*", "x.acme.*"
SeverityCmp = { "gte": Level }
| { "lte": Level }
| { "gte": Level, "lte": Level }
Level = "debug"|"info"|"notice"|"warning"|"error"|"critical"6.2 Matching rules
Allowed keys:
id,type,subject,source,agent,session,run,step,epoch,cause,severity,capture, and extension attribute names (§AEP-0001 5.4).time,seq,aep,traceparent, anddataare NOT filterable: routing never parses payloads; positions are the resume mechanism, not filters; trace context is pass-through join metadata; and a core attribute name is never a legal extension key (AEP-0001 §5.4).A filter key that is neither a filterable core attribute nor a syntactically legal extension-attribute name (AEP-0001 §5.4) MUST be rejected (
invalid-filter): fail closed, never match-all. Legal extension-attribute keys are always accepted; whether any event carries them is a matching question, not a validity question.Equality/inclusion compares JSON values exactly (no coercion). An inclusion array matches if any element matches.
Type patterns:
p.*matches anytypestrictly beginning withpfollowed by a dot (attention.*matchesattention.requested, notattention); patterns compose with inclusion arrays;*alone is NOT a pattern (use{}or omit the key). Patterns apply to vendor types too (x.acme.*).Severity comparison uses the §AEP-0001 8.1 total order, inclusive bounds.
Absent attributes: a predicate on an attribute the Event omits does not match, except
severity,capture, andepoch, where the AEP-0001 defaults (info,metadata,0) are applied before matching.
6.3 Examples (non-normative)
{ "type": ["attention.*", "error.*"], "severity": { "gte": "notice" } }
{ "session": "s_8f2c" }
{ "agent": ["claude-code", "codex"], "type": "tool.denied" }6.4 Capture ceiling at subscribe
A subscription MAY declare capture (a ceiling). Fan-out MUST down-level delivered
Events to the ceiling per AEP-0001 §8.2 / AEP-0002 §5.1 and MUST NOT up-level.
Absent ceiling means "as stored/received". A redacted ceiling delivers
full-source Events at metadata unless the fan-out party is a redacting
re-emitter (AEP-0001 §8.2(e)); mechanical fan-out never mints the redacted
provenance claim.
6.5 Subscription frames (WS)
{ "type": "subscribe", "id": "sub-1",
"filter": { "type": ["attention.*"], "severity": { "gte": "notice" } },
"from": [ { "session": "s_8f2c", "epoch": 3, "seq": 187 } ],
"capture": "metadata" }Endpoint replies { "type": "subscribed", "id": "sub-1" } or
{ "type": "error", "id": "sub-1", "code": "invalid-filter" | "unknown-dialect" | "unauthorized" | "limit-exceeded", "detail": ... }. The code set is closed:
invalid-filter: the subscribe request is syntactically invalid (a malformedfilterorfrommember) or the filter names an illegal key (§6.2 rule 1). Fail closed: never a partial subscription, never match-all.unknown-dialect: the subscription requires a filter dialect the endpoint did not advertise (dialects are declared viacapabilities.filters, §6). No member of the 0.1subscribeframe names a dialect: richer dialects arrive with their own frame surface in a future revision; the code is forward vocabulary, defined so the set stays closed when they do.unauthorized: this connection may not subscribe (§8).limit-exceeded: honoring the subscription would exceed an endpoint resource limit (morefromresume positions than the endpoint replays, more concurrent subscriptions than it fans out, and so on). Limits are the endpoint's own;detailSHOULD name the one that was hit.
Rejection is per-request and in-band: the endpoint answers the offending
subscribe with the error frame, and a refused subscription MUST NOT by
itself close the connection or disturb the other subscriptions on it.
Multiple concurrent subscriptions per connection are REQUIRED server-side
(multiplexed fan-out; disjunction is expressed as multiple subscriptions).
unsubscribe by id is acknowledged with unsubscribed. from is OPTIONAL;
when present, buffered replay precedes live delivery per §5: either an array
of resume positions or the literal string "all" (replay-all, §5; an endpoint
not implementing the value answers the closed-set invalid-filter refusal,
exactly as for any malformed from member).
7. Resume contract (all consuming bindings)
Resume tokens are (session, epoch, seq) triples (AEP-0001 §7). A resuming
consumer supplies the last position it durably processed; the endpoint delivers
strictly after that position.
Gaps are legal; duplicates are legal (dedupe on (source, id), AEP-0001 §7.4);
a below-high-water position is redelivery or backfill, ordered by
(epoch, seq). A new epoch is only ever declared by the emitter via the epoch
attribute (AEP-0001 §7.2).
Binding carriage: SSE Last-Event-ID / from query (§3.3), WS
subscribe.from (§6.5). JSONL resumes by file offset (the file is its own
buffer).
8. Identity, authentication, and backpressure
8.1 Trust per binding
| Binding | Requirement |
|---|---|
| JSONL/stdio | filesystem & process trust; no protocol surface |
| HTTP / SSE / WS on localhost | bearer token RECOMMENDED |
| any off-host binding | TLS REQUIRED + bearer token (or stronger) REQUIRED |
| control (any) | authentication REQUIRED unconditionally (AEP-0004) |
Bearer tokens travel in Authorization: Bearer <token>. For browser EventSource
clients that cannot set headers, servers MAY accept ?access_token=<token> and MUST NOT
log it.
8.2 Identity layers
Naming (envelope agent + source URI; connection-level instance at hello,
§4) is REQUIRED; binding authentication per §8.1; attestation is OPTIONAL (§8.3). Identity is who is
speaking; authorization (who may command whom) is deployment policy in 0.1.
8.3 Identity document (experimental)
A JWS-signed JSON document (A2A AgentCard shape): claims agent, operator,
endpoints, accepts (control types), valid_until; detached or compact JWS
signed by the operator key; carried in hello.identity.card. Verification is a
consumer policy, not a protocol gate. Normative schema deferred until a
multi-tenant relay exists (an open question for a future revision).
8.4 Backpressure
Endpoints MAY drop debug and info Events toward a slow consumer. They MUST NOT
silently drop notice-and-above: the choice is deliver, or disconnect (WS: closing
frame with positions, then close; SSE: terminate stream) so the consumer resumes
exactly. Emitter-side (adapter → collector) buffering guidance: bounded queue,
block-or-drop policy MUST prefer dropping debug/info first and MUST count drops
(x.* extension attribute or adapter log).
9. Adapter conformance guidance and capture redaction
Adapters translate a runtime's native hooks into Events (AEP-0002 Annex A). The
privacy promise of capture is enforced at the adapter for sources whose hooks
deliver full content:
- Configured ceiling. Every adapter MUST expose a capture ceiling (default
metadata), applied to every emitted Event:capturenever exceeds it, and payload fields above it are dropped per the schema annotations (AEP-0002 §5.1). Per-category overrides SHOULD be supported (RECOMMENDED default:attention.*: redactedso prompts remain readable). - Metadata synthesis. At ceiling
metadata, fields required for a usable Event but gated higher MUST be synthesized from structural metadata only (e.g.attention.requested.promptbecomes"{agent} requests {kind}: {subject}"). Adapters MUST NOT smuggle content through metadata-level fields. - Digests before redaction.
*_digestfields are SHA-256 (hex, first 16 bytes) over the canonical JSON of the original content: correlation without content; computed before any redaction. - Redaction pipeline (for
redacted-gated free text at ceilingredacted), normative minimum, applied in order: a. Secrets/credentials: replace with[REDACTED:secret]: PEM blocks;AKIA[0-9A-Z]{16};Bearer-prefixed tokens;(password|passwd|secret| api[_-]?key|token)\s*[=:]\s*\S+values; JWT-shaped strings (eyJ...........). b. Configured names: values of environment variables / config keys the operator lists as secret. c. Home paths: user home directory prefix replaced with~. d. PII class (emails, phone numbers): ON by default atredacted, operator-disableable; replace with[REDACTED:pii]. The pipeline is a floor, not a ceiling: adapters MAY redact more. Rule (a) and (b) failures discovered post-emission are security incidents, not conformance nits. - No up-leveling downstream. Adapters MUST set the Event's
captureto the level actually honored, so every downstream hop can rely on it (AEP-0001 §8.2). - Identifiers are untrusted downstream. Adapters SHOULD keep
session,run, andstepvalues within[A-Za-z0-9._-], starting alphanumeric: the interop-safe grammar every mapped vendor id observed to date fits. The envelope deliberately does not enforce a pattern: at least one adopted source's session ids are contractually arbitrary strings, so a schema constraint would reject conformant vendor ids at the wrong layer. Consumers treat these values (andsubject) as producer-controlled input and encode or sanitize before using them as filesystem, URL, or query components (AEP-0001 §12).
10. Conformance surfaces owed by this document
Fixture classes (in conformance/fixtures/): the attr-match golden corpus
(filter, event, verdict) incl. fail-closed unknown keys; capture down-level at
fan-out; the redaction pipeline corpus.
Frame and connection behavior is not fixture-shaped (frames are not Events) and
lives in the live checker (conformance/live.js):
helloversion selection incl. rejection and hello-first ordering;- subscribe rejection in-band from the closed §6.5 set with connection survival;
- ignore-unknown frame types and capability keys;
- capture ceiling at subscribe;
- resume over
from(gap tolerance, duplicate delivery); - control gating;
- the roster snapshot (§4.1: advertised capability, id echo, entry-per-claim with verbatim declared control blocks, emitter-role refusal in-band with connection survival);
- replay-all (§5: advertised via
replay.all, every held session replayed from its earliest position, no spuriousreplay-exhausted, deterministic refusal where unimplemented).
Two classes need a live, cooperating endpoint rather than fixtures:
replay-exhausted under real buffer eviction (a black-box check cannot force
another endpoint's buffer to evict unaided) and envelope-over-binding
byte-identity round-trips (JSONL line, POST body, SSE data:, and WS frame).
The live checker's opt-in --evict mode covers both against any endpoint whose
operator runs the target with a small replay buffer for the occasion: the
checker floods a session past the advertised buffer, then asserts the exhaustion
markers and envelope identity black-box on both consuming bindings. The
reference stack self-certifies the same way in CI.
11. Security considerations
This section is informative; every rule it mentions is defined where cited.
- The query-string token is the deliberate weak spot: treat its surroundings
accordingly. Bearer tokens belong in
Authorization(§8.1); the?access_token=<token>fallback exists only because browserEventSourcecannot set headers. Servers already may not log it (§8.1), but URLs also transit proxy access logs, browser history, andRefererchains: deployments using the fallback scrub query strings at every logging layer they control, scope such tokens narrowly, and rotate them more aggressively than header-borne ones. - Transport security is deployment-owned by design. The trust table (§8.1) sets the floor: TLS plus a bearer token (or stronger) for anything off-host, authentication unconditionally for control (AEP-0004); and implementations typically terminate TLS at a reverse proxy in front of a plain-HTTP endpoint. The protocol adds no transport crypto of its own; whatever sits between the proxy and the endpoint is inside the trust boundary and needs the same care as the endpoint itself.
- Backpressure doubles as the denial-of-service posture. Bounded replay buffers
(§5), the drop-
debug/info-first rule, and deliver-or-disconnect fornotice-and-above (§8.4) mean a slow, stalled, or hostile consumer costs bounded memory and can always be shed without losing the consumer's ability to resume exactly (§7). The same rules bound what a flood of low-severity Events can occupy. - Digests correlate; they do not conceal.
*_digestfields are computed over the original content before redaction (§9.3) so redacted streams stay joinable. A digest of low-entropy content (a short command line, a known filename) can be confirmed by brute force by anyone holding candidate plaintexts: digests are correlation aids, not encryption, and content that must not be inferable belongs behindcapture: none, not behind a hash. - Redaction failures are incidents, not nits. The pipeline (§9.4) is a normative floor with an explicit rule that secret/credential leaks discovered post-emission are security incidents. The ceiling mechanism only works end-to-end because every fan-out hop re-gates (§6.4, AEP-0001 §8.2) and a hop that cannot classify content degrades it structurally rather than passing it through.
- Filters fail closed. An unknown or illegal filter key is rejected
(
invalid-filter, §6.2) rather than silently matching everything: a subscription can narrow unexpectedly, but never silently widen its intake.
References
- AEP-0001, AEP-0002, AEP-0004.
- RFC 6455 (WebSocket).
- WHATWG SSE (
text/event-stream,Last-Event-ID). - RFC 6750 (bearer tokens).
- NDJSON.
- RFC 7515 (JWS).