CADS-Tunnel docs
Reference

Topology API

The REST surface behind the Topology Editorauthed_topology_router and the public topology_status_router, both in crates/control-plane/src/service.rs. Task-oriented walkthrough: Compose a topology. Concepts, overlay modes, and the exclusive-membership model: The Topology Editor.

Checked directly against source and the 35/35 passing cargo test -p ct-control-plane --lib topology suite (re-run for this page). Two rows below (marked) were also confirmed live against https://bunsenbrenner.org; the rest were not click-tested end to end this pass — no portal login was available while writing this page. Flagged here rather than presented as verified live, matching this site’s own honest-gap convention.

Auth

Every /me/topologies* route accepts either an OIDC bearer token (an Authorization: Bearer header — see Getting a bearer token without a browser) or a real portal session cookie (subject_of_topology in service.rs) — the second exists specifically so the editor’s own client-side fetch() calls can authenticate via the ambient portal session it already has, with no bearer token to hold. Accepting either is not a scope-widening: both resolve to the same verified subject. GET /net/:net_uuid is the one public, unauthenticated route on this page.

Owner isolation is consistent throughout: a topology id that isn’t yours (or isn’t shared with you, where sharing applies) comes back as a plain 404, never a 403 — so probing an id you don’t have access to learns nothing about whether it exists.

Create, list, view

POST /me/topologies — no body. Creates a topology owned by the caller’s subject with a server-generated id and a separate net_uuid.

{"id": "3f9a1c...", "net_uuid": "8b02de..."}

id addresses the topology in every write below; net_uuid only ever keys the public status page — kept separate so sharing a read-only status link never leaks the id needed to edit the graph.

GET /me/topologies — the caller’s own topologies: [{"id", "net_uuid"}].

GET /me/topologies/shared — topologies shared with the caller’s verified session e-mail (topology_shared_list) — never a caller-supplied email, so there’s no way to enumerate another account’s shares. Empty (not an error) when the session has no verified e-mail, including a bearer-token caller — only a real portal login ever carries one.

GET /me/topologies/:id — a composite view. Owner or shared-with viewer only.

{
  "id": "3f9a1c...",
  "net_uuid": "8b02de...",
  "agents": [["<holder-key-hex>", "peer"], ["<holder-key-hex>", "super-peer"]],
  "edges": [{"a": "<holder-key-hex>", "b": "<holder-key-hex>", "channel": "<64 hex, optional>"}],
  "overlay_mode": "baseline",
  "owner": "<subject>"
}

overlay_mode is one of baseline, smart-route, shortcut, random-mesh (defaults to baseline for a topology that never set one). An edge’s channel field is present only if one was explicitly attached via PUT .../edges/channel below — omitted otherwise (EdgeView in service.rs), not null.

GET /me/topologies/:id/editor — the same graph rendered as a self-contained, draggable SVG node-graph page (text/html). Owner or shared-with viewer; a non-owner viewer never sees the share-management or operator-binding panels even though the HTML is otherwise the same document.

Agents (exclusive membership)

POST /me/topologies/:id/agents {"agent": "<holder-key-hex>", "kind": "peer"|"super-peer"} (kind optional, default "peer") — assign an agent you own into a topology you own or are shared into. An agent belongs to at most one topology at a time.

Result Status
assigned 200
agent already assigned to a topology 409
caller doesn’t own the agent 403
topology not owned/shared with caller 404

Revoking (returning the agent to its owner, unassigned) is a separate call the owner or the current topology can make — see crate::topology::AgentAssignment::revoke (crates/control-plane/src/topology.rs:118); not exposed as its own numbered REST verb on this router in the current source, reached instead through the storage layer’s assign/revoke pair the assign handler above calls into.

Edges

POST /me/topologies/:id/edges / DELETE /me/topologies/:id/edges {"a", "b"} — add or remove an undirected edge. Owner or shared-with editor. POST failure (self-loop, duplicate, or no edit access) is 409; DELETE failure (no such edge, or no edit access — deliberately indistinguishable) is 404.

PUT /me/topologies/:id/edges/channel {"a", "b", "channel": "<64 hex>"|null} — attach (or clear, with null/absent) an informational channel-id note on a specific edge. Validated as a channel the caller owns or is allow-listed on (channels_for_email); 400 if channel is present but not 64 hex. Never consulted by authorized_channels/topology_authorizes — purely a note for anyone reading the graph.

Overlay mode and suggestions

PUT /me/topologies/:id/mode {"mode": "baseline"|"smart-route"|"shortcut"|"random-mesh"} — owner-only. 400 on an unrecognized mode token.

POST /me/topologies/:id/suggest {"links": [{"a", "b", "cost"}], "shortcut_budget": <n>} — owner-only. Computes a minimum-latency spanning tree over the topology’s agents from caller-supplied candidate link costs; in shortcut mode, adds capped extra edges on top.

{"mode": "smart-route", "links": [["<a>", "<b>"]], "total_cost": 42, "connected": true}
Condition Status
topology is in baseline mode (nothing to optimize) 409
shortcut_budget exceeds MAX_SUGGEST_BUDGET (16) 400
topology has more than MAX_SUGGEST_AGENTS (64) agents 400

Both caps exist so the O(budget·n³) shortcut search can’t wedge the control plane (service.rs). The response is a plan to act on yourself (draw the suggested edges); computing a suggestion never writes edges on its own.

Operator binding

PUT /me/topologies/:id/operator {"operator_pubkey": "<64 hex>", "proof": "<128 hex>"} — owner-only. proof is operator_pubkey’s ed25519 signature over topology_operator_binding_bytes(topology_id, operator_pubkey) (crates/common/src/channel.rs:838) — proof-of-possession that the caller controls the operator key’s private half, not just its public bytes. 404 (not 403) on either a non-owner topology or a bad proof — deliberately indistinguishable, matching every other owner-isolation check on this page.

Once bound, every declared edge (a, b) additively authorizes channel admission for both a and b, via authorized_channels/topology_authorizes (crates/control-plane/src/storage.rs) — this is the fix for #698’s finding 1 (PR#700 + ct-agent#113); before this route existed, a topology’s edges were real and tested but reachable only from inside the control plane, never actually bindable from the outside. See Compose a topology for how to generate proof (ct-agent channel bind-topology).

Sharing

POST /me/topologies/:id/share {"email": "..."} — owner-only, idempotent. Grants another Keycloak account (matched by their own verified sign-in e-mail) the ability to view the topology and wire in their own agents/edges — never owner-only governance (delete, operator-bind, manage the share list).

POST /me/topologies/:id/share/:email/remove — owner-only, no body. 404 whether the topology isn’t the caller’s or the e-mail was never on the share list.

Public status page

GET /net/:net_uuid — no auth. Renders the topology’s current agents and edges as HTML.

Result Status
topology exists 200
unknown net_uuid 404

Confirmed live against https://bunsenbrenner.org, 2026-08-28: curl -s -o /dev/null -w '%{http_code}\n' https://bunsenbrenner.org/net/nonexistent-uuid-check404. GET /portal/topologies (the session-cookie-authed portal shell that links into this API) also confirmed live: 303 redirect when logged out.

UUID-only access for now — an owner auth-gate on this page is a tracked follow-up, not implemented in the current source. The eventual <net_uuid>.<zone> subdomain form reuses the Browser-Plane routing pipeline (service.rs).

Found an error, or something that didn't work as documented? Open an issue →