Open-source context engine · Apache-2.0

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.

MCP · HTTP · Python / TS / Go SDKs Built for red and blue teams SQLite on a laptop → Postgres on k8s
host:web-01 · every source, over time ask · budget 400
observation packed sources disagree 351 of 400 tokens spent
// 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
}
The problem

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.”

127,717tokens

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 send
616tokens

Mean 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
15of 18

“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 + EPSS
How it works

The 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
  1. One envelopekind: claim or kind: document via HTTP, batch NDJSON, Kafka, Fluent Bit, Vector or the Go gateway.
  2. Append an observationSubject, predicate, object, source, confidence, valid_from/valid_to and recorded_at. Idempotent on event_id.
  3. Enrich asynchronouslyChunk and embed documents, extract claims with an LLM, resolve entities (natural keys → aliases → embeddings → adjudication). Merges are reversible.
  4. PrecomputeA bitemporal graph with time-bounded edges, plus cited per-entity dossiers so reads stay cheap.

Read path

stingy
  1. Resolve the scopeOne subject, or a bounded walk over containment and relation edges. Hub entities are never silently sampled.
  2. RankBM25 and embedding similarity fused by reciprocal rank, weighted by recency and source confidence.
  3. Conflicts firstWhen independent sources disagree on the same instant, that disagreement leads the pack. LoomCtx never picks a side silently.
  4. 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_tokens reports what was spent.
The retrieval contract

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
  1. 1
    Time travel on every readvalid_at asks what was true at a moment. as_of asks what LoomCtx knew at a moment. Incident timelines get both.
  2. 2
    The budget is mandatoryThere is no read without one. Small budgets get the densest facts. LoomCtx packs by value per token, not by position.
  3. 3
    Typed itemsbrief, fact, relation, doc_excerpt or conflict. Conflicts ship first, so the agent sees a disagreement before anything else.
  4. 4
    Citations on everythingEvery item points at the observations behind it, with source and time. GET /v1/observations/{id}/prov exports W3C PROV-O.
  5. 5
    Expansion handlesWhat didn't fit becomes a stateless, HMAC-signed handle with a token estimate. The agent calls expand to pull one thread deeper.
  6. 6
    Honest 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.
  7. 7
    What it actually costcharged_tokens is what the pack spent, always within the budget. A lease lookup with a budget of 600 cost 60.
One store, both sides of the line

Blue asks what happened. Red asks what's reachable.

Blue team

SOC · IR · vuln management · detection engineering
  • Which host held 10.0.4.7 on August 20th?A bitemporal read over DHCP leases. The answer is cited to dhcp-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 analysis
  • What can this account reach?find_path walks 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.

Where it fits

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 ofYou getWith LoomCtx
Vector DB / RAGTop-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 + SQLExact 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 databaseTraversal 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 searchFast 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.
Measured, not claimed

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
BudgetLoomCtxVectorNewest facts only
40079%75%49%
80084%77%75%
1,60086%78%75%
3,20096%86%76%
docs/evaluation.md · 2026-10-14, the current build. Runs since 2026-09-24 all agree within 2 points.

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
docs/models.md · 2026-09-28, AD 2026-10-14 · every model, cost and task below ↓

On Kubernetes

  • Ingest through the gateway237 /s
  • Asks, p5083 ms
  • Ask throughput186 /s
  • Standard AD audits at 500k, one shared Postgres91%
GKE, auth on, real corpus, 0 errors · 2026-09-27; AD on Postgres 2026-10-15

When the primary dies

  • Postgres failover15 s
  • Writes lost0 of 233
  • Soak test18 h
CloudNativePG, 3 instances, primary deleted under load

Identity at scale

  • Entra + Okta observations1.59 M
  • Blind AD attack chains recalled11 / 11
  • Decoy paths leaked0
docs/ad-eval.md · deploy/README.md · 2026-09-27
Model breakdown

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.

1
Core tasksFour agent tasks on real vulnerability data (CISA KEV, NVD, EPSS; 542,100 facts). 35 models, 5 runs each, 2026-09-28. Everything from here to the AD section.
2
Active Directory auditsEight identity audits on generated forests of 10,000 and 500,000 objects. 35 models, 5 runs each, 2026-10-14. Jump to it ↓

Scorecard 1 · Core tasks on real vulnerability data

2-hop exposure“Is web-01 exposed to anything CISA lists as exploited in the wild?” The agent finds the host, walks host → software → CVE, and must name at least 16 of the 32 KEV CVEs with the software each affects. Naming a non-KEV CVE as part of the exposure fails.
Lease“Which host held 10.0.4.7 on 2026-08-20?” A time-travel read. The answer must name laptop-17 and must not present web-01, the earlier holder, as the answer.
Conflict“What do sources disagree about for CVE-2026-0544's CVSS?” The agent must report both values, 9.8 from NIST and 5.5 from the CNA.
OS“What OS does web-01 run?” Two sources disagree. The agent must give both values, not pick one.

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.

Scorecard 2 · Active Directory audits

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 accounts500,000 objects
Users / groups / computers10,000 / 1,500 / 2,000370,000 / 55,000 / 75,000
Facts loaded175,5606,502,512
Import, one container39 s49 min
Standard audits passed93% 794 / 85792% 795 / 860
Models perfect on them (25/25)1824
Standard audits, named queries off90% 772 / 85690% 770 / 855
Harder audits passed82% 414 / 50681% 411 / 506
Median time per standard audit21 s23 s
Agent steps per standard audit5.75.7
Tokens the agent read per standard audit58.5k58.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 objectsOnOffChange
Cross-domain admins93%85%+8
Coverage87%93%-6
Nested groups92%95%-3
Delegation94%82%+12
Trusts97%96%+1
Access harder76%74%+2
Local admin harder82%80%+2
Kerberoastable admins harder85%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 buildStandard 5Harder 3Median audit
Postgres, one shared database91% 777 / 85482%¹21 s
SQLite, a private file per agent92% 787 / 86079%20 s
Postgres, named queries off91% 774 / 85481%¹23 s
SQLite, named queries off89% 756 / 84579%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.

Cross-domain admins“Which dev.local accounts are effective members of Domain Admins in corp.local, and is each enabled?” Five are, up to three groups deep; one is disabled and must be called out.
Coverage“Which groups is lab.local's labsvc-backup in?” That export never collected membership. The right answer says so; a confident “none” or a guessed group fails.
Nested groupsA membership report for one account: five groups, the last four levels up. A group nested into one of its groups, and a sibling group, must not be listed.
Delegation“Which non-DC computers are trusted for unconstrained delegation?” Three servers among the computers; the domain controllers and a constrained-delegation server are distractors.
TrustsEvery trust corp.local has, with its direction, whether it's transitive and whether SID filtering is on. One partner is non-transitive, one quarantined.
Access harder“Which Domain Admins have rights over workstation WS-AUDIT01, and through what?” One through a group holding GenericAll, one through a direct WriteDacl grant. An operator who isn't an admin, and an admin with no grant, are decoys.
Local admin harder“Who is local administrator on WS-AUDIT01?” Local group membership isn't in an AD export. Saying so passes; presenting accounts as its local admins fails.
Kerberoastable admins harder“Which enabled Domain Admins, nested included, are Kerberoastable?” One nested admin has an SPN. A disabled admin with one, and a service account that isn't an admin, are decoys.
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.

Connectors

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.

vulns vulnCISA KEV, NVD, EPSS and CISA Vulnrichment, with cross-scorer CVSS conflicts kept.
osv vulnOSV, GHSA, PYSEC, Go and npm advisories, fused to CVEs by alias.
nmap scannmap -oX reports: hosts, ports, services, software, OS guesses.
ldap identityRaw LDAP / LDIF from Active Directory: membership → ACE → trust reachability.
entra identityEntra ID Graph export: users, groups, service principals, roles, OAuth consents.
okta identityUsers, groups, apps, MFA and admin roles, in the same iam namespace as Entra.
cloud inventoryterraform show -json, tfstate, or an AWS Config snapshot.
ocsf detectionsOCSF vulnerability, detection, security and incident findings, both in and out.
stix threat intelSTIX 2.1 import and export.
attack taxonomyThe MITRE ATT&CK bundle as a shared catalog of techniques.

MCP, for agents

Fifteen tools over stdio or streamable HTTP, aligned to MCP 2026-07-28 with output schemas and structured content.

askask_scopefind_pathlist_entitiesrun_querysearch_documentsexpandrecord_claimanswer

HTTP, for everything

FastAPI with an OpenAPI spec. Ingest single envelopes or up to 1,000 as a batch or NDJSON stream.

/v1/ask/v1/path/v1/list/v1/query/v1/documents/search/v1/expand/v1/ingest/batch

SDKs

Thin clients whose types are generated from, or validated against, the published contracts.

PythonTypeScriptGo

Pipelines

Plug into the shippers you already run, or feed the Go gateway and NATS JetStream directly.

Fluent BitVectorKafka / Redpandabulk NDJSON
Deploy

One container on a laptop. The same contract at cluster scale.

The one-container shape

agent→loomctx · SQLite

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

shippers→ Go gateway ×2–10→ NATS JetStream R=3→ writers→ Postgres + pgvector (HA)

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
Run it

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.

bash
# 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.

Star on GitHub