← First Pair Library

5 Part III: The Interoperability Surfaces

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.

5.1 Chapter 18. The /v1 API and Envelope Auth

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.

5.1.1 Worked example: the guarded /v1, from both sides

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

5.1.2 API reference

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.

5.2 Chapter 19. MCP: One Server, Every Framework

The Model Context Protocol is how agent frameworks discover tools in 2026, and the stack speaks it from both languages:

Any MCP client — Claude Code/Desktop, LangChain via langchain-mcp-adapters, PydanticAI, LlamaIndex, CrewAI, the OpenAI Agents SDK — reaches everything with zero adapter code.

5.2.1 Worked example: an MCP session, by hand

$ 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"]}}

5.2.2 API reference (tools)

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

5.3 Chapter 20. A2A, Tool Schemas, and Adapters

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": {…}}   ← Anthropic

LangChain. TypeDidLangChainToolAdapter wraps a governed agent as a StructuredToolas_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.

5.4 Chapter 21. The Governed Navigator Loop

Sentinel’s centerpiece: the loop that turns a question into a governed, signed, lineage-anchored answer.

  1. Search the semantic model — names, synonyms (including multi-word synonyms via bigrams), descriptions, ai_context.
  2. Gate every matched dataset through RBAC+ODRL; collect receipts; denied sources go into denied_sources and are named in the prompt as off-limits — never planned, never touched.
  3. Plan SQL over allowed Sail sources only, resolving matched metrics with dialect fallback.
  4. Synthesize with any 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.
  5. Sign and record: the answer, plans, and receipts travel in a signed TypeDID envelope; a schema-valid OpenLineage event and an Ed25519 attestation complete the chain.

The Rust side serves the deterministic core as POST /v1/answer and the answer_question MCP tool (one shared function).

5.4.1 Worked example: the loop, receipts and all

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",
)

5.4.2 API reference

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

5.5 Chapter 22. The Cross-Language Contract

Rust and Python are held equivalent by an executable contract (qg-python/tests/test_rust_equivalence.py), not by discipline:

The suite runs 49 Python tests (12 at the start of the interoperability work) alongside 40 Rust tests, in CI on every push.