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: falsein 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).
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:
export MACP_ALLOW_INSECURE=1
export MACP_ALLOW_DEV_SENDER_HEADER=1
cargo runThen:
RUNTIME_ALLOW_INSECURE=true
RUNTIME_USE_DEV_HEADER=true
RUNTIME_DEV_AGENT_ID=control-planeAdd 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:
{
"token": "obs-control-plane-token",
"sender": "control-plane",
"can_start_sessions": false
}If your deployment makes the control-plane the policy admin (optional), set can_manage_mode_registry: true.
Then in the control-plane environment:
RUNTIME_BEARER_TOKEN=obs-control-plane-tokenEach 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.
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, ...)