macp-runtime 0.7.2

MACP reference runtime: a coordination kernel and gRPC server enforcing session boundaries, message validation, append-only history, modes, and governance policy.
Documentation
# Getting Started

This guide walks you from a fresh checkout to your first coordination session. By the end, you will have the runtime running locally and will have completed a full Decision Mode session through the gRPC API.

For protocol concepts like sessions, modes, and the two-plane model, see the [protocol documentation](https://www.multiagentcoordinationprotocol.io/docs).

## Prerequisites

You need a Rust toolchain and the Protocol Buffers compiler. The repo's `rust-toolchain.toml` pins the exact compiler version and rustup installs it automatically on first build; the minimum supported version is 1.89.0.

```bash
# macOS
brew install protobuf

# Ubuntu / Debian
sudo apt-get install -y protobuf-compiler

# Verify
protoc --version
rustc --version
```

## Build and run

Clone the repository and build:

```bash
git clone https://github.com/multiagentcoordinationprotocol/macp-runtime.git
cd macp-runtime
cargo build
```

### Starting a development server

For local development, disable TLS and skip configuring tokens. With no auth resolvers configured, the runtime falls back to dev-mode auth:

```bash
export MACP_ALLOW_INSECURE=1
cargo run
```

The server listens on `127.0.0.1:50051` and treats any `Authorization: Bearer <value>` header as authenticating the caller with sender identity `<value>`.

### Starting a production server

In production, the runtime requires TLS and token-based authentication:

```bash
export MACP_TLS_CERT_PATH=/path/to/server.crt
export MACP_TLS_KEY_PATH=/path/to/server.key
export MACP_AUTH_TOKENS_FILE=/path/to/tokens.json
cargo run
```

See the [Deployment Guide](deployment.md) for the full environment variable reference.

## Your first session

A coordination session has four steps: negotiate the protocol version, create a session, exchange mode-specific messages, and bind the terminal outcome with a commitment.

### Step 1: Initialize

The client sends its supported protocol versions and the runtime selects one. This also exchanges capability information so the client knows which features are available.

```
-> InitializeRequest {
     supported_protocol_versions: ["1.0"],
     client_info: { name: "my-agent", version: "0.1.0" }
   }

<- InitializeResponse {
     selected_protocol_version: "1.0",
     runtime_info: { name: "macp-runtime", version: "0.5.0" },
     supported_modes: [
       "macp.mode.decision.v1",
       "macp.mode.proposal.v1",
       "macp.mode.task.v1",
       "macp.mode.handoff.v1",
       "macp.mode.quorum.v1",
       "ext.multi_round.v1"
     ]
   }
```

### Step 2: Create a session

Send a `SessionStart` envelope to create a Decision Mode session with two participants. Every standards-track session requires four fields in the payload: `participants`, `mode_version`, `configuration_version`, and a positive `ttl_ms`.

```
-> Send(Envelope {
     macp_version: "1.0",
     mode: "macp.mode.decision.v1",
     message_type: "SessionStart",
     message_id: "msg-001",
     session_id: "550e8400-e29b-41d4-a716-446655440000",
     sender: "",
     timestamp_unix_ms: 1712500000000,
     payload: SessionStartPayload {
       intent: "Decide whether to deploy v2.0",
       participants: ["agent://analyst", "agent://reviewer"],
       mode_version: "1.0.0",
       configuration_version: "config.default",
       policy_version: "",
       ttl_ms: 60000
     }
   })

<- Ack { ok: true, session_state: OPEN }
```

The `sender` field is left empty because the runtime overrides it with the authenticated identity. The empty `policy_version` resolves to the built-in `policy.default`, which imposes no governance constraints beyond the mode's own rules.

Session IDs must be either UUID v4/v7 in hyphenated lowercase form or base64url tokens of at least 22 characters. Short or human-readable IDs like `"my-session"` are rejected.

### Step 3: Exchange messages

In Decision Mode, participants propose options, evaluate them, and vote. The session initiator and declared participants can all send proposals. Evaluations and votes reference proposals by ID.

```
-> Send(Envelope { message_type: "Proposal", payload: ProposalPayload {
     proposal_id: "p1",
     option: "deploy-v2",
     rationale: "All tests passing, metrics stable"
   }})
<- Ack { ok: true }

-> Send(Envelope { sender: "agent://analyst", message_type: "Vote", payload: VotePayload {
     proposal_id: "p1",
     vote: "APPROVE",
     reason: "Risk assessment passed"
   }})
<- Ack { ok: true }
```

### Step 4: Commit

The session initiator binds the terminal outcome. The commitment payload must echo the session's bound `mode_version` and `configuration_version` -- the runtime rejects mismatches.

```
-> Send(Envelope { message_type: "Commitment", payload: CommitmentPayload {
     commitment_id: "c1",
     action: "decision.selected",
     authority_scope: "deployment",
     reason: "Unanimous approval for deploy-v2",
     mode_version: "1.0.0",
     configuration_version: "config.default",
     policy_version: "policy.default",
     outcome_positive: true
   }})
<- Ack { ok: true, session_state: RESOLVED }
```

The session is now terminal. Any subsequent messages targeting it are rejected with `SESSION_NOT_OPEN`.

## Authentication

### Development mode

In development mode (no auth resolvers configured), clients send their sender identity as a bearer token:

```
metadata: { "authorization": "Bearer agent://my-agent" }
```

The runtime accepts any bearer value as the sender identity. This fallback is only active while neither `MACP_AUTH_TOKENS_*` nor `MACP_AUTH_ISSUER` is set.

### Production mode

Create a `tokens.json` file that maps bearer tokens to agent identities and capabilities:

```json
[
  {
    "token": "secret-token-for-analyst",
    "sender": "agent://analyst",
    "allowed_modes": ["macp.mode.decision.v1", "macp.mode.task.v1"],
    "can_start_sessions": true,
    "max_open_sessions": 10,
    "can_manage_mode_registry": false,
    "is_observer": false
  },
  {
    "token": "secret-token-for-reviewer",
    "sender": "agent://reviewer",
    "allowed_modes": [],
    "can_start_sessions": true,
    "can_manage_mode_registry": false
  },
  {
    "token": "secret-token-for-auditor",
    "sender": "agent://auditor",
    "can_start_sessions": false,
    "is_observer": true
  }
]
```

Setting `allowed_modes` to an empty array (or omitting it) grants access to all modes. The runtime derives the sender identity from the token, so agents cannot spoof their identity. `is_observer` allows passive-subscribe access to any session, even when the identity is not a declared participant -- useful for monitoring and audit agents. Clients authenticate by sending `Authorization: Bearer <token>` (or the alternate `x-macp-token: <token>` header) in the gRPC metadata.

### JWT mode

The runtime accepts JWT bearer tokens when `MACP_AUTH_ISSUER` is set. Configure a JWKS source (`MACP_AUTH_JWKS_JSON` inline, or `MACP_AUTH_JWKS_URL` fetched + cached) and optionally override `MACP_AUTH_AUDIENCE` (default `macp-runtime`) and `MACP_AUTH_JWKS_TTL_SECS` (default 300). The default algorithm allowlist is `RS256` and `ES256`; `HS256` (shared-secret) requires explicit opt-in via `MACP_AUTH_JWT_ALGS=HS256` (see CHANGELOG 0.5.0 — symmetric keys must never become verifiable just because a JWKS contains an `oct` key).

```bash
export MACP_AUTH_ISSUER=https://issuer.example.com
export MACP_AUTH_AUDIENCE=macp-runtime
export MACP_AUTH_JWKS_URL=https://issuer.example.com/.well-known/jwks.json
cargo run
```

The JWT's `sub` claim becomes the `sender`. An optional `macp_scopes` claim carries capability fields that mirror the static token config:

```json
{
  "sub": "agent://analyst",
  "iss": "https://issuer.example.com",
  "aud": "macp-runtime",
  "exp": 1767225600,
  "macp_scopes": {
    "allowed_modes": ["macp.mode.decision.v1", "macp.mode.task.v1"],
    "can_start_sessions": true,
    "max_open_sessions": 10,
    "can_manage_mode_registry": false,
    "is_observer": false
  }
}
```

When `macp_scopes` is omitted, the identity defaults to permissive: any mode allowed, sessions can be started, and admin/observer flags off. The `is_observer` capability is required for passive-subscribe access to sessions where the caller is neither the initiator nor a declared participant.

### Resolver order

If both JWT and static bearer tokens are configured, the runtime runs the JWT resolver first and then the static resolver. JWT-shaped tokens (containing dots) are only considered by the JWT resolver; opaque tokens are only considered by the static resolver. Dev-mode fallback activates only when **neither** `MACP_AUTH_ISSUER` nor `MACP_AUTH_TOKENS_*` is configured.

## Running the example clients

The repository includes example clients in `src/bin` that demonstrate each mode. Start the development server in one terminal, then run any example in another:

```bash
# Terminal 1: start the server
export MACP_ALLOW_INSECURE=1 && cargo run

# Terminal 2: run examples
cargo run --bin client              # Decision mode
cargo run --bin proposal_client     # Proposal mode
cargo run --bin task_client         # Task mode
cargo run --bin handoff_client      # Handoff mode
cargo run --bin quorum_client       # Quorum mode
cargo run --bin multi_round_client  # Multi-round extension
cargo run --bin fuzz_client         # Error path testing
```

## Common errors

| Error | Cause | Fix |
|-------|-------|-----|
| `UNAUTHENTICATED` | No valid credential provided | In dev mode send `Authorization: Bearer <sender>`; in prod send a configured static bearer or a valid JWT |
| `INVALID_ENVELOPE` | Missing required SessionStart fields | Ensure `participants`, `mode_version`, `configuration_version`, and `ttl_ms > 0` are all present |
| `SESSION_NOT_OPEN` | Session already resolved or expired | Use `GetSession` to check state; start a new session |
| `INVALID_SESSION_ID` | Session ID format not accepted | Use UUID v4/v7 or base64url (22+ characters) |
| `FORBIDDEN` | Sender not authorized for this message | Check the mode's authority rules; ensure the sender is in the participants list |
| `RATE_LIMITED` | Too many requests per minute | Wait for the rate window to expire, or increase the limit via environment variables |

## Next steps

- [**Examples**](examples.md) for worked examples of every mode
- [**API Reference**](API.md) for the full gRPC surface
- [**Modes**](modes.md) for runtime implementation details
- [**Deployment Guide**](deployment.md) for production setup