{"openapi": "3.0.3", "info": {"title": "Trismag", "version": "0.3.0", "description": "Checks untrusted text for prompt-injection patterns and returns a risk level. Deterministic: no model, no network call, same answer every time.\n\nA LOW verdict means no known pattern matched. It is NOT a guarantee that the text is safe -- novel or obfuscated attacks return LOW. Use it as a fast first pass, never as the only control."}, "servers": [{"url": "https://trismag-api.fly.dev"}], "components": {"securitySchemes": {"previewToken": {"type": "apiKey", "in": "header", "name": "X-Access", "description": "Only needed if an access gate is switched on. The service is currently open."}, "apiKey": {"type": "apiKey", "in": "header", "name": "X-Payment", "description": "Your Trismag key. Without it you get the free tier: a risk level, no findings."}}, "schemas": {"Error": {"type": "object", "required": ["error"], "description": "One status per meaning. 400: the request itself is wrong. 401: no key, or one we do not recognise. 402: a key we recognise with nothing left to spend, or no key on a call with no free tier. 413: body too large. 429: too fast. `error` is stable and machine-readable; `detail` is for people.", "properties": {"error": {"type": "string"}, "detail": {"type": "string"}, "keys_url": {"type": "string", "description": "Where to get a key, when that is the fix."}}}, "Verdict": {"type": "object", "properties": {"risk": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"], "description": "The single worst finding present, not an average. CRITICAL: the text tries to take control of the agent reading it. HIGH: tries to move data out or claims authority it does not have. MEDIUM: ADVISORY ONLY -- ordinary text with a link scores MEDIUM constantly; treat it as a log level, not a risk level, and block on HIGH and CRITICAL. LOW: nothing known matched, which is not a guarantee of safety."}, "rule_weight": {"type": "number", "format": "float", "description": "How much rule evidence fired, 0..1. NOT a probability and not calibrated: one CRITICAL finding is 0.45, two findings are 0.90. Useful for tiering policy, useless as a certainty."}, "tier": {"type": "string", "enum": ["preview", "paid"]}, "findings": {"type": "array", "description": "Only present with a key.", "items": {"type": "object", "properties": {"severity": {"type": "string"}, "label": {"type": "string"}, "detail": {"type": "string"}}}}, "scans_remaining": {"type": "integer", "nullable": true}}}}}, "security": [{"previewToken": []}], "paths": {"/v1/triage": {"post": {"operationId": "checkTextForInjection", "summary": "Check whether text is trying to hijack an AI agent", "description": "Send `text` for a single piece of content, or `messages` for a conversation. Use `messages` when an attack could be split across turns so that no single message looks bad.", "security": [{"previewToken": [], "apiKey": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Exactly one of `text` or `messages`. Both, neither, null, or the wrong type is refused with 400 before any allowance is spent -- only one field is ever scanned, so accepting both would leave the other unchecked.", "oneOf": [{"required": ["text"], "not": {"required": ["messages"]}}, {"required": ["messages"], "not": {"required": ["text"]}}], "properties": {"text": {"type": "string", "minLength": 1, "description": "The untrusted text to check."}, "messages": {"type": "array", "minItems": 1, "items": {"type": "string"}, "description": "A conversation, oldest first. Use instead of text."}, "source": {"type": "string", "maxLength": 64, "description": "Optional label echoed back; where the text came from."}}}}}}, "responses": {"200": {"description": "A verdict.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Verdict"}}}}, "401": {"description": "Preview token missing or wrong."}, "402": {"description": "Free allowance spent for today."}}}}, "/v1/sanitize": {"post": {"operationId": "sanitizeText", "summary": "Format untrusted text for human review", "description": "Replaces invisible characters, normalises Unicode and strips control codes, then returns the review text alongside the same risk level /v1/triage would give.\n\nThis is for text a PERSON is about to read. It is not a way to make hostile text safe for a tool-capable model: the words still say what they said, and a model that can act on them still can. Requires a key; there is no free tier on this route.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"text": {"type": "string", "description": "The text to format for human review. One piece at a time; 'messages' is not accepted here."}, "source": {"type": "string"}}, "required": ["text"]}}}}, "responses": {"200": {"description": "Human-review text and the original verdict; instructions may remain.", "content": {"application/json": {"schema": {"type": "object", "properties": {"tier": {"type": "string"}, "risk": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]}, "review_text": {"type": "string"}, "clean_text": {"type": "string", "deprecated": true, "description": "Legacy alias of review_text; not safe for agent execution."}, "deprecation": {"type": "string"}, "text_changed": {"type": "boolean"}, "limitations": {"type": "string"}, "findings_count": {"type": "integer", "description": "How many findings were present in the original."}, "scans_remaining": {"type": "integer", "nullable": true}}}}}}, "400": {"description": "Wrong shape; nothing was charged."}, "401": {"description": "Key missing or not ours."}, "402": {"description": "Key has nothing left to spend."}}}}, "/v1/tools": {"post": {"operationId": "checkMcpTools", "summary": "Check an MCP server's tools before connecting it", "description": "A tool description is injected into a model's context verbatim, so a hostile server can hide instructions there. Give a server URL and we run initialize and tools/list against it -- nothing is ever called -- or paste a tools/list array you already have. Exactly one of 'url' or 'tools'.\n\nA whole manifest is one scan, not one per tool. Without a key you get each tool's risk level and its declared capabilities; the findings detail needs a key.\n\nIt judges whether a description tries to manipulate the model, NOT whether the tool is safe: a shell tool is dangerous and honest, and reports a capability rather than a finding. Nested schema strings and metadata keys are checked. Paginated or oversized remote manifests are refused with scan_complete=false and risk=null. Metadata beyond depth 32 or 10000 nodes per traversal is also refused; never treat an incomplete scan as LOW.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "oneOf": [{"required": ["url"], "not": {"required": ["tools"]}}, {"required": ["tools"], "not": {"required": ["url"]}}], "properties": {"url": {"type": "string", "description": "Public https URL of an MCP server."}, "tools": {"type": "array", "items": {"type": "object"}, "minItems": 1, "description": "The 'tools' array from a tools/list response."}, "source": {"type": "string"}}}}}}, "responses": {"200": {"description": "Per-tool verdicts, worst first.", "content": {"application/json": {"schema": {"type": "object", "properties": {"tier": {"type": "string"}, "risk": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"], "description": "The worst verdict across every tool."}, "tools_scanned": {"type": "integer"}, "scan_complete": {"type": "boolean"}, "tools": {"type": "array", "items": {"type": "object"}}, "server": {"type": "object", "description": "serverInfo, when a URL was given."}, "limitations": {"type": "string"}}}}}}, "400": {"description": "Wrong shape, or the URL could not be checked completely. URL fetch attempts are metered before fetching."}, "422": {"description": "Metadata traversal limit exceeded; scan_complete=false, risk=null. Scan was metered."}, "402": {"description": "Free allowance spent."}}}}, "/v1/keys": {"post": {"operationId": "requestApiKey", "summary": "Get a key (one per email address)", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["email"], "properties": {"email": {"type": "string", "format": "email"}}}}}}, "responses": {"201": {"description": "Key issued. Shown once and never again."}, "409": {"description": "That address already has a key."}}}}, "/v1/usage": {"get": {"operationId": "checkRemainingScans", "summary": "How many scans your key has left", "security": [{"previewToken": [], "apiKey": []}], "responses": {"200": {"description": "Remaining balance."}}}}, "/health": {"get": {"operationId": "getServiceInfo", "summary": "Version, limits, and the service's stated limitations", "security": [], "responses": {"200": {"description": "Service description."}}}}}}