The Reaktor Control Plane

Implementation status · 12 September 2026: the workspace backend now runs in a headless kernel with graph-owned services, shared by the desktop, CLI and MCP adapters. A desktop with its kernel installed advertises eleven read tools, including kernel_graph and connection_status. Since 23 September Codex, Claude Code and Antigravity reach those tools and the workspace through one MCP entry, the door, which also fronts trusted third-party servers: 271 tools in the BestBuds workspace. Reaktor's own MCP tools remain read-only, and third-party writes pass the door's approvals. The CLI executes reviewed tasks through the kernel's planning and receipt path. Remote identity, cross-process sessions and the general graph mutation flow below remain open. Read Workbench Layers for the current architecture and ownership boundaries.

One low-level surface over a running Reaktor app — for both humans (the workbench) and agents (Claude/Codex over MCP, in-process Koog). Read the live graph as a knowledge graph, take advantage of every signal and capability Reaktor exposes, and make permission-gated, reviewed changes through one mutation spine. The toolbox grows as Reaktor does.

Context — one control plane for humans and agents

The target is a shared control plane for operating Reaktor apps — driven the same way by humans (through the workbench) and agents (Claude/Codex over MCP, in-process Koog). Reaktor apps are unusually legible: an app is a graph of typed nodes, ports, edges, routes, actors, and services, and it already emits live telemetry, performance signals, and typed contracts. The goal is to expose all of that as one governed surface so any operator can connect to a running app at a low level with full, governed access — understand it, exercise its capabilities through typed tools, and make changes that are reviewed and reversible.

The live graph is a standout advantage — a self-describing knowledge graph of the running system — but it is one advantage among many. Take advantage of everything the surface exposes: telemetry, performance, cloud, design system, testing, UI inspection, database, auth, actors. This is not a one-off integration; it's a stable contract every Reaktor module plugs new tools into. As Reaktor grows, humans and agents automatically gain those abilities. Build the surface once; the toolbox compounds.

Why it raises code quality

Agents already change code — Claude and Codex edit files today, and that won't change. The control plane doesn't replace that; it grounds it. The same act (an agent writing code) gets dramatically higher quality and confidence because the agent stops working blind and starts working against the running system.

Editing code today

  • Blind to the running system — structure inferred from source.
  • APIs guessed from reading files; easy to call the wrong contract.
  • Effect of a change verified only by re-reading or a slow full build.
  • Mistakes land silently; rollback is manual.

Editing code on the surface

  • Understanding: the live graph + telemetry show what the app actually is and does — deterministic, repeatable context, not a guess.
  • Discovery: typed tools (ports + contracts with schemas) mean the agent calls real contracts, never invented ones.
  • Feedback: performance, traces, and tests give immediate, real signal on a change's effect — and budgets can fail a bad change.
  • Safety: changes land as a validated, dry-runnable, reversible patch — not a blind write.

Net: the same agents, far better code — fewer wrong-API guesses, fewer regressions, tighter loops, and changes that are reviewed and reversible. The quality ceiling rises as Reaktor adds tools (cloud, performance, design system, testing, UI inspection): every new capability is one more thing the agent can ground its work against.

The thesis

Three things any operator needs

  • Observe — read the live graph + every runtime signal (telemetry, performance, state) as a deterministic knowledge graph.
  • Call — exercise the app's capabilities as typed, schema'd tools.
  • Change — make structured mutations that are validated, dry-runnable, reviewed, and reversible.

Operators & transports

  • Humans drive it through the reaktorDesktop workbench (visual graph, panes, command queue).
  • External agents (Claude Code, Codex, Cursor, IntelliJ) drive it over MCP.
  • In-process Koog drives the workbench copilot and the in-app bot on the JVM.
  • All consume the same snapshot/event reader, tool registry, mutation spine, and permission gate.

The unifying idea: the graph is the knowledge graph, capabilities are tools, and every change flows through one validated mutation spine under one permission model. No operator — human or agent — gets a raw, ungoverned handle; everyone gets the same surface, with humans simply granted broader scopes.

Kernel primitives — the shared vocabulary

The Graph Kernel Analysis fixes the vocabulary the whole control plane speaks. Four kernel primitives matter here, and the Surface is built entirely from them:

PrimitiveWhat it isKernel law
SnapshotGraph.snapshot(): GraphSnapshot — a read-only, deterministic, stable-id projection of the running graph.Law 8: snapshots are read-only
Eventthe live change feed — topology, lifecycle, navigation, state, port wiring.Law 1: identity is stable across deltas
Visitorread-only interpretation over the graph; selectors + passes compute views and findings.Law 6: visitors interpret
CommandGraph.execute(command): CommandResult — mutation happens through validation, never by poking objects.Law 7: commands mutate

Two more laws are load-bearing: Law 2 — ports connect by ContractId, the universal identity across polyglot and distributed boundaries (so a tool is addressed the same way whether it's local Kotlin, a TS worker, or a remote actor); and the kernel's heuristic — "can this be a contract, facet, capability, visitor, event, snapshot field, or command? If yes it's a module — don't build 80 frameworks in a trench coat." The Surface adds no new runtime; it is a thin module that projects these primitives to operators.

The four parts of the Surface map one-to-one onto the primitives:

  • GraphReader = Snapshot + Event + Visitor (§Observe)
  • ToolRegistry = Capability + the L1 port shapes (§Call)
  • MutationSpine = Command, realized as a validated GraphPatch (§Change)
  • AuthGate = call-time authorization over every read/call/change (§Identity & permissions)

The Surface — a thin reaktor-agent module

Introduce one framework module — reaktor-agent — that composes existing modules into the contract above. It owns no business logic; it projects the running app to operators and routes every interaction through the auth gate and the telemetry pipeline.

   Humans (workbench)   External agents (MCP)   In-process agents (Koog)
        │                     │   JSON-RPC             │  direct calls
        ▼                     ▼                        ▼
 ┌──────────────────────────────────────────────────────────────┐
 │                  reaktor-agent  (the Surface)                  │
 │                                                                │
 │  GraphReader        ToolRegistry        MutationSpine          │
 │  Snapshot+Event     Capability +        WorkbenchCommand →     │
 │  +Visitor passes    port-shape tools    GraphPatch → Wiring    │
 │                                                                │
 │           AuthGate — call-time authz (every op)                │
 │           Telemetry tap — every op is an OTel span             │
 └──────────────────────────────────────────────────────────────┘
        │ reads / calls / mutates — all gated, all audited
        ▼
 reaktor-graph · reaktor-auth · reaktor-telemetry · reaktor-performance
 reaktor-mcp · reaktor-db · reaktor-ui · reaktor-cloud · shadow · cluster
        │
        ▼
   the running Reaktor app graph

The same module serves all three operator kinds; the only difference is the scope template bound to the caller (§permissions) and the transport (§connection).

Observe — the live graph as a knowledge graph

Reads are visitor passes, not ad-hoc serialization. The Graph Visitors design already specifies the exact mechanisms; the control plane wires them to a transport.

  • The snapshot is a pass. GraphDocumentPass is the framework's first-class graph document — deterministic ordering, stable ids, IntrospectionExcluded + CyclePolicy so an agent can inspect the workbench graph itself without infinite recursion. It supersedes the hand-rolled ReaktorGraphDocumentBuilder and the one-way Graph.toJsonElement(). The determinism contract is why this matters for agents: stable ordering means snapshot diffs are meaningful and the AI context window is repeatable.
  • Queries compose. Selectors return Sequence<VisitArc> with a rich VisitKind (incl. ServiceHandler, ActorMailbox, RepositoryState, ModuleProvides, CrossGraphEdge) and combine with union/intersect/filter/restrictTo<T>/stopAt. Passes form a DAG with a shared PassContext; the agent's read API is "run these passes," not "walk the graph."
  • Findings are structured. Diagnostic passes emit Finding(severity, element, code, …) with stable string codes — wiring.missing, wiring.ambiguous, route.deadend, module.contract.unsatisfied, introspection.cycle. An agent gets typed problems to act on, not prose.
  • The live feed already exists in design. ReactiveSelector.arcs(graph): Flow<VisitArc> and IncrementalVisitor (onAttach/onDetach/onConnect/onDisconnect, driven by graphEvents) are the "watch the graph" API. The control-plane work is to reconcile today's three partial feeds — PortEvent (sync callbacks), the lifecycle StateFlow, and ObjectDatabase.events (origin-tagged SharedFlow) — into one ordered stream, and fix the known GraphTelemetry.instrument() bug where nodes attached after instrument-time are never observed.

State makes it a knowledge graph, not just a wiring diagram. Node state is already externalized where it matters: an ActorNode owns an exclusive ObjectStore (state<S>("name"), durable + serialized + observable), and the refinement plan's StatePort<S> exposes a node's state as a first-class port. The Surface reads state from those — never by reaching into live objects — and serializes it to JsonElement at the boundary so it is safe to ship and impossible to mutate by reference. (One genuinely net-new helper, an InspectionCapability, gives every node a uniform readState/observeState façade over the heterogeneous mechanisms.)

Reason above nodes. The refinement plan's module { provides/consumes } + ModuleContractPass and CrossGraphReachabilityPass let an agent ask higher-level questions — "which module contracts are unsatisfied," "which cross-graph edges are unreachable" — instead of only crawling raw ports. And time-travel is designed twice: GraphHistoryBuffer (a ring of snapshots) and shadow restore --as-of let an operator observe the graph as of a past tick.

Call — capabilities as typed tools

Tools come from ports, and the refinement plan gives ports five canonical shapes. Each maps cleanly to a tool kind, so "what can I call here" is just "what ports does this node expose":

Port shapeTool kindBacked by
RequestPort<In,Out>request/reply tooltoday's Service/RequestHandler (each carries KSerializer<In/Out> + ServiceEndpoint, directly invoke()-able)
MailboxPort<M>send / ask toolActorNode<M> mailbox — ask() gives durable, sequential request/reply
StatePort<S>read-state toolnode StateFlow / ObjectStore
StreamPort<E>subscribe toolFlow/SharedFlow sources
ValuePort<T>read-value toolplain provided value

The one genuinely missing bridge is schema generation: derive a JSON-Schema tool definition from a port's KSerializer descriptor (replacing the hand-built McpToolSchema in reaktor-cloudflare/.../Mcp.kt so schemas are derived, not duplicated). The Workers MCP server already proves the target shape — it handles tools/list / tools/call over JSON-RPC today; it just has no caller and no auth. Reflecting service.handlers into tools is the exact same walk Router.kt already does to build Hono routes.

Change — the unified mutation spine

The corpus currently speaks three mutation vocabularies. Reconciling them is the most important design decision here, and they form a clean layering — not competing options:

  agent intent
      │
      ▼
  WorkbenchCommand        agent-facing, Koog STRUCTURED OUTPUT
  {AddNode, ConnectPorts,  "plan in commands, never raw diffs"
   UpdateSchema, …}        (reaktor-koog-workbench)
      │  compiles to
      ▼
  GraphPatch              the CANONICAL mutation value
  {ConnectPort, AttachNode, (reaktor_graph_visitors_design)
   SetAttribute, Composite}
      │  validated by
      ▼
  Graph.apply(patch,       first-class dryRun → GraphPatchResult
   dryRun=true)            (applied, rejected: List<Finding>)
      │  commits through
      ▼
  Wiring.commit()         validate-all-then-apply, ATOMIC + rollback
                          (graph-refinement-plan L0.2)
      │  optionally proven by
      ▼
  Shadow replay           production-shaped dry-run vs captured traffic
                          (shadow-dev-environment)
      │  staged + reviewed in
      ▼
  CommandQueueActor       Critic reviews; apply by a graph:apply holder
   + Critic loop          (reaktor-koog-workbench)

So: an agent emits a WorkbenchCommand as structured output; it compiles to a GraphPatch (the canonical mutation value the kernel's Graph.execute takes); Graph.apply(patch, dryRun=true) validates it and returns Findings for anything rejected; real application commits atomically through the refinement plan's transactional Wiring (validate-all-then-apply, with rollback); and for risky changes the patch is replayed against captured production traffic in the Shadow Dev Environment before it touches the live app. The whole thing is staged in the existing CommandQueueActor with per-command revert/repair and an activity log that already tags source "agent" vs "devtools".

Note the kernel-level umbrella: Graph.execute(GraphCommand) is the generic API; WorkbenchCommand is its agent-facing projection and GraphPatch is the mutation value it carries. One spine, three names for three layers — never three competing systems. (Multi-agent conflict — Codex and Gemini patching the same port — surfaces as a patch.conflict finding; resolving it is an open question the spine must own.)

Across a process boundary, this spine is carried by the Edit domain of Reaktor Graph Protocol — same GraphPatch, same prepare/execute split, same Critic gate, now reachable from any attached client rather than only in-process. That page also fixes the rule this spine leaves implicit: every patch must declare whether it targets the document, the source or a live runtime.

Identity & permissions

The auth kernel is production-grade and already agent-aware. reaktor-auth/.../kernel/AuthKernel.kt defines PrincipalKind.AGENT as a first-class principal, an AuthContext with scopes/roles/permissions + delegation (on-behalf-of), and an AuthRequirement DSL evaluated by the pure, multiplatform LocalAuthorizer.authorize(). Critically, AuthRequirement.principalKinds defaults to {USER, SERVICE} — so an agent is excluded from a tool unless explicitly granted. The token machinery already mints to audience "manna-mcp" with PersonalAccessToken — MCP is already a first-class audience.

Build each agent's toolset from its scopes — capability security by construction (a missing scope means the tool is never registered, not merely denied):

graph:read            snapshot, query passes, state, findings, traces
graph:propose         emit WorkbenchCommands into the queue (never direct mutate)
graph:apply           apply a reviewed, validated patch        (Codex / human)
tests:run             run Maestro / Playwright / Keploy / shadow replay
cloud:read            read cloud inventory + resource health
db:read               tenant-scoped, read-only query           (no write clauses)
deploy:*              human-only — never granted to an agent
auth:admin            never agent-accessible — RBAC is off-limits

Observed & budgeted — the surface watches itself

The control plane both emits into and consumes the performance & observability pipeline. Graph attribution is sacred: every metric, span, and log carries node.id.

  • Every operator action is a span. A run reads as one timeline — "Codex called inspectNode → query → emitted 3 commands → Critic flagged 1" — through GraphTelemetry.instrument() + the Koog OTel integration, into Grafana LGTM. Agents are observable like everything else.
  • Performance is a tool family. Expose profile.flamegraph(node, window), db.slow_queries(window), build.compare(a,b) (the perf doc's MCP tools) backed by ReaktorPerformanceCollector / ReaktorPerformanceReport (already fully serializable). The PerformanceOverlayVisitor lets an agent read hotspots per node.
  • Budgets gate proposals. ReaktorPerformanceReport.budgetViolations() / requireWithinBudgets() (the same call CI uses) can fail an agent-proposed bundle on an Error-severity regression before it applies.
  • Boundaries inherited: the telemetry cardinality contract (node.id is trace/log-only, never a metric label) and shadow redaction apply to any state or signal an agent reads. Privacy is fail-closed, not advisory.

Connection models & trust profiles

Two transports

  • MCP for external agents (Claude Code, Codex, Cursor, IntelliJ). The Workers MCP server (reaktor-cloudflare/.../Mcp.kt) is the edge transport; the gap is a JVM MCP server in the stub reaktor-mcp module (the official SDKs are already declared). MCP both ways: server for agents, client of external tools (git, filesystem, Playwright). Both halves now exist as the door: it lives in reaktor-tooling, is served by the conductor's workspace mcp, and is the one server agents connect to and the client of every external server they use through it.
  • In-process Koog on the JVM — no serialization tax, full graph context — behind the workbench's AgentInteractor and on reaktorServer.

Two trust profiles, one surface

  • Desktop copilot (Koog in reaktorDesktop): broad scopes — full graph read, propose/apply (Codex), runTests, filesystem/gradle/IDE tools. A trusted operator context.
  • In-app bot (Koog for the Bot): sandboxed — narrow read tools (events/friends/Memgraph), per-conversation BotConversationActor with checkpointed memory, an edge isSafe() pre-filter, and moderation of untrusted user content. Never filesystem or deploy.

Same registry, reader, spine, and gate — the profiles differ only in their scope template and which tools that admits.

A toolbox that grows with Reaktor

Each module registers tools into the same registry, wired to its real interface. The contract is stable; the toolbox expands.

CapabilityReal interfaceToolsState
graph + visitorsGraphDocumentPass, selectors, Findingsnapshot, query, diagnosticsdesigned
services / portsService.handlers → 5 port shapescall request/state/stream/mailboxneeds schema-gen
telemetryGraphTelemetry.instrument()read spans, nav, port eventsready
performanceReaktorPerformanceReport, budgetsflamegraph, slow-queries, budget checkready
design system (reaktor-ui)DesignTokens, ComponentSpec, TokenFactorycomponent inventory, token inspect, generative themeneeds catalog
cloudCloudInventory/CloudProvider/CloudResource"is prod healthy?", open console, read stacksdesign
testingshadow replay/load, Maestro/Keploy runnersrun suites, replay capsule, regressionharness exists
databaseWorkbenchDataApi.query/schema + tenant policyschema, read-only tenant-scoped querypane mocked
authAuthNode, RoleBasedAuthlist providers/sessions/roles (read)read-only
distributed actorsClusterActorRef, ShardCoordinatoraddress + read actors cluster-widedesign

UI/design note: the design-system surface lives in reaktor-ui (real DesignTokens token system, platform-agnostic ComponentSpec catalog, TokenFactory that derives a full scheme from 2–3 colors) — reaktor-tactile is an empty haptics placeholder, not a design system. A ui.generate.theme(...) tool is the safest first generative tool; pair generated copy with Workers-AI isSafe().

Expose-existing vs genuinely net-new

The honest framing: most of this is wiring designed mechanisms to a transport. Only a short list is truly new.

Expose existing

  • GraphDocumentPass — the deterministic snapshot
  • GraphPatch + Graph.apply(dryRun) + Wiring — the mutation + validation + atomic commit
  • ReactiveSelector / IncrementalVisitor — the live feed
  • ObjectStore / StatePort — node state reads
  • CommandQueueActor + Critic — the review queue
  • AuthRequirement / LocalAuthorizer — authz logic
  • Workers Mcp.kt — edge transport; perf collector/budgets; shadow capture/replay; CloudInventory

Genuinely net-new

  • KSerializer → JSON-Schema bridge (tool schemas, derived not duplicated)
  • JVM MCP server — implemented for desktop loopback reads; remote authenticated transport remains open
  • Agent credential minting — an AGENT AuthContext from a PAT (audience reaktor-mcp)
  • Call-time authorization — per-call authorize() (no doc designs this)
  • Unified change-feed reconciling PortEvent + lifecycle + DatabaseEvent
  • InspectionCapability — uniform state façade; ConsumerPort.tryInvoke
  • ComponentCatalog — a runtime index of ComponentSpecs

Build phases (additive — nothing is deleted)

P0 · Read-only knowledge graph over MCP

New reaktor-agent module + a JVM MCP server exposing graph:read tools backed by GraphDocumentPass + query selectors + recent telemetry spans + findings. An agent connects and understands any Reaktor app. Highest value, lowest risk.

Status, 30 September: reached without a new module. The kernel's graph_describe and graph_query and the desktop's read tools reach agents through the door.

P1 · Ports as tools

KSerializer → JSON-Schema generator; auto-register every RequestPort/Service.handler (and read-only StatePort/StreamPort) as a tool. Add tryInvoke. Agents can now call the app (reads first).

P2 · The mutation spine

Wire WorkbenchCommand → GraphPatch → Graph.apply(dryRun) → Wiring.commit through the CommandQueueActor + Critic. Add shadow replay as the behavioral dry-run. Express every change as a patch.

P3 · Subsystem tool families

Replace pane mocks with real adapters — WorkbenchDataApi query, auth read, testing/shadow runners, CloudInventory, telemetry/insights — each a permission-gated toolset.

P4 · In-process Koog

Wire Koog behind AgentInteractor (desktop copilot) and on reaktorServer (bot), building tools from the same registry + scope template. Multi-agent Critic loop; resolve patch.conflict.

P5 · Identity, call-time authz, live state

Agent credential minting + scope templates + per-call authorization; the unified change-feed + InspectionCapability state snapshots; full audit + budget gating.

P6 · Distributed (read-mostly)

Edge MCP (Workers) + JVM surface over ClusterActorRef (see Distributed Actors). Agents read across the cluster; writes stay queue-gated and respect single-writer (leases + fencing tokens) — an agent never bypasses an actor's owner mid-handoff.

Safety & verification

  • Propose, don't poke — every change is a validated GraphPatch through the queue; the Critic gates by withholding approval; graph:apply applies; rollback is the patch inverse.
  • Two dry-runs — Graph.apply(dryRun=true) (structural, returns Findings) and shadow replay (behavioral, vs captured traffic) before any production bundle.
  • Least privilege by construction — a missing scope means the tool isn't in the registry; deploy:* and auth:admin are never agent-accessible.
  • Determinism — deterministic snapshots/selectors so diffs are meaningful and AI context is repeatable; deterministic shadow replay seeded from the capture id.
  • Audited & budgeted — every op is an OTel span + activity-log record tagged agent; budgets can fail a regressing proposal.
  • Privacy fail-closed — telemetry cardinality contract + shadow redaction bound everything an agent can read.
  • Unified Graph - Use before building Reaktor Engine graph importers, cloud/resource nodes, operation providers, or MCP graph tools.
  • Koog Workbench - Use before wiring real agent behavior into reaktorDesktop.
  • Browser-Local AI - Use before adding WebLLM, Chrome built-in AI, or browser-local model providers to Reaktor Web, docs, or workbench surfaces.
  • Security Plan - Use before enabling deploy actions, remote agents, or privileged commands.
  • Gemini DevOps Agent - Use before handing DevOps execution from Codex or Claude to Antigravity CLI, adding agent-run recipes, or wiring build/test/deploy operations into reaktorDesktop.
  • Visual Language - Use when designing the node-based authoring/orchestration language and its editor, compiler, and agentic surface.

References

This document is the connective tissue; each brief below owns a mechanism it builds on:

  1. Graph Kernel Analysis — the primitive vocabulary (Snapshot/Event/Command/Visitor), the 10 laws, and the AI command loop (shape #8).
  2. Graph Visitors — GraphDocumentPass, selectors/passes/Finding, GraphPatch + Graph.apply(dryRun), ReactiveSelector/IncrementalVisitor.
  3. Graph Refinement Plan — the 5 port shapes, transactional Wiring, modules + ModuleContractPass.
  4. Koog in reaktorDesktop — WorkbenchCommand structured output, the Critic loop, permission-as-toolset, MCP both ways.
  5. Koog for the Bot — the sandboxed trust profile; bot as a per-conversation actor with checkpointed memory.
  6. Actor Model & Distributed Actors — ObjectStore state, ask, and ClusterActorRef + single-writer/leases for cluster reach.
  7. Performance & Observability & Shadow Dev Environment — agent spans, perf tools + budget gating, behavioral dry-run.
  8. Cloud Pane — CloudInventory as a JVM-direct tool family.

Key code anchors: reaktor-graph/.../core/{Graph,GraphJson}.kt + node/{ActorNode,ControllerNode}.kt, reaktor-graph-port/.../{port,edge,visitor}/*, reaktor-graph/.../service/{Service,RequestHandler,ServiceNode}.kt, reaktor-telemetry/.../GraphTelemetry.kt, reaktor-performance/.../ReaktorPerformance*.kt, reaktor-auth/.../kernel/AuthKernel.kt + graph/SecuredPort.kt, reaktor-db/.../{ObjectDatabase,ObjectStore,ObjectState}.kt + graph/GraphDb.kt, reaktor-ui/.../core/{tokens,components}/*, reaktor-cloudflare/.../Mcp.kt, the stub reaktor-mcp/, and the workbench command plane under modules/engine/.../workbench/data/.