Skip to content
docs-bundle okf-loom wiki
GraphIndex

Live studio unavailable. Showing a read-only view. Browse the index.

Reference 10 min read

HTTP server routes

Every route the live studio exposes — method, purpose, body shape, response shape, and status codes.

/reference/http_routes.md httpstudioreference

HTTP server routes

scripts/okf-loom serve <bundle> runs a ThreadingHTTPServer with the OKFWikiHandler request handler. The handler serves read routes unconditionally and — when a studio session is live (.okf-loom/session/ exists) — accepts mutating POST routes behind a per-session CSRF token and an Origin/Host allow-list. See index.md for the rest of the reference quadrant.

Default bind is 127.0.0.1:8787. A non-loopback bind (--public or a non-loopback --host) requires --public-ack or an interactive y/N ack. See cli.md § serve.

Route summary

MethodRoutePurpose
GET/Root index page (top-level concepts + subdirs + graph link).
GET/<concept_id>Concept page (rendered MD, frontmatter, backlinks, outgoing, local graph).
GET/<dir>/Directory index page.
GET/__graphFull-page Cytoscape.js force-directed graph.
GET/__searchSearch results (&format=json for SPA fetch).
GET/__raw/<concept_id>Raw markdown body.
GET/__data/graph.jsonGraph JSON for external tools.
GET/__data/content.jsonFull content index JSON.
GET/__data/doc?id=One concept's {rev, html, raw, frontmatter, backlinks, outgoing, headings} for in-place re-render.
GET/__data/eventsFiltered change-list read over events.jsonl.
GET/__eventsSSE stream of change/presence/activity/comment events.
GET/__commentsCanonical comment state from directives.jsonl.
GET/__diffLine diff between two revs of one concept.
GET/__static/<file>Static assets (JS/CSS; bundle overrides first when active-code is on).
POST/__commentUser's comment / threaded reply → directives.jsonl.
POST/__comment-updateUser-facing state/archive transition.
POST/__presenceAgent presence (idle/watching/thinking/editing).
POST/__applyRun a whitelisted UpdateOp via the studio write funnel.
POST/__undoRestore a prior snapshot (single or group).
POST/__previewRender arbitrary in-flight markdown.

Routing normalises a trailing slash (except root) and a trailing .md on concept routes (except /index.md and <dir>/index.md, which are reserved). So /tables/customers.md, /tables/customers, and /tables/customers/ all resolve to the same concept page.

Authentication model

SurfaceGuard
Every GETNone (loopback bind by default).
GET /__diffPer-session CSRF token (X-OKF-Token).
Every POSTPer-session CSRF token AND Origin/Host ∈ studio.allowed_hosts.

The token is generated on scripts/okf-loom serve boot, written to <bundle>/.okf-loom/session/.token (mode 0600), embedded in every served HTML page (CSP-safe JSON data block), and read by the studio client which attaches it as X-OKF-Token on every write. scripts/okf-loom token <bundle> prints it for direct HTTP use; the CLI mutators attach it automatically.

allowed_hosts defaults to ("127.0.0.1", "localhost") and is never empty (fail-closed). A user actor's presence write only takes effect when the current agent presence is already idle or watching, so a browser boot cannot clobber an active agent.

--no-edit flips the studio into a read-only kiosk: live reads stay on, every POST returns 403.

GET routes

/

Root index page. Renders the bundle-root index.md (hand-authored or generated) plus the type-grouped concept listing. The studio bootstrap (token + asset links) is injected into the </head>.

/<concept_id>

Concept page. concept_id is the dotted path without .md (e.g. /tables/orders). Returns 404 for an unknown concept unless the path matches a directory containing concepts, in which case it falls back to the directory index page.

The page carries: rendered body HTML, frontmatter, the "Cited by" backlinks panel, outgoing links, and a local-graph widget.

/<dir>/

Directory index page for the subdirectory <dir>. Renders the <dir>/index.md if present, else a generated listing. Path segments are validated; .., ., and NUL bytes return 404.

/__graph

Full-page Cytoscape.js force-directed graph of the whole bundle. The graph data comes from /__data/graph.json.

GET /__search?q=customer&format=json
Query paramEffect
qThe query string. Capped at MAX_SEARCH_QUERY_CHARS; over-long queries are truncated, not rejected.
formathtml (default) or json.

The HTTP path always uses the lexical backend with a limit of 30. For the other five modes use the CLI. JSON shape matches the SearchResult shape in search_modes.md § Result shape.

/__raw/<concept_id>

Raw markdown body of one concept (Content-Type: text/markdown). Used by popover previews and agent copy/paste. Returns 413 if the body exceeds MAX_RAW_RESPONSE_BYTES.

/__data/graph.json

Graph JSON: nodes (one per concept) + edges (one per internal link, including wikilinks with form="wikilink"). Same shape as graph --format json.

/__data/content.json

Full content index JSON: every concept's id, type, title, description, tags, and outgoing link targets. Same shape as the .okf-loom/index/content.json derived artifact emitted by index --emit-json.

/__data/doc?id=<concept_id>

One concept's rendered + raw + metadata JSON for in-place re-render (the §6 no-refresh live update path).

{
  "id": "tables/orders",
  "rev": "<content-hash>",
  "title": "Orders",
  "type": "Table",
  "html": "<rendered body HTML>",
  "raw": "<raw markdown body>",
  "frontmatter": { "type": "Table", "title": "Orders", "..." : "..." },
  "headings": [{"level": 1, "text": "Schema"}, "..."],
  "backlinks": ["services/checkout"],
  "outgoing": ["tables/customers", "tables/products"]
}

Returns 400 if id is absent, 404 if the id is malformed or unknown.

/__data/events

GET /__data/events?since=<id>&limit=200&actor=agent&concept=tables/orders&order=desc

Filtered read of events.jsonl for the change list. Defaults to newest-first; ?order=asc opts into oldest-first (used by scripts/okf-loom watch --since replay).

{ "events": [ /* §7.2 schema rows */ ], "rev": "<current bundle rev>" }

/__events

SSE stream (text/event-stream). One event per change/presence/activity/comment event. 15-second heartbeat (type=ping). Capped at studio.max_sse_clients concurrent streams (default 32); returns 503 when full or when the studio is absent / --no-watch-ui is set.

Frame typeCarries
readyInitial frame: rev + per-doc rev map (doc_revs).
changed / created / removedA concept mutation (debounced).
graphA graph-affecting mutation (re-fetch the graph).
presenceAgent presence state change.
commentA comment directive was appended or transitioned.
comment_linkA resolve back-stamped an activity event.
pingHeartbeat.
resyncServer instructs client to refetch (rev + per-doc rev map).

X-Accel-Buffering: no is sent so buffering proxies do not stall the stream. A HEAD request to /__events returns an empty body immediately (it must not enter the SSE loop).

/__comments

GET /__comments?state=open&concept=tables/orders

Canonical comment state from directives.jsonl (last-write-wins per id). The studio client consumes this directly rather than reconstructing from the event feed. Each row carries archived, summary, ts, and updated_at. See comment_lifecycle.md.

{ "comments": [ /* see comment_lifecycle.md § JSON shape */ ] }

/__diff

GET /__diff?concept=tables/orders&from=<rev>&to=<rev>

Line diff between two revs of one concept. Requires the CSRF token (because it exposes prior snapshot bytes). Returns 400 if any of concept, from, to is missing; 404 if either snapshot is missing.

{
  "ok": true,
  "concept": "tables/orders",
  "from": "<rev>",
  "to": "<rev>",
  "diff": [
    {"kind": "ctx", "num": 12, "text": "..."},
    {"kind": "del", "num": 13, "text": "..."},
    {"kind": "add", "num": 13, "text": "..."}
  ]
}

/__static/<file>

Static assets (JS modules, CSS). Bundle overrides under .okf-loom/viewer/static/<file> are served first only when the active-code gate is open (bundle declares viewer.allow_active_code: true AND operator consent via --allow-active-code / OKF_LOOM_ALLOW_ACTIVE_CODE). Path traversal is rejected (.. and separators after normalisation; NUL bytes).

POST routes

All POST routes require the CSRF token + Origin/Host allow-list. All bodies are JSON. A body larger than MAX_EVENT_BODY_BYTES returns 413 and closes the connection (the oversized body is not drained). Malformed JSON returns 400. Every mutating endpoint returns 503 when no studio session is attached.

/__comment

Append a user comment or threaded reply.

{
  "concept": "tables/orders",
  "body": "Add a depends_on relation to customers.",
  "anchor": {"heading": "Schema", "snippet": "..."},
  "detail": {"intent": "add_relation"},
  "actor": "user",
  "parent_id": "01JABED9K0"
}
FieldRequiredNotes
conceptyesConcept id the comment is anchored to.
bodyyesThe comment text.
anchornoHeading + snippet for inline placement.
detailnoFree-form metadata (e.g. Quick Action intent).
actornoDefaults to user.
parent_idnoId of the parent comment for a threaded reply. Replying to an archived thread auto-unarchives the parent.

Response 201 Created:

{ "ok": true, "comment": { /* see comment_lifecycle.md § JSON shape */ } }

An Idempotency-Key header deduplicates a re-POST within a 10-minute window; the body fingerprint is part of the cache key.

/__comment-update

User-facing state or archive transition. Archive is a separate track from lifecycle state.

// Dismiss (or reopen) — state track only.
{ "id": "01JABED9K0", "state": "dismissed" }

// Archive — archive track only; state is preserved.
{ "id": "01JABED9K0", "archived": true }

Server-side rules:

RuleFailure
state == "dismissed" on a claimed comment409 "cannot dismiss a claimed comment".
archived: true on a reply (not a root)400 "archive is only available on the top-level comment of a thread".
archived: true when any comment in the thread is not resolved409 with unresolved_ids: [...].

Response 200 OK:

{ "ok": true, "comment": { /* updated record */ } }

The agent does NOT use this endpoint for resolve; it uses scripts/okf-loom comment-resolve. See comment_lifecycle.md § Server-side archive rules.

/__presence

{ "actor": "agent", "state": "editing", "focus": "tables/orders" }
FieldAllowed
stateidle / watching / thinking / editing.
focusConcept id (optional).
actorDefaults to agent.

A user actor's write only takes effect when the current agent presence is already idle or watching (so a browser boot cannot clobber an active agent). Response 200 OK carries presence (plus preserved: true when an agent state was preserved).

/__apply

Run a whitelisted UpdateOp via the studio write funnel. Never accepts a shell/argv string — only a known op kind + args.

{
  "kind": "add_link",
  "target": "tables/orders",
  "args": {
    "target_concept_id": "tables/customers",
    "label": "Customers"
  },
  "group_id": "PASS1",
  "expected_rev": "<content-hash>",
  "actor": "agent"
}
FieldNotes
kindA whitelisted UpdateOp kind (see cli.md § update).
targetThe concept id to mutate.
argsPer-kind args (validated fail-closed).
group_idOptional; groups this write with others for one-click group undo. Must be a safe path token.
expected_revOptional content-hash; mismatch returns 409 (§9.3 collision guard).
actorDefaults to agent.

Response 200 OK:

{ "ok": true, "applied": true, "result": { /* apply_plan result */ } }

Conflict responses (§9.3 / §9.4): 409 with {ok:false, conflict:true, concept, expected_rev, current_rev}.

/__undo

Restore a prior snapshot. Single or group.

// Single
{ "concept": "tables/orders", "rev": "<content-hash>" }

// Group
{ "group_id": "PASS1" }
FieldNotes
concept + revRestore one concept to a prior rev. Both must be safe path tokens.
group_idRestore every member of an undo group.

Response 200 OK:

{ "ok": true, "restored": 3 }

Special cases:

CaseResponse
Every target already at its snapshot rev (replay)409 {ok:false, conflict:true, error:"already_undone", targets, current_state}.
Every snapshot pruned by the history ring cap404 {ok:false, error:"snapshots_pruned", missing}.
Some targets pruned, some restored409 {ok:true, restored:N, partial:true, missing}.

/__preview

Render arbitrary in-flight markdown (DoS-capped).

{ "markdown": "# Hello\n\n**bold** text" }
{ "ok": true, "html": "<h1>Hello</h1><p><strong>bold</strong> text</p>" }

Response shapes (common conventions)

StatusBody
200 / 201JSON {ok: true, ...} or HTML/text.
400JSON {ok:false, error:"…"} with a stable reason prefix.
403Plain text. Cross-origin guard failure or read-only kiosk.
404Plain text Not found / JSON {ok:false, error:"…"}.
409JSON {ok:false, ...} (conflict, dismiss-on-claimed, unresolved-thread archive, etc.).
413Plain text. Oversized body; connection closed.
500Plain text Internal Server Error. Full traceback logged to stderr only (no info disclosure).
503Plain text. Studio absent, SSE full, or --no-watch-ui.

Embedding the server

from http.server import ThreadingHTTPServer
from okf_loom import Bundle
from okf_loom.server import OKFWikiHandler

bundle = Bundle.load("path/to/bundle")
httpd = ThreadingHTTPServer(("127.0.0.1", 8787), OKFWikiHandler)
httpd.bundle = bundle         # attribute the handler reads
# httpd.studio = Studio(...)  # attach for the live studio (optional)
httpd.serve_forever()

Run it in a daemon thread inside another agent process; the bundle can be swapped at runtime (httpd.bundle = Bundle.load(...)). See /how-to/embed_in_harness.md.

See also

All fields
timestamp2026-06-29T00:00:00Z