# RFC-0014 — Protocol v2 Wire Format

- **Status:** **Ratified v1.0 — 2026-05-03.**
- **Date:** 2026-05-03
- **Ratified-By:** CTO via `tracking/sprint-3-extended-v2-plan.md` §8 (G-V2-A through G-V2-G).
- **Author:** 🧠 Agentic Architect.
- **Sprint:** Sprint 3 Extended — v2 protocol reset bundle (RFC-0012/0013/0014).
- **Audience:** Cloud Agents (LLMs); ☁️ cloudflare-native-edge; 🦀 edge-kubelet-engineer; 🛡️ devex-protocol-sec; CTO chair.
- **Depends on:** RFC-0012 v1.0 (tenant model); RFC-0013 v1.0 (hybrid PQC).
- **Will require v3.0 amendments to:** RFC-0001 (envelope `tid`), RFC-0002 (subprotocol header), RFC-0003 (JWS alg + scope grammar). v3.0 amendment window: end-of-sprint-3-extended (2026-05-30).

---

## Change Log

- **v1.0 (2026-05-03):** Ratified. Folded G-V2-A..G G-V2-G per Sprint-3-Extended plan §8. Drop+recreate D1 migration. UUIDv5 tenant_id (ns=`56bdc01c-052e-5f60-abfb-7fa367b284e3`). Pure-TS PQC on Worker, WASM swap deferred to S4. 6MB edge binary cap. Self-sovereign tenant bootstrap (managed-tenant deferred to v3.0). Locked alg=`Ed25519+ML-DSA-65` with internal `alg_id` enum. Tenant DELETE OPTIONAL.
- **v0.1 DRAFT (2026-04-29):** Initial draft.

---

## §1. Abstract

Defines the **v2 wire format** at the WSS, MCP envelope, JWS, and HTTP-endpoint layers. Names every breaking change vs v1, lists every additive surface, sets the deprecation timeline, and gives one end-to-end worked example (tenant init → enroll-token mint → device enroll → WSS connect → tool call) showing every new shape in context. This RFC is the **integration document** for the v2 reset; RFC-0012 owns tenant semantics and RFC-0013 owns crypto choices.

## §2. Motivation

A bundle of independently-correct changes (tenant scoping, hybrid PQC, scope grammar v2) still has to land on the wire as one coherent protocol. v1 has no `tid` slot in any envelope, no place to advertise a tenant in the WSS subprotocol, and no extension point in its `alg` field for hybrid signatures. Bolting these on as optional fields would be ambiguous (which side decides? what does the absence mean?). A clean v2 cutover with a single deprecation window is cheaper than indefinite optional-field handling.

---

## §3. WSS Subprotocol v2

### §3.1 Header

v1 (RFC-0002):
```
Sec-WebSocket-Protocol: aethermesh.v1, node-<node_id_lower>
```

**v2 (this RFC):**
```
Sec-WebSocket-Protocol: aethermesh.v2, tenant-<tenant_id_lower>, node-<node_id_lower>
```

- Both `tenant_id` and `node_id` are lowercase per existing convention.
- Server MUST select `aethermesh.v2` and echo the same three-tuple, or reject with 400.
- A client offering only `aethermesh.v1` after the deprecation date (2026-05-31) MUST receive HTTP 410 Gone with `Link: <https://aethermesh.app/migrate-v2>; rel="help"`.

### §3.2 Auth frame

The first frame on a v2 connection is the auth frame, carrying a hybrid-signed JWS runtime token (RFC-0013 §4, §7) plus the device's hybrid pubkey blob if not already on file. Frame size MUST be ≤ 16 KB (raised from v1's 4 KB; see RFC-0013 §5).

---

## §4. MCP Envelope Changes

### §4.1 New required field: `tid`

Every v2 MCP envelope (request, response, event, error) MUST carry a top-level `tid` field set to the originating tenant's `tenant_id` (UUID-shape per RFC-0012 §3.2):

```json
{
  "v": "2",
  "tid": "4fa3715e-c89d-22b1-e704-6c8519f30a7d",
  "id": "01J…",
  "kind": "tool.invoke",
  "verb": "call",
  "payload": { … }
}
```

Gateway MUST verify that `tid` matches the authenticated session's tenant; mismatch → close with `E_TENANT_DENIED` (§4.3).

### §4.2 Optional `did` at session-init

The server's `auth_ok` reply MAY include the canonical `did` string for audit/UX surfaces:

```json
{ "v":"2", "tid":"4fa3…", "kind":"auth_ok", "did":"did:pkh:eip155:1:0xab58…" }
```

Clients MUST NOT use `did` for any authorization decision (use `tid`); it is informational.

### §4.3 New error codes (additive) — BINDING (G-V2-E)

The JWS `alg` value is locked to the literal string `Ed25519+ML-DSA-65` per RFC-0013 §4 / §7 (G-V2-E ratified). Any other `alg` MUST be rejected with `E_ALG_NOT_SUPPORTED`. The internal `alg_id` enum (Worker `src/auth/alg.ts::AlgId`, edge `crates/manifest::AlgId`) hedges against future IETF rename without a call-site sweep.

| Code                    | HTTP equivalent | Meaning                                                  |
|-------------------------|-----------------|----------------------------------------------------------|
| `E_TENANT_DENIED`       | 403             | Authenticated `tid` does not match resource owner        |
| `E_ALG_NOT_SUPPORTED`   | 400             | JWS `alg` is not `Ed25519+ML-DSA-65` (or whitelisted)    |
| `E_DID_INVALID`         | 400             | DID malformed or method not in §RFC-0012 §3.1 allowlist  |
| `E_TENANT_INTERNAL`     | 500             | Tenant-scoped fault fence tripped (RFC-0012 §4)          |

---

## §5. Breaking-vs-Additive Enumeration

### §5.1 Breaking (require client recompile or re-enroll)

1. WSS subprotocol header changes from `aethermesh.v1, node-…` → `aethermesh.v2, tenant-…, node-…`.
2. JWT `tid` claim is now REQUIRED on every v2 token (RFC-0012 §6).
3. JWS `alg` becomes `Ed25519+ML-DSA-65` hybrid (RFC-0013 §7).
4. Every D1 query becomes tenant-scoped — any custom tooling poking the gateway DB directly breaks.
5. Durable Object routing key changes from `node_id` to `${tenant_id}:${node_id}` — existing DO state orphans (RFC-0012 §5.5).
6. RBAC scope grammar v2 — bare `devices:read` etc. no longer satisfies any check (RFC-0012 §7).
7. Enroll pubkey format becomes hybrid blob (~3 KB PEM); old 80–1024 char Ed25519-only PEM rejected (RFC-0013 §5).

### §5.2 Additive (existing v2 clients keep working as we ship)

1. Error codes `E_TENANT_DENIED`, `E_ALG_NOT_SUPPORTED`, `E_DID_INVALID`, `E_TENANT_INTERNAL`.
2. New endpoints `POST /v1/tenants/init`, `GET /v1/tenants/me`, `DELETE /v1/tenants/me`, `POST /v1/tenants/me/enroll-token` (§6).
3. New audit-log fields `tenant_id`, `did_method`, `alg`.

(The `/v1/...` URL prefix is retained for the lifetime of v2; we are versioning at the protocol layer, not the URL layer. A future v3 may rev the URL prefix to `/v2/`.)

---

## §6. New Endpoints (Tenant Lifecycle)

### §6.1 `POST /v1/tenants/init` — NORMATIVE, self-sovereign (G-V2-F)

**Bootstrap model is self-sovereign for v2.0** per G-V2-F ratification. The DID-holder who completes the challenge becomes the implicit owner of `t:<tid>:*` automatically. **No vendor-side `system:*` override path is minted in v2.0.** Managed-tenant flows are deferred to v3.0 (see RFC-0012 §6).

Body:
```json
{
  "did": "did:pkh:eip155:1:0xab58…",
  "challenge": "<32B base64url, server-issued via prior GET /v1/tenants/challenge>",
  "proof": {
    "alg": "Ed25519+ML-DSA-65",
    "hybrid_pubkey_pem": "-----BEGIN AETHERMESH HYBRID PUBLIC KEY-----…",
    "signature": "<base64url(hybrid_sig over challenge)>"
  }
}
```

Response 200:
```json
{ "tenant_id": "4fa3715e-c89d-22b1-e704-6c8519f30a7d", "status": "active", "created": true }
```
Response 200 (idempotent re-call from same DID): `"created": false`.
Errors: `E_DID_INVALID`, `E_ALG_NOT_SUPPORTED`, `E_INVALID_CLIENT_ASSERTION` (signature fail).

### §6.2 `GET /v1/tenants/me`

Auth: any v2 token. Returns `{ tenant_id, did, did_method, status, created_at }` for the caller's tenant.

### §6.3 `DELETE /v1/tenants/me` — OPTIONAL (G-V2-G descope)

Auth: scope `t:<tid>:tenants:admin` (subset of `t:<tid>:*`). **OPTIONAL at v1.0 per descope ladder (G-V2-G change-log note).** Soft-delete + grace-period sweeper deferred to Sprint 4. Implementations MAY omit this route entirely; if implemented, MUST follow RFC-0012 §3.3 lifecycle.

### §6.4 `POST /v1/tenants/me/enroll-token`

Auth: scope `t:<tid>:devices:write` (or `t:<tid>:*`). Replaces the v1 operator-mediated enroll-token mint. Body identical to v1 `POST /v1/operator/enroll-token` (TTL, max_uses, …). Response carries an `enroll_token` minted **scoped to the caller's tenant**; the resulting device enrolls into that tenant automatically.

---

## §7. Worked Example — End-to-End

Five-step trace; bodies abbreviated for length, full shapes per §4 / §6.

**§7.1 Tenant init** — DID-holder POSTs `/v1/tenants/init` with `{did, challenge, proof:{alg:"Ed25519+ML-DSA-65", hybrid_pubkey_pem, signature}}` → 200 `{tenant_id:"4fa3715e-c89d-22b1-e704-6c8519f30a7d", status:"active", created:true}`.

**§7.2 Mint enroll-token** — tenant-admin POSTs `/v1/tenants/me/enroll-token` (`Authorization: Bearer <JWS alg=Ed25519+ML-DSA-65, claims tid=4fa3715e-…, scope=t:4fa3…:*>`) `{ttl_s:600, max_uses:1}` → 200 `{enroll_token:"et_…", tenant_id:"4fa3715e-…"}`.

**§7.3 Device enroll** — installer POSTs `/v1/devices/enroll` `Bearer et_…` body `{hybrid_pubkey_pem, device_meta}` → 200 `{node_id:"01kpq9rv…", tenant_id:"4fa3715e-…", runtime_token:"<JWS hybrid>"}`.

**§7.4 WSS connect** — `GET /v1/wss` with header
```
Sec-WebSocket-Protocol: aethermesh.v2, tenant-4fa3715e-c89d-22b1-e704-6c8519f30a7d, node-01kpq9rv…
```
Server replies `101` echoing the same three-tuple. First frame (client): `{"v":"2","tid":"4fa3715e-…","kind":"auth","jws":"<runtime_token>"}`. Reply (gateway): `{"v":"2","tid":"4fa3715e-…","kind":"auth_ok","did":"did:pkh:eip155:1:0xab58…"}`.

**§7.5 Tool invoke** — client emits `{"v":"2","tid":"4fa3715e-…","id":"01J…","kind":"tool.invoke","verb":"call","payload":{"tool":"echo.01kpq9rv…..echo","args":{"message":"hi v2"}}}`. Gateway authz: scope MUST contain `t:4fa3715e-…:tools:invoke` (or `t:4fa3715e-…:*`); resource owner of `01kpq9rv…` MUST be `tenant_id=4fa3715e-…`; mismatch → close frame with `E_TENANT_DENIED`.

---

## §8. Deprecation Timeline

| Date         | Event                                                                 |
|--------------|------------------------------------------------------------------------|
| 2026-04-29   | This RFC + RFC-0012 + RFC-0013 published as DRAFTs                      |
| 2026-05-03   | CTO decision day — ratification target                                  |
| 2026-05-04 → 2026-05-30 | Implementation window (🦀 + ☁️ + 🛡️)                          |
| 2026-05-31   | v2 ships; sprint 3 extended closes; v1 endpoints respond `410 Gone`     |
| Sprint 4     | v1 code paths deleted from the codebase                                 |

(HS256 → EdDSA cutover already shipped earlier in Sprint 3 Extended; independent of this bundle.)

---

## §9. Dependency Graph

```
RFC-0014 (this)
   ├── depends on RFC-0012 (tenant model — tid claim, scope grammar, endpoints)
   └── depends on RFC-0013 (hybrid PQC — alg identifier, sig encoding, frame size budget)

RFC-0012 ── depends on RFC-0013
RFC-0013 ── foundational

Downstream amendments queued post-ratification:
   RFC-0001 → v3.0 (envelope tid field)
   RFC-0002 → v3.0 (subprotocol header aethermesh.v2)
   RFC-0003 → v3.0 (alg = Ed25519+ML-DSA-65; scope grammar v2)
   RFC-0007 → v3.0 (enroll pubkey hybrid blob)
```

## §10. References

- RFC-0012 v1.0 RATIFIED 2026-05-03 — Tenant Model & Isolation (same dispatch).
- RFC-0013 v1.0 RATIFIED 2026-05-03 — Post-Quantum Cryptography (same dispatch).
- RFC-0001 v1.3 — MCP envelope (will need v3.0 amendment).
- RFC-0002 v2.2 — WSS subprotocol & pubsub taxonomy (will need v3.0 amendment).
- RFC-0003 v1.4 — RBAC token format (will need v3.0 amendment).
- RFC-0007 v1.1 — Public enrollment (will need v3.0 amendment).
- RFC 6455 — The WebSocket Protocol.
- RFC 7515 — JSON Web Signature (JWS).
- RFC 8259 — JSON.
- `tracking/sprint-3-extended-v2-plan.md` §2 (G-V2-A..G), §8 (CTO sign-off) — ratification source of record.
- RFC 8259 — JSON.
