From 1970b0437b436977e442ba8f0dc9f8a1b1f972b6 Mon Sep 17 00:00:00 2001 From: Ajit Koti Date: Wed, 22 Apr 2026 18:12:26 -0700 Subject: [PATCH] Update Docs --- README.md | 73 ++++++++++++++++++++++++++++++++++------- docs/API.md | 24 ++++++++++++-- docs/README.md | 6 ++-- docs/architecture.md | 48 ++++++++++++++++++++------- docs/deployment.md | 12 ++++++- docs/examples.md | 3 +- docs/getting-started.md | 44 +++++++++++++++++++++++-- docs/sdk-guide.md | 12 +++++-- 8 files changed, 185 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index bd0abca..060776e 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,23 @@ This runtime implements the current MACP core/service surface, five standards-tr - `RegisterExtMode` dynamically registers new extension modes with a passthrough handler - `UnregisterExtMode` removes dynamically registered extensions (built-in modes protected) - `PromoteMode` promotes extensions to standards-track with optional identifier rename +- **Pluggable authentication chain** + - JWT bearer resolver validates signature, issuer, audience, and expiration against a JWKS (inline JSON or URL-fetched with TTL cache); `RS256`, `ES256`, and `HS256` supported + - Static bearer resolver maps opaque tokens to identities via `MACP_AUTH_TOKENS_FILE`/`MACP_AUTH_TOKENS_JSON` + - Resolvers run in chain order (JWT → static); dev-mode fallback only when both are absent + - Identities carry capability flags: `allowed_modes`, `can_start_sessions`, `max_open_sessions`, `can_manage_mode_registry`, `is_observer` +- **Governance policy framework (RFC-MACP-0012)** + - `RegisterPolicy`, `UnregisterPolicy`, `GetPolicy`, `ListPolicies`, `WatchPolicies` RPCs + - Per-mode rule schemas (voting, objection handling, quorum thresholds, acceptance, assignment, handoff acceptance) + - Policies evaluated at commitment time; version binding enforced at SessionStart +- **Session lifecycle observability** + - `ListSessions` enumerates current session metadata + - `WatchSessions` streams `Created`/`Resolved`/`Expired` events with a `Created` initial-sync on connect +- **Session extension plumbing** + - `SessionExtensionProvider` trait and `ExtensionProviderRegistry` let hosts hook lifecycle callbacks for custom session-level extensions carried in the `extensions` map; provider errors are non-fatal +- **Pluggable storage backends** + - File (default), in-memory, RocksDB (`rocksdb-backend` feature), Redis (`redis-backend` feature) + - Checkpoint-based replay and terminal-session log compaction - **Structured logging via `tracing`** - use `RUST_LOG` env var to control log level (e.g. `RUST_LOG=info`) - **Per-mode metrics** @@ -208,25 +225,35 @@ cargo run --bin fuzz_client |---|---| | `Initialize` | implemented | | `Send` | implemented | +| `StreamSession` | implemented (active + passive subscribe) | | `GetSession` | implemented | +| `ListSessions` | implemented | +| `WatchSessions` | implemented | | `CancelSession` | implemented | | `GetManifest` | implemented | | `ListModes` | implemented | -| `ListRoots` | implemented | -| `StreamSession` | implemented | -| `WatchModeRegistry` | implemented | -| `WatchRoots` | implemented | | `ListExtModes` | implemented | | `RegisterExtMode` | implemented | | `UnregisterExtMode` | implemented | | `PromoteMode` | implemented | +| `WatchModeRegistry` | implemented | +| `ListRoots` | implemented | +| `WatchRoots` | implemented | +| `WatchSignals` | implemented | +| `RegisterPolicy` | implemented | +| `UnregisterPolicy` | implemented | +| `GetPolicy` | implemented | +| `ListPolicies` | implemented | +| `WatchPolicies` | implemented | ## Architecture ``` Client Request | - [Transport/gRPC] -- server.rs, security.rs + [Transport/gRPC] -- server.rs + | + [Auth Chain] -- security.rs, auth/*.rs (JWT → static → dev fallback) | [Coordination Kernel] -- runtime.rs | @@ -236,7 +263,9 @@ Client Request mode/*.rs ListModes, ListExtModes, GetManifest, RegisterExtMode, UnregisterExtMode, PromoteMode | - [Storage Layer] -- storage.rs, log_store.rs + [Policy Engine] -- policy/*.rs (commitment-time evaluation) + | + [Storage Layer] -- storage/*.rs, log_store.rs | [Replay] -- replay.rs ``` @@ -249,25 +278,45 @@ See `docs/architecture.md` for detailed layer descriptions. runtime/ ├── src/ │ ├── main.rs # server startup, TLS, persistence, auth wiring -│ ├── server.rs # gRPC adapter and request authentication -│ ├── runtime.rs # coordination kernel and mode dispatch +│ ├── server.rs # gRPC adapter (22 RPCs) and envelope validation +│ ├── runtime.rs # coordination kernel, mode dispatch, lifecycle bus │ ├── mode_registry.rs # single source of truth for mode registration -│ ├── security.rs # auth config, sender derivation, rate limiting +│ ├── security.rs # auth config loader, sender derivation, rate limiting │ ├── session.rs # canonical SessionStart validation and session model │ ├── registry.rs # session store with optional persistence -│ ├── log_store.rs # in-memory accepted-history log cache -│ ├── storage.rs # storage backend trait, FileBackend, crash recovery +│ ├── log_store.rs # in-memory accepted-history log cache + replay helpers │ ├── replay.rs # session rebuild from append-only log +│ ├── stream_bus.rs # per-session broadcast channels │ ├── metrics.rs # per-mode metrics counters +│ ├── auth/ # pluggable auth resolver chain +│ │ ├── chain.rs # resolver chain driver +│ │ ├── resolver.rs # AuthResolver trait, ResolvedIdentity +│ │ └── resolvers/ +│ │ ├── jwt_bearer.rs # JWT validation with JWKS (inline or URL cache) +│ │ └── static_bearer.rs # opaque bearer token → identity map +│ ├── extensions/ # session-extension provider plumbing +│ │ ├── provider.rs # SessionExtensionProvider trait +│ │ └── registry.rs # ExtensionProviderRegistry │ ├── mode/ # mode implementations (standards-track + extensions) │ │ ├── passthrough.rs # generic handler for dynamically registered extensions │ │ └── ... +│ ├── policy/ # governance policy framework (RFC-MACP-0012) +│ │ ├── registry.rs # policy CRUD + broadcast +│ │ ├── evaluator.rs # per-mode commitment evaluation +│ │ └── rules.rs # mode-specific rule schemas +│ ├── storage/ # pluggable storage backends +│ │ ├── file.rs # per-session append-only log + snapshots +│ │ ├── memory.rs # in-memory backend +│ │ ├── rocksdb.rs # RocksDB backend (feature-gated) +│ │ ├── redis_backend.rs # Redis backend (feature-gated) +│ │ └── recovery.rs # crash recovery (.tmp cleanup) │ └── bin/ # local development example clients ├── tests/ │ ├── integration_mode_lifecycle.rs # full-stack integration tests │ ├── replay_round_trip.rs # replay tests for all modes │ ├── conformance_loader.rs # JSON fixture runner │ └── conformance/ # per-mode conformance fixtures +├── integration_tests/ # gRPC boundary tests (Tier 1/2/3) ├── docs/ └── build.rs ``` @@ -309,7 +358,7 @@ MACP_TEST_BINARY=../target/debug/macp-runtime cargo test -- --test-threads=1 The integration suite has three tiers: -- **Tier 1 (Protocol)** — 47 scripted gRPC tests covering all modes, error paths, signals, version binding, dedup, and RFC cross-cutting features +- **Tier 1 (Protocol)** — 74 scripted gRPC tests (including JWT bearer auth, passive subscribe, and policy registry coverage) across all modes, error paths, signals, version binding, dedup, and RFC cross-cutting features - **Tier 2 (Rig Tools)** — 5 tests using [Rig](https://rig.rs) agent framework `Tool` implementations for all MACP operations - **Tier 3 (E2E)** — 3 tests with real OpenAI GPT-4o-mini agents coordinating through the runtime (requires `OPENAI_API_KEY`) diff --git a/docs/API.md b/docs/API.md index 868e016..cedc11f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # API Reference -This is the reference for all 18 gRPC RPCs exposed by the MACP Runtime on `macp.v1.MACPRuntimeService`. The default endpoint is `127.0.0.1:50051`, configurable via `MACP_BIND_ADDR`. +This is the reference for all 22 gRPC RPCs exposed by the MACP Runtime on `macp.v1.MACPRuntimeService`. The default endpoint is `127.0.0.1:50051`, configurable via `MACP_BIND_ADDR`. For protocol-level transport semantics, see the [protocol transports documentation](https://www.multiagentcoordinationprotocol.io/docs/transports). @@ -34,7 +34,7 @@ The client sends its supported protocol versions in descending preference order. Returns `UNSUPPORTED_PROTOCOL_VERSION` if no mutual version exists. -**Capabilities advertised**: `sessions.stream`, `cancellation.cancel_session`, `progress.progress`, `manifest.get_manifest`, `mode_registry.list_modes`, `mode_registry.list_changed`, `roots.list_roots`, `roots.list_changed`, `policy_registry.register_policy`, `policy_registry.list_policies`, `policy_registry.list_changed`. +**Capabilities advertised**: `sessions.stream`, `sessions.list_sessions`, `sessions.watch_sessions`, `cancellation.cancel_session`, `progress.progress`, `manifest.get_manifest`, `mode_registry.list_modes`, `mode_registry.list_changed`, `roots.list_roots`, `roots.list_changed`, `policy_registry.register_policy`, `policy_registry.list_policies`, `policy_registry.list_changed`. ## Message Transport @@ -95,6 +95,26 @@ rpc GetSession(GetSessionRequest) returns (GetSessionResponse) Returns `SessionMetadata` with the session's mode, state, TTL deadline, bound versions, participants, per-participant activity summaries, and initiator identity. Only the session initiator and declared participants can query a session. +### ListSessions + +Enumerates metadata for every session currently held in the registry (including terminal sessions still within the retention window). + +```protobuf +rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) +``` + +Returns a `sessions` array of `SessionMetadata` entries. Authentication is required; the RPC is not filtered by caller identity, so callers should apply their own participation or tenancy checks before exposing results to end users. + +### WatchSessions + +Server-streaming RPC for observing session lifecycle transitions across the runtime. + +```protobuf +rpc WatchSessions(WatchSessionsRequest) returns (stream WatchSessionsResponse) +``` + +On connect, the runtime emits one `Created` event per session currently in the registry (initial sync), then streams live `SessionLifecycleEvent` entries as sessions are `Created`, `Resolved`, or `Expired`. Each event carries `event_type`, the current `SessionMetadata` snapshot, and `observed_at_unix_ms`. The underlying broadcast channel has a bounded capacity -- slow subscribers that fall behind will miss events, so consumers should reconcile with `ListSessions` on reconnect. + ### CancelSession Allows the session initiator to terminate a session. This is a core control-plane operation -- mode authorization does not apply. diff --git a/docs/README.md b/docs/README.md index f8d76a5..4a7e0de 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,9 +8,9 @@ This documentation covers the **runtime implementation** -- how to build, config ## What the runtime provides -The runtime ships as a single binary that exposes 18 gRPC RPCs over TLS. It supports the five standards-track coordination modes (Decision, Proposal, Task, Handoff, Quorum) and one built-in extension mode for iterative convergence. A governance policy framework evaluates rules at commitment time, and pluggable storage backends (file, RocksDB, Redis, or in-memory) handle persistence with append-only logs and checkpoint-based replay. +The runtime ships as a single binary that exposes 22 gRPC RPCs over TLS. It supports the five standards-track coordination modes (Decision, Proposal, Task, Handoff, Quorum) and one built-in extension mode for iterative convergence. A governance policy framework evaluates rules at commitment time, and pluggable storage backends (file, RocksDB, Redis, or in-memory) handle persistence with append-only logs and checkpoint-based replay. -Authentication is handled through bearer tokens mapped to agent identities, with per-sender rate limiting for both session creation and message throughput. In development, a header-based identity shortcut lets you get started without configuring tokens. +Authentication is layered as a resolver chain: JWT bearer (when an issuer and JWKS are configured), then static bearer tokens, with a dev-mode fallback only when neither is set. Identities expose capability flags -- `allowed_modes`, `can_start_sessions`, `max_open_sessions`, `can_manage_mode_registry`, and `is_observer` -- that are enforced on every request. Per-sender sliding-window rate limits cover both session creation and message throughput. Session lifecycle transitions can be observed in real time through `ListSessions` and `WatchSessions`, and accepted envelope history can be replayed into a stream via passive subscribe. ## Documentation @@ -20,7 +20,7 @@ Authentication is handled through bearer tokens mapped to agent identities, with ### Implementation reference - [**Architecture**](architecture.md) -- Rust layer design, request processing flows, concurrency model, and source layout -- [**API Reference**](API.md) -- All 18 gRPC RPCs with request/response fields, authentication, and rate limiting +- [**API Reference**](API.md) -- All 22 gRPC RPCs with request/response fields, authentication, and rate limiting - [**Modes**](modes.md) -- Runtime implementation details for each mode's state machine - [**Policy**](policy.md) -- Policy registration, JSON rule examples, evaluation internals, and error handling diff --git a/docs/architecture.md b/docs/architecture.md index a884856..870927b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,7 +6,7 @@ This page describes the runtime's internal design: its layer structure, how requ ## Layers -The runtime is organized into six layers, each with a clear responsibility boundary: +The runtime is organized into seven layers, each with a clear responsibility boundary: ``` Agents (external) @@ -14,15 +14,24 @@ Agents (external) v gRPC (tonic) +-----------------------------------------------------------+ | Transport Layer (src/server.rs) | -| 18 RPC handlers, authentication, envelope validation, | -| sender derivation, rate limiting | +| 22 RPC handlers, envelope validation, sender | +| derivation, rate limiting | ++-----------------------------------------------------------+ + | + v ++-----------------------------------------------------------+ +| Auth Layer (src/security.rs, src/auth/*.rs) | +| Resolver chain: JWT bearer → static bearer → dev-mode | +| fallback; capability flags (allowed_modes, | +| max_open_sessions, is_observer, …) | +-----------------------------------------------------------+ | v +-----------------------------------------------------------+ | Coordination Kernel (src/runtime.rs) | | Session lifecycle, deduplication, TTL enforcement, | -| mode dispatch, signal broadcast | +| mode dispatch, signal broadcast, session lifecycle | +| observability (WatchSessions) | +-----------------------------------------------------------+ | v @@ -53,7 +62,9 @@ Agents (external) +-----------------------------------------------------------+ ``` -The **transport layer** terminates gRPC connections, authenticates requests using bearer tokens or development headers, validates envelope structure, overrides the sender field with the authenticated identity, and enforces per-sender rate limits. It never touches session state directly. +The **transport layer** terminates gRPC connections, delegates authentication to the auth layer, validates envelope structure, overrides the sender field with the authenticated identity, and enforces per-sender rate limits. It never touches session state directly. + +The **auth layer** implements a pluggable resolver chain. Each resolver inspects the request metadata and returns either a verified identity, a pass (the credential isn't ours, try the next resolver), or a hard reject (the credential is ours but invalid). The built-in resolvers are `jwt_bearer` (validates signature, issuer, audience, and expiration via a JWKS) and `static_bearer` (looks up opaque tokens in a preloaded map). When neither is configured, the layer falls back to dev-mode auth where any bearer value becomes the sender -- strictly for local development. Every resolved identity carries capability flags (`allowed_modes`, `can_start_sessions`, `max_open_sessions`, `can_manage_mode_registry`, `is_observer`) that later authorization checks consult. The **coordination kernel** is the heart of the runtime. It manages session creation, deduplication, lazy TTL expiration, and dispatches messages to the appropriate mode handler. It also broadcasts signals on the ambient plane and publishes accepted envelopes to streaming subscribers. @@ -115,9 +126,9 @@ The critical property is the **commit point**: the log entry is persisted to dur Bidirectional streaming works similarly but binds the stream to a single session: -The first envelope on the stream establishes the session binding. All subsequent envelopes must target the same session. The client receives all accepted envelopes for that session (from any participant), delivered through a per-session broadcast channel. +The first frame on the stream establishes the session binding. It can be either (a) an envelope -- the stream then behaves as an active participant, or (b) a passive-subscribe frame (`subscribe_session_id` + `after_sequence`) -- the runtime replays accepted envelopes from log index `after_sequence` and then delivers live envelopes on the same stream. All subsequent envelope frames must target the same session. Passive subscribe is authorized for session initiators, declared participants, and identities carrying the `is_observer` capability; non-participants receive an inline `FORBIDDEN` error and the stream stays open. A frame that sets both `envelope` and `subscribe_session_id` is rejected with `InvalidArgument`. -Application-level errors like validation failures or authorization denials are sent as inline error messages on the stream without closing it. Transport-level errors like authentication failure terminate the stream. If the client falls behind the broadcast buffer (capacity: 256 envelopes), the stream is terminated with `ResourceExhausted` and the client must reconnect. +Application-level errors like validation failures or authorization denials are sent as inline error messages on the stream without closing it. Transport-level errors like authentication failure terminate the stream. If the client falls behind the broadcast buffer (capacity: 256 envelopes), the stream is terminated with `ResourceExhausted` and the client must reconnect -- optionally resuming via passive subscribe with an `after_sequence` derived from the last envelope it saw. ## Key types @@ -189,7 +200,7 @@ The runtime processes different sessions in parallel but serializes access withi - **Cross-session parallelism**: Messages targeting different sessions are processed concurrently with no coordination between them. -- **Stream bus**: Each session has a `tokio::sync::broadcast` channel (capacity 256) for delivering accepted envelopes to `StreamSession` subscribers. A separate global broadcast channel handles ambient signals via `WatchSignals`. +- **Stream bus**: Each session has a `tokio::sync::broadcast` channel (capacity 256) for delivering accepted envelopes to `StreamSession` subscribers. A separate global broadcast channel handles ambient signals via `WatchSignals`, and a third broadcast channel (capacity 64) carries session lifecycle events (`Created`, `Resolved`, `Expired`) for `WatchSessions` subscribers. - **Background tasks**: A periodic cleanup task runs every 60 seconds (configurable via `MACP_CLEANUP_INTERVAL_SECS`) to expire sessions that have exceeded their TTL and evict terminal sessions from memory after a retention period. @@ -204,17 +215,30 @@ At runtime, the registry supports dynamic extension management: `RegisterExtMode ``` src/ main.rs -- Startup, TLS config, persistence wiring, background tasks - server.rs -- gRPC adapter (18 RPCs), auth, validation, streaming - runtime.rs -- Coordination kernel, session lifecycle, mode dispatch + server.rs -- gRPC adapter (22 RPCs), envelope validation, streaming + runtime.rs -- Coordination kernel, session lifecycle, mode dispatch, + session lifecycle broadcast session.rs -- Session model, SessionStart validation, ID rules - security.rs -- Auth config, sender derivation, rate limiting + security.rs -- Auth config loader, sender derivation, rate limiting error.rs -- Error types and RFC error code mapping registry.rs -- In-memory session registry - log_store.rs -- In-memory log cache + log_store.rs -- In-memory log cache; passive-subscribe replay helpers stream_bus.rs -- Per-session broadcast channels for StreamSession metrics.rs -- Atomic per-mode counters mode_registry.rs -- Mode lookup, extension lifecycle replay.rs -- Session rebuild from logs, checkpoint fast path + auth/ + mod.rs -- Public exports for chain, resolver, resolvers + chain.rs -- AuthResolverChain: walks resolvers in order + resolver.rs -- AuthResolver trait, ResolvedIdentity, AuthError + resolvers/ + mod.rs -- Built-in resolver exports + jwt_bearer.rs -- JWT bearer resolver (inline JWKS or URL, with cache) + static_bearer.rs -- Opaque bearer token → identity map resolver + extensions/ + mod.rs -- Public exports for extension plumbing + provider.rs -- SessionExtensionProvider trait, SessionOutcome + registry.rs -- ExtensionProviderRegistry lifecycle fan-out mode/ mod.rs -- Mode trait and standard descriptors decision.rs -- Decision mode state machine diff --git a/docs/deployment.md b/docs/deployment.md index b1a1449..26eef84 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -8,7 +8,7 @@ Before exposing the runtime to production traffic, ensure these four items are c 1. **TLS certificates** -- Set `MACP_TLS_CERT_PATH` and `MACP_TLS_KEY_PATH` to valid PEM files. The runtime refuses to start without TLS unless `MACP_ALLOW_INSECURE=1` is set. -2. **Authentication tokens** -- Create a `tokens.json` mapping bearer tokens to agent identities and set `MACP_AUTH_TOKENS_FILE`. See the [Getting Started guide](getting-started.md) for the token format. +2. **Authentication** -- Configure at least one of the resolvers. For opaque bearer tokens, create a `tokens.json` mapping tokens to agent identities and set `MACP_AUTH_TOKENS_FILE`. For JWT bearer tokens, set `MACP_AUTH_ISSUER` together with a JWKS source (`MACP_AUTH_JWKS_JSON` inline or `MACP_AUTH_JWKS_URL` fetched + cached). Both can be configured at once -- JWT-shaped tokens are routed to the JWT resolver and opaque tokens to the static resolver. See the [Getting Started guide](getting-started.md) for the token format and JWT claim layout. 3. **Data directory** -- Ensure `MACP_DATA_DIR` points to a directory with write permissions. This is where session logs and snapshots are stored. @@ -55,6 +55,16 @@ The runtime supports four storage configurations, selected via `MACP_STORAGE_BAC **Memory-only mode** disables persistence entirely. Set `MACP_MEMORY_ONLY=1` for testing or ephemeral workloads. All session data is lost when the process exits. +## Authentication + +The runtime applies a pluggable resolver chain assembled at startup: + +1. **JWT bearer** (active when `MACP_AUTH_ISSUER` is set) -- validates signature, issuer, audience, and expiration against a JWKS. Supported algorithms: `RS256`, `ES256`, `HS256`. The `sub` claim becomes the sender; an optional `macp_scopes` claim carries capability flags (`allowed_modes`, `can_start_sessions`, `max_open_sessions`, `can_manage_mode_registry`, `is_observer`). +2. **Static bearer** (active when `MACP_AUTH_TOKENS_FILE` or `MACP_AUTH_TOKENS_JSON` is set) -- looks up opaque tokens in a preloaded identity map. Accepts `Authorization: Bearer ` or the alternate `x-macp-token: ` header. +3. **Dev-mode fallback** -- activates only when **neither** JWT nor static bearer is configured. Any `Authorization: Bearer ` header authenticates the caller as sender `` with full capabilities. Intended strictly for local development. + +If a credential matches a resolver but fails verification (expired JWT, unknown static token), the request is rejected with `UNAUTHENTICATED` -- the chain does **not** fall through to a later resolver. + ## Crash recovery When persistence is enabled, the runtime rebuilds all sessions from their append-only logs on startup. This process is fully automatic: diff --git a/docs/examples.md b/docs/examples.md index a53d2b4..75742b3 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -69,9 +69,10 @@ A coordinator starts a session using `ext.multi_round.v1`, participants exchange Key behaviors to note: -- The stream starts observing from the bind point -- there is no replay of earlier history. - Use `SessionStart` to create a new session over the stream, or send a session-scoped message to attach to an existing one. +- For observers and late joiners, send a passive-subscribe frame (`subscribe_session_id` + `after_sequence`) as the first frame -- the runtime replays accepted history starting at `after_sequence` and then delivers live envelopes on the same stream. Use `after_sequence = 0` to replay from session start. - Mixed-session streams (envelopes targeting different sessions) are rejected. +- A single frame must not carry both `envelope` and `subscribe_session_id` -- the stream terminates with `InvalidArgument`. - Application errors are delivered inline without closing the stream. ## Extension Mode Lifecycle diff --git a/docs/getting-started.md b/docs/getting-started.md index 6fc1d52..abc168c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -176,7 +176,8 @@ Create a `tokens.json` file that maps bearer tokens to agent identities and capa "allowed_modes": ["macp.mode.decision.v1", "macp.mode.task.v1"], "can_start_sessions": true, "max_open_sessions": 10, - "can_manage_mode_registry": false + "can_manage_mode_registry": false, + "is_observer": false }, { "token": "secret-token-for-reviewer", @@ -184,15 +185,52 @@ Create a `tokens.json` file that maps bearer tokens to agent identities and capa "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 grants access to all modes. The runtime derives the sender identity from the token, so agents cannot spoof their identity. Clients authenticate by sending `Authorization: Bearer ` in the gRPC metadata. +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 ` (or the alternate `x-macp-token: ` header) in the gRPC metadata. ### JWT mode -The runtime also 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 `MACP_AUTH_AUDIENCE` (default `macp-runtime`) and `MACP_AUTH_JWKS_TTL_SECS` (default 300). The JWT's `sub` claim becomes the sender; an optional `macp_scopes` claim carries the same capability fields as the static token config (`allowed_modes`, `can_start_sessions`, `max_open_sessions`, `can_manage_mode_registry`, `is_observer`). +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). Supported signature algorithms are `RS256`, `ES256`, and `HS256`. + +```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 diff --git a/docs/sdk-guide.md b/docs/sdk-guide.md index 6eb4639..77e51a3 100644 --- a/docs/sdk-guide.md +++ b/docs/sdk-guide.md @@ -9,12 +9,14 @@ For protocol-level SDK conformance requirements, see the [protocol SDK parity do A well-built MACP SDK takes care of seven concerns so that application code can focus on coordination logic: 1. **gRPC transport** -- Connection management, TLS configuration, and metadata injection. -2. **Authentication** -- Storing tokens and attaching them to every request. +2. **Authentication** -- Storing the caller's bearer credential (opaque static token or JWT) and attaching it to every request as `Authorization: Bearer `. 3. **Envelope construction** -- Building protobuf-encoded envelopes with correct version and mode fields. 4. **Message ID generation** -- Producing unique IDs for deduplication. 5. **Session ID generation** -- Creating IDs in an accepted format (UUID v4/v7 or base64url). 6. **Error handling** -- Distinguishing transient from permanent failures and applying appropriate retry logic. -7. **Streaming** -- Managing `StreamSession` connections, handling inline errors, and recovering from lag. +7. **Streaming** -- Managing `StreamSession` connections (including passive subscribe), handling inline errors, and recovering from lag. + +The runtime treats JWT and opaque static tokens interchangeably at the wire level: both travel in `Authorization: Bearer`. A JWT is detected by the presence of dots in the token; otherwise it is treated as opaque. SDKs do not need to know which resolver the server is running. ## Starting a connection @@ -107,7 +109,11 @@ Application-level errors (validation failures, authorization denials) are delive ### Handling stream lag -The runtime's broadcast buffer holds 256 envelopes per session. If a client falls behind, the stream terminates with `ResourceExhausted`. Your SDK should detect this, call `GetSession` to learn the current session state, reconnect with a new stream, and resume processing. +The runtime's broadcast buffer holds 256 envelopes per session. If a client falls behind, the stream terminates with `ResourceExhausted`. Your SDK should detect this, reconnect with a new stream, and use a passive-subscribe frame with `after_sequence` set to the log index of the last envelope it saw -- the runtime will replay missed history and then resume live delivery on the same stream. + +### Observer identities + +Non-participant agents (audit agents, dashboards, read-only observers) should be provisioned with an identity that carries `is_observer: true`. Observers can open passive-subscribe streams for any session and consume its accepted history and live envelopes without being listed in the session's `participants`. Attempting to `Send` into a session as an observer still requires normal participation rules, so passive observation does not bypass mode authority. ## Capability negotiation