opensredevopsintegrations

OpenSRE Integrations — Complete Visual Guide

Share X in

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

Model picks a tool by name + description Chat surface only Tool tools/<vendor>_tool/ builds config, maps evidence Diagnostics sort + state allowlists, caps Client one GET, typed error kinds, no raise Vendor HTTP endpoint, e.g. :8222 /varz Normalised result + one evidence citation the agent can quote Forward: model → tool → vendor. Return: shaped data plus evidence. Every hop is a separate module so each can be tested without the layer above it.

The eight files you write — and which layer each one owns

BUILD ORDER FILE 1 · Config 2 · Client 3 · Shaping 4 · Query 5 · Verifier 6 · Setup 7 · Tools config.py normalised model, validators, classify() — the single definition of "configured" FACADE + client.py one transport, typed error kinds, never raises on expected failures monitoring.py pure shape_* / summarize_* functions: vendor JSON → typed dicts diagnostics.py get_* per endpoint, with allowlists and result caps validation.py + verifier.py cheap reachability probe, registered so setup and health agree setup.py field declarations only; persistence and tiering come from shared setup_flow tools/<vendor>_tool/__init__.py one package per tool — metadata, availability, injected params, evidence FACADE + Env-var names live outside the package, under config/constants/ — one home for all static names.

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.

envNATS_MONITOR_URL — required, scheme + port checked
secretNATS_MONITOR_USERNAME / NATS_MONITOR_PASSWORD — only used when a proxy fronts the port
toolsserver status, connections, subscriptions, JetStream streams, JetStream consumers, cluster status
tests115 test functions across 10 files, 49 fixture payloads
failure modesclient 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

READ IN PRIORITY ORDER RESOLVE ONCE AT EXECUTION 1 · Process Env an exported var, or a line loaded from the project .env FIRST 2 · Credentials File owner-only JSON under the agent home, read when env is empty No OS Keyring exactly two read tiers exist, and a switch drops to env only Resolved Config one object per vendor, normalized, secret-aware ENV WINS Injected Params stripped from the public input schema entirely protected at execution, so injected beats model-supplied MODEL NEVER SEES THIS Hard rule: never read a secret env name with a bare getenv. Use the shared resolver — env first, then the credentials file. Writes run the other way: a sensitive name lands in the credentials file and in .env; every other name lands in .env only. Setup writes the env file before the integration store on purpose, so a failed write can never leave a store-only integration that looks configured at runtime and then fails a deploy preflight that reads the env file. A name ending in URL counts as public, so a webhook URL goes to .env only — but it often embeds a token, so mask it.

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

Tool Package integrations/<vendor>/tools/ declaration of truth Discovery tuple list the dotted package path or the registry never imports it INVISIBLE Catalog + verifier wiring service entry with aliases, classifier, verify path, model field NO STATUS Docs page + navigation entry the page exists but is unregistered in navigation without a warning UNREACHABLE Example env file new required variables go in the example file — never the real one SAFE The nastiest class of integration bug: everything passes, tests are green, and the agent simply cannot see the tool.

Review gates worth knowing

GateWhat it enforces
Border testTools 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 linterPackage boundaries. Importing upward — a lower tier reaching into a surface — is an error, not a warning.
Constant-condition testNo dead product logic disabled by a hardcoded false to silence a test.
Static analysisProtocol 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-integrationsThe 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.

namealways
descriptionalways — this is the model’s only selection signal
source / surfacesalways — decides which turns can see it
use_casescommon — phrase as situations
is_availablewhen the tool needs configuration

Discovery and edge cases

For tools that list, search, or inspect:

nestinghandle folder and nested resource layouts where the upstream has them
volumecap or paginate deliberately
partialssurface them — truncated flags, fetch errors
orderingpreserve 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.

Share this post X in

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.