Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ RUNTIME_USE_DEV_HEADER=true # Defaults to false in production (dev-only)
RUNTIME_DEV_AGENT_ID=control-plane
RUNTIME_REQUEST_TIMEOUT_MS=30000 # Warn if < 5000

# ── Runtime auth (JWT-mint mode, preferred) ──────
# When MACP_AUTH_SERVICE_URL is set, RuntimeCredentialResolverService mints a
# short-lived RS256 JWT for the control-plane (scopes: is_observer=true,
# can_start_sessions=false) instead of using the static RUNTIME_BEARER_TOKEN.
# Tokens are cached until TTL minus a 30s refresh buffer and 10s clock-skew;
# concurrent refreshes are deduped. On mint failure the resolver falls back to
# the static Bearer so a brief auth-service outage doesn't fail every call.
MACP_AUTH_SERVICE_URL= # e.g. https://auth.internal — leave blank to disable
MACP_AUTH_SERVICE_TIMEOUT_MS=5000
MACP_AUTH_TOKEN_TTL_SECONDS=3600
MACP_AUTH_TOKEN_SENDER=control-plane

# ── Session polling (observer mode) ──────────────
# Control-plane polls GetSession(sessionId) until the initiator agent opens
# the session, then subscribes read-only via StreamSession.
Expand Down
40 changes: 15 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ The control plane is an observer. **It never calls `Send`** on the runtime.

## Invariants (see `../ui-console/plans/direct-agent-auth.md` §Invariants)

1. The control-plane runtime identity is least-privilege: `can_start_sessions: false` in runtime's `MACP_AUTH_TOKENS_JSON`.
1. The control-plane runtime identity is least-privilege: `can_start_sessions: false, is_observer: true` — either encoded in a minted short-lived JWT (preferred) or in a static entry in the runtime's `MACP_AUTH_TOKENS_JSON`.
2. The control-plane never calls `Send` — enforced by an invariant lint test (`src/runtime/observer-invariant.spec.ts`).
3. `POST /runs` accepts only a scenario-agnostic `RunDescriptor`. Fields like `kickoff[]`, `participants[].role`, `policyHints`, `commitments[]`, `initiatorParticipantId` are rejected (`forbidNonWhitelisted: true`).
4. `sessionId` ownership: allocated by the control-plane (UUID v4) at `POST /runs` and returned to the caller, which distributes it to agents via bootstrap.
5. Cancellation authority stays with the initiator agent unless the scenario's policy explicitly delegates to the control-plane (see `metadata.cancellationDelegated`).
6. The observer `StreamSession` writes exactly one passive-subscribe frame (`{subscribeSessionId, afterSequence}`) per RFC-MACP-0006 §3.2 and then **keeps the write side open** — half-closing would signal "client is done" and stop live-envelope broadcast.

## Endpoints

Expand Down Expand Up @@ -97,42 +98,31 @@ npm run drizzle:migrate
npm run start:dev
```

Make sure the runtime is running at `RUNTIME_ADDRESS`. For dev auth against the reference runtime profile:

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

Then:
Make sure the runtime is running at `RUNTIME_ADDRESS`. For dev auth against the reference runtime profile, start the runtime with `MACP_ALLOW_INSECURE=1 MACP_ALLOW_DEV_SENDER_HEADER=1` (see [runtime/docs/getting-started.md#authentication](../runtime/docs/getting-started.md#authentication) → *Development mode*) and set on the control-plane:

```bash
RUNTIME_ALLOW_INSECURE=true
RUNTIME_USE_DEV_HEADER=true
RUNTIME_DEV_AGENT_ID=control-plane
```

## Production runtime auth
## Runtime auth (observer identity)

Add one entry to the runtime's `MACP_AUTH_TOKENS_JSON` for the control-plane. It is a **read-only observer** and must not have session-start authority:
The control-plane has **exactly one** runtime identity with fixed scope `is_observer: true, can_start_sessions: false`. `RuntimeCredentialResolverService` resolves credentials per gRPC call using a three-step fallback chain:

```json
{
"token": "obs-control-plane-token",
"sender": "control-plane",
"can_start_sessions": false
}
```
| Mode | Trigger | Control-plane env |
| --- | --- | --- |
| JWT mint (preferred) | `MACP_AUTH_SERVICE_URL` set | `MACP_AUTH_SERVICE_URL`, `MACP_AUTH_SERVICE_TIMEOUT_MS`, `MACP_AUTH_TOKEN_TTL_SECONDS`, `MACP_AUTH_TOKEN_SENDER` |
| Static Bearer | JWT disabled or mint fails | `RUNTIME_BEARER_TOKEN` (must match an entry in the runtime's `MACP_AUTH_TOKENS_JSON` with `can_start_sessions: false`) |
| Dev header | `RUNTIME_USE_DEV_HEADER=true`, local only | `RUNTIME_DEV_AGENT_ID` |

If your deployment makes the control-plane the policy admin (optional), set `can_manage_mode_registry: true`.
For the runtime-side token configuration, TLS, and the full production auth story, see:

Then in the control-plane environment:
```bash
RUNTIME_BEARER_TOKEN=obs-control-plane-token
```
- [runtime/docs/getting-started.md#authentication](../runtime/docs/getting-started.md#authentication) — dev / production / JWT modes and resolver order
- [runtime/docs/deployment.md#authentication](../runtime/docs/deployment.md#authentication) — production resolver chain (JWT → static bearer → dev fallback); TLS env vars live in [§ Production checklist](../runtime/docs/deployment.md#production-checklist) and [§ Environment variables](../runtime/docs/deployment.md#environment-variables)
- [python-sdk/docs/auth.md#observer-identities](../python-sdk/docs/auth.md#observer-identities) — observer-identity pattern (the shape the control-plane uses) and `expected_sender` guardrail

Each agent additionally gets its own entry (with `can_start_sessions: true` for the initiator). Per-agent tokens are **not** shared with the control-plane — the scenario layer distributes them to agents via bootstrap. See `../ui-console/plans/direct-agent-auth.md` for the full onboarding flow.
Per-agent tokens are **not** held by the control-plane — the scenario layer distributes them to agents via bootstrap. See `../ui-console/plans/direct-agent-auth.md` for the onboarding flow.

## Migration from pre-2026-04 control-plane

Expand Down
23 changes: 4 additions & 19 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,11 @@ Rate limit: 100 requests per 60 seconds per client. Payload limit: 1MB.

### Upstream runtime auth (observer identity)

The control-plane has **exactly one** runtime identity — its own least-privilege
Bearer token. It never calls `Send`; agents authenticate to the runtime directly
(RFC-MACP-0004 §4). Its entry in the runtime's `MACP_AUTH_TOKENS_JSON` must have
`can_start_sessions: false`.
The control-plane has **exactly one** runtime identity. It never calls `Send`; agents authenticate to the runtime directly (RFC-MACP-0004 §4). The scope is fixed: `is_observer: true, can_start_sessions: false`.

| Env var | Purpose |
| --- | --- |
| `RUNTIME_BEARER_TOKEN` | Control-plane's own observer Bearer token. Used for every runtime call (`Initialize`, `GetSession`, `StreamSession`, `ListPolicies`, etc.). |
| `RUNTIME_USE_DEV_HEADER` | Local dev fallback — sends `x-macp-agent-id: <RUNTIME_DEV_AGENT_ID>` when no Bearer token is configured. Requires `MACP_ALLOW_DEV_SENDER_HEADER=1` on the runtime. |
Configuration, env vars, and the three-step fallback chain (JWT mint → static Bearer → dev header) are documented in [ARCHITECTURE.md § Runtime Credential Resolution](./ARCHITECTURE.md#runtime-credential-resolution). For the runtime-side token configuration (`MACP_AUTH_TOKENS_JSON` shape, JWT claim expectations, TLS/mTLS), see [runtime/docs/getting-started.md#authentication](../../runtime/docs/getting-started.md#authentication) and [runtime/docs/deployment.md#authentication](../../runtime/docs/deployment.md#authentication).

Per-agent tokens are **not** held by the control-plane. They live in the scenario
layer (examples-service) and flow to agents via their bootstrap.
Per-agent tokens are **not** held by the control-plane — they live in the scenario layer (examples-service) and flow to agents via their bootstrap.

---

Expand Down Expand Up @@ -542,15 +535,7 @@ Policy events are produced when:

### Policy Rule Schemas (RFC-MACP-0012)

Rules are opaque to the control plane (passed through as JSON to the runtime), but must conform to the RFC's per-mode schemas:

| Mode | Rule Sections |
|------|---------------|
| **Decision** | `voting` (algorithm, threshold, quorum, weights), `objection_handling` (block_severity_vetoes, `veto_threshold`), `evaluation` (required_before_voting, `minimum_confidence`), `commitment` (authority, `designated_roles`, require_vote_quorum) |
| **Quorum** | `threshold` (type: `n_of_m`/`percentage`/`weighted`, value), `abstention` (`counts_toward_quorum`, `interpretation`), `commitment` |
| **Proposal** | `acceptance` (`criterion`), `counter_proposal` (`max_rounds`), `rejection` (`terminal_on_any_reject`), `commitment` |
| **Task** | `assignment` (`allow_reassignment_on_reject`), `completion` (`require_output`), `commitment` |
| **Handoff** | `acceptance` (`implicit_accept_timeout_ms`), `commitment` |
Rules are opaque to the control-plane — the request body is passed through as JSON to `runtime.RegisterPolicy`. Per-mode rule schemas (Decision / Proposal / Task / Handoff / Quorum), worked examples, and evaluation semantics are documented canonically in [runtime/docs/policy.md](../../runtime/docs/policy.md) — see *Rule examples by mode*, *How evaluation works*, and *Commitment authority*.

---

Expand Down
Loading
Loading