QueryGraph does not compete with agent frameworks — it is the governed data and semantics plane they plug into. Goshawk built these doors; Sentinel guards them.
querygraph serve --port 8080 exposes the platform over
HTTP:
| Route | What |
|---|---|
GET /v1/health |
service, API version, release |
POST /v1/navigator/bundle |
the four-layer semantic bundle |
GET /v1/qglake/story |
the Resilience Desk with full evidence chain |
POST /v1/audit/verify-envelope |
envelope verification (a bad signature is a 200 + receipt, not a 5xx) |
POST /v1/models/import/{osi,croissant} |
semantic-model registry imports |
GET /v1/models, GET /v1/models/{name} |
registry listing and fetch |
GET /v1/search?q= |
search names, descriptions, ai_context, semantic types, ontology terms |
POST /v1/answer |
search → plan → synthesize → sign (Chapter 21) |
GET /.well-known/agent-card.json |
the A2A card (Chapter 20) |
With --require-auth, the governed routes
(models/import/*, answer) demand a signed
TypeDID envelope in the x-qg-envelope
header. The contract binds three things: the action must be
invoke; the resource must equal the request path (no
cross-route replay); and the payload must carry bodySha256,
the hash of the exact request body (no body swapping). The signature
verifies against the envelope’s own did:key verification
method.
An unauthenticated request gets a 401 whose receipt teaches the contract (captured from a live server):
$ curl -s -X POST http://127.0.0.1:8080/v1/answer \
-H 'content-type: application/json' -d '{"question":"?"}'{
"error": "missing x-qg-envelope header",
"receipt": {
"allowed": false,
"path": "/v1/answer",
"contract": {
"header": "x-qg-envelope",
"action": "invoke",
"resource": "<request path>",
"payload": {"bodySha256": "<sha256 hex of request body>"},
"signature": "ed25519 over querygraph-typedid-signing-v1"
}
}
}The Python client satisfies it in two lines:
from querygraph.api_auth import governed_post
from querygraph.typedid import TypeDidAgent
result = governed_post(
"http://127.0.0.1:8080", "/v1/answer",
{"question": "what is fiscal capacity?"},
TypeDidAgent.new("ApiClient"),
)The equivalence suite proves this live: it boots
qg-rust serve --require-auth, posts without the header
(asserting the 401 receipt), then with a Python-minted header (asserting
the governed answer).
querygraph serve --port 8080 [--require-auth]. The
x-qg-envelope header is a compact-JSON TypeDID envelope;
the middleware checks, in order: signature validity (against the
envelope’s did:key verification_method),
action == "invoke",
resource == <request path>, and
payload.bodySha256 == sha256(body). Failure returns
401 {error, receipt: {allowed: false, path, checks, contract}}.
Clients: querygraph.api_auth.mint_envelope_header /
governed_post.
The Model Context Protocol is how agent frameworks discover tools in 2026, and the stack speaks it from both languages:
querygraph mcp-serve, official
SDK): tools search_semantic_model,
resolve_metric (dialect fallback),
check_access (the dual gate — denials are receipts),
build_navigator_bundle, run_qglake_story,
verify_envelope, and answer_question;
resources qg://story/resilience-desk and
qg://models/current; stdio, SSE, and streamable-HTTP
transports; --osi loads your model, --rights
your governance JSON.querygraph mcp-serve): a
dependency-free JSON-RPC 2.0 implementation of the MCP handshake
(protocol 2024-11-05) over stdio, with the same governed surface,
sharing the /v1 internals (the answer core is literally the
same function).Any MCP client — Claude Code/Desktop, LangChain via
langchain-mcp-adapters, PydanticAI, LlamaIndex, CrewAI, the
OpenAI Agents SDK — reaches everything with zero adapter code.
$ printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| querygraph mcp-serve{"id": 1, "result": {"protocolVersion": "2024-11-05",
"serverInfo": {"name": "querygraph", "version": "0.4.0"}, …}}
{"id": 2, "result": {"tools": ["build_navigator_bundle",
"run_qglake_story", "verify_envelope", "import_semantic_model",
"search_semantic_models", "answer_question"]}}| Tool | Arguments | Returns |
|---|---|---|
search_semantic_model (py) /
search_semantic_models (rs) |
term |
matches: kind/name/dataset |
resolve_metric (py) |
name, dialect="SAIL_SQL" |
expression, with dialect fallback |
check_access (py) |
principal, resource,
action |
the full dual-gate decision + receipt |
build_navigator_bundle |
dataset name/description/landing/data URL/creator/agent | the four-layer bundle |
run_qglake_story |
— | the Resilience Desk report |
verify_envelope |
envelope |
verification report |
import_semantic_model (rs) |
osi or croissant document |
import summary |
answer_question |
question |
answer + plans + envelope (+ receipts in py) |
Python resources: qg://story/resilience-desk,
qg://models/current. Transports: stdio (both), SSE and
streamable-HTTP (Python).
The Agent Card. Both implementations publish the
same Linux Foundation Agent2Agent v0.3.0 card — served at
/.well-known/agent-card.json, printed by
agent-card — declaring five skills
(navigator-bundle, qglake-story,
verify-envelope, import-semantic-model,
semantic-search) and a security scheme that documents the
TypeDID envelope contract. The card is a cross-language contract: the
equivalence suite asserts the two implementations publish identical
skills, capabilities, and security schemes.
Tool schemas. For function-calling runtimes rather than MCP, one agent exports both flavors — accepted by OpenAI, Anthropic, Mistral, vLLM, Ollama, and most local servers:
agent = TypeDidAgent.new("FinanceAgent")
agent.to_tool_schema() # {"type": "function", "function":
# {"name": "FinanceAgent",
# "parameters": {…}}} ← OpenAI
agent.to_tool_schema(flavor="anthropic") # {"name": "FinanceAgent",
# "input_schema": {…}} ← AnthropicLangChain. TypeDidLangChainToolAdapter
wraps a governed agent as a StructuredTool —
as_tool() for sync runtimes, as_async_tool()
for async ones. Every adapter result carries the signed envelope with
the answer; nothing hands back a bare string.
Sentinel’s centerpiece: the loop that turns a question into a governed, signed, lineage-anchored answer.
ai_context.denied_sources and
are named in the prompt as off-limits — never planned, never
touched.Callable[[str], str] — the
openai_compatible_llm helper binds Ollama, vLLM, llama.cpp,
LM Studio, or OpenRouter — or deterministically when no model is given.
Same governance either way; the deterministic path is the golden
baseline.The Rust side serves the deterministic core as
POST /v1/answer and the answer_question MCP
tool (one shared function).
from querygraph.navigator_loop import GovernedNavigatorLoop
loop = GovernedNavigatorLoop.demo() # or (your_osi_doc, your_rights, llm=…)
result = loop.answer(
"Where do fiscal capacity and energy burden overlap with health risk?"
)Captured from a real run:
{
"answer": "Answerable from governed sources
sail.qg_lakehouse.access_2018__access_data,
sail.qg_lakehouse.government_finance__countydata via 3 planned
queries. Restricted sources were denied with receipts:
sail.qg_lakehouse.haalsi_baseline__restricted_raw.",
"denied_sources": ["sail.qg_lakehouse.haalsi_baseline__restricted_raw"],
"plans": [
"SELECT * FROM sail.qg_lakehouse.government_finance__countydata",
"SELECT `monthly_cost` FROM sail.qg_lakehouse.access_2018__access_data",
"SELECT SUM(total_revenue - mandated_spend)
FROM sail.qg_lakehouse.government_finance__countydata"
]
}Swap in a live model with one callable — the governance is unchanged:
from querygraph.navigator_loop import openai_compatible_llm
loop = GovernedNavigatorLoop.demo(
llm=openai_compatible_llm("http://localhost:11434", "llama3.2"),
llm_name="ollama:llama3.2",
)| Surface | Essentials |
|---|---|
GovernedNavigatorLoop(document, rights, llm=None, llm_name=…, agent_name=…, principal=…) |
the loop; llm: Callable[[str], str] |
.answer(question) -> NavigatorAnswer |
answer, synthesized_by,
matches,
plans: [PlannedQuery{dataset, source, sql, metric}],
receipts, denied_sources,
envelope, openlineage,
attestation |
GovernedNavigatorLoop.demo(llm=…, llm_name=…) |
the Resilience Desk demo configuration |
openai_compatible_llm(base_url, model, api_key=None) |
binds /v1/chat/completions — Ollama, vLLM, llama.cpp,
LM Studio, OpenRouter |
| Rust | POST /v1/answer {question};
answer_question MCP tool; shared
answer_over_models core |
Rust and Python are held equivalent by an executable contract
(qg-python/tests/test_rust_equivalence.py), not by
discipline:
navigator output is
byte-identical modulo timestamps.did:key.run_id_for("test") is the same UUID in both
languages).qg-rust serve --require-auth rejects a bare POST (401 +
receipt) and accepts a Python-minted header (200 + answer).The suite runs 49 Python tests (12 at the start of the interoperability work) alongside 40 Rust tests, in CI on every push.