Gunnar

nordisk/Skidbladnir

.nornir/airgapped-agent.md

<!-- nornir:class: history -->
# Airgapped-agent guide

How to drive a **local, offline model** over a large Rust workspace using nornir
as the ground-truth provider — "go through the source and crates, find
dependencies / function calls" without the model ever walking raw source (where
it hallucinates edges). This is the practical companion to the *Explore* and
*Architecture* chapters in [design.md](design.md).

## The principle: tooling-first, model-second

A small local model is good at *summarizing and navigating* structured facts and
bad at *deriving* them from raw code. So don't ask it to "read the repo and work
out the call graph" — point it at nornir's MCP server and let it **query**. nornir
already holds every projection (deps, calls, symbols, full-text, vectors) keyed by
git SHA, served over MCP for exactly this. Reserve a frontier model (e.g. Opus)
for judgement; offload "index it, list it, find it" to the local one.

## Model + harness (as of mid-2026)

- **Model (24 GB VRAM, agentic tool-calling):** Qwen3-Coder-Next (MoE, ~256K
  context, fits a single 24 GB GPU) or Qwen3.6-27B (Apache-2.0, first-class
  tool-calling). Both are strong on Rust for mechanical work.
- **Harness:** Aider / opencode / Cline over Ollama or vLLM. (Claude Code is
  Anthropic-only.) Point the harness's MCP client at a running `nornir-mcp`.
- nornir stays **embedded or client, never both on one warehouse** — see the lock
  rule in [design.md](design.md). For a shared/served setup, run `nornir-server`
  and let the agent host talk to it.

## The query surface (what the model calls)

Start every session — and after any context reset — with orientation, then drill
in. None of these require a checked-out binary except where noted.

**Orient**
- `repo_overview <repo>` — one call: internal `depends_on` + `dependents` (with the
  crates that justify each edge), build-order position, and a knowledge digest
  (symbol + call-edge counts, sample symbol names). The cheapest "where am I".

**Crate dependencies (Mímir)**
- `deps_of` / `dependents_of` — direct neighbours.
- `affected_by_change <repos>` — the invalidation set (changed + transitive
  dependents) in build order — the re-check set for a release.
- `build_order` · `dep_path <from> <to>` (why does A depend on B?) ·
  `external_dep_users`.

**Function calls & symbols**
- From **syn** (source, always available once scanned): `knowledge_callers` /
  `knowledge_callees` / `knowledge_call_path` / `knowledge_symbol_lookup` /
  `knowledge_defined_in`.
- From **DWARF** (binary truth, persisted — see below): `dwarf_callers` /
  `dwarf_callees` / `dwarf_call_path` / `dwarf_symbol_lookup` / `dwarf_defined_in`.
  Each takes an optional `sha` for time-travel. Inlined edges only.

**Search**
- `search` (BM25 lexical) and `vector_search` (semantic; hybrid = RRF of both).

## Populating the facts (operator, once per repo/release)

The query tools read what's been persisted. Seed it with:

```sh
nornir map <repo>                       # syn scan + workspace index build + snapshot
nornir introspect symbols --persist \
        --repo <repo> target/debug/<bin>   # DWARF symbols + callgraph → dwarf_* tables
```

`nornir map` is the one-shot for the syn/knowledge + full-text side;
`introspect symbols --persist` adds the DWARF side from a debug-info binary (keyed
to the member's HEAD SHA). Both write to the warehouse, so every later query — and
every past SHA — is a warehouse read, no re-scan.

## Why this beats raw-source RAG

- **No hallucinated edges.** Call/dep graphs come from `cargo metadata`, `syn`, and
  DWARF — not the model's guess.
- **Fits the context budget.** The model pulls a few hundred bytes of answer per
  query instead of dumping files; a 24 GB model with a modest KV cache is plenty.
- **Time-travel for free.** Every fact is SHA-keyed, so "as of release X" is just a
  `sha` argument.
- **Airgapped.** No code leaves the machine; the model only ever sees query results.