The Topology Editor
Every other explanation page on this site covers a fixed shape: one tunnel, one channel, one
pipeline. The Topology Editor (/me/topologies/*) is different — it lets you compose your own
agents (yours, or ones shared to you) into a named graph, view it as a draggable node-graph, and get
algorithmic suggestions for how to wire it. Source-grounded throughout (crates/control-plane/src/
topology.rs, service.rs’s authed_topology_router), and re-run hermetically for this page — 33/33
topology tests passing right now (cargo test -p ct-control-plane --lib topology).
Creating one, and what a “node” actually is
POST /me/topologies (OIDC bearer, same /me/* gate as everywhere else on this site) creates a
topology owned by your subject, returning a generated id and a separate net_uuid — the id you
address it by, the uuid that keys its public status page (below). Under the hood, a topology node’s
id is literally your agent’s 32-byte holder key, the exact same identity Agent-Fabric channels
already use — there’s no separate node-registration step or id mapping to keep in sync.
POST /me/topologies/:id/agents {"agent": "<holder-key-hex>"}— assign one of your agents into the topology.POST/DELETE /me/topologies/:id/edges {"a": "...", "b": "..."}— add or remove an undirected edge between two node ids. Removing someone else’s edge (or one that doesn’t exist) is a404, deliberately indistinguishable from “not your topology” — a non-owner learns nothing about the graph’s shape either way.GET /me/topologies/:id— the graph as JSON:agents,edges,overlay_mode.GET /me/topologies/:id/editor— the same graph as a self-contained, draggable SVG node-graph page, click-to-connect for composing edges visually instead of raw JSON calls.
Exclusive membership, not shared: an agent belongs to at most one topology at a time. Assigning an
already-assigned agent is a 409; the only way back is a revoke, which returns the agent to its
original owner — not free-for-all claimable by whichever topology reaches for it next.
A public, read-only status page
GET /net/:net_uuid (no auth) renders the topology’s current agents and edges — confirmed live:
querying an unknown uuid returns a real 404, not a silent empty page. This is the one part of the
feature unaffected by the ongoing /me/* outage note elsewhere on this site, since it was designed to
be public from the start (an easy link to share a network’s current shape, the same instinct behind
this platform’s other public read surfaces like GET /registry/pipelines).
Overlay modes and “suggest” — a planner, not a live router
Each topology has an overlay mode: baseline (direct — every declared pair relays straight to
each other) or one of three complex-adaptive modes (smart-route, shortcut, random-mesh),
settable via PUT /me/topologies/:id/mode.
RoutingApproach (the overlay-mode
type) is used by exactly three places in the whole workspace — the control plane's HTTP handler, its
storage layer, and the type definition itself. It is never read by ct-edge,
ct-agent, or ct-client. Choosing a mode, and the
suggest endpoint below, do not currently change how your agents actually connect — this
is a network planning tool, not a live traffic-routing control, however natural it would be
to assume otherwise from the UI alone.
POST /me/topologies/:id/suggest {"links": [{"a","b","cost"}], "shortcut_budget": <n>} is a real,
non-trivial algorithm, not a stub: it computes the minimum-latency spanning tree over your
caller-supplied candidate link costs, and in shortcut mode adds capped extra edges on top
(shortcut_budget, hard-capped at 16 server-side against O(budget·n³) blowup). baseline mode
returns 409 — direct-only has nothing to optimize. What you get back is a suggested plan
(links, total cost, whether it’s fully connected) for you to act on yourself — see the next section
for what “acting on it” currently means.
The genuinely real part: an edge can authorize a live channel — almost
This is the one place the graph does reach into live behavior, and it’s worth being precise about exactly how far that reaches today.
Per authorized_channels/topology_authorizes in storage.rs (both covered by passing tests,
re-confirmed for this page): the channel-admission gate consults declared topology edges
additively, alongside the existing channel-members mechanism documented in
Agent-Fabric channels. Concretely — a
declared edge (a, b) in a topology authorizes the corresponding channel’s admission for both a
and b, derived the same way channel_id_for_link
already computes it elsewhere on this site. Remove the edge and the authorization is gone too — “no
per-channel bookkeeping,” straight from the source comment.
PUT /me/topologies/:id/operator
{"operator_pubkey", "proof"} is a real, owner-scoped route (checked directly in
service.rs's authed_topology_router, handler topology_set_operator):
proof is a signature over topology_operator_binding_bytes, proving you actually
control the operator key's private half, not just its public bytes — a bad proof or a non-owner
topology both come back as the same 404, so probing a topology id learns nothing either
way. Once bound, drawn edges genuinely do authorize real channel admission through
authorized_channels/topology_authorizes as described above — this used to be
the honest caveat on this page ("no route to bind an operator"); it no longer applies.
A drawn edge alone isn’t enough — the holder still needs a registered Noise key
CADS-Tunnel#697, decided and shipped
2026-09-06: a topology-authorized holder with no registered, attested Noise key for the derived
channel is now refused at admission (a plain 404, identical to a non-member) rather than
silently let through into a session that could never actually work. Before this, the edge would
pair such a holder, count a rendezvous success, and then both sides would hard-fail on “no peer
Noise key” and re-park in an endless loop — a metric that lied and a slot the honest peer’s own
join attempts kept losing.
What this means in practice: drawing an edge in the editor and binding the operator key
(above) authorizes the channel, but each holder on that edge still needs its own Noise key
registered the normal way — POST /me/channels/:channel/members with the holder’s attested
key, the same route Set up an Agent-Fabric channel
already documents for the direct-address path. A topology edge is declared intent; the
key-registration step is what actually makes it live.
How you’d notice this happening: the control plane logs ct-cp: channel-authorize NO
[topology-unkeyed] channel=... holder=... naming the exact channel and holder, and GET /status
exposes a running total as channel_authorize_refused_topology_unkeyed — non-zero means a drawn
edge is live whose endpoint never completed the key-registration step.
Composing with others: super-peers, sharing, and channel link-info
Three additive capabilities on top of the base graph above — none of them change the exclusive-membership or edge-authorization rules already described, they extend what a node or an edge can carry.
Super-peer nodes. POST /me/topologies/:id/agents {"agent": "...", "kind": "super-peer"} (or
"peer", the default) marks a node’s role — rendered in the editor with a distinct border and an “SP”
badge. This is purely a rendering/informational hint: the graph’s actual admission semantics are
unchanged by it, a super-peer node is still just an agent id in the edge graph. The real, running process
that hint describes is ct-agent channel super-peer
— mark the node here, then bring up the real relay separately.
Sharing a topology by e-mail. A topology is, by default, visible and editable only by its owning
subject. POST /me/topologies/:id/share {"email": "..."} (owner-only) additively grants another Keycloak
account — matched by their own verified sign-in e-mail, same convention as
channel allow-listing — the ability to
view the topology and wire in their own agents/edges via GET /me/topologies/shared and the
editor itself, but never owner-only governance (delete, operator-bind, or manage the share list). Remove
with POST /me/topologies/:id/share/:email/remove. The editor’s own share panel (visible only to the
owner) lists current collaborators and offers add/remove inline.
Explicit edge -> channel association. PUT /me/topologies/:id/edges/channel {"a", "b", "channel": "<hex, or omit to clear>"}
lets you attach a real, already-registered channel id to a specific edge as link info the editor
displays — validated as a channel you own or are allow-listed on (channels_for_email, the same account
relationship Set up an Agent-Fabric channel’s self-service claim
uses), not channel membership itself. This is purely informational/documentation on the edge — it is
never consulted by authorized_channels/topology_authorizes, which still only ever derive the
authorized channel from the edge’s two node ids as described above. Use it to record “this edge is
carrying that pre-existing channel” for anyone reading the graph, including the
tunnel-plus-channel case where the channel behind
an edge is also serving something over a Browser-Plane tunnel.
Should you use this today?
For visualizing and planning which of your agents should talk to whom, composing with a collaborator, and (once operator-bound) actually authorizing the channels behind your declared edges — yes, all of the above is real and working. Task-oriented walkthrough of the whole flow above, end to end: Compose a topology; every endpoint’s exact request/response shape: Topology API. For the mechanics of bringing up the channels/tunnels/super-peers a topology describes, see Set up an Agent-Fabric channel, Run a super-peer, and Serve a tunnel and a channel together.
A sibling feature, Declarative network policy
(/me/networks/*), is in a similar position for a different reason: it’s a role/sensitivity-based
access-control language rather than a graph you draw, and (unlike the topology-edge path above) still
has no live enforcement wired to it as of this writing.