# Active RPC wire protocol (`ARP1`, version 1)
**Status:** normative. This document defines the envelope every Active RPC
transport carries, the capability table that travels with it, and the error codes
peers may rely on.
Active RPC is a capability-based, promise-pipelined RPC for Active Infinity
payloads. The envelope is deliberately transport-agnostic: the in-process link,
shared memory, TCP, QUIC and the gRPC/Connect bridge all carry these bytes (the
first two skip serialization but preserve the shape).
```text
0 4 magic "ARP1"
4 1 version (1)
5 1 kind
6 2 flags
8 8 request id
16 .. method, arguments, payload, capability table
```
All integers are little-endian.
---
## 1. Header
| 0 | 4 | magic `41 52 50 31` (`ARP1`) |
| 4 | 1 | version (`1`) |
| 5 | 1 | kind |
| 6 | 2 | flags |
| 8 | 8 | request id (echoed by the response) |
### 1.1 Kinds
| 1 | `Request` | a method call |
| 2 | `Response` | a successful result (first argument is the value) |
| 3 | `Error` | a failure (method field carries the error code) |
| 4 | `Hello` | capability handshake |
### 1.2 Flags
| 0 | `redact` | the caller asks for redacted output |
| 1 | `read_only` | the capability is read-only |
Unknown flag bits are refused (`rpc/wire`).
---
## 2. Body
```text
payload : u32 length | bytes (an Active Infinity image, optional)
caps : u32 count | capability*
```
An envelope with trailing bytes after the capability table is refused.
### 2.1 Arguments
| 0 | value | value encoding below |
| 1 | promise | `u64` promise id |
| 2 | capability | 16-byte capability id |
A promise argument makes the call **pipelined**: the kernel keeps the dependent
call at the provider and dispatches it in the batch that resolves the promise.
A capability argument transfers authority explicitly.
### 2.2 Values
| 0 | `Null` | — |
| 1 | `Bool` | `u8` (`0`/`1`) |
| 2 | `Int` | `i64` |
| 3 | `Float` | IEEE-754 binary64 bits |
| 4 | `Text` | `u32` length + UTF-8 |
| 5 | `List` | `u32` count + value* |
| 6 | `Map` | `u32` count + (`u32` length + key, value)* |
| 7 | `Secret` | — (a marker: there is no encoding for secret bytes) |
| 8 | `Module` | `u32` length + Infinity image bytes |
| 9 | `Path` | `u32` length + UTF-8 dotted path |
`Secret` is the *only* representation of secret material on the wire. There is
no tag that carries secret bytes, so no encoder in this crate can leak one.
---
## 3. Capability descriptors
A capability table is `u32 count` followed by descriptors:
| id | 16 bytes |
| methods | `u32` count + (`u32` length + UTF-8)* |
| budget | seven `u64` ceilings: calls, bytes, wall ms, nodes, depth, array length, value size |
| read_only | `u8` |
| redact | `u8` |
| path scope | `u8` present flag + (`u32` length + UTF-8) |
A descriptor whose budget disables a ceiling (any `0`) is refused with
`rpc/capability-table`: there is no unlimited profile on the wire.
The table is a *description*, never proof. A receiver must look the id up in its
own registry; an id it did not mint is refused with `rpc/unknown-capability`.
---
## 4. Envelope limits
`WireLimits` bounds total bytes, arguments, method length, payload, descriptors
and methods per descriptor. Defaults accept real modules (64 MiB envelopes) and
`WireLimits::strict()` is the profile for adversarial surfaces. Exceeding a limit
is `rpc/wire` (or `rpc/capability-table` for the table).
---
## 5. Error codes
Codes are permanent. A peer branches on the code in an `Error` envelope, never on
a message.
| `rpc/unknown-capability` | the id is not in the receiver's registry |
| `rpc/method-not-granted` | the capability or provider does not grant the method |
| `rpc/attenuation-escalation` | an attenuation would widen authority |
| `rpc/read-only` | a mutating method on a read-only capability |
| `rpc/path-not-granted` | a path argument is outside the capability's scope |
| `rpc/tool-policy` | the tool policy refuses the call |
| `rpc/revoked` | the capability (or an ancestor) was revoked |
| `rpc/budget-exceeded` | a call, byte or lane budget is exhausted |
| `rpc/unknown-lane` | the lane does not exist or was destroyed |
| `rpc/unknown-channel` | the channel does not exist |
| `rpc/unknown-promise` | the promise id was never minted here |
| `rpc/promise-cancelled` | the promise was cancelled |
| `rpc/promise-resolved` | the promise was already resolved or rejected |
| `rpc/unresolvable-promise` | the promise can never settle |
| `rpc/provider` | the provider failed the call |
| `rpc/domain` | an Active or Infinity domain operation failed (the message carries the Active class, e.g. `X001`) |
| `rpc/payload` | a payload is not a valid Active Infinity image |
| `rpc/wire` | the envelope is malformed or exceeds a limit |
| `rpc/capability-table` | the capability table is malformed or disables a ceiling |
| `rpc/transport` | the transport failed |
| `rpc/transport-unavailable` | the transport is not built or not supported here |
| `rpc/timeout` | a deadline elapsed |
| `rpc/pipelined-cycle` | pipelined calls form a dependency cycle |
| `rpc/invalid-argument` | a caller-supplied argument is out of range |
---
## 6. Transports
| in-process | none | envelopes move by value; `bytes_written() == 0` |
| TCP | `u32` length + envelope | cross-process, `MAX_FRAME_BYTES` = 64 MiB |
| shared memory | `u32` length + envelope in a ring | zero-copy reads via `with_frame` |
| QUIC | `u32` length + envelope on one bi-directional stream, one stream per call | TLS with a self-signed certificate on loopback |
| Connect/gRPC | HTTP/1.1 `POST /active.rpc.v1.Agent/Call`, body framed as `[0][len u32 BE][envelope]`, content type `application/connect+active-infinity` or `application/grpc+active-infinity` | a malformed frame is a `400` with an `rpc/…` body; the same framing fits an HTTP/2 gRPC server |
A malformed frame is always an error at the frame boundary: no transport retries,
guesses or forwards a partially decoded envelope.
---
## 7. Versioning
- The version byte changes only for an incompatible layout change.
- Kind, flag, argument, value and error codes are permanent.
- New value or argument tags may be added in a later version; a reader refuses a
tag it does not know rather than skipping it, because a skipped argument would
change the call's meaning.
---
## 8. What a decoded envelope guarantees
1. Every length and count fit inside the buffer; nothing allocates on a claim.
2. The body ends exactly at the end of the capability table.
3. Every capability descriptor has a valid budget and a non-empty id.
4. No argument carries secret bytes: `Secret` is a marker.
5. The envelope can be re-encoded to the same bytes.