A scenario-agnostic, observer-only control plane for the MACP runtime.
This service is the backend that a Next.js UI talks to for run lifecycle, live stream projection, replay, traces, metrics, and artifacts.
The control plane is an observer. It never calls Send on the runtime.
- UI: browse runs, launch, render graphs and traces.
- Scenario layer (e.g.
examples-service): compile scenarios → produce a genericRunDescriptorfor this service + per-agent bootstrap for the initiator + participant agents. - Agents: authenticate to the runtime directly with their own Bearer tokens and emit their own envelopes (SessionStart, kickoff, Proposal / Evaluation / Vote / etc.) via
macp-sdk-pythonormacp-sdk-typescript. - Control plane: allocates
sessionId, pollsGetSession(sessionId)until the initiator agent opens it, then subscribes read-only toStreamSession(sessionId). Projects canonical events for the UI. - Runtime: authoritative orchestrator of MACP envelopes and modes.
- 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'sMACP_AUTH_TOKENS_JSON. - The control-plane never calls
Send— enforced by an invariant lint test (src/runtime/observer-invariant.spec.ts). POST /runsaccepts only a scenario-agnosticRunDescriptor. Fields likekickoff[],participants[].role,policyHints,commitments[],initiatorParticipantIdare rejected (forbidNonWhitelisted: true).sessionIdownership: allocated by the control-plane (UUID v4) atPOST /runsand returned to the caller, which distributes it to agents via bootstrap.- Cancellation authority stays with the initiator agent unless the scenario's policy explicitly delegates to the control-plane (see
metadata.cancellationDelegated). - The observer
StreamSessionwrites 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.
POST /runs— accepts aRunDescriptor; returns{runId, sessionId, status, traceId}GET /runs/:id— run recordGET /runs/:id/state— projected UI stateGET /runs/:id/events— canonical eventsGET /runs/:id/stream— SSE of live eventsPOST /runs/:id/cancel— UI cancel (Option A: proxies to initiator agent's cancelCallback; Option B: calls runtime.CancelSession when policy-delegated)POST /runs/validate— preflight validationPOST /runs/:id/clone— clone with optional tag overrides (session context overrides rejected)POST /runs/:id/replay— replay descriptor
These endpoints return 410 Gone. Agents emit envelopes via the SDKs directly:
POST /runs/:id/messagesPOST /runs/:id/signalPOST /runs/:id/context
GET /runtime/manifest,/runtime/modes,/runtime/roots,/runtime/healthGET /runtime/policies,POST /runtime/policies,DELETE /runtime/policies/:id
GET /runs/:id/traces,/runs/:id/artifacts,/runs/:id/metricsGET /dashboard/overview,/dashboard/agents/metricsGET /healthz,/readyz,/metrics,/docs(dev only)
{
"mode": "live",
"runtime": { "kind": "rust" },
"session": {
"sessionId": "optional — UUID v4/v7 or base64url 22+",
"modeName": "macp.mode.decision.v1",
"modeVersion": "1.0.0",
"configurationVersion": "config.default",
"policyVersion": "policy.default",
"ttlMs": 600000,
"participants": [
{ "id": "fraud-agent" },
{ "id": "risk-agent" },
{ "id": "growth-agent" }
],
"metadata": {
"source": "examples-service",
"sourceRef": "fraud/high-value-new-device@1.0.0",
"environment": "production",
"cancelCallback": {
"url": "http://initiator.internal/agent/cancel",
"bearer": "opt-in-shared-secret"
}
}
},
"execution": {
"idempotencyKey": "fraud-high-value-new-device-demo-1",
"tags": ["demo", "fraud"],
"requester": { "actorId": "coordinator", "actorType": "service" }
}
}Response: { "runId": "<uuid>", "sessionId": "<uuid>", "status": "queued", "traceId": "..." }
cp .env.example .env
npm install
npm run drizzle:migrate
npm run start:devMake 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 → Development mode) and set on the control-plane:
RUNTIME_ALLOW_INSECURE=true
RUNTIME_USE_DEV_HEADER=true
RUNTIME_DEV_AGENT_ID=control-planeThe 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:
| 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 |
For the runtime-side token configuration, TLS, and the full production auth story, see:
- runtime/docs/getting-started.md#authentication — dev / production / JWT modes and resolver order
- runtime/docs/deployment.md#authentication — production resolver chain (JWT → static bearer → dev fallback); TLS env vars live in § Production checklist and § Environment variables
- python-sdk/docs/auth.md#observer-identities — observer-identity pattern (the shape the control-plane uses) and
expected_senderguardrail
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.
If you're upgrading from a control-plane version that had POST /runs/:id/{messages,signal,context}, those endpoints now return 410 Gone. Agents must migrate to macp-sdk-python or macp-sdk-typescript and authenticate directly to the runtime. RUNTIME_AGENT_TOKENS_JSON is removed; its entries move to the runtime's MACP_AUTH_TOKENS_JSON (one per agent) and to the scenario layer's per-agent bootstrap.
runs(withruntime_session_idpopulated at creation)runtime_sessionsrun_events_raw,run_events_canonicalrun_projections,run_artifacts,run_metricsrun_outbound_messages,audit_log,webhooks,webhook_deliveries
src/
controllers/ # NestJS controllers
runs/ # run manager, observer executor, stream consumer
runtime/ # observer-only runtime provider, proto decoder, credential resolver
events/ # canonical event normalizer + SSE hub
projection/ # UI read models
replay/ # deterministic replay endpoints
metrics/ # metrics aggregation
artifacts/ # artifact registration/listing
storage/ # Drizzle repositories
db/ # Drizzle schema + database service
telemetry/ # OpenTelemetry bootstrap and manual spans
dto/ # request/response schemas for OpenAPI
contracts/ # TypeScript interfaces (RunDescriptor, RuntimeProvider, ...)