# ai-surface report schema 1.0 (flagship contract)

This is the **frozen contract** for the flagship merge (`feat/flagship-ui`).
Everything is built against it:

- the **engine** (detectors) produces `Finding`s in this shape
- the **UI viewer** renders the emitted JSON directly and does **not** re-scan
- the **funnel** (`cross_promo`) attaches `bridges` to findings
- the **CI/PR diff** in the GitHub Action consumes the same JSON

Canonical example: [`fixtures/sample_report.json`](../fixtures/sample_report.json),
regenerated by `python3 fixtures/generate_sample.py`. Build against the fixture;
do not wait on other tracks.

Source of truth for the types: `src/ai_surface/types.py`.

## Design rules (do not violate)

1. **Discovery stays severity-free.** Inventory-only findings (llm-sdk, agent,
   gateway, infra, env-key, plain api) have `severity: null` and `audit: null`.
   Absence of severity means "inventoried, not assessed", on purpose.
2. **Severity comes only from a deep-dive audit layer.** Today those are the MCP,
   agent, and RAG audits. When present, `finding.severity` = the max severity
   across `finding.audit.risk_flags`.
3. **Secrets carry names/types only, never values.** A captured secret value is
   never read into the report. The local CLI makes no network calls, so no value
   leaves the machine.
4. **Optional fields are always emitted** (null / [] when N/A) so consumers need
   no guards.
5. **Static only.** No runtime execution. Runtime validation is the paid
   platform; this tool routes to it via `bridges`.

## Top-level object

```jsonc
{
  "schema_version": "1.0",
  "tool_version": "1.0.3",
  "scan_root": "/abs/path",
  "scan_timestamp": "2026-06-11T00:00:00+00:00",   // ISO 8601 UTC
  "detectors_run": ["llm_sdks", "mcp_audit", "api_endpoints", ...],
  "findings_count": 4,
  "summary": { ... },        // see Summary
  "findings": [ { ... } ],   // see Finding
  "errors": ["..."]          // non-fatal per-detector errors
}
```

## Summary (computed by the reporter, drives the UI header)

```jsonc
"summary": {
  "total_findings": 4,
  "by_category": { "llm-sdk": 1, "agent-framework": 1, "mcp-server": 1, "api": 1 },
  "by_severity": { "critical": 1 },          // only findings that HAVE severity
  "top_risks": ["MCP Server: stripe-mcp: ...", ...],  // severity-ordered, <=10
  "bridges_available": ["agent-validation", "mcp-runtime", "api-runtime"]
}
```

## Finding

```jsonc
{
  "surface": "MCP Server: stripe-mcp",     // user-facing name
  "category": "mcp-server",                // one of the categories below
  "evidence": {
    "files": ["..."],
    "snippet": "...",                      // <=200 chars
    "line_numbers": [4],
    "metadata": { }                        // category-specific, see API keys below
  },
  "permissions": ["create_charge", "refund"],
  "risk_indicators": ["financial action exposed"],  // plain-English, severity-free
  "detector_name": "mcp_audit",
  "severity": "critical",                  // null unless audited
  "audit": { ... },                        // null unless deep-dived (see Audit)
  "bridges": [ { ... } ]                   // paid upgrade paths (see Bridge)
}
```

### Categories

| category | meaning | detector | severity? |
|---|---|---|---|
| `llm-sdk` | LLM provider SDK call sites | `llm_sdks` | no |
| `agent-framework` | agent definitions + tools | `agent_frameworks` | no |
| `mcp-server` | MCP servers (discovery + **deep-dive audit**) | `mcp_audit` | yes (audit) |
| `model-gateway` | proxy/routing layers | `model_gateways` | no |
| `ai-infra` | self-hosted runtimes, cloud endpoints | `ai_infra` | no |
| `env-key` | AI provider key **names** | `env_keys` | no |
| `api` | **NEW** HTTP/REST endpoints + OpenAPI specs | `api_endpoints` | no (today) |

### `evidence.metadata` keys for `category: "api"`

So the UI and the API-runtime bridge read them uniformly:

| key | example |
|---|---|
| `method` | `"POST"` or `"*"` |
| `path` | `"/v1/orders/{id}/refund"` |
| `source_spec` | `"openapi.yaml"` (if a spec was found) |
| `auth` | `"bearer"` / `"none"` / `"unknown"` |
| `framework` | `"fastapi"` / `"express"` / `"spring"` |

## Audit (deep-dive layer; MCP first)

```jsonc
"audit": {
  "risk_flags": [
    {
      "flag": "secrets-in-env",      // stable machine id
      "severity": "critical",        // critical|high|medium|low|info
      "description": "Live Stripe secret key present in MCP env block",
      "owasp": ["LLM02"],            // OWASP LLM Top 10 ids
      "remediation": "Move the key to a secrets manager; reference by name only."
    }
  ],
  "secrets": [
    { "name": "STRIPE_SECRET_KEY", "secret_type": "stripe-key",
      "confidence": "high", "severity": "critical", "location": ".mcp.json:env" }
  ],
  "trust_score": null,               // 0-100 if source is in a known registry
  "trust_label": "unknown",          // verified|community|unknown
  "registry_match": "unknown",       // known|unknown|""
  "owasp_mappings": ["LLM02", "LLM06", "LLM03"]  // flattened for quick badges
}
```

## Bridge (the funnel; one free scan -> paid platform)

```jsonc
{
  "sku": "mcp-runtime",              // agent-validation | mcp-runtime | api-runtime
  "label": "Run MCP runtime validation in APIsec",
  "url": "https://apisec.ai/...?utm_source=ai-surface&utm_medium=ui&utm_campaign=oss-funnel"
}
```

| sku | source surface | paid destination |
|---|---|---|
| `agent-validation` | AI/agent surface | agent-native AEV |
| `mcp-runtime` | MCP findings | MCP runtime validation |
| `api-runtime` | discovered APIs | API outside-in runtime testing (NG platform) |

All bridge URLs carry `utm_source=ai-surface`, `utm_medium=<cli|ui|pr-comment>`,
`utm_campaign=oss-funnel` for attribution.
