index.md
HTML wrapper for HTML-only fetchers; content is shown verbatim.
Raw file
Links (extracted)
Clickable list for reviewers; avoids copy/paste line-wrap issues.
# if.api deep dive v1.0.1 (locked) (adapters, boundaries, examples)
Danny Stocker | Infrafabric Research | ds@infrafabric.io | 2026-02-01
Status:
- LOCKED snapshot (v1.0.1). Do not edit this file in place.
- Future changes MUST be published as a new numbered doc.
Supersedes:
- `docs/228-if-api-deep-dive-whitepaper-2026-01-31.md`
- `docs/231-if-api-deep-dive-whitepaper-2026-02-01.md` (v1.0)
Errata (v1.0 → v1.0.1):
- Correct `if.bus` posture: `if.bus` is `preview` (not `roadmap`) and a runtime exists (no public bus endpoints claimed).
Scaffolding: if.whitepapers.bible (docs/182-if-whitepapers-bible.md)
Black/white:
- Verified claims in this document are limited to repo-local bytes under `/root/`, public URLs explicitly listed, and deterministic local behavior when a command is shown under "Verify".
- This document does not claim a deployed `if.api` runtime service on this host; `if.api` is `status=preview` in `if.registry.json`.
- This document does not claim correctness, completeness, safety, or compliance guarantees. Receipts (when present) prove byte integrity only.
- This document contains no secret values. It may reference opaque secret pointers (e.g., `secret_ref`) but never raw tokens/keys.
If you cannot reproduce a statement with a verify command, a referenced file, or a public URL, treat it as narrative, not evidence.
## Table of contents
- 0) Executive summary (one screen)
- 0b) Ecosystem primer (for first-time readers)
- 1) What `if.api` is (and is not)
- 2) The adapter boundary (why it exists)
- 2b) Normalization example (before/after)
- 3) Contracts: `if.api.adapter` and `if.api.adapter_spec`
- 3b) Quick start: building your first adapter
- 3c) Schema evolution: v1 → v2 (parallel run)
- 3d) Multi-tenant adapters: discovery + isolation
- 3e) Anti-patterns (what not to normalize)
- 4a) Where `if.api` sits relative to `if.bus`, `if.trace`, `/llm`, `/mcp`
- 4b) Cross-cutting: `if.security` posture
- 4c) Composition: `if.api` → `if.api` + (optional) `if.bus` envelopes
- 4d) Agent/LLM messages: structured requests (no vibes)
- 5) Example 1: GitHub webhook normalization (demo)
- 6) Example 2: DOI resolver (demo)
- 6b) Example 2b: Resolver dispatch/chaining (`if.api` → `if.api`)
- 7) Example 3: Ad decisioning (Revive Adserver) (proposed)
- 8) Guardrails: rate limits, caching, idempotency, secrets, classification
- 8b) Failure modes: drift, 429s, signature failures, spec conflicts
- 9) Ship checklist: making an integration reviewable
- 10) Verify checklist (boring on purpose)
- 11) Appendix (links, one URL per line)
*If “integration” isn’t reproducible without private access, it’s not an integration — it’s a story.*
---
## 0) Executive summary (one screen)
`if.api` is the “expansion slot” layer: it normalizes provider quirks at the edge so the rest of the system can stay boring.
What it does:
- Defines how external APIs/webhooks are described (`adapter`) and how those descriptions become enforceable contracts (`adapter_spec`).
- Encourages black/white capability claims and evidence pinning (what is verified vs unknown).
- Enables stable, reviewable demos where outputs can be bound to `if.trace` receipts (brand `if.trace`).
What it does not do:
- It is not the message backbone (`if.bus`).
- It is not the public evidence surface (`/llm`).
- It is not a secret store (it only carries *references* to secrets).
- It is not “an API gateway that proxies everything” (it is a normalization boundary with explicit scope).
One-line mental model:
- `if.api` is the adapter at the wall: it translates “provider reality” into “our contract”, then gets out of the way.
*If you put provider quirks into every downstream component, you’re choosing sprawl — you just haven’t paid the invoice yet.*
---
## 0b) Ecosystem primer (for first-time readers)
You can read this paper without any other Infrafabric docs, but these names show up a lot:
- `if.registry.json`: the canonical registry of product/module IDs, their `status`, and preferred `path` shapes.
- `if.trace`: receipt-backed evidence for “these bytes exist” (not “these bytes are true”).
- `/llm`: public, no-login review surface (HTML-first) for packs, inventories, and derived views.
- `/mcp`: token-gated surface for tenant/NDA artifacts; same black/white discipline, different classification.
- `if.bus`: async envelope contract + internal runtime (`status=preview`); do not assume public endpoints exist.
*If a system needs insider context to review, it will fail its first external audit.*
---
## 1) What `if.api` is (and is not)
### What it is
`if.api` is the layer where you:
- decide what an integration *means* (IDs normalized, canonical fields, minimal claims),
- encode constraints (rate limits, payload limits),
- pin evidence for claims (docs URLs + retrieval time + sha256),
- and produce outputs that can be receipt-backed.
In this repo, the canonical “truth” about `if.api` identity is registry-driven (product id + path shape).
Verified (repo-local):
- `if.api` registry entry: `if.registry.json` (`status=preview`, `path=/if/api`)
- `if.api` contract schemas:
- `schemas/if-api/adapter.schema.json`
- `schemas/if-api/adapter_spec.schema.json`
- `if.api` public inventory (black/white): `docs/32-if-api-integrations-inventory.md`
### What it is not
`if.api` is not:
- a claim that “we have all these integrations deployed”
- a compliance wrapper
- a replacement for deterministic evidence (`if.trace`) or the published review surface (`/llm`)
- a generic “proxy” where any call is allowed without an adapter spec
*If an integration can’t say what it is not, it will quietly become everything.*
---
## 2) The adapter boundary (why it exists)
Every provider has a different kind of “weird”:
- IDs sometimes change type across endpoints (int vs string).
- rate limits are undocumented, conditional, or per-tenant.
- pagination semantics differ (cursor vs page vs time window).
- webhook deliveries are lossy, duplicated, or unordered.
- auth is a moving target (OAuth scopes, token refresh, app review, etc.).
Without an adapter boundary, downstream code ends up re-implementing each provider’s quirks repeatedly, and you lose:
- consistency (same entity has different shapes in different places),
- auditability (nobody knows which endpoint was used),
- enforceability (rate limits and payload bounds are “guidelines” not gates),
- reviewability (no stable contract to bind receipts against).
Two “first principles” rules:
1) **Normalize at the edge**: convert provider-specific shapes into canonical shapes immediately.
2) **Make claims testable**: every capability claim should be true/false/unknown, with evidence pins where possible.
```mermaid
flowchart LR
Raw["Provider reality: JSON/webhook/HTML"]
subgraph Edge["Adapter boundary (if.api)"]
Norm["Normalize + enforce: if.api adapter"]
Spec["adapter_spec: limits + claims"]
Spec --> Norm
end
Raw --> Norm
Norm --> Canon["Canonical output: stable JSON contract"]
subgraph Consumers["Downstream (stable contract)"]
Trace["if.trace receipt (optional)"]
LLM["/llm pack (public)"]
MCP["/mcp pack (private)"]
Bus["if.bus envelope (preview; internal)"]
end
Canon --> Trace
Canon --> LLM
Canon --> MCP
Canon --> Bus
```
*If you don’t build a wall, every downstream component becomes the wall.*
---
## 2b) Normalization example (before/after)
Provider payloads are allowed to be messy. Canonical outputs are not.
Illustrative “before” (provider reality):
```json
{
"id": 123,
"createdAt": "2026-01-31T12:34:56Z",
"url": "https://example.com/x?y=1",
"extra": {"weird": true}
}
```
Illustrative “after” (canonical contract):
```json
{
"event_type": "provider.example.v1",
"provider": "example",
"id": "123",
"occurred_at": "2026-01-31T12:34:56Z",
"target_url": "https://example.com/x?y=1"
}
```
What changed (the point of the wall):
- IDs are normalized (often strings) so type drift doesn’t leak downstream.
- Timestamps are normalized (UTC ISO8601).
- Provider-only fields either become explicit canonical fields or get dropped.
- The adapter spec names what is enforced (rate limits, payload bounds, signature checks), so downstream code doesn’t have to.
*If the “after” shape isn’t enforceable, you didn’t normalize — you translated vibes.*
---
## 3) Contracts: `if.api.adapter` and `if.api.adapter_spec`
`if.api` has two separate but related “contract layers”:
1) `if.api.adapter` (metadata)
- identity (`adapter_id`, `label`, `version`)
- provider metadata (`provider`, `base_url`)
- auth *type* (never raw secrets)
- high-level capabilities
2) `if.api.adapter_spec` (enforceable spec)
- endpoint map (names + methods + paths)
- structured rate limits with pinned evidence records
- black/white capability claims (`true` / `false` / `unknown`)
- constraints (max payload, etc.)
This split is deliberate:
- The adapter is the “what exists” label.
- The spec is the “what we can safely claim and enforce” contract.
Schema anchors (repo-local):
- `schemas/if-api/adapter.schema.json`
- `schemas/if-api/adapter_spec.schema.json`
*If the contract is only prose, it will be re-interpreted by whoever is late and tired.*
---
## 3b) Quick start: building your first adapter
1) Define scope (and the explicit “not” list).
2) Choose a versioning strategy:
- Recommend SemVer for `adapter.version` (`MAJOR.MINOR.PATCH`).
- Breaking change policy: bump `MAJOR` when the output contract changes shape/meaning; keep the previous major runnable until consumers migrate.
3) Write the contracts first:
- `if.api.adapter`: identity + provider metadata + auth type (no secrets).
- `if.api.adapter_spec`: endpoints, constraints, rate limits + evidence pins, capability claims.
4) Implement normalization + enforcement:
- Validate inputs; fail fast on malformed requests.
- Call provider; record “observed behavior” (status codes, retry headers) without leaking secrets.
- Emit canonical JSON outputs; keep provider quirks out of consumers.
- Emit observability: structured logs (`adapter_id`, request id, `outcome`, latency bucket) + minimal counters.
- Warning: do not log raw provider bodies (or raw inbound payloads) unless you have sanitized/redacted them first; never publish raw captures on `/llm`.
5) Plan for evolution:
- Prefer additive changes (new fields; never re-meaning old ones).
- If you must break shape/type, introduce a new `event_type` or a new adapter major version; do not silently mutate existing outputs.
6) Multi-tenant discipline (when `tenant_id` exists):
- Treat `tenant_id` as a first-class partition key for rate limits, caches, and secrets.
- Never let one tenant’s spikes or failures consume another tenant’s budgets.
7) Make it reviewable:
- Provide a runnable demo + `if.trace` receipts + an HTML-first review surface.
Anti-patterns (fast smell test):
- “We’ll just proxy the provider and normalize later.”
- “We’ll log headers so we can debug auth.”
- “We’ll change the JSON shape; the frontend can adapt.”
See also:
- Section 3c for parallel-run mechanics (v1 + v2 simultaneously).
- Section 3d for multi-tenant isolation strategies.
- Section 3e for a fuller anti-pattern list.
*If you can’t write the contract before the code, you’re not building an adapter — you’re building a one-off.*
---
## 3c) Schema evolution: v1 → v2 (parallel run)
If the output contract changes shape or meaning, treat it as a new contract, not an in-place edit.
Goal:
- keep existing consumers working (v1) while new consumers migrate (v2)
- make routing explicit so caches, receipts, and debugging stay unambiguous
Three things that must be versioned together (or you will lie to yourself):
- the adapter implementation (producer identity)
- the output event type (contract identity)
- the validateable schema (shape)
Parallel run (conceptual routing):
```mermaid
flowchart LR
Client -->|v1| A1["Adapter v1"] --> Provider
Client -->|v2| A2["Adapter v2"] --> Provider
A1 --> O1["Output contract v1"]
A2 --> O2["Output contract v2"]
```
Common routing patterns (pick one; don’t mix accidentally):
- **Versioned paths**: `/.../v1/...` and `/.../v2/...` (explicit; cache-friendly)
- **Versioned negotiation**: `Accept: application/vnd.if.api.<name>.v1+json` (requires careful cache variation)
- **Explicit version parameter**: request includes `contract_major=1|2` (easy to reason about; easy to misuse)
Migration checklist (OSE: ops + security + evidence):
- run both majors for a defined deprecation window; do not “flip” silently
- partition caches and rate limits by `(tenant_id, adapter_id, major)` to avoid cross-talk
- publish receipts for v1 and v2 outputs separately (integrity only), and keep verify commands runnable
Deprecation discipline (make it measurable):
- Publish a migration note with v2 (what changed, what to test).
- Define v1 sunset criteria (date and/or sustained near-zero usage); do not assume migration.
- Remove v1 only after the criteria are met; keep the last-known-good v1 demo/receipt links available for audit.
Black/white: this repo contains schemas, specs, and demos; it does not claim a deployed routing plane.
*If you can’t run v1 and v2 at the same time, you don’t have migration — you have a flag day.*
---
## 3d) Multi-tenant adapters: discovery + isolation
Multi-tenancy is mostly an ops problem disguised as a feature.
Discovery (where to look first):
- Inventory (repo-local): `docs/32-if-api-integrations-inventory.md`
- Product identity and path shapes (repo-local): `if.registry.json` (`if.api` is `preview`)
- Example spec file (repo-local): `docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json`
Isolation rules (design intent):
- Every request must resolve a `tenant_id` (explicit or derived) *before* any provider call.
- Partition these by `tenant_id`:
- rate limit buckets (per tenant, per endpoint)
- caches (include `tenant_id` and contract major in cache keys)
- secret resolution (`secret_ref` includes a tenant segment)
- receipts and published artifacts (public `/llm` vs tenant `/mcp`)
- Do not share raw provider responses across tenants; normalize first, then cache canonical outputs if you can.
Tenant discovery failure (fail closed):
- If `tenant_id` cannot be resolved before the provider call, stop and return a 4xx; do not fall back to a shared/default tenant bucket.
- Derive `tenant_id` from authenticated context; do not trust untrusted query params/headers as tenant selectors.
Rate limits (practical):
- Providers often mix “per-app” and “per-user” limits. Model both:
- a global limiter (protects the provider relationship)
- a per-tenant limiter (protects tenant fairness)
- When in doubt: fail closed (429) rather than create retry storms that burn global budgets.
OSE note:
- **Ops** wants bounded retries + stable dashboards.
- **Security** wants zero secret leakage and explicit classification boundaries.
- **Evidence** wants receipts + runnable verify steps for any demo or claim.
*If you don’t name the partition key, the incident will.*
---
## 3e) Anti-patterns (what not to normalize)
These are the mistakes that turn an adapter boundary into a permanent debugging tax.
Data/contract anti-patterns:
- Normalizing timestamps to epoch seconds (lose timezone semantics); prefer UTC ISO8601.
- Leaking provider field names into canonical outputs (“just pass through `providerFooBar`”).
- Treating unstable numeric IDs as integers (overflow, string/int drift); normalize to strings when in doubt.
- Returning “whatever the provider returned” and calling it “canonical”.
Versioning/migration anti-patterns:
- Mutating an existing output contract in place (no major bump, no parallel run).
- Reusing the same `event_type` for a different meaning (“same name, new semantics”).
- Shipping v2 without publishing how to keep v1 consumers alive.
Secrets/logging anti-patterns:
- Logging headers or query params “for debugging” (auth leaks happen this way).
- Storing tokens/keys in adapter specs or receipts (even “temporarily”).
Multi-tenant anti-patterns:
- Sharing cache keys or rate limit buckets across tenants (one tenant can DoS the rest).
- Publishing tenant-specific payloads on `/llm` instead of `/mcp`.
Evidence anti-patterns:
- Treating `if.trace` receipts as proof of truth (receipts prove byte integrity only).
- Claiming limits/capabilities without pinned evidence (URL + retrieved time + sha256).
*If you normalize the wrong things, you’ll spend your life debugging the right things.*
---
## 4a) Where `if.api` sits relative to `if.bus`, `if.trace`, `/llm`, `/mcp`
Conceptual boundary (black/white):
- `if.api` is `preview` (registry); contracts + demos exist.
- `if.bus` is `preview` (registry); a reference runtime exists and an internal sandbox runtime is documented (no public bus endpoints claimed).
Practical integration rule of thumb:
- **Synchronous needs** (request/response): do it at the adapter boundary (`if.api`).
- **Asynchronous needs** (telemetry, retries, fanout, long-running work): route through a bus contract (`if.bus`) when available in your environment (no public endpoints are implied).
- **Evidence needs**: bind bytes to `if.trace` receipts, and publish on `/llm` (public) or `/mcp` (tenant/NDA) based on classification.
Public docs (black/white posture):
- `docs/30-if-bus-and-api-integrations.md`
- `docs/31-if-bus-whitepaper.md`
- `docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md`
*If you use async for a decision the page needs right now, you’ve built latency by design.*
---
## 4b) Cross-cutting: `if.security` posture
`if.security` is the cross-cutting posture for how modules describe and act on security-relevant findings.
Black/white:
- `if.security` is `roadmap` in `if.registry.json` (no claim of a deployed security runtime).
- The posture is still useful: it defines shared primitives so each module doesn’t invent its own “security story”.
Verified anchors:
- Posture doc (repo-local): `docs/89-if-security-posture.md`
- Security signal schema (repo-local): `schemas/if-bus/security_signal.schema.json`
How it cross-cuts `if.api` (design target):
- Treat “secret-like material observed” and “signature validation failed” as **security signals**, not log spam.
- Use evidence kinds (`observed|derived|hypothesis`) so automation can fail closed (never automate CONTAIN/HALT on hypothesis).
- Keep `/llm` public-safe: any tenant payloads or NDA examples go to `/mcp`.
Security signal flow (conceptual):
```mermaid
flowchart LR
Adapter["if.api adapter"] -->|signal| Sig["if.bus.security_signal (public-safe)"]
Sig --> Triage["Ops/Sec triage (human)"]
Sig --> Gov["if.gov deliberation (optional)"]
Triage -.->|action (manual/policy-gated)| Ctrl["if.bus control plane (pause/quarantine)"]
Gov -.->|action (manual/policy-gated)| Ctrl
```
Legend:
- Solid arrows = telemetry signals (public-safe; no secrets).
- Dashed arrows = side-effect actions; manual/policy-gated (not autonomous; not a public endpoint claim).
Minimal `security_signal` shape (illustrative; see schema for full contract):
```json
{
"schema_id": "if.bus.security_signal",
"schema_version": "1.0.0",
"signal_id": "sec-01J...ULID",
"emitted_utc": "2026-01-31T12:34:56Z",
"detector": {"detector_id": "secrets.detect", "version": "1.0.0", "kind": "regex"},
"severity": "high",
"finding_type": "secret_like_token",
"summary": "Detector matched a token-like pattern in an inbound payload; excerpt is redacted.",
"recommended_action": "quarantine",
"redactions": {"redacted_excerpt": "Authorization: Bearer [REDACTED]"},
"extensions": {"if.security": {"evidence_kind": "observed"}}
}
```
Public pack (HTML-first):
https://infrafabric.io/llm/products/if-security/security-posture/2026-01-11/index.html
Preview demo receipt (`if.security.secrets.detect`, integrity only):
https://infrafabric.io/static/trace/n6DSejaslZrB2ciVjLiBCg
Canonical path shape (registry):
https://infrafabric.io/if/security/
*If every module writes its own security posture, none of them have one.*
---
## 4c) Composition: `if.api` → `if.api` + (optional) `if.bus` envelopes
Composition means adapter outputs are stable enough to become inputs to other adapters without re-learning provider quirks.
Three useful composition patterns:
- **Dispatch**: one adapter selects a specific resolver/adapter based on input scheme, then delegates.
- **Enrichment**: adapter A produces a canonical event, adapter B adds derived fields (still canonical; no provider leaks).
- **Fanout** (optional): adapter output is wrapped in an `if.bus` envelope for async delivery and retries.
Envelopes (important distinction):
- `if.api` produces **canonical JSON payloads** (your contract).
- `if.bus` wraps those payloads in a **bus envelope** (transport contract).
Verified schema anchor (repo-local):
- `schemas/if-bus/envelope.schema.json`
Note (black/white): `if.bus` is `preview` in `if.registry.json`; the envelope example below is schema-backed, but no public bus endpoints are implied.
Minimal `if.bus` envelope example (illustrative; semver schema_version required):
```json
{
"schema_id": "if.bus.envelope",
"schema_version": "0.1.0",
"event_id": "01J...ULID",
"emitted_utc": "2026-01-31T12:34:56Z",
"producer": {"kind": "if.api.adapter", "id": "if.api.resolver.dispatch", "version": "0.1.0"},
"event": {"name": "if.api.resolver.dispatch.resolved"},
"routing": {"topic": "if.api.resolutions", "partition_key": "tenant:demo"},
"payload_media_type": "application/json",
"payload": {"event_type": "resolver.dispatch", "selected_adapter_id": "if.api.resolver.doi", "resolved": {"target_url": "https://..."}},
"payload_sha256": "sha256-of-canonical-payload-bytes"
}
```
Black/white: `if.bus` is `preview` in the registry; envelope wrapping is a transport contract, not a public deployment claim.
*If adapters can’t compose, you didn’t build a platform — you built a pile.*
---
## 4d) Agent/LLM messages: structured requests (no vibes)
When you ask an LLM (or any agent) to design or invoke an adapter, require a machine-checkable request shape.
This avoids “agent-only language” by using boring, typed fields humans can audit.
Minimal shape (illustrative):
```json
{
"intent": "resolve_identifier",
"actions": [
{
"tool": "if.api.resolver.doi",
"args": {"doi": "10.1234/example"},
"require_receipt": true,
"dry_run": false
}
],
"constraints": {"latency_sla_ms": 3000, "payload_max_bytes": 1048576},
"reason_codes": ["user_request"],
"evidence": [{"kind": "spec", "url": "https://doi.org/10.1234/example", "retrieved_at": "2026-01-31T12:34:56Z", "sha256": "..."}],
"risk": {"level": "low"},
"trace": {"parent_event_id": "01J...ULID"}
}
```
Rules of thumb:
- Prefer structured data over custom DSLs; if you add expressions (policy/scoring), pin test vectors.
- Validate the request schema before any provider call; enforce `adapter_spec` constraints the same way.
*If a tool call can’t be described as data, it will be described as vibes.*
---
## 5) Example 1: GitHub webhook normalization (demo)
Use case:
- Input: GitHub “push” webhook JSON.
- Output: canonical “event envelope” JSON that downstream systems can consume without GitHub-specific branching.
Black/white:
- This is documented as a receipt-backed demo in `docs/32-if-api-integrations-inventory.md`.
- This example shows the *shape*; it is not a claim of live webhook delivery on this host.
Minimal input shape (illustrative):
```json
{
"ref": "refs/heads/main",
"after": "e7c1…",
"repository": {"full_name": "org/repo"},
"pusher": {"name": "alice"}
}
```
Minimal normalized output shape (illustrative):
```json
{
"event_type": "github.push",
"source": {"provider": "github", "repo": "org/repo"},
"refs": {"ref": "refs/heads/main", "after": "e7c1…"},
"actor": {"label": "alice"}
}
```
Public demo (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-github-webhook-demo/2026-01-05/index.html
Receipt (if.trace, brand `if.trace`):
https://infrafabric.io/static/trace/Z_kiBtTQHztidcgYYaSRy4UK
*If you can’t normalize a webhook, you’ll normalize your incident response instead.*
---
## 6) Example 2: DOI resolver (demo)
Use case:
- Input: a DOI string (or URL form).
- Output: resolved metadata + canonicalized target URL, returned as normalized JSON.
Why it belongs in `if.api`:
- providers vary in response shapes and redirect behavior
- DOI resolution is “identifier → metadata”, not “integrity”
Illustrative output shape:
```json
{
"event_type": "resolver.doi",
"input": {"doi": "10.1234/example"},
"resolved": {
"title": "…",
"publisher": "…",
"target_url": "https://…"
}
}
```
Public demo (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-doi-resolver-demo/2026-01-07/index.html
Receipt (if.trace, brand `if.trace`):
https://infrafabric.io/static/trace/EKMtQbLWtQLZEERUqcKX1g
*If you confuse “resolved” with “verified”, you will ship a trust bug dressed as a feature.*
---
## 6b) Example 2b: Resolver dispatch/chaining (`if.api` → `if.api`)
Use case:
- Input: a generic identifier string (DOI, SWHID, OCI digest, etc.).
- Output: a single canonical response shape while delegating to a specific resolver adapter.
Why it matters:
- Each resolver stays separately testable and receipt-backed.
- A dispatcher can compose adapters without leaking provider quirks into consumers.
Illustrative dispatch response shape:
```json
{
"event_type": "resolver.dispatch",
"input": {"identifier": "10.1234/example"},
"selected_adapter_id": "if.api.resolver.doi",
"resolved_event": {"event_type": "resolver.doi", "resolved": {"target_url": "https://..."}}
}
```
Related demos (HTML-first):
https://infrafabric.io/static/hosted/review/if-api-doi-resolver-demo/2026-01-07/index.html
https://infrafabric.io/static/hosted/review/if-api-swhid-resolver-demo/2026-01-07/index.html
Related receipts (if.trace; integrity only):
https://infrafabric.io/static/trace/EKMtQbLWtQLZEERUqcKX1g
https://infrafabric.io/static/trace/gtwHpclpxuWVxvcRPDecFQ
*If every resolver needs a bespoke client, you didn’t normalize — you multiplied.*
---
## 7) Example 3: Ad decisioning (Revive Adserver) (proposed)
Context:
- Revive Adserver is an open-source ad server (formerly OpenX).
- GGQ needs deterministic, constraint-aware ad selection for fixed page slots (top banner vs right-rail pavés).
Black/white:
- This section is a proposed integration design, not a deployment claim.
- Backward-compatibility constraint: GGQ v9 already consumes an `ads.json` response shape; preserve that shape even if the upstream provider changes.
- Stable consumer schema (repo-local): `schemas/if-api/ggq_ads_json.schema.json`
- Proposed provider adapter_spec schema (repo-local): `schemas/if-api/ggq_revive_ads_adapter_spec.schema.json`
- Draft spec instance (repo-local): `docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json`
Decisioning rule (first principles):
- **Ad selection is synchronous** (the page needs the creative immediately).
- Therefore: decisioning belongs in an `if.api` adapter (request/response), not on an async bus path.
- Telemetry (impressions/clicks), sync jobs, and audit exports can go async later.
Stable consumer contract (GGQ v9, illustrative):
- Input (HTTP GET): `page=<home|destination|itineraires|article|map>`, optional `destination_id=<int>`, optional `locale=<fr|en>`
- Output (JSON):
```json
{
"locale": "fr",
"page": "article",
"destination_id": 234,
"banner": {"id": 123, "href": "https://…", "image_url": "https://…", "alt": "…", "type": "banniere"},
"paves": [{"id": 628, "href": "https://…", "image_url": "https://…", "alt": "…", "type": "pave"}],
"inline": null
}
```
Proposed adapter boundary:
- The `if.api` adapter talks to Revive (provider reality) and emits the stable `ads.json` contract (our contract).
- Decisioning stays synchronous; telemetry (impressions/clicks) can go async later.
Non-negotiable guardrails (slot safety):
- Banner slot must only receive banner-shaped creatives; otherwise return `null` and let the frontend fallback path render.
- Pavé slots can be more flexible, but still must enforce max byte size + mime type + safe URL allowlist.
- `tenant_id` does not need to appear in the public `ads.json` response, but it must partition secret refs, rate limits, caches, and rotation state.
Slot enforcement snippet (illustrative; fail closed on layout safety):
```python
def filter_eligible(slot_spec, creatives):
eligible = []
for c in creatives:
if c.get("type") not in slot_spec["allowed_types"]:
continue
if (c.get("width"), c.get("height")) not in slot_spec["allowed_sizes"]:
continue
if int(c.get("byte_size") or 0) > slot_spec["max_bytes"]:
continue
if not is_https_url(c.get("image_url")):
continue
if not is_allowlisted_host(c.get("image_url")):
continue
eligible.append(c)
return eligible
def pick_many_deterministically(eligible, limit):
# Stable selection: sort by (priority desc, id asc). No randomness => reproducible for debugging/receipts.
eligible = sorted(eligible, key=lambda c: (-int(c.get("priority") or 0), str(c.get("id") or "")))
return eligible[:limit]
banner = pick_many_deterministically(filter_eligible(BANNER_SPEC, revive_creatives), limit=1)
banner = banner[0] if banner else None
paves = pick_many_deterministically(filter_eligible(PAVE_SPEC, revive_creatives), limit=PAVE_SPEC["max_count"])
```
Determinism note:
- Stable sorting (no randomness) keeps outputs reproducible for debugging and receipt-backed demos.
- If you later add weighting, rotation, or frequency capping, treat it as a stateful change that must be documented and test-vectored.
Repo-local POC (non-authoritative, but runnable):
- `scripts/if_api_ggq_ads_revive_adapter_poc.py`
*If you let arbitrary creative sizes into a fixed layout, you’re not selling ads — you’re selling broken pages.*
---
## 8) Guardrails: rate limits, caching, idempotency, secrets, classification
### Rate limits (enforceable, not vibes)
If a provider publishes rate limits, treat them as a contract surface:
- capture them in `adapter_spec.rate_limits`
- pin evidence (URL + retrieved_at + sha256)
- implement enforcement (backoff, caching, retry discipline)
### Caching (explicit)
Caching is not an optimization; it is part of the correctness story when providers are flaky.
Rule:
- cache must be explicit in the adapter spec (what is cached, TTL, invalidation signal)
### Idempotency (repeatable without corruption)
For write-like operations:
- require idempotency keys
- record stable operation IDs
- emit deterministic receipts for “what bytes were used”
### Secrets (references only)
Hard rule:
- adapters never embed raw secrets in specs, logs, or published artifacts
- adapters refer to secrets by opaque references (`secret_ref` / templates)
Illustrative `secret_ref` shape:
```json
{
"auth": {
"type": "api_key",
"secret_ref": "secrets://if.api/<tenant>/revive/api_key"
}
}
```
Resolution model (design intent):
- `secret_ref` is a pointer string; the value lives in the operator secret store (not in this repo).
- Rotation should change the secret value without changing the `secret_ref`.
- Logging must redact any secret-shaped values (headers, query params) and should prefer stable identifiers (`adapter_id`, request id, and `secret_ref`).
- Receipts and `/llm` artifacts must never contain secrets; if they contain a `secret_ref`, it is still only a reference.
Secret resolution failure (fail closed):
- If `secret_ref` fails to resolve (missing, expired, revoked), fail closed (5xx/503) and log `adapter_id` + `secret_ref` (not the secret).
- Do not fall back to cached credentials; it creates a stale-auth window and hides rotation failures.
### Classification wall (`/llm` vs `/mcp`)
Rule:
- public evidence belongs in `/llm`
- tenant/NDA evidence belongs in `/mcp` (token-gated)
Reference:
https://infrafabric.io/llm/platform/mcp-integration/2026-01-25/index.md.txt
*If your adapter spec doesn’t name its guardrails, your incident report will.*
---
## 8b) Failure modes: drift, 429s, signature failures, spec conflicts
The happy path is the easy part. The adapter boundary exists so failure behavior is explicit and repeatable.
Failure decision tree (conceptual):
```mermaid
flowchart TD
A["Request arrives"] --> B["Validate input + auth"]
B -->|invalid| R400["Return 400 or 401"]
B --> C["Call provider"]
C -->|200 + expected shape| D["Normalize + enforce"]
D --> E["Return canonical JSON"]
C -->|429 or 503| F["Backoff + cache"]
F -->|cache hit| E
F -->|cache miss| G["Return safe fallback"]
C -->|schema drift| H["Fallback + open incident"]
B -->|webhook sig fail| R403["Reject 403"]
```
Specific failure modes to name in the adapter spec (and tests):
- **Provider API changes without notice**: treat as drift; fail closed where safety matters, otherwise return a minimal fallback and mark capability as `unknown` until re-pinned.
- **Rate limits hit mid-request** (`429`, `Retry-After`): prefer serving cached or degraded responses over retry storms; update observables to match reality.
- **Webhook signature validation fails**: reject and log (without leaking secrets); do not “accept anyway”.
- **Tenant context cannot be resolved**: fail closed (4xx); do not default to a shared tenant bucket.
- **Secret resolution fails** (`secret_ref` missing/expired/revoked): fail closed; do not “keep going” with stale cached auth.
- **Spec conflicts with observed behavior**: record both (spec vs observables), then either update the spec or implement a compensating rule, but don’t pretend it’s fine.
*If failure modes aren’t written down, they will be invented live during the outage.*
---
## 9) Ship checklist: making an integration reviewable
To ship an integration as reviewable (not just “exists”):
1) Register the product/module correctly (do not invent IDs).
2) Publish a minimal adapter spec (schemas + examples + boundaries).
3) Provide a runnable demo (no private infra required).
4) Bind outputs to `if.trace` receipts (brand `if.trace`).
5) Publish an HTML-first review surface with a plain-text fallback.
Registry and scaffolding entrypoints (one URL per line):
https://infrafabric.io/llm/if.registry.json.txt
https://infrafabric.io/llm/entrypoint.json.txt
https://infrafabric.io/llm/sot/index.json.txt
*If a reviewer can’t reproduce the demo, you didn’t ship a demo — you shipped an invitation to trust you.*
---
## 10) Verify checklist (boring on purpose)
Repo-local (no network):
```bash
python3 -m py_compile scripts/if_api_doi_resolver_demo.py
python3 -m py_compile scripts/if_api_swhid_resolver_demo.py
python3 -m py_compile scripts/if_api_ggq_ads_revive_adapter_poc.py
python3 -m py_compile scripts/if_blackboard.py
# expect: exit code 0
```
Registry + schemas (repo-local):
```bash
python3 -c 'import jsonschema, referencing'
# expect: exit code 0
python3 - <<'PY'
import json
reg=json.load(open('if.registry.json'))
products=reg.get('products') or []
entry=next((p for p in products if p.get('product_id')=='if.api'), None)
print((entry or {}).get('status',''))
print((entry or {}).get('path',''))
PY
# expect:
# preview
# /if/api
python3 - <<'PY'
import json
for p in [
'schemas/if-api/adapter.schema.json',
'schemas/if-api/adapter_spec.schema.json',
'schemas/if-api/ggq_ads_json.schema.json',
'schemas/if-api/ggq_revive_ads_adapter_spec.schema.json',
'schemas/if-bus/envelope.schema.json',
'schemas/if-bus/security_signal.schema.json',
]:
json.load(open(p))
print('ok')
PY
# expect: ok
python3 - <<'PY'
import json
import jsonschema
from referencing import Registry, Resource
from referencing.jsonschema import DRAFT202012
adapter_schema=json.load(open('schemas/if-api/adapter.schema.json'))
spec_schema=json.load(open('schemas/if-api/adapter_spec.schema.json'))
revive_spec_schema=json.load(open('schemas/if-api/ggq_revive_ads_adapter_spec.schema.json'))
registry = Registry()
for s in [adapter_schema, spec_schema, revive_spec_schema]:
schema_id = s.get("$id")
if schema_id:
registry = registry.with_resource(schema_id, Resource.from_contents(s, default_specification=DRAFT202012))
validator = jsonschema.Draft202012Validator(spec_schema, registry=registry, format_checker=jsonschema.FormatChecker())
spec={"spec_id":"if.api.example@0.1.0","adapter":{"adapter_id":"if.api.example","label":"Example adapter","version":"0.1.0"}}
validator.validate(spec)
validator2 = jsonschema.Draft202012Validator(revive_spec_schema, registry=registry, format_checker=jsonschema.FormatChecker())
revive_spec_instance=json.load(open('docs/data/if_api_adapter_spec.ggq_revive_ads.v1.json'))
validator2.validate(revive_spec_instance)
print("schema_ok")
print("ggq_revive_spec_ok")
PY
# expect:
# schema_ok
# ggq_revive_spec_ok
```
Public URLs (optional):
```bash
curl -fsSI https://infrafabric.io/static/hosted/review/if-api-github-webhook-demo/2026-01-05/index.html | head -n 5
# expect: HTTP/2 200
curl -fsSI https://infrafabric.io/static/trace/Z_kiBtTQHztidcgYYaSRy4UK | head -n 5
# expect: HTTP/2 200
curl -fsSI https://infrafabric.io/static/hosted/review/if-api-swhid-resolver-demo/2026-01-07/index.html | head -n 5
# expect: HTTP/2 200
curl -fsSI https://infrafabric.io/static/trace/gtwHpclpxuWVxvcRPDecFQ | head -n 5
# expect: HTTP/2 200
curl -fsSL --retry 5 --retry-delay 1 --connect-timeout 5 --max-time 15 'https://ww1.guidesgq.com/__backend/ggq/_v9/api/ads.json?page=article&cb=0' -o /tmp/if_api_ads.json
# expect: exit code 0
python3 - <<'PY'
import json, jsonschema
j=json.load(open('/tmp/if_api_ads.json'))
print(sorted(j.keys()))
required={'banner','destination_id','inline','locale','page','paves'}
missing=required - set(j.keys())
assert not missing, f"missing keys: {sorted(missing)}"
print('ads_keys_ok')
schema=json.load(open('schemas/if-api/ggq_ads_json.schema.json'))
jsonschema.Draft202012Validator(schema).validate(j)
print('ads_schema_ok')
PY
# expect:
# (prints sorted keys)
# ads_keys_ok
# ads_schema_ok
```
*If the verify steps are hand-wavy, the system will be too.*
---
## 11) Appendix (links, one URL per line)
Further reading (suggested order):
1) Orientation:
- `if.registry.json`
- `docs/32-if-api-integrations-inventory.md`
2) Boundaries + evidence surfaces:
- `docs/31-if-bus-whitepaper.md`
- `docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md`
3) Public surfaces (no-login):
https://infrafabric.io/llm/products/if-api/
https://infrafabric.io/if/trace/
https://infrafabric.io/llm/blackboard/index.md.txt
if.api inventory (repo-local):
- docs/32-if-api-integrations-inventory.md
if.bus posture (repo-local):
- docs/205-if-bus-full-stack-whitepaper-v1.md
- docs/207-if-bus-2026-01-22-full.md
MCP boundary whitepaper (repo-local):
- docs/212-mcp-integration-full-stack-whitepaper-2026-01-25.md