{"id":"915c46d0-fd59-41d2-927f-0ee5c0ff4667","slug":"glasir","kind":"mcp","name":"glasir","summary":"Deterministic code intelligence for repositories: local dependency graphs, impact analysis, and bounded MCP evidence.","owner":"Attackwave","repoUrl":"https://github.com/Attackwave/glasir","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-29T05:32:57.000Z","addedAt":"2026-11-08T04:45:49.547Z","logoUrl":"https://avatars.githubusercontent.com/u/51136146?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools"],"tags":["code-intelligence","dependency-graph","rust","static-analysis"],"runtime":"docker","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-11-08T04:46:51.719Z","sample":false,"url":"https://conduits.dev/tools/glasir","language":"Rust","transports":["stdio"],"install":{"docker":"ghcr.io/attackwave/glasir:v0.11.6"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Glasir\n\n[![ci](https://github.com/Attackwave/glasir/actions/workflows/ci.yml/badge.svg)](https://github.com/Attackwave/glasir/actions/workflows/ci.yml)\n[![codeql](https://github.com/Attackwave/glasir/actions/workflows/codeql.yml/badge.svg)](https://github.com/Attackwave/glasir/actions/workflows/codeql.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Attackwave/glasir/badge)](https://scorecard.dev/viewer/?uri=github.com/Attackwave/glasir)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/16445/badge)](https://www.bestpractices.dev/projects/16445)\n\n**Deterministic code intelligence for repositories.** Glasir builds a local,\nqueryable graph of definitions, references, dependencies, and change impact,\nthen serves bounded evidence over [MCP](https://modelcontextprotocol.io).\nIt gives engineering tools structural answers instead of an unfiltered file\ndump: what calls this, what breaks if it changes, and how two symbols connect.\n\n*Glasir is the tree with golden leaves that stands before Valhalla, \"the fairest\namong gods and men\" — a tree whose every branch is visible from the ground.*\n\nOne Rust binary. No Python, no Node, no database, no GPU, and **no model\nanywhere in the index or query path** — the same tree and the same question\nalways produce the same answer.\n\n> Production deployments pair the Core with Glasir Control for tenant policy,\n> mTLS boundaries, audited administration, cross-repository review, and\n> provider pull-request checks.\n\n## Quickstart\n\nInstall with the package manager you already use:\n\n```sh\nbrew install attackwave/glasir/glasir                      # macOS, Linux\nscoop bucket add glasir https://github.com/Attackwave/scoop-glasir\nscoop install glasir                                       # Windows\n```\n\nOr download the archive for your platform from\n[Releases](https://github.com/Attackwave/glasir/releases), check it against\nthe `.sha256` beside it or its build provenance (`gh attestation verify\n<archive> --repo Attackwave/glasir`, or offline with `--bundle` and the\nrelease's `.intoto.jsonl`), and put `glasir` on your `PATH`:\n\n| Platform | Archive |\n|---|---|\n| Linux x86_64 | `glasir-<version>-linux-x86_64.tar.gz` |\n| Linux ARM64 | `glasir-<version>-linux-arm64.tar.gz` |\n| macOS Apple Silicon | `glasir-<version>-macos-arm64.tar.gz` |\n| macOS Intel | `glasir-<version>-macos-x86_64.tar.gz` |\n| Windows x86_64 | `glasir-<version>-windows-x86_64.zip` |\n\nThe Linux binaries are statically linked and run on any distribution,\nincluding Alpine and older enterprise releases. A binary downloaded by hand on\nmacOS is not yet notarized, so macOS refuses its first start; clear the flag\nonce, after checking the checksum: `xattr -d com.apple.quarantine glasir`.\nHomebrew installs are not affected.\n\nClients that read the [MCP registry](https://registry.modelcontextprotocol.io)\nlist Glasir as `io.github.Attackwave/glasir`; that entry runs the container\nimage with the repository mounted read-only.\n\nThen, in the repository you want to ask about:\n\n```sh\nglasir install .\n```\n\nThat finds the coding assistants installed on your machine — by their program\non `PATH`, their settings in your home directory, or their files in the\nproject — and registers Glasir with each, in the project's own configuration:\n\n| Assistant | File |\n|---|---|\n| Claude Code | `.mcp.json` |\n| Codex | `.codex/config.toml` |\n| Gemini CLI | `.gemini/settings.json` |\n| Qwen Code | `.qwen/settings.json` |\n| Antigravity | `.agents/mcp_config.json` |\n| Cursor | `.cursor/mcp.json` |\n| VS Code (Copilot) | `.vscode/mcp.json` |\n| OpenCode | `opencode.json` |\n\nThe server it registers runs with `--watch`, so the graph follows your edits\nwhile the assistant runs. It also installs git hooks that keep the graph\ncurrent after a pull, checkout or rebase. For Claude Code it adds a\n`SessionStart` hook to `.claude/settings.local.json`: in an analysed tree, the\nsession starts with a note to load the graph tools, because Claude Code defers\nMCP tools and an assistant that has not loaded them searches text instead.\n`--no-hook` skips both kinds. A registration file it creates holds absolute paths to your machine,\nso it is added to `.gitignore`; one your team already tracks is merged into,\nnever replaced, and nothing in your home directory is touched. With no\nassistant detected it writes a neutral `glasir-mcp.json` for any MCP client.\n`--platform <name>` picks one explicitly.\n\nSeveral assistants on one tree share one server. What a client starts is a\nsmall relay to that tree's backend, which it starts when none runs: one graph,\none watcher, whatever number of assistants are open. The backend ends a minute\nafter the last client closes, and is started again when one returns. After ten\nminutes without a request it lets the graph go and stops watching, and the next\nrequest loads it again (`--sleep-after <minutes>`, `0` keeps it). A relay a\nclient forgets to close costs a few megabytes and no CPU. `glasir servers`\nlists what runs; `--stop-replaced` ends a server a client replaced without\nclosing it. On Windows each client keeps its own server.\n\nMost assistants ask before they start a new server: Claude Code and Qwen Code\nwant the server approved, Codex and Gemini CLI read project settings only in a\nfolder you have trusted, and Antigravity asks before its first use of an MCP\ntool. `install` prints the step for each one it wrote.\n\nIn Claude Code the plugin does the same without a step per repository, once\n`glasir` is on your `PATH`:\n\n```text\n/plugin marketplace add Attackwave/glasir\n/plugin install glasir@glasir\n```\n\nIt serves whichever git repository a session starts in, and starts nothing\nelsewhere. Its generated files go into `.git/info/exclude`, so nothing tracked\nchanges. Use the plugin or `glasir install` in a tree, not both: each brings\nits own copy of the server.\n\nRestart the assistant and ask it something about the code: *\"what breaks if I\nchange `parse_config`?\"*, *\"how does a request reach the database?\"*. The\nanswers come from `impact`, `query_graph`, `shortest_path` and twelve more tools\nlisted under [What it serves](#what-it-serves).\n\n```sh\nglasir status .             # what is registered, what the graph holds\nglasir why \"your question\"  # what the search saw, when an answer surprises you\nglasir view .               # the graph as a map in the browser, one region per subsystem\nglasir review               # the uncommitted change as a Markdown review: risk, dependents, tests\nglasir export . > graph.json  # the graph as node-link JSON, for another tool\nglasir servers              # running servers; --stop-replaced ends those a client replaced\nglasir uninstall .          # remove the registration and the hooks\n```\n\n### What gets indexed\n\nEverything your `.gitignore` files leave in, at every level of the tree, minus\nbuild output and dependencies (`target`, `node_modules`, dot-directories).\nA `.glasirignore` beside them uses the same syntax and wins over them, so it\ncan both exclude more and bring back what git ignores:\n\n```gitignore\n# .glasirignore\n!plan.md              # untracked, but worth asking about\n!/reference/          # git ignores the whole directory ...\n/reference/scans/     # ... of which only this part stays out\n```\n\nA tree holding a copy of itself, such as an amalgamated header or a vendored\nsnapshot, defines each name twice, and calls to an ambiguous name stay unlinked.\n`overview` names such directories; exclude the copy here.\n\nAs in git, a file inside an ignored directory comes back only once the\ndirectory does (`!/reference/` above). Only ignore files inside the tree\ncount — not your global excludes file and not `.git/info/exclude` — so the\nsame commit gives the same graph on every machine, whatever each developer\nignores locally.\n\nInstruction files for coding assistants (`AGENTS.md`, `CLAUDE.md`,\n`GEMINI.md` and their `.local.md` variants, `COPILOT.md`, `OPENAI.md`,\n`CHATGPT.md`) and `CONTRIBUTING.md`, `CHANGELOG.md`, `CODE_OF_CONDUCT.md` are\nleft out by default: they describe how to work on the code in the words a\nquestion is asked in, and outrank the code they describe. A project that wants\none indexed names it in `.glasirignore`, e.g. `!AGENTS.md`. A change to either file\ntakes effect on the next analysis, and immediately under `--watch`.\n\nOf `.github`, the workflows, local actions and `scripts/` are indexed although\nit is a dot-directory: a workflow's jobs are symbols, `needs` connects them,\nand `uses: ./…` and the scripts a step runs connect the job to those files,\nso `impact` on a script and `review` of a workflow see each other.\n\n## Why a graph\n\nSearch finds text. A graph preserves the relationships that make a repository\nbehave as a system.\n\nFor example, answering \"what is affected if `authorize` changes?\" with search\nrequires finding candidate files, interpreting each call site, and repeatedly\nreconstructing transitive dependencies. `impact` follows stored,\nprovenance-labelled edges and returns affected symbols grouped by distance.\nLikewise, `shortest_path` shows the evidence-backed route between two symbols\ninstead of asking a caller to infer one from unrelated file excerpts.\n\n| Need | Search and file reading | Glasir |\n|---|---|---|\n| Find text mentioning a name | Useful | Useful |\n| Find direct callers | Manual interpretation required | Direct graph query |\n| Assess transitive impact | Reconstruct dependencies repeatedly | Bounded traversal with edge provenance |\n| Explain how A reaches B | Infer a path from excerpts | Return a concrete path |\n\nThe graph does not claim to translate arbitrary natural language. Search uses\ndeterministic identifier and text matching; unresolved or ambiguous symbols\nremain visible as such rather than being guessed.\n\n## Benchmarking\n\nGlasir ships regression benchmarks, not vendor-comparison claims. They compare\nthe checked-in corpus with a documented grep-and-read baseline and fail when a\nconfigured recall floor regresses. Results change as the corpus, questions and\nimplementation change, so this README intentionally does not publish static\npercentages or token counts.\n\n```sh\nglasir benchmark .\nglasir benchmark . --check\npython3 bench/scale.py target/release/glasir --check   # 100k vs 1M lines\n```\n\n[Benchmark methodology](docs/benchmarking.md) defines the corpus, baseline,\ntoken estimate, limits, and interpretation. It also explains why results from\nthis repository are not a substitute for an evaluation on a customer's code.\n\n## Performance\n\nIndexing and query latency depend on source shape, parser tier, storage,\nhardware, and the requested graph traversal. The `analyse` command reports the\nmeasured cold-index time and graph size for a repository; running it a second\ntime measures snapshot reuse. Evaluate performance with the exact release,\nrepository shape, and deployment limits that will be used in production.\n\nSnapshots are stored beside the source tree and validated before reuse. A warm\nstart maps the stored graph instead of reparsing the full tree, provided the\nsource and extraction configuration still match.\n\n## What it serves\n\nFifteen MCP tools, over stdio for a local editor or Streamable HTTP for a remote\nagent:\n\n- `overview` — the subsystems of an unfamiliar tree and the way into each;\n  in a Rust workspace its crates, and each subsystem named by its crate\n- `query_graph` — the subgraph relevant to a question; given a file's path,\n  every definition in that file\n- `shortest_path` — how one symbol reaches another\n- `explain_node` — callers, callees and subsystem of one symbol\n- `impact` — what breaks if this changes, grouped by distance\n- `cycles` — dependency cycles, compiler-resolved edges only by default\n- `get_code_snippet` — the source a symbol names, from the range the parser\n  recorded, so nothing has to open the file or guess a line; a Markdown\n  section returns that section, not the whole document, and `following: N`\n  reads on through the next N definitions. An assembly routine that branches\n  or falls into another says where it continues\n- `find_callers` — the direct callers of one symbol, as one list, and the\n  definitions that use it by name without calling it (a type in a signature,\n  a constant read, a base class)\n- `detect_changes` — a git diff mapped to the definitions whose lines it\n  touches (a document's sections likewise, new files included) and what\n  depends on them, with a risk level\n  and its reasons: `high` when changed code has callers and no test reaching\n  it or a removed or renamed definition is still called (those callers are\n  listed), `medium` when it has dependents or when files the history says\n  usually change too are left out, `low` otherwise\n\nThree answer what to do *before* a commit, which is when an answer is\ncheapest to act on:\n\n- `affected_tests` — the tests that reach a symbol or the uncommitted diff,\n  nearest first, a page at a time (`limit`, `offset`), with the files to run\n  and, for Rust, the `cargo test` runs that cover them. Recognises the test conventions of\n  each language (`tests/`, `spec/`, `__tests__/`, `x_test`, `x.spec`,\n  `FooTest`, test attributes such as `#[test]` and `@Test`); a name such as\n  `test_x` alone counts only in Python and Ruby, whose runners collect by name\n- `co_changes` — the files that change in the same commits as one file, from\n  the git history, marking those no edge in the graph connects: the hidden\n  coupling — a template, a migration, a client in another language — that\n  `impact` cannot see because no code names it\n- `check_architecture` — the architecture contract in `glasir-rules.txt`\n  (`deny <path> -> <path>`, `no-cycles`) checked against the graph, every\n  broken rule with the edges that break it; pass rules ad hoc to test a\n  boundary before writing it down. Under `serve --watch` the graph follows\n  the working tree, so a change is checked before it is committed\n- `find_unused` — definitions nothing in the tree refers to, confirmed against\n  every text file rather than the call graph alone, with tests, annotated and\n  generated code, overrides and framework conventions left out. Definitions in\n  a Rust file no `mod` reaches from a crate root are listed apart, as code the\n  compiler never builds; `overview` names those files. On six real\n  repositories in five languages, every name it reported appeared nowhere but\n  in its own definition; what it cannot see — a library's public API, a name\n  built at run time — is stated in each answer\n- `change_history` — the commits that changed one definition, newest first,\n  with their messages (the first `body_lines` of each, 6 by default): why the\n  code is the way it is, before it is changed. A\n  commit counts when its changed lines fall inside the definition as the file\n  was then, not inside one nested in it. Read from git when asked; commit\n  messages are kept out of the search index, where measured they crowded out\n  the code they describe\n\nOne is for joining trees:\n\n- `http_surface` — the HTTP routes this tree declares with their handlers, and\n  the requests it sends with their senders, as matchable segments. Within a\n  tree `find_callers` and `impact` already follow a request to its handler;\n  Glasir Control uses this to do the same across the repositories of a\n  workspace, where the client and its server live apart\n\nEvery tool declares itself read-only, idempotent and closed-world\n(`readOnlyHint`, `openWorldHint: false`), so a client in a planning or\nread-only mode may call all of them. The five that list without bound —\n`impact`, `find_callers`, `find_unused`, `overview` and `detect_changes` — take\n`limit` and `offset` and return `next_offset` while more remain; the order is\nstable, so pages add up to the whole answer. In `find_callers`, `limit` also caps\n`undecided_callers` and `named_elsewhere` (`named_elsewhere_total` keeps the\nfull count); in `detect_changes` one `offset` pages `changed_symbols` and the\ndependents together (`changed_symbols_total` keeps the count), and the risk\nlevel always judges the whole change. Every count declares its `minimum` and `maximum` in the\ntool's input schema: a value below the minimum is refused, one above the\nmaximum is held to it.\n\n**\"Nothing calls this\" is checked against the source.** The graph records\ncalls; a type in a signature, a constant read or a handler wired up in a\ntemplate is a use it has no edge for. `impact`, `find_callers` and\n`affected_tests` therefore also name the files whose text names the symbol\nwithout an edge connecting them (`named_elsewhere`), for every name the tree\ndefines exactly once — so an interface injected into three controllers is no\nlonger reported as breaking nothing.\n\n**Every answer comes twice: as prose and as `structuredContent`.** The text is\nlaid out for a person to read; the structured half is the same facts as data,\nagainst a schema each tool declares, so an agent parses fields instead of\nmatching on wording that may be reworded. The text is rendered from that value,\nso the two cannot disagree.\n\n`GET /health` answers `{\"status\":\"ok\",...}` without a token, so a container\norchestrator can probe liveness; everything else needs one.\n\nUnder `serve --watch` a `GET` opens an event stream that pushes\n`notifications/resources/updated` when the tree is re-indexed, so a client\nholding cached answers learns they describe a tree that has moved on.\n\n**The same commit answers the same way, on any machine.** No model and no\nsampling in the index path: embedding signatures are hashed, every ranking\nbreaks ties on node id, and `glasir selfcheck` analyses a fixture twice from\ncold and requires ids, edges, partition, embeddings, layout and search\nrankings to match. That is what makes a retrieved answer citable rather than\nmerely plausible.\n\n**Every edge says how it was resolved.** `extracted` is compiler-verified\n(SCIP/LSP), `inferred` is syntax from native scanners, `ambiguous` is a name match.\nA method called on a value whose type is unknown is `ambiguous` too, however\nunique its name: `x.is_empty()` may be the standard library's. An answer is\ntherefore citable: a caller can tell what the tool knows from what it guessed. In Python, Go and JavaScript/TypeScript a call through an import —\n`util.Do()`, `from pkg.b import helper as h; h()`, `import * as x from './x'` —\nresolves to the file or package the import names, so a name defined in several\nplaces still links to the right one instead of to none. Erlang and Elixir\nresolve a call through its module, and Java a call through a variable to the\nclass its declared type names. Rust reads `use crate::…`, `super::…` and\n`self::…` the same way, and a method on a typed value finds the `impl` that\ndefines it wherever it is; a type outside the tree (`HashMap`) holds its own.\nA Rust call matched by name alone reaches only its own crate and the crates\nits `Cargo.toml` depends on by path, and a standard method (`len`,\n`is_empty`, `iter`, …) on a value of unknown type reaches nothing in the tree;\n`find_callers` lists such calls from the definition's crate as undecided.\n\n**A request reaches its handler.** A client's HTTP call is an edge to the\nserver handler whose route it names: routes from Spring annotations, NestJS\ndecorators, ASP.NET controllers and minimal APIs, Express-style and Go registrations, Python\ndecorators and OpenAPI documents (`paths`, verb, `operationId`); requests from\nAngular's `HttpClient`, `fetch`, .NET's `HttpClient`, Spring's `RestTemplate`,\nMockMvc and Go's `http.NewRequest`, with URLs assembled from constants,\ntemplates or `+`. `find_callers`, `impact` and `affected_tests` follow these\nedges, so a test or a front end calling an endpoint counts as its caller.\n\n## Languages\n\n148 languages are registered in `parsers/`, with native scanners and no\nexternal grammar dependency. Among them Rust, Python, JavaScript,\nTypeScript, Go, Java, C, C++, C#, Ruby, PHP, Swift, Scala, Kotlin, Bash, Lua,\nElixir, Dart, Haskell, Zig, Perl, SQL, HCL, R, Julia, OCaml, Solidity,\nErlang and assembly (Motorola 68000, GNU as and NASM: labels, `EQU`\nconstants, macros — including ones that define the name they are given —\nand calls through `JSR`/`BSR`/`BRA`/`call`, a routine falling into the next\nlabel, and indirect calls: through a `DC.L` table, or through a field a table\nwas stored into elsewhere, `MOVE.L MN_OPS(A1),A2` / `MOVE.L OP_READ(A2),A2` /\n`JSR (A2)` reaching the entry at offset `OP_READ` of every such table. Values\nare followed through registers, branches, calls and the routines they are\nhanded to, across files. Where an indirect call cannot be traced, `impact` and\n`find_callers` name the nearest ones instead of ending silently; library calls\nthrough a base in A6 (`JSR _LVOOpen(A6)`) are counted apart from them). The\nchecked-in language fixtures exercise extraction floors for every\nsupported language variant, so an empty or materially degraded scanner fails\nthe build rather than quietly yielding nothing. Markdown is indexed as a source\nin its own right — sections are nodes and backticked names are edges to the\ncode they name.\n\nBundled extraction rules work without setup. The public override interface is\ncurrently limited to Go, Rust, and Ruby; other bundled rule files are internal\nimplementation detail and are not a compatibility promise. To replace a\nsupported language rule explicitly:\n\n```sh\nglasir analyse . --language-rules /path/to/rules\nglasir serve . --watch --language-rules /path/to/rules\n```\n\nRules are validated before indexing, stay fixed until restart, and participate\nin snapshot validity. See [language rule maintenance](parsers/README.md) for the\nschema, examples, tests and current extraction limits.\n\n## Asking in other languages\n\n**English is the preferred language for questions.** Code is almost always\nnamed in English, and `query_graph` matches the words of a question against\nidentifiers and documentation — it does not translate.\n\nEverything structural is independent of language: `impact`, `find_callers`,\n`shortest_path`, `explain_node`, `cycles` and `overview` work on symbols and\nedges, and documentation and comments in any language are indexed.\n\nA question in another language still finds English code when its technical\nwords share a stem with the code's — which technical vocabulary often does:\n*server*, *serveur*, *servidor*; *compaction*, *compactage*, *compactación*;\nGerman *Konfiguration* reaches `configuration` and *Aktion* reaches `action`,\nbecause German `k` and `z` are matched against English `c`. Asked in\nGerman, French and Spanish, the same questions about this repository land in\nthe right file, though English ranks the exact symbol highest.\n\nWhat does not work is a word pair with no common stem — *Raum* and `room`,\n*Leinwand* and `canvas`. Bridging those needs a dictionary or a language model,\nand Glasir deliberately has neither in its query path, so the same question\nalways returns the same answer. When a question misses, the reply lists the\nrepository's own vocabulary so the assistant can ask again in its words.\n\n| Question language | Status |\n|---|---|\n| English | Preferred; the reference for all benchmarks |\n| German | Supported: question words are filtered, spelling differences to English are bridged |\n| Other Latin-script languages | Works through shared technical stems; question words such as *comment* or *qué* are not filtered and count as search terms |\n| Other scripts (Cyrillic, CJK, …) | Not measured; free-text questions are unlikely to match English code, structural tools are unaffected |\n\nIn practice the assistant in front of Glasir usually phrases its tool calls in\nthe code's own terms, whatever language you ask it in.\n\n## Running it for a team\n\n```\nglasir serve /srv/repo --http 0.0.0.0:8899   # refuses to start: no auth\nglasir token add anna --days 90              # prints the token once\nglasir serve /srv/repo --http 0.0.0.0:8899 --watch\n```\n\n- **Per-user tokens**, stored as SHA-256 only. Revoking takes effect on the next\n  request, without a restart.\n- **Audit log** in JSONL: timestamp, identity, tool, question and result size,\n  written off the request path so a full disk costs records, never requests.\n- **A non-loopback address with no authentication refuses to start.** Loopback\n  stays open — that is the single-machine case.\n- `Origin` validation and a bounded connection pool, per the MCP transport spec.\n\n## Deployment boundary\n\nFor an enterprise deployment, run the separate\n[Glasir Control](https://github.com/Attackwave/glasir-control) service at the\npublic edge. It supplies OIDC resource-server validation, repository routing,\ncode-host permission mirroring, central audit and the public TLS boundary.\nGlasir Core remains the private data plane for an individual source tree and\ndoes not expose those control-plane capabilities itself. Run every Core process\nwithout a public shared secret:\n\n```sh\ncd /srv/alpha && CONTROL_TOKEN=\"$(glasir token add control)\"\nglasir serve /srv/alpha --http 7001 --behind-control-plane --watch\n# rights.tsv in glasir-control contains the one-time CONTROL_TOKEN for alpha.\n```\n\n`--behind-control-plane` refuses `--token` or a missing per-service token and\nremains loopback-only unless the explicit CIDR gate below is supplied. The\npublic proxy validates the browser origin and removes it before forwarding;\nthe Core keeps its loopback DNS-rebinding defence.\n\nFor Kubernetes, the Core may bind to a pod interface only with an explicit\nnetwork boundary. Pair the Core's Service with a default-deny NetworkPolicy and\npass the exact pod CIDR of the control plane:\n\n```sh\nglasir serve /srv/alpha --http 0.0.0.0:7001 --behind-control-plane \\\n  --control-plane-cidr 10.1.0.0/16 \\\n  --tls-cert /run/tls/core.crt --tls-key /run/tls/core.key \\\n  --control-plane-client-ca /run/tls/ca.crt --watch\n```\n\nThe CIDR is an additional connection gate, not an authentication substitute:\nthe per-service token remains mandatory. Connections outside it are dropped\nbefore HTTP parsing and receive no oracle response. IPv6 is intentionally not\naccepted by this first networked data-plane mode.\n\nFor a production control plane, CIDR plus the per-service token is only a\nfirst boundary: use mutual TLS. `--control-plane-client-ca` is accepted only\nwith `--behind-control-plane` and a server certificate/key. The Core then\nrequires the forwarding control-plane client certificate to chain to that CA;\nthe certificate name must match the Core Service DNS name. Keep the CA,\nserver certificate/key and control-plane client certificate in a workload\nsecret (or an external-secrets provider), rotate them independently, and do\nnot terminate this hop at a plaintext sidecar.\n\nThe Core's standalone scope excludes public identity administration and\ncross-repository orchestration. TLS is available (`--tls-cert` / `--tls-key`)\nfor standalone use or terminates at the reverse proxy. Current Core boundaries\nand guarantees are documented in `docs/architecture.md`.\n\n## Building\n\nTagged releases publish signed, multi-architecture (`linux/amd64`,\n`linux/arm64`) Core images to GHCR. Release assets include a CycloneDX SBOM;\nthe release workflow also creates build provenance for every archive and for\nthe published image manifest, and a keyless Cosign signature for the latter. Deploy by digest, not a mutable\ntag. CI also rebuilds the production Dockerfile and verifies its non-root\nruntime on every change.\n\n```\ncargo build --release\ncargo test          # root checks and CLI integration tests\ncargo test --manifest-path parsers/Cargo.toml --target-dir target/parser-tests\nglasir selfcheck   # the same checks in one pass, in order\nglasir benchmark . --check          # recall against bench/baseline.txt\nglasir langcheck bench/langs --check # extraction against bench/languages.txt\nglasir langcheck bench/langs --anomalies --check  # names no fixture may show\n```\n\nThe benchmark is a guard, not a report: a set falling below its floor fails the\nbuild.\n\n`langcheck` guards the other half. The recall floors read this repository, which\nis Rust, so they cannot see a scanner that stopped extracting from Julia; this\nreports definitions and edges per 1,000 lines per language, plus the share of\nreferences attributed to the file rather than to an enclosing definition — the\nnumber that moves from 3% to 99% when a language keyword stops being\nrecognised.\n\n`--anomalies` reads what the counts cannot: a definition named like a keyword\nor like a word its own rule file gives a role (`fun ref charge` read as `ref`),\nnames holding whitespace or quotes, and ranges that cannot exist, with the\nplaces to read. Pointed at real code it is a triage report; on the fixtures\nit fails the build.\n\n## Operating it\n\n`/health` answers without a credential for liveness. `/ready` is the deployment\nprobe: it proves a graph is loaded and returns its `indexed_at`, publish\n`generation` and any last re-index error. A server asleep after\n`--sleep-after` reports `\"asleep\":true` with the size and `indexed_at` of the\ngraph the next request loads again. `/metrics` answers without a\ncredential with Prometheus counters — calls, errors and time per tool, refusals,\na latency histogram, and the graph's size. `/mcp` stays behind the token either\nway.\n\nSet `--max-index-age 900` on a watched production server to make `/ready`\nreturn `503` once its loaded graph is older than the agreed 15-minute SLO. The\nlast valid graph remains available while the orchestrator replaces or repairs\nthe instance.\n\n## Several repositories at once\n\nA client and the server it talks to usually live in two repositories. Serve\nthe directory holding both, and they are one graph:\n\n```sh\ncd ~/work            # holds shop-web/ and shop-api/, each a git repository\nglasir install .     # registers the server for this directory, hooks in each repository\n```\n\nAn HTTP request in one repository reaches its handler in the other, so\n`find_callers`, `impact` and `affected_tests` cross the boundary without a\ncontrol plane. Symbols carry the repository as the first path segment\n(`shop-api/src/orders.rs#create`). The tools that ask git — `detect_changes`,\n`affected_tests`, `co_changes`, `change_history`, `review` — ask each\nrepository and prefix what it answers; a revision that only one repository\nhas diffs that one. A directory with no repository at or under it is refused\nrather than reported as unchanged.\n\n## Pull requests\n\n`glasir review [rev]` prints what `detect_changes` and `affected_tests` say\nabout a change, as Markdown: the risk level and its reasons, the changed\nsymbols, their dependents by distance, the tests that reach the change, and\nthe files the graph does not cover. `--fail-on medium` makes it a gate.\n\nThe repository ships a composite GitHub Action that runs it on every pull\nrequest and keeps one comment up to date:\n\n```yaml\non: pull_request\npermissions:\n  contents: read\n  pull-requests: write\njobs:\n  review:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0          # the base commit has to be in the clone\n      - uses: Attackwave/glasir@v0.13.2\n        with:\n          fail-on: high           # optional: block the merge at this risk\n```\n\nThe graph is built on the runner from the checked-out tree; no source leaves\nit. The action downloads the release named by its own tag and checks the\narchive against the published SHA-256 and its build provenance (releases\nbefore 0.11.0 carry none and are refused). A repository that pins actions to\na commit writes `Attackwave/glasir@<commit of the tag> # v0.13.2`; the action\nthen downloads that version's release.\n\n## Feedback and contributing\n\nReport a bug or ask for a feature in\n[GitHub Issues](https://github.com/Attackwave/glasir/issues). Changes come in as\npull requests; [CONTRIBUTING.md](CONTRIBUTING.md) lists what a pull request\nneeds. Report a vulnerability privately as [SECURITY.md](SECURITY.md) describes.\n\n## Licence\n\nApache-2.0. See `LICENSE` and `NOTICE`.\n\nEvery dependency linked into the binary is permissively licensed — MIT,\nApache-2.0, ISC, BSD-3-Clause, Unicode-3.0 or Unlicense — with no copyleft.\nEach release archive carries their copyright notices and licence texts in\n`THIRD-PARTY-LICENSES.txt`, generated by `cargo about` from the tagged\n`Cargo.lock`; `about.toml` lists the accepted licences and the release fails on\nany other. CI also generates a CycloneDX SBOM on every run, so the claim is\ncheckable rather than asserted.\n"}