Interfaces & ops

MCP tools & deployment

The Open Brain is a single self-contained .NET 10 service that exposes the team's entire semantic code index to AI agents over the Model Context Protocol at https://brain.motlogs.com, backed by Qdrant. This page catalogs the MCP tools agents call — 46 declared (McpService.cs's FullToolList), of which 25 are advertised today — the response shapes they return, and the deployment, Qdrant, re-ingest, and enforcement machinery that keeps the service running.

Transport — streamable HTTP + stdio

The primary transport is streamable HTTP — POST /mcp, JSON-RPC 2.0, protocol version 2024-11-05 (McpService.cs:26), wired at Program.cs:1586; GET /mcp (Program.cs:1631) is a discovery/health card. JSON-RPC methods are dispatched (McpService.cs:197-203): initialize, ping, tools/list, tools/call, resources/list, resources/read; a null response (notifications) returns HTTP 204. Bearer auth (Program.cs:1586-1616) resolves a presented Authorization: Bearer <token> against (a) the shared Mcp:BearerToken → identity "shared", or (b) a registered per-developer token via TokenRegistry → real {developerId, team}; a registered token always wins over the shared token, a mismatch returns 401, the compare is constant-time (HttpAccessGate.ConstantTimeEquals, Services/HttpAccessGate.cs:132-139), and with no shared token configured the service is public but registered tokens are still honoured. Note that /mcp does not waive the bearer for a loopback/tailnet peer the way /api/* does (see the access gate below) — every caller, on-host or remote, presents the same header. stdio mode (Program.cs:40, --mcp) binds nothing in Kestrel, routes logging to stderr so stdout is a clean JSON-RPC channel, runs RunStdioLoopAsync() (McpService.cs:131), then exits.

Why it matters

One server satisfies both the shared remote brain (Claude Code, Antigravity, any MCP client) and a locally-piped agent, with per-developer identity flowing into telemetry and attribution either way.

Connecting — the recipe

Add the server as an http-type MCP entry (Claude Code: ~/.claude.json's mcpServers, or claude mcp add). Remote, off-host:

{
  "mot-ledger": {
    "type": "http",
    "url": "https://brain.motlogs.com/mcp",
    "headers": { "Authorization": "Bearer <your token>" }
  }
}

On-host — a session running directly on the Hetzner box, or tunnelled to it — talks to the service on its loopback port, skipping nginx and TLS entirely: http://localhost:5005/mcp (or 127.0.0.1), same JSON shape, same bearer header (the port and lack of TLS are the only difference — the bearer requirement above still applies even to loopback). GET /mcp without a body returns the discovery/health card (server name, transport, protocol version) and needs no auth, so it is the quickest way to confirm the service is up before wiring the full config.

MCP resources

Two read-only resources are read at session start (McpService.cs:1128-1173): ledger://manifest (what's indexed and freshness per repo) and ledger://global-arch (the GLOBAL_ARCH.md system spec).

Why it matters

An agent orients before its first query — it knows which repos are fresh and has the system-wide spec in hand, which prevents confident-but-wrong answers later.

The root argument — searching your own worktree

The main index mirrors main. query_ledger, read_span and symbol_lookup each accept an optional root: the absolute path of a registered checkout overlay (a worktree/workspace, e.g. /root/code/motmaster-openbrain/.claude/worktrees/<id>, or a ws-*/wt-* checkout). When set and registered, a file that differs between that checkout and the indexed mirror is served from the checkout instead — its uncommitted or unmerged edits become findable within minutes — and overlay hits are marked ⊕ from your checkout. symbol_lookup's symbol projection is still built from the main index only, so a registered root there gets a note that overlay symbols aren't in that projection yet and matches keep coming from the main index. find_definition / find_references / find_implementations take the same idea via root too, but there it names a known repo id or any registered worktree path, defaulting to Precise:DefaultRoot — not the overlay mechanism, the precise lane's own workspace resolution.

A root must be registered first: POST /api/admin/overlay/register {root} (loopback-only). An unregistered or unaccepted root, or the overlay being disabled, is ignored with a one-line hint rather than an error — a missing registration never turns a good query into a failure. A PostToolUse hook (motmaster-infra/claude-hooks/overlay-refresh.sh) fires this automatically after an Edit/Write/ MultiEdit inside an accepted checkout shape, debounced to one refresh per root per 30s — see Governance layer for the hook wiring.

Why it matters

Without root a session gets main's version of a file it has already changed and may plan against code that no longer exists — the overlay closes that gap for anyone working in a worktree.

What tools/list advertises — 25 of 46, today

The ledger declares 46 tools. Measured over 7,657 instrumented calls (the original pre-WP0.8 baseline), eight accounted for 98.9% of use — query_ledger alone was 84.8% — and find_similar had never been called once, in those 7,657 calls or in 69 local transcripts.

That surface is not free: every advertised schema is carried in the model's context for the whole session, and before its first call the model must fetch those schemas (see schema preload below). The tail was costing more than it earned, so tools/list trims to a CoreTools set (McpService.cs:264-279) — but that set itself grew on 2026-09-12 (WP0.8): eight reference/refactor-safety tools that were previously hidden were un-hidden after the change-safety suite found they were never being exercised while invisible (docs/LEDGER_SCALE_PLAN.md §10 WP0.8). Two independent gates decide the final advertised list (McpService.FilterTools, McpService.cs:312-323): Mcp:AdvertiseAllTools bypasses the CoreTools trim, but the three precise-lane tools are hidden whenever Precise:Enabled=false regardless of that flag — an advertised-but-non-functional tool is worse than an absent one.

GroupAdvertisedWhy these
The measured workhorsesquery_ledger, recent_activity, node_neighbours, invariants_for, tests_for, why_query, concept_neighbours, orient_me 98.9% of measured usage.
Reading a cited spanread_span, symbol_lookup Used in the benchmark transcripts, and the cheap way to read a citation the ledger just gave.
Mandated workflowtodo_list, todo_add, todo_complete, feedback Low volume and would look droppable on usage alone. CLAUDE.md mandates the todo flow and feedback closes the learning loop, so they are kept deliberately.
Un-hidden 2026-09-12 (WP0.8)reference_set, blast_radius, blast_radius_diff, refactor_plan, cross_stack_path, prior_decisions, find_similar, caller_recall_audit Zero-usage while hidden was itself the finding — the change-safety suite could not tell whether better blast-radius tools help because they were never advertised. Advertising them changed usage decisively: reference_set went from 0 calls to 16 in the very next re-test (docs/evidence/change-safety-retest-2026-09-12/README.md).
Precise C# lane — advertised only when Precise:Enabled=true find_definition, find_references, find_implementations Roslyn-backed, compiler-verified answers (WP3.1). Live in production as of 2026-09-13 07:0x UTC (docs/LEDGER_SCALE_PLAN.md §12) — so the count advertised today is 22 + 3 = 25 of the 46 declared.
Nothing became uncallable — only unadvertised. tools/call resolves against the full switch, never the advertised subset, so every script, hook and automation that calls a tail tool by name still works — except the three precise-lane tools while Precise:Enabled=false, which return a plain {"error": "precise lane disabled (Precise:Enabled=false)"} result rather than a thrown exception, so a caller that doesn't check the advertised list first still degrades cleanly (McpService.PreciseDisabledResult, McpService.cs:2899-2904). Setting Mcp:AdvertiseAllTools=true restores the full list (precise tools excepted, per the rule above). Both properties are pinned by McpToolSurfaceTests, which also asserts the list actually shrinks — a trim that trims nothing is worse than none, because it looks done.

The full MCP tool catalog

All 46 are registered in tools/list and dispatched by name in McpService.cs; the ones not in the advertised set above are callable but not advertised. A read-focused subset is also exposed to the in-app chat loop via ExecuteToolForChatAsync.

ToolGroupWhat it does
query_ledgerSearchAuthoritative semantic search over the whole codebase; the mandatory first call. Supports depth, edge_kinds, repo/layer pre-filters, skeleton detail, toon layout, lane attribution, and a root checkout overlay (see below).
ledger_askSearchAsk a natural-language question, get a synthesised, cited answer in one call instead of raw chunks.
symbol_lookupSearchResolve a function/class/interface/SQL-object name to exact definition coordinates (repo, file, line-span).
read_spanSearchReturn an exact line range from indexed sources — token-bounded, no vector lookup.
reference_setSearch/RefactorUSE WHEN you need every place a symbol is used before a rename or signature change (complete, approximate: a word-boundary text sweep across all repos, not a compiler). NOT FOR "what does X do" (query_ledger) or "where is X defined" (symbol_lookup).
find_similarGraph/TraceUSE WHEN porting a pattern or hunting duplicates: structurally similar code (mode code, same language) or semantically similar design (mode design, cross-language) to an anchor.
node_neighboursGraph/TraceWalk the dependency graph up/down/both — "what calls X / what X depends on".
concept_neighboursGraph/TraceCross-language/cross-repo chunks in the same k-means concept cluster (C#↔TS↔SQL of one concept).
cross_stack_pathGraph/TraceUSE WHEN a question crosses layers — screen → API route → stored procedure → tables — for a route or symbol. NOT FOR single-layer lookups.
blast_radiusGraph/TraceUSE WHEN you are about to edit files and want to see what is downstream in the dependency graph (pass changed_files or a commit, and hops). Graph preview only — confirm callers with reference_set before relying on it.
tests_forRefactor-safetyTest files/chunks that exercise a symbol (resolves anchor, walks callers upstream).
refactor_planRefactor-safetyUSE WHEN planning a multi-file change to a symbol or file: returns the ordered touch points, the tests that cover them and the invariants to check. NOT FOR single-file edits.
blast_radius_diffRefactor-safetyUSE WHEN a refactor must not miss a caller: compares the graph's blast radius with the text-sweep reference set for the same anchor and reports what each misses (a caller-recall % plus in_both / graph_only / lexical_only buckets).
caller_recall_auditRefactor-safetyUSE WHEN deciding whether blast_radius can be trusted for a symbol: measures how completely the graph knows its callers against a text sweep, per repo.
prior_decisionsTeam/LearningUSE WHEN you are about to decide something that may already have been decided: returns decisions extracted from earlier sessions that touched this area, with their reasons — context to revisit, never a veto.
drift_sinceRefactor-safetyStructural delta of a file since a time/commit — only symbols added/removed/moved/signature-changed.
find_definitionPrecise (C#)USE WHEN you need to jump to where a C# symbol is DECLARED — the compiler-verified source location. NOT FOR "what does X do" (query_ledger) or "why does this exist" (why_query). Roslyn MSBuildWorkspace; advertised only when Precise:Enabled=true.
find_referencesPrecise (C#)USE WHEN you need EVERY compiler-known usage of a C# symbol before a rename or signature change — recall and precision 1.0 against the compiler, not a text sweep. NOT FOR prose questions (→ query_ledger) or a first look at unfamiliar code (→ orient_me). Falls back to the SQL reference lane (kind:"sql") or the deterministic text sweep when the compiler can't resolve the symbol; every hit is labelled truth:"compiler" or truth:"text".
find_implementationsPrecise (C#)USE WHEN you need the concrete classes BEHIND an interface, or the overrides of a virtual/abstract member — compiler- verified, not a text guess. NOT FOR "how is this used" (→ find_references) or "what does this do" (→ query_ledger). Roslyn SymbolFinder.FindImplementationsAsync.
orient_meOrientationSingle-call landing pad for a file: identity, quality, git, tests, invariants, concept siblings, graph link.
task_introOrientationCanonical session opener: fans out to query_ledger + orient_me on the top hit + invariants_for.
why_queryOrientationWhy a file exists / was last changed: git provenance, inferred intent, ADR pointers, load-bearing status.
invariants_forOrientationLoad-bearing rules: CLAUDE.md / LOAD_BEARING / AGENTS.md / INVARIANTS.md + must/never chunks.
recent_activityOrientationMost recently modified files (git last-modified desc), optionally per-file history or per-repo.
cluster_cohortCohort/AggregateGroup a facet-selected population into families by clustering design vectors.
cohort_consistencyCohort/AggregateFind the odd-one-out / inconsistency within a cohort that should look alike.
cohort_countCohort/AggregateCount a facet-selected population grouped by repo/layer/pattern — the "how many" tool.
count_sprocsCohort/AggregateCount DISTINCT stored-procedure definitions parsed from .sql, grouped by repo.
fix_queue_listQuality/Fix-queueLists approved findings (bugs, inconsistencies, UX) awaiting a code fix, across every repo.
fix_get_briefQuality/Fix-queueComplete self-contained fix brief for one finding: issue+evidence, target files+blast radius, current code, acceptance criteria.
fix_mark_statusQuality/Fix-queueAdvance a finding through its lifecycle so the shared queue never double-hands.
todo_listTodoEverything still outstanding across every repo — the cross-repo todo list, filterable by repo or by the session identity that raised each item.
todo_addTodoRecord work left undone (deferred, skipped, discovered-but-not-fixed) so it outlives the session; deterministic ids, so re-adding updates in place.
todo_completeTodoClose a todo once the work is verified done — the list is only worth reading if "done" means done.
learning_needsTeam/LearningTeam/developer capability analysis from the quality rubric + recurring finding classes, attributed by git author.
prior_workTeam/Learning"Has anyone worked on this before?" — past sessions that touched a file/task, labelled by outcome.
recall_conversationTeam/LearningSemantic recall over past (redacted) conversation transcript spans.
feedbackTeam/LearningSubmit useful/not-useful feedback on a prior query_ledger result (closes the learning loop).
mark_visitedContext/RecallRecord a confirmed code node this session so later turns cite path:L#-L#.
recall_visitedContext/RecallReturn the session's confirmed nodes so the agent cites verified coordinates instead of re-reading.
bench_listBenchmarkThe token-efficiency benchmark queue + aggregate report (ledger vs direct-file cost).
bench_get_taskBenchmarkOne pending benchmark question + the exact two-path run protocol + the bench_record call.
bench_recordBenchmarkRecord per-path token usage for a question and mark it done.
sync_ledgerAdminTrigger a background re-ingest of the configured repos (high-risk tier).
policy_checkAdmin/EnforcementRisk-tiered verdict (allow/deny/review) for a proposed tool+path+action — MCP-as-policy-enforcer.
suggest_personasGovernanceReads a task and suggests expert sub-agent personas to wrap it in (Performance Tuner, Legal Expert, Security Reviewer, SQL Optimiser, …), each with a recommended model + tool scope. Advisory; backed by the governance persona library — see the Governance layer.
Why it matters

The catalog moves agents from "grep-and-read the whole tree" to targeted, token-cheap, graph-aware retrieval — search, trace, orient, refactor-safely, and learn from prior work, all through one authenticated endpoint.

Span-first hits — every result leads with where the code is

A query_ledger hit used to open with its intent sentence and a labelled metadata row, and on the default path carried no line coordinates at all: an agent had to read to the bottom of a block to learn it could not navigate from it. Span-first reverses that order — path L<from>-<to> becomes the hit heading, the intent drops to the first bullet, and the metadata collapses to one unlabelled separator-delimited row whose schema is stated once in a legend line at the top of the response.

Nothing is dropped: repo, layer, pattern, chunk type, confidence band and percent, build sha, graph link, snippet, graph footer and the ```json cited block all survive. Removing the repeated labels more than pays for the coordinates and the legend, so the response is smaller — replaying the 20 frozen token queries over the t0 snapshot gives −3,902 bytes (−3.14%), about −49 estimated tokens per query, assuming a pessimistic 11-character coordinate on every hit. Config-gated on Mcp:SpanFirstLayout, so the flag flip is the single attributable change.

Why it matters

The only actionable fact in a hit is where the code is. Leading with it means an agent can go straight to read_span instead of scanning to a footer to find out whether it can.

The cited block — ledger_id, start_line, end_line, indexed_sha

Every rendered response that cites files closes with a fenced ```json cited block — one entry per distinct file, deduplicated. Beyond the always-present repo/file pair, an entry carries whichever of these the call site actually had a real hit object for (a SearchResult or SymbolMatch, never fabricated): ledger_id (the chunk's stable identity — survives a file rename), start_line/end_line (the coordinates actually shown in the rendered heading, so read_span lands on the same lines the agent read), and indexed_sha (the commit the chunk was indexed at — SymbolMatch hits never carry this field). This is what lets follow-up tooling — the audit script's per-hit follow rate, the Ask drawer's "View code" button — survive a file rename instead of matching on a bare path.

Compact defaults — Mcp:DefaultFormat / Mcp:DefaultLimit (toon is live)

An explicit format/limit argument on a query_ledger call always wins, but absent one, it resolves through two optional config keys instead of a hardcoded "markdown"/5: Mcp:DefaultFormat ("markdown" or "toon") and Mcp:DefaultLimit (1–20, clamped). An invalid value (unparseable, or not one of the accepted formats) falls back to the code default and logs once rather than failing the call.

Both keys are no-op in the shipped appsettings.json code defaults — but the live production config is not the code default: at 19:08 UTC on 2026-09-12 (deploy #4, docs/LEDGER_SCALE_PLAN.md §12), the live values were flipped to Mcp:DefaultFormat=toon, Mcp:DefaultLimit=3, and verified live by a query_ledger call with no limit argument returning 3 toon rows. So today, any caller that omits format/limit gets the dense toon layout at 3 rows, not 5-row markdown — still overridable per call. The change is being judged by the nightly per-hit follow rate.

symbol_lookup hands over the definition, not an errand

On an exact match symbol_lookup already knew the file and the line range — and then printed a literal instruction, Read it: read_span(...), telling the agent to spend its next turn fetching what the ledger could have handed over in the one it was already paying for. Seven of the twenty recorded read_span calls in the real-session benchmark followed that line. The tool was manufacturing its own follow-up turn.

With Mcp:InlineSymbolSpan on, an exact match inlines the definition itself — read from the source file, not the indexed chunk, so it is immune to the chunk cap that leaves large members internally truncated in the corpus — padded by Mcp:InlineSymbolSpanContextLines (default 20) on each side and bounded by the same Retrieval:ReadSpanMaxLines cap (default 400) that read_span would have applied. The whole inline section of a response is further bounded by one read_span's worth of lines, so a 20-match lookup cannot cost more than the turn it removes.

The read_span handle stays — it is how an agent pages past a capped or clamped span — but it is no longer phrased as an instruction to fetch what is already on screen. Three cases deliberately keep the legacy handle instead of inlining: a fuzzy (contains) match, because a partial match is a real disambiguation step and inlining several probably-wrong bodies would spend tokens rather than save them; a spent line budget or unreadable file; and a definition whose start is past end-of-file, where stale coordinates would otherwise print the wrong lines.

Preloading the tool schemas

Claude Code defers MCP tool schemas by default: the tools arrive as names only, and before its first call the model must spend an entire turn calling ToolSearch to fetch a schema. Only then can it make the call it wanted to make. Measured over the 21 recorded ledger-arm sessions in the real-session benchmark:

 Ledger armControl arm
Sessions opening with a ToolSearch turn21 / 21 0 / 21
Total ToolSearch calls240
Share of all turns spent on ToolSearch13.8% 0%

The control scores zero because it loads no MCP server at all, so it has nothing to defer — the deferral was a fixed startup tax on one side of a two-arm comparison, measuring the harness rather than the ledger. Every session that opens by consulting the ledger, which is what CLAUDE.md instructs every session to do, pays that turn. Setting ENABLE_TOOL_SEARCH=false — as an environment variable in front of claude, or as an env key in ~/.claude/settings.json — removes it. Full guidance and accepted values: docs/TOOL_SCHEMA_PRELOAD.md.

Benchmark answer keys are excluded server-side

The benchmark's reference answers were once excluded only as a caller argument: BenchRunner passed exclude_paths on its own calls, nothing else did, and exclude_paths is unadvertised, so no real session ever sent one. On the 2026-08 real-session run, 6 of 21 ledger sessions were served answer-key prose as ranked query_ledger hits. Worse, the documents had already been deleted from the sources tree — ingest is upsert-only and nothing walks Qdrant asking whether a file still exists, so about 19 chunks outlived their files by weeks and kept being retrieved.

Retrieval/BenchDocGuard.cs makes it a server rule instead, applied inside the retrieval path regardless of what the caller passed: hybrid search merges the server list into its exclusions (a union, so the bench keeps its own), symbol_lookup applies it per point because it never went through hybrid search at all, and read_span refuses by path because it answers from disk where no index change can reach it. Retrieval:ExcludeBenchDocs defaults true — this is an integrity fix, not an experiment, so the safe state is the default state, the flag exists only for emergency rollback, and an unparseable value degrades to on rather than throwing inside search. A localhost-gated POST /api/admin/qdrant/purge-bench-docs then deletes every matching point, all versions and deliberately not is_latest-filtered, because a superseded version still carries the full answer-key text in its payload.

Why it matters

An exclusion that lives in one caller is not an exclusion. A second copy of the path list is exactly how this hole opened, which is why the guard borrows BenchRunner's list rather than restating it.

PR pre-flight — asking the ledger what a diff disturbs

tools/pr-preflight.sh takes a PR, asks the live ledger what the diff touches, and prints a report: affected callers with real file:line anchors, cross-repo impact, invariants touched, and a call table recording every request it made with its outcome. It is print-only by default — posting a comment requires an explicit --post — costs $0 and calls no model. A run where a ledger call had failed exits non-zero and renders that section as "Could not ask the ledger", never as a clean empty. The acceptance capture against a real merged PR is docs/PR_PREFLIGHT_ACCEPTANCE.md.

Whether the ledger actually makes change planning safer is not settled by that tool existing. The change-safety suite found parity, and found that blast_radius, reference_set, refactor_plan and cross_stack_path "were on the allowlist and were called zero times across all 20 ledger sessions". Its own conclusion: "Nothing here supports building the PR pre-flight bot on a better-blast-radius claim, and nothing here refutes that claim either — it was never exercised." See docs/CHANGE_SAFETY_RESULTS.md §9, and the summary on the Overview.

policy_check — the risk-tiered gate

Given a proposed action (tool + optional path + intent), policy_check returns a machine-readable allow | deny | review verdict an MCP gateway or hook consults before the action runs. It is backed by the pure, testable PolicyService (Services/PolicyService.cs): ReadOnly → allow (every discovery tool; unknown tools default here); Write → allow-but-flagged (feedback, mark_visited, bench_record, fix_mark_status); HighRisk → review (sync_ledger + configured). The tier map is config-overridable and UNIONed onto defaults; ToWire emits stable lower-case strings; and the verdict attaches the relevant invariants for the target path. It is documented in full on the Team analysis page.

Why it matters

Proportional control — read-heavy discovery stays frictionless while mutating or irreversible actions get gated to a human, without hard-coding what is risky.

Token-efficiency benchmark (bench_*)

Proves and measures that answering via the ledger costs fewer tokens than reading files directly, as a real A/B. Benchmark/BenchRunner.cs runs each pending discovery question through two isolated agentic loops with HARD-separated tool sets: (A) LEDGER = a single query_ledger tool hitting the local MCP endpoint (http://127.0.0.1:5005); (B) DIRECT = grep/read over /opt/arch-ledger/sources/current. It records each path's real token usage (input/cache_read/cache_write/output) from Anthropic API usage blocks, with loops capped at 40 turns / 4096 tokens. An opt-in correctness gate (BenchGate, off by default via Benchmark:Judge:Enabled) enforces that a cheaper answer only counts as a win if it is BOTH strictly cheaper AND at least as correct (an LLM judge). Results persist in the Benchmark Qdrant collection, and the system-wide ratio is surfaced by EfficiencySummary in TeamMetricsService. The in-session drain flow (bench_list → bench_get_task → run both ways in fresh contexts → bench_record) is $0 and deterministic; the real-API A/B spends money, is gated on Anthropic:ApiKey, and only runs on an explicit trigger (model default claude-opus-5).

Why it matters

It turns "the ledger saves tokens" into a measured, correctness-gated, per-question ledger — the number behind the Ledger Token Saving tile.

Deployment topology

An agent posts to https://brain.motlogs.com/mcp (HTTPS + JSON-RPC + Bearer) → nginx (TLS terminator) → 127.0.0.1:5005 .NET 10 service. The publish is a self-contained linux-x64 binary, shipped as a tarball via scp over SSH. The systemd unit (Deploy-Hetzner.ps1:573-591) sets WorkingDirectory=/opt/arch-ledger, ExecStart=/opt/arch-ledger/MOTLogs.ArchitecturalLedger, Restart=always, ASPNETCORE_URLS=http://*:5005; the service name is arch-ledger. The nginx vhost (:120-181) redirects 80→443, terminates TLS 1.2/1.3 with a wildcard cert, and proxies to http://127.0.0.1:5005 with WebSocket upgrade + forwarded headers, while location /api/admin/ { return 404; } blocks admin at the proxy (loopback/SSH-tunnel only). Deploy flags include -NoNginx, -ConfigureNginxOnly, -RestartOnly, plus source-sync-only and transcript-sync-only modes; the graphify CLI is installed on-host via uv tool.

Why it matters

A single self-contained binary behind a TLS-terminating proxy is trivial to reason about and restart, and the admin surface is blocked at two layers (proxy 404 + app-level tailnet gate) so it can't leak.

Public API access gate (WP0.7)

Every /api/* and /hook/* request now passes one gate (Program.cs, registered beside the audit-trail middleware, delegating its actual decision to the pure, unit-tested HttpAccessGate.IsApiRequestAuthorized in Services/HttpAccessGate.cs — trusted-peer loopback/tailnet detection, public-path matching, and bearer/registry resolution are each a standalone testable predicate there, not inline logic only a booted host could exercise): a small Security:PublicPaths allow-list (/api/health, /api/ask, /api/symbols, /api/gate-health by default — the latter two already self-gate or were already public, so this just leaves them alone) is let through with no check; everything else needs a trusted loopback/tailnet peer (Security:TrustLoopback / Security:TrustTailnet, both default true) or a bearer — either the shared Mcp:BearerToken or a TokenRegistry-registered per-developer token, the same rule POST /mcp already applies — presented as Authorization: Bearer … or ?token=…. This closed the previous hole: GET /api/todos had no gate of its own and returned the full cross-repo findings list to anyone on the internet. Per-route checks that already existed (IsLocalhost/IsGraphAuthorized) are unchanged and still run afterwards. Because /api/stats is deliberately not on the allow-list, the dashboard (index.html, script.js, admin.html, app.html) now needs a token to load unless it arrives over an SSH tunnel/tailnet session (trusted peer, no token needed). Per decision D8 the dashboard is bearer-only rather than exempted, so every page routes its /api/* calls through wwwroot/token.js's authFetch: it attaches Authorization: Bearer … once a token is known, accepts one the same way the graph viewer always has (?token=<Mcp:BearerToken>, stored to localStorage and stripped from the URL on read), and on a genuine 401 shows a small one-time banner asking the viewer to paste the token, then retries the failed call once. The token itself is never embedded in any served page or script. Security:Enforce=false in the runtime settings file is the documented rollback if this gate ever needs to come out without a redeploy.

Why it matters

The gate used to be a property each route had to remember to add; now it's a property of the surface — a new /api/* route is closed by default and has to be deliberately added to the allow-list to become public, rather than the other way around.

Qdrant operations

The image is PINNED to qdrant/qdrant:v1.15.4 (1.16.x panics on scout_vehicles.location — an empty geo index unwraps None at gridstore.rs:333), runs with --ulimit nofile=1048576 (the default 1024 panics), and uses two bind mounts — storage (/var/lib/qdrant) and snapshots (/var/lib/qdrant-snapshots, else snapshots vanish on docker rm) — with loopback-only ports 6333/6334 and restart-always. The host needs 125 GB RAM and ≥16 GB swap (scout_vehicles peaks ~118 GB). The ledger collection motlogs_unified_architecture (~2,241 points, named vectors code_vector/design_vector, 384-dim Cosine) is tiny next to scout_vehicles (135M points / 686 GB).

Gotcha: after a Qdrant restart the ledger caches dead gRPC subchannels and reports totalChunks: 0 — always wait for healthz then systemctl restart arch-ledger.
Why it matters

The pinned image, raised ulimit, and separated snapshot mount are hard-won production settings — each one prevents a specific crash or data-loss mode, so the vector DB stays up under a 686 GB co-tenant.

Re-ingest / backfill runbook

A cache-preserving rebuild re-chunks and re-embeds without paying for the Groq analysis cache again.

  1. Pre-flight — verify the commits on the host binary, that repo paths point at motmaster-*, that Qdrant is healthy, and take a snapshot.
  2. Rebuild — POST /api/admin/qdrant/rebuild (guarded — NEVER clear-collection; returns 202).
  3. Monitor — /api/admin/ingest-status (2–4h at MaxParallelism:2, watching BudgetSkippedChunks).
  4. Post-passes — POST /api/admin/concepts/run + /api/admin/cross-stack/run (materialises the G1 controller→sproc edges).
  5. Measure — GET /api/admin/eval/relevance (the 24-question golden set) as the no-regression gate.
  6. Snapshot — take a fresh snapshot.

Byte-identical content stays Groq-cold-free (~$0). The high-risk sync_ledger MCP tool triggers the background re-ingest programmatically.

Why it matters

A full re-vector of the corpus is a routine, ~$0, gated operation — with a golden-set eval as the no-regression contract — so tuning changes ship on evidence, not hope.

Enforcement hook

Every agent is nudged or forced to consult the ledger before touching code, via a PreToolUse hook. As of 2026-09-13 08:19 UTC this is a single, user-level, versioned wiring — not per-repo copies — installed from motmaster-infra/claude-hooks/. Full detail, including the current gate mode, the A/B under way, and /api/gate-health, is on the Governance layer page; this section states only what's relevant to the MCP surface itself: the ledger MCP tools are always exempt (mcp__mot-ledger__* never gates, or the gate would be unsatisfiable), and every non-ledger MCP tool is gated the same as Read/Grep/Bash/Edit/Write (pattern ^mcp__(?!mot-ledger__).+) — so a different MCP server's tool is subject to the same consult requirement as a file edit.

Why it matters

It guarantees answers are grounded in the ledger rather than model memory — the tool is the constraint, so compliance is mechanical rather than advisory (while that rule itself is under a pre-registered A/B — see Governance).

Note: there is no AGENTS.md/CLAUDE.md at the project root — the governing CLAUDE.md is one level up at /root/code/motmaster-openbrain/CLAUDE.md. bench_* defaults to claude-opus-4-8 and only spends when Anthropic:ApiKey is set; the in-session drain path is $0.

Open Brain — the MOTMaster Architectural Ledger · generated from source 2026-07-07