{
  "openapi": "3.1.0",
  "info": {
    "title": "OTONOMI Runtime Inspection API",
    "description": "Inspect prompts, model output, tool calls, and MCP metadata before an AI agent acts.",
    "version": "1.0.0"
  },
  "servers": [{ "url": "https://otonmi.com", "description": "Hosted API" }],
  "paths": {
    "/api/inspect": {
      "post": {
        "operationId": "inspect",
        "summary": "Inspect one unit of agent activity",
        "description": "Runs the runtime detectors, then applies a policy. Policy resolution: (1) the policy bound to the API key always wins — the x-otn-app-id header is ignored for such a key; (2) for a key with no bound policy, the x-otn-app-id header selects the key owner's policy with that app id; (3) otherwise the owner's default policy; (4) otherwise the built-in default (every category enforced by severity: CRITICAL/HIGH block, MEDIUM flags). Policies are cached per edge isolate for up to 30 seconds, so a policy change can take up to ~30 s to apply to all traffic. Allowlists (approved models, tools, MCP servers) evaluate the names the caller reports — metadata.model, metadata.mcp_server and tool_call.name — so they constrain honest integrations and gateways you control, not a client that misreports them. Use `action` to decide what to do with the payload: pass it through, block it, or forward the redacted copy in `texts` (mutate). Inspected content (input.content + tool_call.arguments + metadata) is capped at 256 KB.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "x-otn-app-id",
            "in": "header",
            "required": false,
            "description": "Selects the owner's policy whose app id matches (letters, digits, '.', '_', '-'; max 64) — only for keys without a bound policy; a key's own policy always wins. Unknown or malformed values fall through to the default policy.",
            "schema": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InspectRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection completed",
            "headers": {
              "X-Request-Id": {
                "description": "Same value as request_id in the body; use for support/log correlation",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/InspectionResult" },
                    { "$ref": "#/components/schemas/MinimalInspectionResult" }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" },
                "example": { "error": "Invalid input" }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" },
                "example": { "error": "invalid api key" }
              }
            }
          },
          "403": {
            "description": "API key lacks the inspect scope",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" },
                "example": { "error": "insufficient scope" }
              }
            }
          },
          "429": {
            "description": "Per-key rate limit exceeded — 120 inspections/minute/key, sliding window",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": { "type": "integer" },
                "description": "Window size (120)"
              },
              "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Reset": {
                "schema": { "type": "integer" },
                "description": "Epoch milliseconds when the window resets"
              },
              "Retry-After": {
                "schema": { "type": "integer" },
                "description": "Seconds to wait before retrying"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RateLimitError" },
                "example": {
                  "error": "Rate limit exceeded",
                  "limit": 120,
                  "remaining": 0,
                  "reset": "2026-08-06T17:46:40.000Z"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected inspection error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" },
                "example": { "error": "Internal server error" }
              }
            }
          },
          "413": {
            "description": "Inspected content (input.content + tool_call.arguments + metadata) exceeds 256 KB",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" },
                "example": { "error": "Payload too large" }
              }
            }
          },
          "503": {
            "description": "Required protection dependency unavailable (fail-closed guard). Also returned as { \"error\": \"policy unavailable\" } when the key has a bound policy that cannot be loaded — the request is never evaluated under a weaker policy.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "oton_sk_..." }
    },
    "schemas": {
      "InspectRequest": {
        "type": "object",
        "required": ["input"],
        "properties": {
          "input": { "$ref": "#/components/schemas/InspectionInput" },
          "context": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "detail": {
                "type": "string",
                "enum": ["minimal", "standard", "full"],
                "default": "standard",
                "description": "minimal: verdict, action, flagged, latency_ms, request_id (+ texts) — no findings. standard: the full InspectionResult. full: standard plus a per-category `categories` map. Unknown values fall back to standard."
              }
            }
          }
        }
      },
      "InspectionInput": {
        "type": "object",
        "required": ["role", "content"],
        "properties": {
          "role": { "type": "string", "enum": ["user", "assistant", "tool", "system"] },
          "content": { "type": "string" },
          "tool_call": {
            "type": "object",
            "required": ["name", "arguments"],
            "properties": {
              "name": { "type": "string" },
              "arguments": { "type": "object", "additionalProperties": true }
            }
          },
          "metadata": { "type": "object", "additionalProperties": true }
        }
      },
      "Finding": {
        "type": "object",
        "required": ["severity"],
        "properties": {
          "id": { "type": "string" },
          "severity": { "type": "string", "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] },
          "category": {
            "type": "string",
            "description": "Runtime: injection | tool-poisoning | excessive-agency | exfiltration | supply-chain | secret | policy (unapproved model / tool / MCP server, or a blocked phrase from your policy). Repo scans also emit: dependency | config | quality.",
            "enum": [
              "injection",
              "tool-poisoning",
              "excessive-agency",
              "exfiltration",
              "supply-chain",
              "secret",
              "policy",
              "dependency",
              "config",
              "quality"
            ]
          },
          "mode": {
            "type": "string",
            "enum": ["enforce", "block", "flag", "mask", "monitor"],
            "description": "The policy mode this finding was evaluated under. enforce: by severity. block: always blocks. flag / mask: at most flag (mask also redacts; available for secret, exfiltration and policy only — if more than 500 values would need masking the request is blocked instead). monitor: reported only, never changes the verdict."
          },
          "description": { "type": "string" },
          "evidence": {
            "type": "string",
            "description": "Matched text. Secret values are redacted before the response is built — safe to log."
          },
          "detector_source": {
            "type": "string",
            "description": "Stable detector id — useful for suppression rules"
          },
          "owasp_category": {
            "type": "string",
            "description": "OWASP mapping when applicable, e.g. LLM01"
          },
          "cve_references": { "type": "array", "items": { "type": "string" } },
          "file_path": { "type": "string" },
          "line_start": { "type": "integer" },
          "line_end": { "type": "integer" }
        },
        "additionalProperties": true
      },
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      },
      "RateLimitError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "limit": { "type": "integer" },
          "remaining": { "type": "integer" },
          "reset": { "type": "string", "format": "date-time" }
        }
      },
      "InspectionResult": {
        "type": "object",
        "required": ["verdict", "findings", "latency_ms", "request_id"],
        "properties": {
          "verdict": { "type": "string", "enum": ["allow", "flag", "block"] },
          "findings": { "type": "array", "items": { "$ref": "#/components/schemas/Finding" } },
          "latency_ms": { "type": "integer", "minimum": 0 },
          "request_id": { "type": "string" },
          "action": { "$ref": "#/components/schemas/InspectionAction" },
          "texts": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Present when action is mutate: input.content with each masked value replaced by [REDACTED:<TYPE>] (e.g. [REDACTED:AWS_ACCESS_KEY], [REDACTED:SSN], [REDACTED:EMAIL])."
          },
          "tool_arguments": {
            "type": "object",
            "additionalProperties": true,
            "description": "Present when masking changed input.tool_call.arguments: the redacted copy, same shape."
          },
          "policy": {
            "description": "The policy that was applied; null means the built-in default.",
            "oneOf": [
              {
                "type": "object",
                "required": ["id", "name"],
                "properties": {
                  "id": { "type": "string", "format": "uuid" },
                  "name": { "type": "string" }
                }
              },
              { "type": "null" }
            ]
          },
          "categories": {
            "type": "object",
            "additionalProperties": { "type": "boolean" },
            "description": "Only with context.detail = full: whether each category (injection, secret, tool-poisoning, excessive-agency, exfiltration, supply-chain, policy) produced a finding."
          }
        }
      },
      "InspectionAction": {
        "type": "string",
        "enum": ["pass", "block", "mutate"],
        "description": "pass: forward the payload unchanged. block: stop it (verdict is block). mutate: forward the redacted copy in texts / tool_arguments instead of the original."
      },
      "MinimalInspectionResult": {
        "type": "object",
        "description": "Response when context.detail is minimal.",
        "required": ["verdict", "action", "flagged", "latency_ms", "request_id"],
        "properties": {
          "verdict": { "type": "string", "enum": ["allow", "flag", "block"] },
          "action": { "$ref": "#/components/schemas/InspectionAction" },
          "flagged": { "type": "boolean", "description": "verdict is not allow" },
          "latency_ms": { "type": "integer", "minimum": 0 },
          "request_id": { "type": "string" },
          "texts": { "type": "array", "items": { "type": "string" } }
        }
      }
    }
  }
}
