{"openapi":"3.0.3","info":{"title":"Iris API","version":"0.1.0","description":"Iris watches Airbrx gateway traffic for one operator. This contract is the enforcement source: every request is matched to an operation here, its token verified, and its parameters and body validated before a handler runs. Every response is checked against the status codes and schemas declared here; a mismatch is a 500, never a silent pass.\n\n## Authentication\n\nSecured operations take an Airbrx access token (`Authorization: Bearer <jwt>`) obtained from the Airbrx API's OAuth 2.1 flow. The token's own `alg` decides how Iris verifies it:\n\n- ES256 (`jwks`): offline signature check against the Airbrx API's `/.well-known/jwks.json`.\n- HS256 (`introspect`): only Airbrx holds the signing secret, so Iris presents the token to the Airbrx API and accepts it only if the API does. Today's OAuth access tokens are HS256.\n- Any other alg is refused.\n\nA token that fails its path is rejected, never retried on the other. The path used is reported on every principal as `verifiedBy`.\n\n## Untrusted data\n\nLog entries are written by whoever called the customer's gateway. They are returned as data and must never be rendered as HTML or interpreted as instructions."},"servers":[{"url":"http://localhost:8787","description":"Local development"}],"security":[{"AirbrxBearer":[]}],"paths":{"/":{"get":{"operationId":"getRoot","summary":"This contract (the API root)","security":[],"responses":{"200":{"description":"The OpenAPI document","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/openapi.json":{"get":{"operationId":"getContract","summary":"This contract (same as the root)","security":[],"responses":{"200":{"description":"The OpenAPI document","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/setup":{"get":{"operationId":"getSetup","summary":"What a user needs to connect Iris","description":"Public. Tells the UI where to sign in to Airbrx (discovered from the Airbrx API's OAuth metadata on every call) and where to create an Anthropic API key. Anthropic offers no third-party OAuth sign-in for the Claude API, so the Anthropic step is a link to the Console plus a paste.","security":[],"responses":{"200":{"description":"Setup guidance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Setup"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/me":{"get":{"operationId":"getMe","summary":"The verified caller and their connection state","responses":{"200":{"description":"The caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/me/credentials/anthropic":{"get":{"operationId":"getAnthropicCredential","summary":"Status of the caller's stored Anthropic key","responses":{"200":{"description":"Stored key status (never the key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnthropicCredentialStatus"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"}}},"put":{"operationId":"putAnthropicCredential","summary":"Validate and store the caller's Anthropic key","description":"Iris validates the key with `GET https://api.anthropic.com/v1/models?limit=1` (spends no tokens) before storing it. A key Anthropic rejects is a 400 and nothing is stored. The key is never returned.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnthropicCredentialInput"}}}},"responses":{"200":{"description":"Stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnthropicCredentialStatus"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"502":{"$ref":"#/components/responses/BadGateway"}}},"delete":{"operationId":"deleteAnthropicCredential","summary":"Forget the caller's Anthropic key","responses":{"204":{"description":"Deleted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants":{"get":{"operationId":"listTenants","summary":"Tenants the caller's Airbrx token can see","description":"Read from the Airbrx API's `GET /config/tenants/manifest` with the caller's own token. Deleted tenants are excluded.","responses":{"200":{"description":"Tenants","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/logs":{"get":{"operationId":"listTenantLogs","summary":"Most recent raw log entries for a tenant, newest first","description":"Walks the Airbrx API's raw-log listing (year, month, day, hour) from the newest entry backwards, stepping back over empty hours, and returns up to `limit` entries. The walk is bounded: it stops after a fixed number of listing calls and says so in `walk.exhausted`.","parameters":[{"$ref":"#/components/parameters/TenantId"},{"name":"limit","in":"query","required":false,"description":"Entries to return.","schema":{"type":"integer","minimum":1,"maximum":25}}],"responses":{"200":{"description":"Log entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/runs":{"post":{"operationId":"createRun","summary":"Run Iris over a tenant now (read-only signals)","description":"Reads the summaries, then runs the signals over proxy_logs, and records the findings. Writes nothing to Airbrx. Needs logs, summaries, rules and query grants on the tenant.","parameters":[{"$ref":"#/components/parameters/TenantId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunRequest"}}}},"responses":{"201":{"description":"The run record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"502":{"$ref":"#/components/responses/BadGateway"}}},"get":{"operationId":"listRuns","summary":"Runs for a tenant, newest first","parameters":[{"$ref":"#/components/parameters/TenantId"}],"responses":{"200":{"description":"Runs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/runs/{runId}":{"get":{"operationId":"getRun","summary":"One run record, with its findings","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/RunId"}],"responses":{"200":{"description":"The run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/replays":{"post":{"operationId":"createReplay","summary":"Replay one rule change against the tenant's recent traffic","description":"Evaluates the current ruleset and a candidate (the current ruleset with one rule added, replaced or removed) over every request in the window, using a copy of the gateway's rule evaluation. Reports conformance of that copy, projected hits, matched-rule changes, newly denied requests and newly shared answers. Writes nothing.","parameters":[{"$ref":"#/components/parameters/TenantId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}}},"responses":{"200":{"description":"The replay report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Replay"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals":{"post":{"operationId":"createProposal","summary":"Propose the template fix for one finding","description":"Builds the finding's one-rule change from its template, lints and replays it, and stores it awaiting approval. Writes nothing to Airbrx. The same finding and change return the existing proposal.","parameters":[{"$ref":"#/components/parameters/TenantId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposeRequest"}}}},"responses":{"200":{"description":"Already proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"422":{"$ref":"#/components/responses/Unprocessable"},"502":{"$ref":"#/components/responses/BadGateway"}}},"get":{"operationId":"listProposals","summary":"Proposals for a tenant, newest first","parameters":[{"$ref":"#/components/parameters/TenantId"}],"responses":{"200":{"description":"Proposals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals/{proposalId}":{"get":{"operationId":"getProposal","summary":"One proposal, with its replay","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/ProposalId"}],"responses":{"200":{"description":"The proposal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals/{proposalId}/approve":{"post":{"operationId":"approveProposal","summary":"Apply the proposal to the tenant's rules, as you","description":"Re-reads the ruleset and refuses (409) if it changed since the proposal was built; lints again; writes the full before-image to the ledger; then PUTs the ruleset with your token. Needs PUT on the tenant's rules.","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/ProposalId"}],"responses":{"200":{"description":"The proposal after the decision","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals/{proposalId}/reject":{"post":{"operationId":"rejectProposal","summary":"Reject the proposal","description":"Records the decision. Writes nothing to Airbrx.","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/ProposalId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectRequest"}}}},"responses":{"200":{"description":"The proposal after the decision","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals/{proposalId}/rollback":{"post":{"operationId":"rollbackProposal","summary":"Restore the ruleset from before this proposal was applied","description":"Refuses (409) if the ruleset changed after the apply, so another edit is never undone. Needs PUT on the tenant's rules.","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/ProposalId"}],"responses":{"200":{"description":"The proposal after the decision","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/proposals/{proposalId}/explain":{"post":{"operationId":"explainProposal","summary":"Ask Claude to explain the proposal","description":"Builds an evidence packet from the proposal and its replay and asks Claude, on your Anthropic key, for a structured explanation and recommendation. Answered from the stored explanation when the packet hasn't changed. Counts against the tenant's monthly token ceiling.","parameters":[{"$ref":"#/components/parameters/TenantId"},{"$ref":"#/components/parameters/ProposalId"}],"responses":{"200":{"description":"The proposal, with its explanation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proposal"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/usage":{"get":{"operationId":"getUsage","summary":"This month's Claude usage and ceiling for the tenant","parameters":[{"$ref":"#/components/parameters/TenantId"}],"responses":{"200":{"description":"Usage","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/tenants/{tenantId}/settings":{"get":{"operationId":"getSettings","summary":"The tenant's Iris settings","parameters":[{"$ref":"#/components/parameters/TenantId"}],"responses":{"200":{"description":"Settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantSettings"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"502":{"$ref":"#/components/responses/BadGateway"}}},"put":{"operationId":"putSettings","summary":"Change the tenant's Iris settings","description":"Needs PUT on the tenant's rules: whoever can change the rules decides what Iris may spend.","parameters":[{"$ref":"#/components/parameters/TenantId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantSettingsInput"}}}},"responses":{"200":{"description":"Settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantSettings"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"502":{"$ref":"#/components/responses/BadGateway"}}}}},"components":{"securitySchemes":{"AirbrxBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Airbrx access token from the Airbrx API's OAuth 2.1 authorization-code flow (PKCE S256)."}},"parameters":{"TenantId":{"name":"tenantId","in":"path","required":true,"description":"Airbrx tenant ID. Restricted to characters that are safe in an upstream URL path.","schema":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$"}},"RunId":{"name":"runId","in":"path","required":true,"description":"A run ID.","schema":{"type":"string","pattern":"^r-[0-9]{8}t[0-9]{6}z-[0-9a-f]{6}$"}},"ProposalId":{"name":"proposalId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}}},"responses":{"BadRequest":{"description":"The request does not match this contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, invalid, expired or unverifiable token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The token is valid but lacks a scope this call needs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PayloadTooLarge":{"description":"Body over the size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnsupportedMediaType":{"description":"Body is not application/json","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadGateway":{"description":"An upstream (Airbrx or Anthropic) failed or answered unexpectedly","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The state changed: the proposal was already decided, or the ruleset moved since it was built","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unprocessable":{"description":"No template applies to this finding","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"The tenant's monthly Claude token ceiling is reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","additionalProperties":false,"required":["error","code"],"properties":{"error":{"type":"string","description":"Human-readable message."},"code":{"type":"string","pattern":"^[a-z0-9-]+$","description":"Stable machine-readable code, e.g. `token-expired`, `request-invalid`."},"details":{"type":"array","items":{"type":"string"},"description":"One line per validation failure."}}},"Setup":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","airbrx","anthropic"],"properties":{"schemaVersion":{"type":"integer","minimum":1},"generatedAt":{"type":"string","format":"date-time"},"airbrx":{"$ref":"#/components/schemas/AirbrxSetup"},"anthropic":{"$ref":"#/components/schemas/AnthropicSetup"}}},"AirbrxSetup":{"type":"object","additionalProperties":false,"required":["apiBase","authorizationEndpoint","tokenEndpoint","registrationEndpoint","scope","redirectUri","consentPreset","requiredGrants"],"properties":{"apiBase":{"type":"string","format":"uri"},"authorizationEndpoint":{"type":"string","format":"uri"},"tokenEndpoint":{"type":"string","format":"uri"},"registrationEndpoint":{"type":"string","format":"uri"},"scope":{"type":"string"},"redirectUri":{"type":"string","format":"uri","description":"The exact redirect URI the UI must register and use."},"consentPreset":{"type":"string","description":"The consent-page choice that grants what Iris needs. The read-only preset does not include POST /tenants/{tenantId}/query, so Customize is required."},"requiredGrants":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["method","path"],"properties":{"method":{"type":"string","enum":["GET","POST","PUT","DELETE"]},"path":{"type":"string"}}}}}},"AnthropicSetup":{"type":"object","additionalProperties":false,"required":["keysUrl","workspacesUrl","limitsUrl","steps","keyPattern"],"properties":{"keysUrl":{"type":"string","format":"uri","description":"Claude Console page where the user creates an API key."},"workspacesUrl":{"type":"string","format":"uri"},"limitsUrl":{"type":"string","format":"uri"},"steps":{"type":"array","items":{"type":"string"}},"keyPattern":{"type":"string","description":"Regular expression the key must match (the same one this contract enforces on PUT)."}}},"Me":{"type":"object","additionalProperties":false,"required":["principalId","email","airbrx","anthropic","setupComplete"],"properties":{"principalId":{"type":"string","pattern":"^[0-9a-f]{32}$"},"email":{"type":"string"},"airbrx":{"type":"object","additionalProperties":false,"required":["verifiedBy","expiresAt","patId","tenantIds","patName","access"],"properties":{"verifiedBy":{"type":"string","enum":["jwks","introspect"]},"expiresAt":{"type":"string","format":"date-time"},"patId":{"type":"string","nullable":true,"description":"The PAT the Airbrx consent minted behind this token, when the token says."},"tenantIds":{"type":"array","items":{"type":"string"}},"patName":{"type":"string","nullable":true,"description":"The name of the PAT the Airbrx consent minted behind this token."},"access":{"type":"array","nullable":true,"description":"What the token lets Iris do, per tenant, read from its signed scopes claim. Null for a token without a scope map (a login-issued user JWT), whose reach the API decides by role at call time.","items":{"type":"object","additionalProperties":false,"required":["tenantId","capabilities"],"properties":{"tenantId":{"type":"string"},"capabilities":{"type":"object","additionalProperties":false,"required":["readLogs","readSummaries","readRules","queryLogs","writeRules"],"properties":{"readLogs":{"type":"boolean"},"readSummaries":{"type":"boolean"},"readRules":{"type":"boolean"},"queryLogs":{"type":"boolean"},"writeRules":{"type":"boolean"}}}}}}}},"anthropic":{"allOf":[{"$ref":"#/components/schemas/AnthropicCredentialStatus"}],"nullable":true},"setupComplete":{"type":"boolean"}}},"AnthropicCredentialInput":{"type":"object","additionalProperties":false,"required":["apiKey"],"properties":{"apiKey":{"type":"string","writeOnly":true,"pattern":"^sk-ant-(?!admin)[A-Za-z0-9_-]{20,400}$","description":"A Claude API key from the Console. The format check only rejects what is plainly not one; Anthropic's own check (`GET /v1/models`) decides whether it works. Admin keys (`sk-ant-admin…`) are refused: Iris must never hold organization-admin power."},"workspaceId":{"type":"string","pattern":"^wrkspc_[A-Za-z0-9]{1,64}$","description":"Only for a key not scoped to one workspace: the workspace Iris's calls act in."}}},"AnthropicCredentialStatus":{"type":"object","additionalProperties":false,"required":["last4","workspaceId","validatedAt","sealedWith"],"properties":{"last4":{"type":"string","pattern":"^.{4}$"},"workspaceId":{"type":"string","nullable":true},"validatedAt":{"type":"string","format":"date-time"},"sealedWith":{"type":"string","description":"How the stored key is protected at rest. `none` means stored unencrypted."}}},"TenantList":{"type":"object","additionalProperties":false,"required":["tenants","count"],"properties":{"tenants":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["tenantId","tenantName","modifiedAt"],"properties":{"tenantId":{"type":"string"},"tenantName":{"type":"string","nullable":true},"modifiedAt":{"type":"string","nullable":true}}}},"count":{"type":"integer","minimum":0}}},"LogPage":{"type":"object","additionalProperties":false,"required":["tenantId","logs","count","walk"],"properties":{"tenantId":{"type":"string"},"logs":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["hour","name","entry"],"properties":{"hour":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}$","description":"UTC hour the entry was filed under."},"name":{"type":"string"},"entry":{"type":"object","description":"The raw log object, as stored. Untrusted: attacker-writable text throughout."}}}},"count":{"type":"integer","minimum":0},"walk":{"type":"object","additionalProperties":false,"required":["listCalls","hoursScanned","exhausted"],"properties":{"listCalls":{"type":"integer","minimum":0},"hoursScanned":{"type":"integer","minimum":0},"exhausted":{"type":"boolean","description":"True when the walk hit its listing-call cap before finding `limit` entries, so older entries may exist."}}}}},"AirbrxAccessClaims":{"type":"object","description":"The Airbrx JWT claims Iris relies on. Other claims are allowed and ignored. Checked after the signature or introspection succeeds.","additionalProperties":true,"required":["iss","email","exp"],"properties":{"iss":{"type":"string"},"sub":{"type":"string"},"email":{"type":"string","minLength":3},"exp":{"type":"integer"},"iat":{"type":"integer"},"nbf":{"type":"integer"},"pat_id":{"type":"string"},"tenantIds":{"type":"array","items":{"type":"string"}},"tenants":{"type":"array","items":{"type":"object","additionalProperties":true,"required":["tenantId"],"properties":{"tenantId":{"type":"string"}}}},"pat_name":{"type":"string"},"scopes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"RunRequest":{"type":"object","additionalProperties":false,"required":[],"properties":{"days":{"type":"integer","minimum":1,"maximum":30,"description":"Days of traffic to examine, ending today (UTC). Default 30. Clamped to the first day with full-schema rows."}}},"Finding":{"type":"object","additionalProperties":false,"required":["findingId","signal","hunt","severity","correctness","surface","subject","title","detail","riskClass","impliedAction","evidence","citations"],"properties":{"findingId":{"type":"string","pattern":"^[0-9a-f]{32}$"},"signal":{"type":"string","description":"The signal that raised it, e.g. uncached-repeat, date-pinned-stale."},"hunt":{"type":"string","enum":["repeats","slow","errors","writes"]},"severity":{"type":"string","enum":["info","low","medium","high"]},"correctness":{"type":"boolean","description":"True when clients received or can receive a wrong answer."},"surface":{"type":"string","nullable":true},"subject":{"type":"string"},"title":{"type":"string","description":"Built by Iris from counts and IDs."},"detail":{"type":"string","description":"May quote statement text from the logs: untrusted."},"riskClass":{"type":"string","nullable":true,"enum":["cache-add","cache-extend","cache-fix","key-change","removal","invalidation-add","invalidation-narrow","deny"]},"impliedAction":{"type":"string","nullable":true},"evidence":{"type":"object","description":"Signal-specific counts. String values may be log-derived and untrusted."},"citations":{"type":"object","additionalProperties":false,"required":["queryHashes","ruleIds","logIds"],"properties":{"queryHashes":{"type":"array","items":{"type":"string"}},"ruleIds":{"type":"array","items":{"type":"string"}},"logIds":{"type":"array","items":{"type":"string"}}}}}},"RunSummary":{"type":"object","additionalProperties":false,"required":["runId","startedAt","completedAt","outcome","findings","correctnessFindings","wasteShare"],"properties":{"runId":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"outcome":{"type":"string","enum":["ok","incomplete"]},"findings":{"type":"integer"},"correctnessFindings":{"type":"integer"},"wasteShare":{"type":"number","nullable":true}}},"RunList":{"type":"object","additionalProperties":false,"required":["runs","count"],"properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/RunSummary"}},"count":{"type":"integer"}}},"Run":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","runId","tenantId","startedAt","completedAt","outcome","incomplete","window","rowsInWindow","quickView","surfaces","waste","coverage","findings"],"properties":{"schemaVersion":{"type":"integer"},"generatedAt":{"type":"string","format":"date-time"},"runId":{"type":"string"},"tenantId":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"outcome":{"type":"string","enum":["ok","incomplete"]},"incomplete":{"type":"array","description":"Every part of the evidence the run could not examine, and why.","items":{"type":"object","additionalProperties":false,"required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}}},"window":{"type":"object","additionalProperties":false,"required":["from","to","requestedFrom","firstRecordedDay","lastRecordedDay"],"properties":{"from":{"type":"string"},"to":{"type":"string"},"requestedFrom":{"type":"string"},"firstRecordedDay":{"type":"string","nullable":true},"lastRecordedDay":{"type":"string","nullable":true}}},"rowsInWindow":{"type":"integer"},"quickView":{"type":"object","nullable":true,"description":"The summaries at run time. latestRunOnly covers only the dates log-summary last processed."},"surfaces":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["surface","hits","nonHits","hitP50","nonHitP50"],"properties":{"surface":{"type":"string"},"hits":{"type":"integer"},"nonHits":{"type":"integer"},"hitP50":{"type":"integer","nullable":true},"nonHitP50":{"type":"integer","nullable":true}}}},"waste":{"allOf":[{"type":"object","additionalProperties":false,"required":["repeatBackendExecutions","readOnlyBackendExecutions","readOnlyExecutions","share","modelledMs"],"properties":{"repeatBackendExecutions":{"type":"integer"},"readOnlyBackendExecutions":{"type":"integer"},"readOnlyExecutions":{"type":"integer"},"share":{"type":"number"},"modelledMs":{"type":"integer"}}}],"nullable":true,"description":"Read-only executions that reached the backend when the same statement already had earlier that UTC day. modelledMs is latency, not measured warehouse cost."},"coverage":{"type":"object","additionalProperties":false,"required":["statementsExamined","sqlCalls","summaryCalls"],"properties":{"statementsExamined":{"type":"integer"},"sqlCalls":{"type":"integer"},"summaryCalls":{"type":"integer"}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}}}},"ReplayRequest":{"type":"object","additionalProperties":false,"required":["change"],"properties":{"change":{"type":"object","additionalProperties":false,"required":["op"],"properties":{"op":{"type":"string","enum":["none","add","replace","remove"],"description":"none replays the current ruleset alone: conformance and calibration only."},"rule":{"type":"object","additionalProperties":true,"required":["id"],"properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,128}$"}},"description":"One complete rule, for add and replace. Linted with the Airbrx rules linter before the replay."},"ruleId":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,128}$","description":"For remove."}}},"days":{"type":"integer","minimum":1,"maximum":30},"targetQueryHashes":{"type":"array","maxItems":200,"items":{"type":"string","pattern":"^[0-9a-f]{64}$"},"description":"For a deny change: the statements it is meant to stop. Any other statement it would refuse counts as collateral."}}},"Replay":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","tenantId","change","lint","window","rowsReplayed","rulesInForceSince","conformance","before","after","calibration","matchedRuleChanges","hits","modelledMs","newlyDenied","sharedAnswers","unknown","outcome","incomplete"],"properties":{"schemaVersion":{"type":"integer"},"generatedAt":{"type":"string","format":"date-time"},"tenantId":{"type":"string"},"change":{"type":"object","description":"The change replayed, as requested."},"lint":{"type":"object","description":"The Airbrx rules linter result for the candidate ruleset: valid, errors, warnings."},"window":{"type":"object","additionalProperties":false,"required":["from","to","requestedFrom"],"properties":{"from":{"type":"string"},"to":{"type":"string"},"requestedFrom":{"type":"string"}}},"rowsReplayed":{"type":"integer"},"rulesInForceSince":{"type":"string","nullable":true,"description":"From the tenant manifest: the last config or rules change. Requests from then on ran under the current ruleset."},"conformance":{"type":"object","description":"The current ruleset evaluated with no help from the log, against the rule the gateway recorded, on requests since rulesInForceSince: agree, disagree, unknown, examples."},"before":{"type":"object","additionalProperties":false,"required":["rows","deny","bypass","hit","miss","unknown"],"properties":{"rows":{"type":"integer"},"deny":{"type":"integer"},"bypass":{"type":"integer"},"hit":{"type":"integer"},"miss":{"type":"integer"},"unknown":{"type":"integer"}}},"after":{"type":"object","additionalProperties":false,"required":["rows","deny","bypass","hit","miss","unknown"],"properties":{"rows":{"type":"integer"},"deny":{"type":"integer"},"bypass":{"type":"integer"},"hit":{"type":"integer"},"miss":{"type":"integer"},"unknown":{"type":"integer"}}},"matchedRuleChanges":{"type":"object","description":"Requests whose rule or outcome changes: rows, users, clients, examples."},"hits":{"type":"object","description":"Projected hits before and after, and the difference."},"modelledMs":{"type":"object","description":"Latency saved and added by the change (modelled from response times, not warehouse cost)."},"newlyDenied":{"type":"object","description":"Requests the candidate would refuse that the current ruleset does not: rows, users, clients, fail-closed (UNKNOWN statements), collateral, statements. Log-derived SQL is untrusted."},"sharedAnswers":{"type":"object","description":"Cache keys answering more than one user, before and after."},"unknown":{"type":"object","description":"Requests whose outcome depends on context the log does not keep, by rule and condition."},"outcome":{"type":"string","enum":["ok","incomplete"]},"incomplete":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}}},"calibration":{"type":"object","additionalProperties":false,"required":["rows","projectedHits","actualHits"],"description":"On requests logged under the current ruleset: hits the replay projects for it against hits the gateway logged. The fixed TTL grid undercounts by design.","properties":{"rows":{"type":"integer"},"projectedHits":{"type":"integer"},"actualHits":{"type":"integer"}}}}},"ProposeRequest":{"type":"object","additionalProperties":false,"required":["runId","findingId"],"properties":{"runId":{"type":"string","pattern":"^r-[0-9]{8}t[0-9]{6}z-[0-9a-f]{6}$"},"findingId":{"type":"string","pattern":"^[0-9a-f]{32}$"}}},"RejectRequest":{"type":"object","additionalProperties":false,"required":[],"properties":{"reason":{"type":"string","maxLength":1000}}},"Proposal":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","proposalId","tenantId","runId","findingId","signal","riskClass","title","change","reason","baseRulesHash","replay","gate","state","createdAt","createdBy","appliedChangeId","history"],"properties":{"schemaVersion":{"type":"integer"},"generatedAt":{"type":"string","format":"date-time"},"proposalId":{"type":"string"},"tenantId":{"type":"string"},"runId":{"type":"string"},"findingId":{"type":"string"},"signal":{"type":"string"},"riskClass":{"type":"string","nullable":true},"title":{"type":"string","description":"From the finding; built by Iris."},"change":{"type":"object","description":"Exactly one rule change: { op: add | replace | remove, rule | ruleId }."},"reason":{"type":"string","description":"Why the template chose this change."},"baseRulesHash":{"type":"string","description":"Hash of the ruleset the proposal was built on. Approve refuses if the ruleset has changed since."},"replay":{"$ref":"#/components/schemas/Replay"},"gate":{"type":"object","additionalProperties":false,"required":["passes","reasons"],"properties":{"passes":{"type":"boolean","description":"Whether it would clear the bar for automatic application. Advice only under Recommend."},"reasons":{"type":"array","items":{"type":"string"}}}},"state":{"type":"string","enum":["awaiting-approval","applied","rejected","superseded","rolled-back"]},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"decidedAt":{"type":"string","format":"date-time"},"decidedBy":{"type":"string"},"appliedChangeId":{"type":"string","nullable":true},"rolledBackChangeId":{"type":"string"},"history":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["at","state","actor"],"properties":{"at":{"type":"string","format":"date-time"},"state":{"type":"string","enum":["awaiting-approval","applied","rejected","superseded","rolled-back"]},"actor":{"type":"string"},"note":{"type":"string","nullable":true}}}},"explanation":{"$ref":"#/components/schemas/Explanation"}}},"ProposalSummary":{"type":"object","additionalProperties":false,"required":["proposalId","findingId","signal","riskClass","title","state","createdAt","decidedAt","gatePasses"],"properties":{"proposalId":{"type":"string"},"findingId":{"type":"string"},"signal":{"type":"string"},"riskClass":{"type":"string","nullable":true},"title":{"type":"string"},"state":{"type":"string","enum":["awaiting-approval","applied","rejected","superseded","rolled-back"]},"createdAt":{"type":"string","format":"date-time"},"decidedAt":{"type":"string","nullable":true},"gatePasses":{"type":"boolean"}}},"ProposalList":{"type":"object","additionalProperties":false,"required":["proposals","count"],"properties":{"proposals":{"type":"array","items":{"$ref":"#/components/schemas/ProposalSummary"}},"count":{"type":"integer"}}},"Explanation":{"type":"object","additionalProperties":false,"required":["packetHash","promptVersion","model","effort","generatedAt","requestedBy","output","usage"],"properties":{"packetHash":{"type":"string","description":"Hash of the evidence packet explained. The same packet is answered from here at no cost."},"promptVersion":{"type":"string"},"model":{"type":"string","description":"The model that answered (a fallback model can differ from the setting)."},"effort":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"requestedBy":{"type":"string"},"output":{"type":"object","additionalProperties":false,"required":["summary","whatChanges","whyItMatters","risks","checksBeforeApproving","recommendation","recommendationReason","confidence","citedFacts"],"properties":{"summary":{"type":"string"},"whatChanges":{"type":"string"},"whyItMatters":{"type":"string"},"risks":{"type":"array","items":{"type":"string"}},"checksBeforeApproving":{"type":"array","items":{"type":"string"}},"recommendation":{"type":"string","enum":["approve","reject","needs-a-person"]},"recommendationReason":{"type":"string"},"confidence":{"type":"string","enum":["low","medium","high"]},"citedFacts":{"type":"array","items":{"type":"string"}}},"description":"Claude's answer. Written from Iris's evidence packet; may quote untrusted log text."},"usage":{"type":"object","additionalProperties":false,"required":["input","output","cacheRead","cacheWrite","estimatedUsd"],"properties":{"input":{"type":"integer"},"output":{"type":"integer"},"cacheRead":{"type":"integer"},"cacheWrite":{"type":"integer"},"estimatedUsd":{"type":"number","nullable":true}}}}},"TenantSettings":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","model","effort","monthlyTokenCeiling"],"properties":{"schemaVersion":{"type":"integer"},"generatedAt":{"type":"string","nullable":true},"updatedBy":{"type":"string"},"model":{"type":"string","enum":["claude-opus-5-5","claude-sonnet-5-5","claude-haiku-5-5"]},"effort":{"type":"string","enum":["low","medium","high"]},"monthlyTokenCeiling":{"type":"integer","minimum":0,"maximum":1000000000}}},"TenantSettingsInput":{"type":"object","additionalProperties":false,"required":[],"properties":{"model":{"type":"string","enum":["claude-opus-5-5","claude-sonnet-5-5","claude-haiku-5-5"]},"effort":{"type":"string","enum":["low","medium","high"]},"monthlyTokenCeiling":{"type":"integer","minimum":0,"maximum":1000000000}}},"Usage":{"type":"object","additionalProperties":false,"required":["schemaVersion","generatedAt","tenantId","month","calls","tokens","estimatedUsd","unpricedCalls","byModel","priceTableVersion","ceiling"],"properties":{"schemaVersion":{"type":"integer"},"generatedAt":{"type":"string","format":"date-time"},"tenantId":{"type":"string"},"month":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}$"},"calls":{"type":"integer"},"tokens":{"type":"integer"},"estimatedUsd":{"type":"number","description":"An estimate from a price table; the Anthropic invoice is authoritative."},"unpricedCalls":{"type":"integer"},"byModel":{"type":"object","description":"Calls and tokens by the model that answered."},"priceTableVersion":{"type":"string"},"ceiling":{"type":"integer"}}}}}}