Your agent needs the right 400 tokens, with receipts.
LoomCtx remembers everything your tools have said about a host, an IP, an account or a CVE, and when it was true. When an agent asks, LoomCtx answers with a small, cited, time-aware context pack that fits the token budget the agent gave it. Sources that disagree come first. Anything cut is listed so the agent can fetch it.
// POST /v1/ask → "what operating system does this run?" (abridged) { "items": [ { "kind": "conflict", "text": "CONFLICT on host:web-01 runs_os: nmap-scan says 'ubuntu-22.04' as of 2026-08-18; asset-inventory says 'fedora-44' as of 2026-07-03", "citations": [ "obs_7f3a…", "obs_2c91…" ] }, { "kind": "fact", "text": "web-01 listens_on_port 443", … } ], "handles": [{ "hint": "76 more 'logged_in_by' observations" }], "omitted_handles": 4, "charged_tokens": 351 }
Agents get nothing, or they get everything.
Security teams hold years of facts about the same things, spread across scanners, inventories, identity providers, DHCP logs and advisories. An agent asked about one host gets either a blank or a dump of rows. LoomCtx sits in between: “what do we know about web-01 that matters to Tuesday's alert, in under 800 tokens, with citations.”
Mean prompt when the model is handed the full record for each question. The largest ran to 1.87 million tokens, and six multi-hop questions were too big to send at all.
full-context baseline · 96% on the 47 it could sendMean prompt for a LoomCtx context pack at a 3,200-token budget. Accuracy: 96% on all 53 questions, including the multi-hop ones.
loomctx@3200 · same corpus, same reader model“Which CISA-KEV CVEs affect software this host runs?” LoomCtx answered it in 15 of 18 runs. Vector search, newest facts only and full context got 0 of 18.
542,099 real observations: CISA KEV + NVD + EPSSThe write path is greedy. The read path is stingy.
Take in everything with its provenance, enrich it in the background, then answer each read from cheap precomputed material, inside the budget the caller set.
Two front doors, one model
Structured claims become observations directly. Documents are stored verbatim, then mined for claims with character-level provenance. Retrieval never cares which door a fact came through.
Bitemporal from day one
Every fact records when it was true and when LoomCtx learned it. “Which host had 10.0.4.7 during the incident window?” is one query, and it returns laptop-17, not web-01, which held the lease until August 10.
The graph is the index, not the payload
Agents never query the graph. They ask a question with a budget and get a pack. A multi-hop question (“KEV CVEs on software this host runs”) is a single scoped ask.
Append-only, never silently wrong
Observations are never edited, only superseded, and corrections are recorded. Redaction suppresses history and never rewrites it. When LoomCtx didn't look at everything, the pack says so.
Malleable by construction
Register your own entity types and predicates per namespace, swap SQLite for Postgres, plug in your own enrichers. None of it needs a fork. The tutorial builds a vuln-management domain without touching core code.
Every derived fact is cited
Each pack item carries the observation ids behind it. A faithfulness check fails any answer that cites an id the model was never shown.
Write path
greedy- One envelope
kind: claimorkind: documentvia HTTP, batch NDJSON, Kafka, Fluent Bit, Vector or the Go gateway. - Append an observationSubject, predicate, object, source, confidence,
valid_from/valid_toandrecorded_at. Idempotent onevent_id. - Enrich asynchronouslyChunk and embed documents, extract claims with an LLM, resolve entities (natural keys → aliases → embeddings → adjudication). Merges are reversible.
- PrecomputeA bitemporal graph with time-bounded edges, plus cited per-entity dossiers so reads stay cheap.
Read path
stingy- Resolve the scopeOne subject, or a bounded walk over containment and relation edges. Hub entities are never silently sampled.
- RankBM25 and embedding similarity fused by reciprocal rank, weighted by recency and source confidence.
- Conflicts firstWhen independent sources disagree on the same instant, that disagreement leads the pack. LoomCtx never picks a side silently.
- Pack to budgetValue-per-token selection up to the budget. Every item is cited. What didn't fit becomes a signed expansion handle, and
charged_tokensreports what was spent.
Anatomy of a context pack.
The pack is the API. MCP, HTTP and all three SDKs serialize this same shape, and it is published as JSON Schema and OpenAPI, drift-checked in CI.
POST /v1/ask { "namespace": "netsec", "subject": { "type": "host", "key": "web-01" }, "question": "who logged in this week?", "as_of": null, "valid_at": null, 1 "budget": 800 } 2 200 OK { "items": [ { "kind": "fact", 3 "text": "jdoe logged into web-01 on 2026-09-01 14:03 UTC.", "score": 0.92, "citations": [{ "observation_id": "…", 4 "source": "auth-log", "observed_at": "2026-09-01T14:03:00Z" }] } ], "handles": [ 5 { "id": "h2_…", "kind": "more", "est_tokens": 2664, "hint": "74 more 'logged_in_by' observations for host:web-01" }, { "id": "h2_…", "kind": "conflict", "est_tokens": 71, "hint": "CONFLICT on host:web-01 runs_os did not fit the budget" } ], "omitted_handles": 3, "truncated": [{ "entity": { "type": "product", 6 "key": "linux:linux_kernel" }, "gathered": 1000, "total": 2740 }], "charged_tokens": 781 } 7
- 1Time travel on every read
valid_atasks what was true at a moment.as_ofasks what LoomCtx knew at a moment. Incident timelines get both. - 2The budget is mandatoryThere is no read without one. Small budgets get the densest facts. LoomCtx packs by value per token, not by position.
- 3Typed items
brief,fact,relation,doc_excerptorconflict. Conflicts ship first, so the agent sees a disagreement before anything else. - 4Citations on everythingEvery item points at the observations behind it, with source and time.
GET /v1/observations/{id}/provexports W3C PROV-O. - 5Expansion handlesWhat didn't fit becomes a stateless, HMAC-signed handle with a token estimate. The agent calls
expandto pull one thread deeper. - 6Honest truncationIf LoomCtx stopped gathering before it saw everything, the pack says how much it read. An agent never mistakes a partial read for a full one.
- 7What it actually cost
charged_tokensis what the pack spent, always within the budget. A lease lookup with a budget of 600 cost 60.
Blue asks what happened. Red asks what's reachable.
Blue team
SOC · IR · vuln management · detection engineeringWhich host held 10.0.4.7 on August 20th?
A bitemporal read over DHCP leases. The answer is cited todhcp-log, not guessed from the current state.Which KEV-listed CVEs affect software web-01 runs?
A scope walk from host → software → CVE, filtered by CISA KEV and scored with EPSS, delivered as one pack.Is CVE-2026-0544 a 9.8 or a 5.5?
NIST and the CNA disagree. LoomCtx puts that disagreement first and cites both, rather than quietly picking one.
Red team
pentest · adversary emulation · attack-path analysisWhat can this account reach?
find_pathwalks group membership → ACE → trust from raw LDAP/LDIF, Entra ID and Okta exports. Every hop is cited, and disabled accounts are flagged on the path.Which exposed services run exploitable versions?
nmap service fingerprints joined to NVD, OSV and KEV through cross-vendor product resolution.What's in the cloud estate, and what does it connect to?
Terraform state or an AWS Config snapshot, read into the same graph as the scans and identities.
Both teams use the same engine and the same packs. Each API key is scoped by namespace, read/write, entity type, source and expiry, so an engagement's data stays inside that engagement.
Not another vector database.
LoomCtx doesn't replace your SIEM, your graph or your RAG pipeline. It's the layer that decides what an agent actually gets to read.
| Instead of | You get | With LoomCtx |
|---|---|---|
| Vector DB / RAG | Top-k chunks by similarity. No idea when a fact held, who asserted it, or that two sources disagree. | Append-only, bitemporal observations. Conflicts lead the pack, and every item cites the observations behind it. |
| Postgres + SQL | Exact answers to questions you already know how to phrase, and however many rows come back. | A mandatory budget on every read, ranked slices, handles for what was cut, and truncated when the read was partial. |
| A graph database | Traversal power, but the agent talks to the graph and gets whatever the query returns. | The graph is the index. A multi-hop question is one ask_scope call, and the answer comes back as a budgeted pack. |
| SIEM search | Fast search over events. Entities are a pivot, not a memory. | Entity-centric memory across sources, with provenance, conflict detection and time travel on every read. |
| “Give the agent the DB” | Full access, a context window full of rows, and no record of what the model saw. | Scoped keys, budgets, handles, per-item citations, and a check that catches an invented citation. |
The numbers, with the method in the repo.
Every figure here comes from a scorecard or load test you can re-run from the repository. The method, the corpus and the misses are written up next to the results.
Retrieval accuracy by token budget
53 questions over 542,099 real vulnerability observations. The reader is deepseek-chat-v3.1; each figure is the mean of 3 runs. All three start from the same facts with the same token budget and differ only in which facts they pick: LoomCtx's ranked pack, the facts most similar to the question (vector), or simply the newest ones.
Show as table
| Budget | LoomCtx | Vector | Newest facts only |
|---|---|---|---|
| 400 | 79% | 75% | 49% |
| 800 | 84% | 77% | 75% |
| 1,600 | 86% | 78% | 75% |
| 3,200 | 96% | 86% | 76% |
Which models can drive it
Four agent tasks over MCP, five runs each, on 35 models. Rule-scored, and a correct answer without a citation fails.
- Models with a perfect 20/2015 of 35
- Runs citing data the model never saw0 of 686
- Cheapest clean pass per 4-task run$0.01
- Whole 35-model sweep, 5 runs each≈ $46
- Perfect on the 5 standard AD audits, 500,000-object forest24 of 35
On Kubernetes
- Ingest through the gateway237 /s
- Asks, p5083 ms
- Ask throughput186 /s
- Standard AD audits at 500k, one shared Postgres91%
When the primary dies
- Postgres failover15 s
- Writes lost0 of 233
- Soak test18 h
Identity at scale
- Entra + Okta observations1.59 M
- Blind AD attack chains recalled11 / 11
- Decoy paths leaked0
Which model should drive it, and what it costs.
LoomCtx doesn't ship a model. Your agent brings its own, so we ran 36 of them through the same four tasks, five times each, against real data. Every number below is what the run measured and what OpenRouter billed.
Scorecard 1 · Core tasks on real vulnerability data
Every task also has to be faithful: it must cite at least one observation the model was shown in that run, cite nothing it wasn't shown, and name no CVE that doesn't exist. A correct answer with no citation fails.
Speed, cost and reliability
Each dot is a model: how long one run took end to end, what it cost, and how often it was right. The best picks sit bottom left in solid blue; those are named. Both axes are log scale; hover any dot for its name and numbers. It starts on the core tasks; switch to the Active Directory audits to see the same trade-off for the five standard audits on the 500,000-object forest.
Core tasks · reliability against cost
One row per result, from all 20 tasks passed down. Each dot is a model, placed by what one four-task run cost. Further left is cheaper; hover a dot for its name. The count in brackets is how many models got that result. * Mistral Large was rate-limited, so only 5 of its 20 task-runs were scored.
Core tasks · cost per correct answer
What a run cost, divided by the tasks it got right. This ranks price and reliability together. Log scale. * Mistral Large had only 5 scored task-runs.
| Model | Passed | 2-hop | Lease | Conflict | OS | $ / run | $ / correct | Est. / month | Time / run | Steps | Tokens read / run | List $ / M in · out | Notes |
|---|
Order. Complete rows first, then pass rate, then cost as the table shows it, then speed. A cost gap too small to print doesn't outrank a model that answers several times faster. Click any column header to re-sort.
Reading the columns. A run is all four tasks once, so $ / run and Time / run cover four questions. $ / correct is total cost divided by tasks passed. Est. / month is questions per day × 30 × the cost of one task (a run divided by four). Treat it as a rough guide: real questions vary, and what an agent pays is mostly what it reads (the tokens column), not what it writes.
Setup. OpenRouter, 2026-09-28, temperature 0, up to 20 steps and 8,000 output tokens per turn. The corpus was 542,100 real observations (CISA KEV, NVD, EPSS and seed hosts), run as Kubernetes Jobs. List prices are OpenRouter's at the time of the run. mistral-small-2603 isn't listed: it was rate-limited upstream, with 1 of 20 task-runs scored. Full method and the command to re-run it: docs/models.md.
92% at half a million objects, the same as at 10,000.
The same agents, pointed at a generated Active Directory export instead. The large forest holds 370,000 users, 55,000 groups and 75,000 computers (6.5 million facts) across a parent domain, a child domain and two external trusts, plus a second domain exported without group membership. Five standard reviews an identity team runs, five times each per model, pass 92% of the time there and 93% on a 10,000-account forest. Three harder audits, scored separately, show where agents still struggle. The data is synthetic.
10,000 accounts against 500,000 objects
| 10,000 accounts | 500,000 objects | |
|---|---|---|
| Users / groups / computers | 10,000 / 1,500 / 2,000 | 370,000 / 55,000 / 75,000 |
| Facts loaded | 175,560 | 6,502,512 |
| Import, one container | 39 s | 49 min |
| Standard audits passed | 93% 794 / 857 | 92% 795 / 860 |
| Models perfect on them (25/25) | 18 | 24 |
| Standard audits, named queries off | 90% 772 / 856 | 90% 770 / 855 |
| Harder audits passed | 82% 414 / 506 | 81% 411 / 506 |
| Median time per standard audit | 21 s | 23 s |
| Agent steps per standard audit | 5.7 | 5.7 |
| Tokens the agent read per standard audit | 58.5k | 58.9k |
| Spend on the standard audits, 35 models × 5 | $59.68 | $60.79 |
Same 35 models, five runs each, one build, named queries on unless noted. Only the import grows with the data: what an agent reads depends on the question, not on the size of the directory.
Named queries, on and off
A named query is a prepared question a connector ships with the schema. The LDAP connector registers seven, such as enabled_members, account_groups, kerberoastable_users and unconstrained_delegation_hosts. An agent runs one in a single call and gets exact, cited rows back:
run_query("enabled_members", group="Domain Admins")
Without them, the agent builds the same answer itself, for instance with a list call that walks member_of into the group and filters on enabled, or with a scoped ask. Every other tool is the same.
"Named queries off" is the same engine, data and models with run_query hidden (LOOMCTX_MCP_QUERIES=0). It measures what the prepared queries add over the agent composing its own calls. The headline numbers use the default, on.
| Audit, 500,000 objects | On | Off | Change |
|---|---|---|---|
| Cross-domain admins | 93% | 85% | +8 |
| Coverage | 87% | 93% | -6 |
| Nested groups | 92% | 95% | -3 |
| Delegation | 94% | 82% | +12 |
| Trusts | 97% | 96% | +1 |
| Access harder | 76% | 74% | +2 |
| Local admin harder | 82% | 80% | +2 |
| Kerberoastable admins harder | 85% | 82% | +3 |
They help most where a query matches the question (cross-domain admins, delegation, Kerberoastable admins) and cost a few points on coverage and nested groups. Overall: standard audits 92% on against 90% off; harder audits 81% against 77%.
On Postgres, the scale deployment
| 500,000 objects, one build | Standard 5 | Harder 3 | Median audit |
|---|---|---|---|
| Postgres, one shared database | 91% 777 / 854 | 82%¹ | 21 s |
| SQLite, a private file per agent | 92% 787 / 860 | 79% | 20 s |
| Postgres, named queries off | 91% 774 / 854 | 81%¹ | 23 s |
| SQLite, named queries off | 89% 756 / 845 | 79% | 25 s |
35 models, five runs each, all eight audits. Postgres 17 with pgvector served about 13 agents at once from one database, as a Kubernetes deployment would; SQLite gave each agent its own copy. Measured 2026-10-15. ¹ The shared database had writes on, and later runs cited findings two agents saved there, so the Postgres access, local-admin and delegation cells are not a clean comparison; they will be re-run on a fresh copy with writes off.
What differs, and what doesn't
Same answers. Replaying 643 recorded agent calls against both returned the same totals and the same listings on every one.
Per audit, it goes both ways. Cross-domain admins scored higher on SQLite, access and local admin higher on Postgres. The swings sit in a few models that flip all five runs together on tiny input differences, here the observation ids each store mints when it loads. That is store-to-store variance, not the database.
Speed. Small lookups pay a network hop on Postgres (16 ms against 4 ms at the median); scoped questions take the same time on both; the slowest call of the whole sweep took 29 s. Loading the 6.5 million facts into Postgres takes 45 minutes.
| Model | Standard 5 | Harder 3 | Standard, 10k | Standard, no named queries | Admins | Coverage | Groups | Delegation | Trusts | Access | Local admin | Kerberoast | $ / 5 audits | Time / 5 audits |
|---|
Same rules and order. Scored as sets, on what an answer claims: every expected account, group, host or trust property present, no distractor put forward as an answer, and faithful citations. The local-admin audit is the one exception on citations: its right answer is that the data isn't there, which may have nothing to cite. Every failing answer was read by hand before these numbers were published. Standard 5, Harder 3, the audit columns, cost and time are the 500,000-object forest with named queries on (the default). Standard, 10k is the five standard audits on the 10,000-account forest; Standard, no named queries is the large forest with the connector's prepared queries hidden, so agents build every listing themselves (90% overall). Rows marked * lost task-runs to upstream rate limits.
Where agents still struggle. The three harder audits combine conditions (nested membership and an ACL grant, nested membership and an account flag) or ask something the export can't answer. Across all models they pass 81% of the time, at both sizes, against 92% for the standard five. They are scored and shown separately so the headline stays comparable with the standard audits published in September.
Some models are slow. Models marked † took over a minute and a half per audit at the median, so their timing is rough and they are a poor fit for an agent someone is waiting on.
Speaks the formats you already export.
Connectors parse files you already have. They don't scan anything or call your cloud APIs. The one exception is the vulnerability connector, which pulls public feeds in one command with no API keys.
nmap -oX reports: hosts, ports, services, software, OS guesses.iam namespace as Entra.terraform show -json, tfstate, or an AWS Config snapshot.MCP, for agents
Fifteen tools over stdio or streamable HTTP, aligned to MCP 2026-07-28 with output schemas and structured content.
HTTP, for everything
FastAPI with an OpenAPI spec. Ingest single envelopes or up to 1,000 as a batch or NDJSON stream.
SDKs
Thin clients whose types are generated from, or validated against, the published contracts.
Pipelines
Plug into the shippers you already run, or feed the Go gateway and NATS JetStream directly.
One container on a laptop. The same contract at cluster scale.
The one-container shape
Run docker compose up loomctx to get one engine and one SQLite file with vector search. No queue, and the LLM is optional. This is enough for an engagement laptop or a demo.
The scale shape
Stateless engines autoscale from 2 to 8, with enrichment workers as a StatefulSet. Kubernetes manifests ship in four shapes: default, scale, scale-pg and scale-pg-ha. Validated on GKE.
- Scoped API keys: stored hashed, hot-reloaded, and scoped by namespace, r/w, entity type, source and expiry
- Signed handles: HMAC-SHA256, so expansion handles can't be forged or altered
- Rate limits per grant, with a stricter bucket for LLM calls and
Retry-After - Default-deny NetworkPolicies, PDBs, read-only root filesystem, non-root user
- Supply chain: digest-pinned images, SHA-pinned actions, Trivy gate, SBOM and provenance
- Data lifecycle: tombstone redaction, retention purge, JSONL export and migration
- Bring any model: OpenRouter, Anthropic, OpenAI, Groq, Venice, or self-hosted Ollama / vLLM
- 1,088 tests, run on SQLite and on a Postgres lane, plus a scale-shape smoke test in CI
Five minutes, Docker only.
Nothing installs on your host. The seed dataset includes the moments shown on this page: the reassigned IP lease, the OS conflict on web-01, and a disputed contract owner.
# start the engine and load the demo dataset docker compose up -d --wait loomctx docker compose exec loomctx python -m loomctx.seed load # optional: real data next to it (CISA KEV + NVD + EPSS) docker compose exec loomctx python -m loomctx.connectors.vulns load \ --db /data/loomctx.db --bridge-seed-hosts # ask a focused, budgeted question curl -s -X POST localhost:8000/v1/ask \ -H 'content-type: application/json' -d '{ "namespace": "netsec", "subject": {"type": "ip", "key": "10.0.4.7"}, "question": "which host was assigned this ip?", "budget": 600 }'
Open source, Apache-2.0.
The source is open under Apache-2.0. It ships with the demo dataset behind this page: a reassigned IP lease, a real CVSS disagreement between NIST and a CNA, and an agent over MCP that walks one host to the KEV-listed CVEs its software carries, citing only what it was shown.