# Stainless SDK provenance classifier (v0.8.3+)

`agent_airlock.sdk_provenance.classify_sdk_lineage` is a **pure-function
classifier** that scans an MCP server's User-Agent header and
response-body head for markers indicating the server was generated by
a specific SDK toolchain.

## What this is NOT

This is **not** a runtime probe, proxy, or sidecar. agent-airlock's
`@Airlock` decorator wraps a Python tool function the agent calls;
it does NOT intercept outbound HTTP, so the decorator never sees a
server's response headers on its own. The decorator-in-process model
is a deliberate anti-pivot choice (see [`ROADMAP_2026.md` §1][roadmap]).

This module is a **building block**. A tool function that itself
calls out to an MCP server can pass the response's UA / first bytes
through this classifier and tag the result onto the trajectory event.

[roadmap]: https://github.com/sattyamjjain/agent-airlock/blob/main/ROADMAP_2026.md

## Anchor

[Anthropic announced the acquisition of Stainless on 2026-05-13][stainless]
and the wind-down of hosted Stainless products, including the SDK
generator. Operators wanting visibility into which MCP servers in
their inventory were generated by the deprecated Stainless toolchain
can call this classifier from their own audit hooks.

[stainless]: https://www.anthropic.com/news/anthropic-acquires-stainless

## Quickstart

```python
from agent_airlock import (
    SDKLineage,
    classify_sdk_lineage,
)

# Inside YOUR tool function (agent-airlock does not auto-probe):
response = http_client.get("https://my-mcp-server.example/info")
match = classify_sdk_lineage(
    user_agent=response.request.headers.get("User-Agent"),
    response_body_head=response.text[:4096],
)
if match.lineage == SDKLineage.STAINLESS:
    logger.info(
        "mcp_server_lineage_stainless",
        match_source=match.match_source,
        matched_pattern=match.matched_pattern,
    )
# Tag the trajectory event with the lineage label regardless of outcome.
audit_event["sdk_lineage"] = match.lineage.value
```

## Match dataclass

```python
@dataclass(frozen=True)
class SDKLineageMatch:
    lineage: SDKLineage              # STAINLESS | UNKNOWN
    match_source: str | None         # "user_agent" | "response_body" | None
    matched_pattern: str | None      # the literal substring that fired
    detail: str
```

A UA match short-circuits — the body is not scanned when the UA
already names Stainless. Matching is case-insensitive substring on
both surfaces.

## Default markers

```python
DEFAULT_STAINLESS_UA_PATTERNS = frozenset({
    "stainless",
    "stainless-sdk",
    "stainless-node",
    "stainless-python",
})

DEFAULT_STAINLESS_BODY_MARKERS = (
    "auto-generated by Stainless",
    "Generated by Stainless",
    "@stainless-generated",
    "stainless-codegen",
)
```

Only the **first 4 KB** of the response body is scanned. Operators
who want deeper scans should ship the response head through some
other tool — keeping the agent-airlock surface lightweight is
deliberate.

## Operator overrides

```python
match = classify_sdk_lineage(
    user_agent=ua,
    response_body_head=head,
    extra_ua_patterns=frozenset({"acme-internal-gen"}),
    extra_body_markers=("AcmeCodeGen-v3",),
)
```

A match on an operator-added pattern still yields `SDKLineage.STAINLESS`
because there is currently only one lineage label. Future versions
may add additional lineage labels (e.g. `OPENAPI_GENERATOR`,
`STAINLESS_INHERITED`) when operator demand justifies the
taxonomy expansion.

## Factory

```python
from agent_airlock.policy_presets import stainless_provenance_probe_defaults

cfg = stainless_provenance_probe_defaults()
# {
#   'preset_id': 'stainless_provenance_probe_2026_05_19',
#   'severity': 'info',
#   'default_action': 'tag_only',
#   'advisory_url': 'https://www.anthropic.com/news/anthropic-acquires-stainless',
#   'ua_patterns': frozenset({'stainless', 'stainless-sdk', ...}),
#   'body_markers': ('auto-generated by Stainless', ...),
#   ...
# }
```

`default_action` is `"tag_only"` — this preset never refuses or
blocks. It is visibility-only.

## Honest scope

- **No automatic probing.** agent-airlock does not intercept HTTP
  on its own. Operators wire this classifier into their own audit
  hooks.
- **One lineage label today.** Only Stainless is recognised. The
  enum can be extended.
- **Substring matching, not signature verification.** A determined
  party could spoof the markers. This is a visibility primitive,
  not a tamper-resistant attestation.
- **4 KB scan window.** A banner past byte 4096 of a response is
  missed by design.

## Related

- [`OpenAPIDriftGuard`](openapi-drift-guard.md) (v0.8.1) — runtime
  payload-shape validation when an OpenAPI spec is available.
- [`MetisInspiredCorpusBlockRateGuard`](metis-inspired-corpus-block-rate.md)
  (v0.8.2) — release-gate primitive scoring a guard chain against
  a deterministic exploit-shape corpus.
