OpenSRE Integrations — Complete Visual Guide
Every layer explained with animated diagrams · worked example: the NATS integration · 92 vendor packages, 175 vendor tool packages
Anatomy · Call Path · Layers
What An Integration Actually Is
An OpenSRE integration is not a client library. It is a capability surface for an LLM: a normalized config object, one client, one verification function, and a set of narrowly-scoped tools the agent can call during an incident. Everything a vendor does for the agent lives under one directory — integrations/<vendor>/ — including that vendor’s tools.
92
vendor packages under integrations/
76
of them ship a tools/ sub-package
175
vendor tool packages (*_tool)
7
skill cards under integrations/
The runtime call path — one tool invocation, end to end
The eight files you write — and which layer each one owns
Layer contract
⚙
Config normalizes, it does not fetch
Stripping, scheme checks, numeric coercion and defaults live in one pydantic model. A password is deliberately exempt from stripping — whitespace is valid credential content.
↯
Client owns transport and error taxonomy
Expected failures (auth, 404, wrong port, non-JSON) return typed errors the agent can read. Nothing raises into the model loop.
⇄
Shaping is pure
No I/O. That is why a 49-file fixture suite can cover the parsing layer with no server running.
Worked example — NATS
A read-only HTTP integration against the nats-server monitoring port (default 8222). Six tools, one environment variable that is actually required.
| env | NATS_MONITOR_URL — required, scheme + port checked |
|---|---|
| secret | NATS_MONITOR_USERNAME / NATS_MONITOR_PASSWORD — only used when a proxy fronts the port |
| tools | server status, connections, subscriptions, JetStream streams, JetStream consumers, cluster status |
| tests | 115 test functions across 10 files, 49 fixture payloads |
| failure modes | client port vs monitoring port detected from the protocol error |
Where a new tool belongs
Placement is decided by the tool’s purpose, not by convenience or file size. Does the tool’s reason to exist depend on one specific vendor?
Single Vendor
A Datadog query, a GitHub mutation, a NATS stream lookup → integrations/<vendor>/tools/. The default case.
No Vendor
Local process introspection, sandboxed execution, guidance retrieval → tools/system/.
Two Or More
Logic that genuinely reads from one vendor and hands off to another → tools/cross_vendor/. Narrow.
Presentation Only
Code two surfaces both need (CLI + REPL) → surfaces/shared/. A different axis entirely.
Build Order · Code · Wiring
Build Order
Write bottom-up. Each step is independently testable, and each step is reviewable on its own. The order matters for one reason: if you start at the tool, you will invent a config shape and then have to retrofit it into the credential contract.
1
Declare the env-var names under config/constants/
Before any feature module exists. Shared static names live in a leaf module so any layer can import them without a cycle — never inline a name in two places.
2
Write config.py — model, validators, classify()
The model is strict: unknown fields are rejected, so a typo in an operator’s .env fails loudly at startup instead of silently disabling a tool. Secrets resolve through the shared credential helper, never a bare env read.
3
Write client.py — one transport, typed errors
Build the HTTP client from the config (timeout, TLS verification, basic auth only when credentials exist). Then the fetch helper that turns expected failures into a small enum of causes the agent can reason about.
4
Write the pure shaping module
Vendor payloads arrive with Go-style durations, raw byte counts, and names that change between versions. Convert once, in one place, with no I/O — this is the layer your fixtures test.
5
Write the query layer with allowlists
One function per endpoint. Sort keys, sort directions and state filters are enumerated constants, not free strings — a model cannot invent a parameter the vendor does not support. Result sets are capped on purpose.
6
Write the verifier and register it
Two cheap calls — a health endpoint then a detail endpoint — and one human-readable detail line. Cross-field rules (either/or, all-or-nothing) belong here, not in a setup-only check, so setup and integrations verify can never disagree about what “configured” means.
7
Declare the setup spec — fields only
Prompt text, defaults, and which env var each field writes. The credential tier is derived from the env-var name, so a field never chooses its own tier and cannot write a password into a plain-text config file.
8
Write the tools — one package each
Narrow purpose, a description that names the situations it answers, availability gated on real configuration, and credentials declared as injected params rather than model arguments.
9
Wire discovery, catalog, docs and tests
Four separate registrations — each one is a place where a forgotten line makes your work invisible rather than broken. Tab 3 is entirely about these.
The config model
Normalize at the edge; downstream layers assume clean values. Note the password exemption and the scheme guard.
# integrations/<vendor>/config.py
class VendorConfig(StrictConfigModel):
"""Normalized <vendor> connection settings."""
url: str = ""
username: str = ""
password: str = ""
timeout_seconds: int = Field(default=10, gt=0)
@field_validator("*", mode="before")
@classmethod
def _strip_strings(cls, value, info):
# whitespace is valid credential content — never strip a password
return value if info.field_name == "password" else value
@field_validator("url", mode="before")
@classmethod
def _normalize_url(cls, value) -> str:
raw = str(value or "").strip().rstrip("/")
if raw and not raw.startswith(("http://", "https://")):
raise ValueError("must start with http:// or https://")
return raw
@property
def is_configured(self) -> bool:
return bool(self.url)
def classify(credentials: dict, record_id: str):
"""Normalize a store record; return (config, service) or (None, None)."""
The client — expected failures become data
The single most valuable pattern in the integration. A wrong-port mistake, a proxy 401, a JSON parse miss, an upstream 500: each gets its own machine-readable kind, because the agent’s next move depends on which one it was. The NATS client detects “you pointed me at the client port instead of the monitoring port” from a protocol error rather than from guessing at text.
class FetchErrorKind(StrEnum):
TRANSPORT = "transport"
WRONG_PORT = "wrong_port"
AUTH = "auth"
NOT_FOUND = "not_found"
HTTP = "http"
BODY = "body"
def get_json(client, config, path, *, accept=(HTTPStatus.OK,)):
"""GET a monitoring path; never raises for HTTP or transport problems."""
try:
response = client.get(path)
except httpx.RemoteProtocolError:
return None, FetchError(
kind=FetchErrorKind.WRONG_PORT,
message=("did not answer HTTP; the URL probably points at "
"the client port instead of the monitoring port"),
)
except httpx.RequestError as err:
return None, FetchError(FetchErrorKind.TRANSPORT, str(err))
if response.status_code in accept:
try:
return FetchResult(response.status_code, response.json()), None
except ValueError:
return None, FetchError(
FetchErrorKind.BODY, "non-JSON body — wrong port or wrong service")
return None, FetchError(FetchErrorKind.HTTP, "upstream error")
The tool — what makes it agent-safe
Six declarations carry the whole contract. The last three are the ones people skip.
@tool(
name="get_<vendor>_server_status",
description="Return one server's status from its monitoring endpoint: "
"version, uptime, health result, connection counts against "
"the limit, slow consumers, throughput, JetStream usage.",
source="<vendor>",
surfaces=(ToolSurface.CHAT,),
use_cases=[
"Checking whether a server is up and near its connection limit",
"Seeing throughput plus memory during an incident",
],
is_available=vendor_is_available, # hidden when unconfigured
injected_params=("url", "username", "password", "verify_ssl"),
extract_params=vendor_extract_params, # credentials come from resolved config
evidence_mapper=_map_status, # one quotable citation per call
)
def get_<vendor>_server_status(url: str, username: str = "",
password: str = "",
verify_ssl: bool = True) -> dict:
"""Return one server's status from its monitoring endpoint."""
return get_server_status(VendorConfig(url=url, username=username,
password=password, verify_ssl=verify_ssl))
Pure shaping
Where vendor JSON becomes something an LLM reads reliably. No I/O, which is exactly why it is cheap to test exhaustively.
def parse_go_duration(value): # "1m52s" → 112.0
def ns_to_seconds(value): # 150877844740 → float secs
def shape_varz(payload) -> dict:
"""Normalize a /varz payload into typed fields."""
Units, byte counts and uptime strings get normalized exactly once, so no tool ever re-parses a raw vendor string.
Query layer with caps
Sort keys and state filters are enumerated constants exported from the package. The model cannot invent a parameter the vendor will reject.
ALLOWED_CONNZ_SORT = (...)
ALLOWED_CONNZ_STATE = (...)
CLOSED_ONLY_SORTS = (...)
DEFAULT_CONNECTION_LIMIT = 50
MAX_CONNECTION_LIMIT = 200
Capping is not laziness — it is what keeps one call from flooding the agent’s context window mid-incident. When you do truncate, say so in the payload.
Tests: fixtures over mocks
Mock the transport boundary once, then drive the whole stack — config, catalog classification, tool, evidence mapper — from real-shaped payload files. The NATS suite has 49 of them, including deliberately invalid ones (an unsupported sort key, an unsupported state) so the allowlist rejections are pinned rather than assumed.
tests/integrations/test_<vendor>.py # config, normalization, classify, validation
tests/tools/test_<vendor>_<endpoint>_tool.py # one per tool
tests/tools/test_<vendor>_tools_port_injection.py # secrets do not leak through the schema
tests/e2e/<vendor>/test_<vendor>_e2e.py # full pipeline, transport mocked
tests/integrations/<vendor>/fixtures/*.json # realistic payloads, incl. negative cases
Rules · Secrets · Traps
The Rules That Make It Reviewable
Every rule below exists because its absence produced a specific failure someone had to debug. They are not style preferences.
Credential resolution — two read tiers, one resolved truth
Agent rules (repo-owned)
✋
Never hand-roll persistence in a setup handler
The shared setup flow writes the env file and credentials file first, then the integration store, then a best-effort org-vault mirror. A surface handler that writes only the store produces an integration that looks configured at runtime and fails a deploy preflight that reads the env file. Use the shared spec and hand it your collected values.
⚖
Cross-field rules belong in the verifier
Either/or, all-or-nothing, and incomplete-combination rejections go in the verifier — never a setup-only check, because health checks call only the verifier. A UI picker is not a guarantee: an agent or a hand-edited store bypasses it.
⇅
The tier is derived, never chosen
A field’s env-var name decides the tier, not the field. A terminal token/key/secret/password — or a connection-string name — goes to the credentials file and .env; anything else goes to .env alone. A field cannot opt out, which is the point.
⧉
Skill cards are human-owned
Skill guidance files are natural-language cards describing how to use a tool. Agents may only suggest changes — never create, edit, rename, move, or delete them. Deterministic vendor behaviour belongs in a real tool, not in a card’s helper scripts.
Session rules (operator-owned)
🎫
No work without a ticket
Every change needs an issue with acceptance criteria first, then failing tests, then the minimum implementation. No retroactive tickets, no deferred test passes.
🔒
No elevated privileges, ever
No root, no hosts-file edits, no DNS changes. If a task seems to require one, stop and describe what the operator must do themselves.
📝
Facts, labelled
A document is evidence of itself, never of the world it describes. If a claim would change a decision, verify it — and if it cannot be verified, say so instead of hedging.
🔁
Close the loop on CI
Push is not done. Watch the checks, fix root causes rather than skipping, and treat a post-merge failure as unfinished delivery.
The four registrations — a forgotten line is invisible, not broken
Review gates worth knowing
| Gate | What it enforces |
|---|---|
| Border test | Tools import the tier’s public API, never an internal leaf module. This is what stops a tool from coupling to a private helper and breaking on refactors. |
| Import linter | Package boundaries. Importing upward — a lower tier reaching into a surface — is an error, not a warning. |
| Constant-condition test | No dead product logic disabled by a hardcoded false to silence a test. |
| Static analysis | Protocol method bodies are docstring-only. Mixed import styles, unused module constants, and implicit string concatenation inside a list all fail review-quality checks. |
| make verify-integrations | The human-facing probe: one integration at a time, or the whole set, printing a status table. |
Metadata conventions
Declare what the tool needs and what it answers. Across 175 vendor tool packages the usage block is common and an outputs block is rare — but no vendor tool declares an output schema or a retrieval control at all, so match the surrounding convention rather than inventing a new field nobody reads.
| name | always |
|---|---|
| description | always — this is the model’s only selection signal |
| source / surfaces | always — decides which turns can see it |
| use_cases | common — phrase as situations |
| is_available | when the tool needs configuration |
Discovery and edge cases
For tools that list, search, or inspect:
| nesting | handle folder and nested resource layouts where the upstream has them |
|---|---|
| volume | cap or paginate deliberately |
| partials | surface them — truncated flags, fetch errors |
| ordering | preserve causal order when time matters |
Definition of done
Core completeness
Config, normalization, validators and classify() in place, facade left thin · catalog resolution and env loading wired · verification path wired · integration-local client if it makes remote calls · tool layer wired and stable · CLI setup flow updated · setup <name> parity or documented out of scope · new required variables in the example env file · credentials follow the resolution contract · make verify-integrations passes.
Docs and tests
A docs page shipped or updated in the same PR, registered in navigation · unit tests for config and normalization · tool contract or equivalent schema coverage · a realistic-fixture regression test · an end-to-end test that exercises the investigation pipeline with the transport mocked at the boundary.
Enjoyed this post?
Get the next one in your inbox — only when I ship something worth reading.
Newsletter form not configured.
Or follow on Substack for the newsletter.
Comments via GitHub Discussions
Comments not configured. Set GISCUS env vars to enable.