{
  "openapi": "3.1.0",
  "info": {
    "title": "Saram control plane",
    "description": "Saram sends synthetic personas through your product in a real browser and returns what they found as\nevidence: **issues** a replay reproduced, and **persona feedback** - verdicts and opportunities - quoted from\nwhat each persona did and said. This API starts runs, follows them while they execute, and reads their\nresults, together with the files, credentials and keys a run needs.\n\n## Authentication\n\nEvery route but `GET /api/v1/healthz` wants `Authorization: Bearer <credential>`. The credential is an API\nkey (`sk_saram_...`), minted in the dashboard or with `POST /api/v1/api-keys` and shown once, or the access\ntoken of a person signed in to the dashboard. A missing, unknown, expired or revoked credential gets one\nindistinguishable `401`. A key carries scopes, and a route it lacks the scope for answers `403` naming it:\n\n- `runs:read` - read runs, sessions, steps, results, evidence, personas and usage, list a run's share links,\n  and validate a run spec.\n- `runs:write` - start, pause, resume, cancel and delete runs, retest an issue, and mint and revoke share links.\n- `files:write` - upload, list and delete the files a persona can bring into a run.\n- `credentials:write` - manage your own model provider keys, test-account logins and ChatGPT connection.\n- `keys:admin` - mint, list and revoke API keys, and manage billing.\n\nA key minted without `scopes` gets `runs:write` and `runs:read`.\n\n## A run, end to end\n\n1. `POST /api/v1/runs/validate` with a run spec: every check `POST /api/v1/runs` makes, the sessions it\n   would plan and the most it can bill on your plan (`cost_ceiling`: dollars, a one-line basis, and who\n   bills its model calls) - and no run.\n2. `POST /api/v1/runs` with the same spec: `202` with the `run_id`.\n3. Poll `GET /api/v1/runs/{run_id}`. `status` moves `pending`, `running`, `observing`, then stops on\n   `completed`, `failed` or `cancelled`. Only `completed` means there are results to read.\n4. Read `GET /api/v1/runs/{run_id}/issues`, `/verdicts` and `/opportunities`, or the whole artifact at\n   `GET /api/v1/runs/{run_id}/artifact`.\n\n## Issues and persona feedback\n\nAn issue whose reading says `may_file_ticket_alone: true` was reproduced by a replay and can become a\nticket as it stands. Verdicts and opportunities are persona feedback: subjective, tied to the evidence and\nthe persona's own words, and never a ticket on their own.\n\n## Conventions\n\n- Responses leave an unset field out rather than writing `null`.\n- An error is JSON with a `detail` that says what was refused and why.\n- A run, credential or key of another tenant's answers the same `404` as one that does not exist.\n- Starting, following and reading runs are also MCP tools at `https://api.saram.sh/mcp`, for a coding agent,\n  behind the same credential and the same scopes.\n",
    "version": "0.16.1"
  },
  "servers": [
    {
      "url": "https://api.saram.sh",
      "description": "Saram"
    }
  ],
  "paths": {
    "/api/v1/healthz": {
      "get": {
        "tags": [
          "health"
        ],
        "summary": "Healthz",
        "description": "Whether the API is up: `{\"status\": \"ok\"}`. The one route that takes no credential.",
        "operationId": "healthz",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": {
                    "type": "string"
                  },
                  "type": "object",
                  "title": "Response Healthz"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/workos": {
      "post": {
        "tags": [
          "identity"
        ],
        "summary": "WorkOS Webhook",
        "description": "Mirror one WorkOS organisation, membership or user event into this deployment's own tables.\n\nThe provider knows who a person is and which organisation they chose; these tables decide what that\norganisation may touch. This route is what keeps the\ntwo in step: an invitation accepted at AuthKit becomes a `memberships` row, a member removed there\nstops being able to read this tenant's runs, and an organisation renamed there is renamed here.\n\nEvery branch is idempotent because the delivery is not: WorkOS redelivers until it gets a 2xx, so a\nrepeated `created` re-asserts the same row rather than failing. An event this deployment has no\nreading for is acknowledged as `ignored` rather than refused, because a 4xx would make WorkOS retry\nit forever.\n\nAn organisation nobody has onboarded is `ignored` too, unless the deployment creates tenants on first\nsight: a tenant is the unit of the invoice, and creating an organisation at the provider must not create\none here by itself.",
        "operationId": "workos_webhook",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookAck"
                }
              }
            }
          },
          "401": {
            "description": "the WorkOS-Signature header is missing, malformed, stale, or does not match the body"
          },
          "422": {
            "description": "the body is not a JSON object"
          },
          "503": {
            "description": "this deployment has no WorkOS webhook secret configured, so no delivery can be verified"
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/webhooks/stripe": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Stripe Webhook",
        "description": "Apply one Stripe event to the tenant's plan, balance, card on file or billing standing.\n\nA top-up invoice's `invoice.paid`, `invoice.payment_failed` or `invoice.payment_action_required` signals\nits `TopUpWorkflow` and writes nothing: the workflow reads the invoice from Stripe and credits it once, so\na lost or repeated delivery changes nothing but how soon. A Pro invoice paid grants the month's included\ncredit; one that failed marks the tenant `past_due` and does not suspend it. `customer.subscription.*`\nmoves the tenant's plan for a subscription marked Pro. `setup_intent.succeeded` and `customer.updated`\nmirror the card on file. Any other invoice keeps the arrears rule: a failed payment suspends, a paid one\nlifts a suspension **billing** caused, so an operator's own suspension is not undone by a payment.\n\nEvery branch is idempotent and an event with no reading here is `ignored` rather than refused, for\nthe reason the identity webhook says: Stripe redelivers until it gets a 2xx, and a 4xx would have it\nretrying for days.",
        "operationId": "stripe_webhook",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookAck"
                }
              }
            }
          },
          "401": {
            "description": "the Stripe-Signature header is missing, malformed, stale, or does not match the body"
          },
          "422": {
            "description": "the body is not a JSON object"
          },
          "503": {
            "description": "this deployment has no Stripe webhook secret configured, so no delivery can be verified"
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/webhooks/github": {
      "post": {
        "tags": [
          "credentials"
        ],
        "summary": "GitHub Webhook",
        "description": "Keep the Saram GitHub App's installation links true to GitHub: unlink one uninstalled, follow a rename.\n\n`installation.deleted` unlinks the installation from whichever tenant it was linked to, and\n`installation_target.renamed` moves the link to the account's new login. Neither ever links an\ninstallation to a tenant: a delivery names a GitHub user, and a GitHub user is not a member of anything\nhere, so `installation.created` is `ignored`; the setup URL links. Every branch is idempotent, and an event\nwith no reading here is `ignored` with a 200, as the other vendors' webhooks answer.",
        "operationId": "github_webhook",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookAck"
                }
              }
            }
          },
          "401": {
            "description": "the X-Hub-Signature-256 header is missing, malformed, or does not match the body"
          },
          "422": {
            "description": "the body is not a JSON object"
          },
          "503": {
            "description": "this deployment has no GitHub App webhook secret configured, so no delivery can be verified"
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/shared/session": {
      "get": {
        "tags": [
          "shares"
        ],
        "summary": "Get Shared Session",
        "description": "One session's replay, as a public share link shows it: the persona's name, the mission's goal, the\ntarget's origin, the result, the recording, and each step's action, target, narration, page and window in\nthe video. Never what the persona typed, a URL's query or fragment, dialogs, console or network detail,\nrefs, ids, screenshots, verdicts or opportunities.\n\nThe token in `X-Saram-Share` is the credential, and the share host's server is the caller: a browser never\ncalls this route. One answer counts one view on the link's record. Any reason there is no replay - the\ntoken missing, malformed, unknown, revoked or expired, the run's evidence expired or the run deleted,\nsharing turned off - is the same 404. Every answer is `no-store`, `noindex` and `no-referrer`.",
        "operationId": "get_shared_session",
        "parameters": [
          {
            "name": "X-Saram-Share",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself.",
              "title": "X-Saram-Share"
            },
            "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedSession"
                }
              }
            }
          },
          "404": {
            "description": "no replay for this token, whatever the reason - missing, malformed, unknown, revoked or expired, the run's evidence expired or the run deleted, sharing turned off - deliberately one answer"
          },
          "429": {
            "description": "too many requests from one address; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/shared/session/video": {
      "get": {
        "tags": [
          "shares"
        ],
        "summary": "Get Shared Video",
        "description": "The recording of the session a share link names, streamed with `Range`, `If-Range` and `ETag` exactly as\n`GET /api/v1/blobs` streams evidence to its owner, and `HEAD` for its headers alone.\n\nThe recording is found from the session the token names, never from a ref in the request. The token is in\n`X-Saram-Share`, so the share host's server proxies it; the page's video element asks the share host. A\nrevoked link's next range is the 404, so a player that already buffered some keeps only that. One link may\nhave only so many streams open at once (429).",
        "operationId": "get_shared_video",
        "parameters": [
          {
            "name": "X-Saram-Share",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself.",
              "title": "X-Saram-Share"
            },
            "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself."
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Range"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated.",
              "title": "If-None-Match"
            },
            "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated."
          },
          {
            "name": "If-Modified-Since",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date.",
              "title": "If-Modified-Since"
            },
            "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date."
          },
          {
            "name": "If-Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200.",
              "title": "If-Range"
            },
            "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "video/webm": {}
            },
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Partial Content"
          },
          "304": {
            "description": "Not Modified: If-None-Match or If-Modified-Since matched, so the caller's copy is current",
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no replay for this token, whatever the reason - missing, malformed, unknown, revoked or expired, the run's evidence expired or the run deleted, sharing turned off - deliberately one answer; or the session has no recording"
          },
          "416": {
            "description": "Range Not Satisfiable"
          },
          "429": {
            "description": "too many requests from one address, or this link already has as many video streams open as it may; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "x-internal": true
      },
      "head": {
        "tags": [
          "shares"
        ],
        "summary": "Head Shared Video",
        "description": "The recording of the session a share link names, streamed with `Range`, `If-Range` and `ETag` exactly as\n`GET /api/v1/blobs` streams evidence to its owner, and `HEAD` for its headers alone.\n\nThe recording is found from the session the token names, never from a ref in the request. The token is in\n`X-Saram-Share`, so the share host's server proxies it; the page's video element asks the share host. A\nrevoked link's next range is the 404, so a player that already buffered some keeps only that. One link may\nhave only so many streams open at once (429).",
        "operationId": "head_shared_video",
        "parameters": [
          {
            "name": "X-Saram-Share",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself.",
              "title": "X-Saram-Share"
            },
            "description": "The share link's token, shs_ plus 32 letters and digits: the last path segment of the link. The share host's server sends it; a browser never calls this route itself."
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Range"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated.",
              "title": "If-None-Match"
            },
            "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated."
          },
          {
            "name": "If-Modified-Since",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date.",
              "title": "If-Modified-Since"
            },
            "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date."
          },
          {
            "name": "If-Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200.",
              "title": "If-Range"
            },
            "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "video/webm": {}
            },
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Partial Content"
          },
          "304": {
            "description": "Not Modified: If-None-Match or If-Modified-Since matched, so the caller's copy is current",
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no replay for this token, whatever the reason - missing, malformed, unknown, revoked or expired, the run's evidence expired or the run deleted, sharing turned off - deliberately one answer; or the session has no recording"
          },
          "416": {
            "description": "Range Not Satisfiable"
          },
          "429": {
            "description": "too many requests from one address, or this link already has as many video streams open as it may; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/demo/runs": {
      "post": {
        "tags": [
          "demo"
        ],
        "summary": "Start Demo",
        "description": "Start the homepage demo on one site: Rosa, Saram's skeptical buyer, opens the address and looks around for\nup to 20 steps, and the replay is a share link once she is done. No credential: the page on saram.sh calls\nthis with the visitor's pasted address and the Turnstile widget's token.\n\nRefused with a reason code - never a run - for an address the demo will not open, a failed bot check, a day's\nlimit for this address or this site, the day's spend ceiling, every place taken, or the demo closed. Poll\n`GET /api/v1/demo/runs/status` with the `demo_token` every `poll_after_s` seconds.",
        "operationId": "start_demo",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DemoRunWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoStarted"
                }
              }
            }
          },
          "403": {
            "description": "reason bot_check: the Turnstile token was missing or not confirmed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoRefusal"
                }
              }
            }
          },
          "404": {
            "description": "the demo is not on in this deployment"
          },
          "422": {
            "description": "reason invalid_url (not an http or https address on its usual port, or no body to read), private_target (a private, cluster or local address; for the visitor's own machine the message carries how to expose it with a tunnel), or denied_host (Saram's own sites, or one the operator has closed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoRefusal"
                }
              }
            }
          },
          "429": {
            "description": "reason address_limit or target_limit: today's demos for this address, or for this site, are used up until the next UTC midnight. Also, without a reason, too many requests from one address",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoRefusal"
                }
              }
            }
          },
          "503": {
            "description": "reason daily_ceiling (today's demos are all used up), busy (as many demos are running as may; retry_after_s says about when one should be free) or demo_off (the demo is closed right now)",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoRefusal"
                }
              }
            }
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/demo/runs/status": {
      "get": {
        "tags": [
          "demo"
        ],
        "summary": "Get Demo Status",
        "description": "Where one demo has got to: `pending` until Rosa has a browser, `running` with `steps_taken` while she\nacts, `done` with `share_url` - the replay, for 7 days - once she has finished, `failed` when the site never\nloaded, and `refused` with `busy` when no browser came free in time and the demo was called off.\n\nThe `demo_token` from the POST, in `X-Saram-Demo`, is the only credential. Every reason there is no demo for\nit is the same 404. Every answer is `no-store`, `noindex` and `no-referrer`.",
        "operationId": "get_demo_status",
        "parameters": [
          {
            "name": "X-Saram-Demo",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The demo's token, dmo_ plus 32 letters and digits, from the POST's 202. Valid for 24 hours. A header and never a URL, so it reaches no log.",
              "title": "X-Saram-Demo"
            },
            "description": "The demo's token, dmo_ plus 32 letters and digits, from the POST's 202. Valid for 24 hours. A header and never a URL, so it reaches no log."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoStatus"
                }
              }
            }
          },
          "404": {
            "description": "no demo for this token, whatever the reason - missing, malformed, unknown or past its 24 hours, the demo's run deleted or its replay withdrawn, or the demo not on in this deployment - deliberately one answer"
          },
          "429": {
            "description": "too many requests from one address; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/runs": {
      "post": {
        "tags": [
          "runs"
        ],
        "summary": "Create Run",
        "description": "Start a run: send the personas in the spec through the target, one session per persona per mission.\n\nThe body is a run spec. It is checked in full before anything starts - the contract, the references\nbetween its parts, your model providers and credentials - and a spec that fails any check is a 422 naming\nthe field and the reason, with no run created. `POST /api/v1/runs/validate` makes the same checks\nwithout starting anything, and says what the run would cost. A 402 is a run your plan cannot pay for.\n\nThe answer is the `run_id` at once, while the run starts in the background: follow it with\n`GET /api/v1/runs/{run_id}` until `status` is terminal.",
        "operationId": "create_run",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunSpec"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The run is accepted and has started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartedRun"
                }
              }
            }
          },
          "402": {
            "description": "no run is started. On a plan that bills steps (pilot), because this tenant's month has billed its monthly cap (the detail carries what the month has billed and what the cap is, and monthly_cap the same numbers). On a plan that prices Usage (free, pro), because the available balance does not cover the most Usage this run can use (the detail carries the ceiling, the balance, and what to change, and usage_ceiling the same numbers as POST /api/v1/runs/validate answers them); such a tenant never meets the monthly cap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequired"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "runs"
        ],
        "summary": "List Runs",
        "description": "Your runs, newest first: up to `limit` (50 unless you ask for up to 500), optionally only those with\none `status` or against one target.",
        "operationId": "list_runs",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "default": 50,
              "title": "Limit"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Status"
            }
          },
          {
            "name": "target",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "entry URL or target name, exact",
              "title": "Target"
            },
            "description": "entry URL or target name, exact"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}": {
      "get": {
        "tags": [
          "runs"
        ],
        "summary": "Get Run",
        "description": "One run: its status, what it has cost and billed so far, the artifact's summary once there is one,\nand every session with where it has got to.\n\nPoll this while a run is open. `status` moves `pending`, `running`, `observing`, then stops on\n`completed`, `failed` or `cancelled`, and only `completed` means there are results to read. Live fields\n(`billed_steps`, each session's `heartbeat_step`) climb while the run does. A run of another tenant's is a\n404, the same as a run that does not exist.",
        "operationId": "get_run",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunDetail"
                },
                "example": {
                  "run_id": "run_2026-09-26_4f1c2a",
                  "status": "completed",
                  "target": "https://example.com",
                  "tenant_id": "ten_2026-06-02_3f9a2c14",
                  "created_by": "key_2026-09-01_8d1e04b7",
                  "caller": "acme/ci",
                  "confirmation_rate": 1.0,
                  "repro_attempts": 2,
                  "repro_gate_discards": 0,
                  "repro_gate_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/repro_gate.json",
                  "billed_steps": 27,
                  "cost_usd": 0.59,
                  "actor_cost_usd": 0.38,
                  "judge_cost_usd": 0.21,
                  "usage": 30.114,
                  "usage_usd": 0.48,
                  "refunded_usd": 0.0,
                  "usage_state": "settled",
                  "sessions_per_hour": 49.7,
                  "artifact_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/artifact.json",
                  "summary": {
                    "personas_run": 2,
                    "missions": 1,
                    "sessions": 2,
                    "completed": 1,
                    "abandoned": 1,
                    "blocked": 0,
                    "errored": 0,
                    "budget_exhausted": 0,
                    "issues_total": 1,
                    "issues_reproducible": 1,
                    "issues_marker_matched": 0,
                    "tester_errors": 0,
                    "opportunities_total": 0,
                    "verdicts": {
                      "positive": 1,
                      "negative": 1,
                      "undecided": 0,
                      "top_reasons": [
                        "The sign-up form failed without saying why.",
                        "Signing up was fast."
                      ]
                    },
                    "confirmation_rate": 1.0,
                    "reproduction": {
                      "candidates": 2,
                      "merged": 1,
                      "evaluated": 1,
                      "confirmed": 1,
                      "inconclusive": 0,
                      "excluded": 0
                    },
                    "gate_discards": 0,
                    "path_diversity": 1.0,
                    "cost_usd": 0.59,
                    "actor_cost_usd": 0.38,
                    "judge_cost_usd": 0.21,
                    "wall_clock_s": 214
                  },
                  "session_counts": {
                    "completed": 1,
                    "abandoned": 1
                  },
                  "packs": [
                    "general"
                  ],
                  "created_at": "2026-09-26T14:02:11Z",
                  "started_at": "2026-09-26T14:02:14Z",
                  "finished_at": "2026-09-26T14:05:47Z",
                  "evidence_expires_at": "2026-12-25T14:02:11Z",
                  "evidence_expired": false,
                  "workflow_id": "run-run_2026-09-26_4f1c2a",
                  "budgets": {
                    "steps_per_session": 30
                  },
                  "run_spec_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_spec.json",
                  "files": [],
                  "sessions": [
                    {
                      "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                      "persona_id": "dana_first_timer@1",
                      "pack": "general",
                      "tags": [
                        "general",
                        "novice",
                        "desktop",
                        "first_time"
                      ],
                      "mission_id": "sign_up",
                      "status": "abandoned",
                      "result": "abandoned",
                      "end_reason": "patience_exhausted",
                      "fault": "none",
                      "steps": 18,
                      "end_step": 18,
                      "abandon_step": 18,
                      "tokens_in": 212340,
                      "tokens_out": 3120,
                      "tokens_cache_read": 168000,
                      "tokens_cache_write": 0,
                      "model": "anthropic:claude-sonnet-5",
                      "cost_usd": 0.26,
                      "price_source": "genai-prices/0.0.41",
                      "wall_clock_s": 142.6,
                      "uncertain_steps": [],
                      "resumed_at_steps": [],
                      "verdict_decision": "negative",
                      "outcome_read": "abandoned",
                      "usage": 21.412,
                      "usage_usd": 0.34,
                      "refunded_usd": 0.0,
                      "workflow_id": "run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                      "agent_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/agent.ndjson",
                      "surface_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/surface.ndjson",
                      "video_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/video.webm",
                      "video_url": "/api/v1/blobs?ref=s3%3A%2F%2Fsaram-evidence%2Frun_2026-09-26_4f1c2a%2Frun_2026-09-26_4f1c2a.sess_01_dana_first_timer%2Fvideo.webm",
                      "recorded_under_mask": false,
                      "created_at": "2026-09-26T14:02:14Z",
                      "finished_at": "2026-09-26T14:04:39Z"
                    },
                    {
                      "session_id": "run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer",
                      "persona_id": "marcus_fast_skimmer@1",
                      "pack": "general",
                      "tags": [
                        "general",
                        "power_user",
                        "desktop",
                        "skimmer",
                        "fast"
                      ],
                      "mission_id": "sign_up",
                      "status": "completed",
                      "result": "completed",
                      "end_reason": "success_criteria_met",
                      "fault": "none",
                      "steps": 9,
                      "end_step": 9,
                      "tokens_in": 88410,
                      "tokens_out": 1460,
                      "tokens_cache_read": 70200,
                      "tokens_cache_write": 0,
                      "model": "anthropic:claude-sonnet-5",
                      "cost_usd": 0.12,
                      "price_source": "genai-prices/0.0.41",
                      "wall_clock_s": 71.3,
                      "uncertain_steps": [],
                      "resumed_at_steps": [],
                      "verdict_decision": "positive",
                      "outcome_read": "completed",
                      "usage": 8.702,
                      "usage_usd": 0.14,
                      "refunded_usd": 0.0,
                      "workflow_id": "run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer",
                      "agent_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/agent.ndjson",
                      "surface_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/surface.ndjson",
                      "video_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/video.webm",
                      "video_url": "/api/v1/blobs?ref=s3%3A%2F%2Fsaram-evidence%2Frun_2026-09-26_4f1c2a%2Frun_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer%2Fvideo.webm",
                      "recorded_under_mask": false,
                      "created_at": "2026-09-26T14:02:14Z",
                      "finished_at": "2026-09-26T14:03:27Z"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/{signal}": {
      "post": {
        "tags": [
          "runs"
        ],
        "summary": "Signal Run",
        "description": "Pause, resume or cancel a run in flight: the last path segment is `pause`, `resume` or `cancel`.\n\n`pause` starts no new session and holds the running ones; `resume` releases a paused run, including one\nthat paused itself on reaching `budget.total_cost_usd`; `cancel` stops the running sessions, starts no\nmore, and ends the run `cancelled` without judging what they did. A run that has already finished is a\n409.",
        "operationId": "signal_run",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "signal",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Signal"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "The signal is delivered to the run.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunSignalled"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/artifact": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Artifact",
        "description": "The run's whole artifact: every issue, verdict, opportunity and tester error, each session's outcome,\nthe summary, and the evidence refs behind them, as one document in the published artifact format.\n\nA 404 until the run has assembled one: a `completed` run has one, and a run that failed or was cancelled\nbefore assembly has none. The per-kind views (`/issues`, `/verdicts`, `/opportunities`,\n`/tester-errors`) serve the same records with how each may be acted on beside them.",
        "operationId": "get_artifact",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "title": "Response Get Artifact"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/validate": {
      "post": {
        "tags": [
          "runs"
        ],
        "summary": "Validate Run",
        "description": "The dry run: everything `POST /runs` checks, the sessions it would plan, the most it can bill on the\ncaller's plan (`cost_ceiling`, on every plan), and no run.\n\nEvery refusal `POST /runs` would give is the same 422 here, and a 200 means that spec is accepted as\nwritten - so a caller can learn the API, and check a spec they are about to spend money on, without\nspending it.",
        "operationId": "validate_run",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunSpec"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpecValidation"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/runs/{run_id}/delete": {
      "post": {
        "tags": [
          "runs"
        ],
        "summary": "Delete Run",
        "description": "Delete one of your runs, for good: its traces, screenshots, video and checkpoints, its own documents\n(the spec, the artifact), and its record - the run, its sessions, issues, verdicts, opportunities and\nretests. Every route then answers the run with the 404 an unknown run gets.\n\nYour own tenant's runs and no other: another tenant's run is that same 404, so a delete neither reaches\nnor confirms a run that is not yours. Asking again answers what the first delete did, so a retry is safe.\nA run that is still running, or has a retest still running, is a 409 and nothing is deleted: cancel it\nand delete it once it has stopped. What the run was billed stays in your usage.",
        "operationId": "delete_run",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The run is deleted, now or by an earlier request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunDeleted"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "409": {
            "description": "the run, or a retest of it, is still running: nothing is deleted under something still writing to it. Cancel it and delete it once it has stopped"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/sessions/{session_id}/shares": {
      "post": {
        "tags": [
          "shares"
        ],
        "summary": "Create Share Link",
        "description": "Publish one finished session's replay by a link anyone holding it can open, with no sign-in.\n\nThe link plays the session's recording with the persona's steps and narration beside it. Whatever the browser\nshowed that is not masked - the test account's email, any text the product rendered - anyone with the link\nsees, and `warning` in the answer says what that is: show it to whoever posts the link. Revoke it with\n`POST /api/v1/shares/{share_id}/revoke`, and it stops playing on the next request.\n\nThe `url` is shown once, in this answer: only a hash of it is kept, so a lost link is replaced by minting\nanother and revoking the first. It expires after `expires_in_days` (30 unless you ask for 1 to 90), and never\nlater than the run's evidence does. A session that has not ended yet is a 409, as is a run whose evidence\nhas expired, and a session the browser pool did not record under its mask (`masked` in the answer says the\nsession's was). Sharing is off for every tenant until Saram turns it on, and a 403 says so.",
        "operationId": "create_share_link",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Session Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ShareWrite"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link is minted. Its URL is in this answer and in no other.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareCreated"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope, or public sharing is off for this tenant: only Saram turns it on, because a recording shows what the browser showed, but for what the pool masked"
          },
          "404": {
            "description": "no such run or session of yours - the same answer for another tenant's as for none"
          },
          "409": {
            "description": "the session has not ended, so its recording is not stored yet; the run's evidence has expired and there is no recording left to share; or the browser pool did not record the session under its mask, which paints over what the persona typed"
          },
          "503": {
            "description": "this deployment has no share host to open a link on"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/shares": {
      "get": {
        "tags": [
          "shares"
        ],
        "summary": "List Share Links",
        "description": "Every public link minted on a run's sessions, newest first - live, expired and revoked - with who minted\neach, when it expires and how often it was opened. Never a link itself: that was shown once, when it was\nminted. `public_shares` says whether your tenant may share at all; while it is off no link plays.\n`mask_required` says whether a session of yours is shared only when the browser pool recorded it under its\nmask (the session row's `recorded_under_mask`): while it is true, a share of any other is a 409.",
        "operationId": "list_share_links",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/shares/{share_id}/revoke": {
      "post": {
        "tags": [
          "shares"
        ],
        "summary": "Revoke Share Link",
        "description": "Stop a public link playing: the next request for it is refused, a video already playing included once it\nasks for its next range. Any key or member of your tenant that may write runs can revoke any of its links,\nnot only the one who minted it.\n\nThe link's record stays, with who revoked it and when, so the list still says what was shared. Asking again\nanswers the same record. Another tenant's link is the 404 an unknown id gets.",
        "operationId": "revoke_share_link",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "share_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Share Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The link is revoked, now or by an earlier request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareSummary"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "404": {
            "description": "no link of yours has that id - the same answer for another tenant's link as for none"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/sessions": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "List Sessions",
        "description": "A run's sessions, one per persona per mission: each with its status, result, whose fault an error\nwas, its tokens, cost and Usage, and - while it runs - where it has got to. `pack` narrows them to one\npersona cohort.",
        "operationId": "list_sessions",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "pack",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "persona pack (cohort), exact",
              "title": "Pack"
            },
            "description": "persona pack (cohort), exact"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/sessions/{session_id}/steps": {
      "get": {
        "tags": [
          "sessions"
        ],
        "summary": "Get Steps",
        "description": "One session's steps, both halves aligned: what the persona decided and said, beside what the browser\ndid and saw - url, inputs, dialogs, downloads, network failures, console errors, screenshot and video\nwindow.\n\nReadable while the session runs. Page through a long session with `from` and `to`, or tail a live one by\npassing the previous answer's `last_step_id` as `after` until `status` is no longer `in_flight`.",
        "operationId": "get_steps",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Session Id"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0
                },
                {
                  "type": "null"
                }
              ],
              "description": "Inclusive lower bound on step_id. Mutually exclusive with after, which is the same bound minus one.",
              "title": "From"
            },
            "description": "Inclusive lower bound on step_id. Mutually exclusive with after, which is the same bound minus one."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0
                },
                {
                  "type": "null"
                }
              ],
              "title": "To"
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0
                },
                {
                  "type": "null"
                }
              ],
              "description": "Exclusive lower bound: return the steps written after this step_id. The cursor for tailing a session that is still running - pass back the last_step_id of the previous page. Mutually exclusive with from.",
              "title": "After"
            },
            "description": "Exclusive lower bound: return the steps written after this step_id. The cursor for tailing a session that is still running - pass back the last_step_id of the previous page. Mutually exclusive with from."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StepsPage"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/issues": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Issues",
        "description": "The product defects the run found, each with its evidence and repro steps, and beside them how each\nmay be acted on: an issue whose reading says `may_file_ticket_alone` was reproduced by a replay and can\nbecome a ticket as it stands; any other must be reported with its evidence and marked unconfirmed.\n\nAnswers from the moment the run exists: an empty list with the run's `status` while it is still going,\nand a result only once that status is terminal.",
        "operationId": "get_issues",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssueList"
                },
                "example": {
                  "run_id": "run_2026-09-26_4f1c2a",
                  "status": "completed",
                  "issues": [
                    {
                      "id": "iss_signup_submit_500",
                      "type": "bug",
                      "severity": "high",
                      "reproducible": true,
                      "reproduction": {
                        "attempted": true,
                        "result": "reproduced",
                        "attempts": 2,
                        "notes": "Both replays filled the sign-up form with a password containing a space and pressed Create account: POST /api/signup answered 500 and the page did not change."
                      },
                      "persona_specific": false,
                      "claim": {
                        "action_step": {
                          "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                          "step_id": 7
                        },
                        "expected": "Create account submits the form and signs the new user in.",
                        "observed": "POST /api/signup answered 500; the form stayed on screen with every field filled and no message.",
                        "verification": {
                          "status": "verified",
                          "method": "marker",
                          "markers": [
                            "failed_requests"
                          ],
                          "note": "The same failed request at the judged step on both attempts; the claim check found the title supported by the network line, and the failure is the product's."
                        },
                        "attribution": "product",
                        "check": {
                          "verdict": "supported",
                          "quote": "[step 7] click -> button[type=submit]: POST /api/signup 500",
                          "quote_step": {
                            "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                            "step_id": 7
                          },
                          "reason": "The request the button sends fails on the server, which is what the title claims.",
                          "checked_by": "judge"
                        }
                      },
                      "affected_sessions": [
                        "run_2026-09-26_4f1c2a.sess_01_dana_first_timer"
                      ],
                      "affected_personas": [
                        "dana_first_timer@1"
                      ],
                      "affected_count": 1,
                      "affected_fraction": 0.5,
                      "reached_sessions": [
                        "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                        "run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer"
                      ],
                      "first_seen_step": 7,
                      "location": {
                        "route": "/signup",
                        "selector": "button[type=submit]",
                        "element_description": "Create account button under the sign-up form",
                        "viewport": "1440x900"
                      },
                      "title": "Create account fails with a server error when the password contains a space",
                      "description": "On /signup, submitting the form with a password that contains a space sends POST /api/signup, which answers 500. The page shows no error and keeps the form filled, so the persona pressed Create account three more times before giving up.",
                      "persona_rationale": "I pressed the button four times and nothing happened, so I assumed the site was broken.",
                      "evidence": {
                        "step_refs": [
                          {
                            "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                            "step_id": 7
                          },
                          {
                            "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                            "step_id": 10
                          }
                        ],
                        "screenshots": [
                          "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/shots/0007.png"
                        ]
                      },
                      "repro_steps": [
                        {
                          "action": "navigate",
                          "target": "https://example.com/signup",
                          "expect": "sign-up form visible"
                        },
                        {
                          "action": "fill",
                          "target": "form#signup",
                          "expect": "a password containing a space"
                        },
                        {
                          "action": "click",
                          "target": "button[type=submit]",
                          "expect": "a signed-in page"
                        }
                      ],
                      "preconditions": [
                        "viewport 1440x900",
                        "auth_state anonymous"
                      ],
                      "baseline_status": "new",
                      "confidence": 0.92
                    }
                  ],
                  "reading": [
                    {
                      "issue_id": "iss_signup_submit_500",
                      "may_file_ticket_alone": true,
                      "subjective_fields": [
                        "persona_rationale"
                      ],
                      "advisory": "SUBJECTIVE. `persona_rationale` is persona-voiced narration from an affected session, not a statement of observed behaviour. Quote it as the persona's words; the objective claim is `description`."
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/issues/{issue_id}": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Issue",
        "description": "One issue in full - its claim and how it was checked, repro steps, preconditions and evidence - with\nits reading and the evidence refs to fetch. A 404 until the run's artifact is assembled; an id that is\none of the run's tester errors is a 404 that says where to read it instead.",
        "operationId": "get_issue",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "issue_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Issue Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssueDetail"
                },
                "example": {
                  "run_id": "run_2026-09-26_4f1c2a",
                  "issue": {
                    "id": "iss_signup_submit_500",
                    "type": "bug",
                    "severity": "high",
                    "reproducible": true,
                    "reproduction": {
                      "attempted": true,
                      "result": "reproduced",
                      "attempts": 2,
                      "notes": "Both replays filled the sign-up form with a password containing a space and pressed Create account: POST /api/signup answered 500 and the page did not change."
                    },
                    "persona_specific": false,
                    "claim": {
                      "action_step": {
                        "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                        "step_id": 7
                      },
                      "expected": "Create account submits the form and signs the new user in.",
                      "observed": "POST /api/signup answered 500; the form stayed on screen with every field filled and no message.",
                      "verification": {
                        "status": "verified",
                        "method": "marker",
                        "markers": [
                          "failed_requests"
                        ],
                        "note": "The same failed request at the judged step on both attempts; the claim check found the title supported by the network line, and the failure is the product's."
                      },
                      "attribution": "product",
                      "check": {
                        "verdict": "supported",
                        "quote": "[step 7] click -> button[type=submit]: POST /api/signup 500",
                        "quote_step": {
                          "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                          "step_id": 7
                        },
                        "reason": "The request the button sends fails on the server, which is what the title claims.",
                        "checked_by": "judge"
                      }
                    },
                    "affected_sessions": [
                      "run_2026-09-26_4f1c2a.sess_01_dana_first_timer"
                    ],
                    "affected_personas": [
                      "dana_first_timer@1"
                    ],
                    "affected_count": 1,
                    "affected_fraction": 0.5,
                    "reached_sessions": [
                      "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                      "run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer"
                    ],
                    "first_seen_step": 7,
                    "location": {
                      "route": "/signup",
                      "selector": "button[type=submit]",
                      "element_description": "Create account button under the sign-up form",
                      "viewport": "1440x900"
                    },
                    "title": "Create account fails with a server error when the password contains a space",
                    "description": "On /signup, submitting the form with a password that contains a space sends POST /api/signup, which answers 500. The page shows no error and keeps the form filled, so the persona pressed Create account three more times before giving up.",
                    "persona_rationale": "I pressed the button four times and nothing happened, so I assumed the site was broken.",
                    "evidence": {
                      "step_refs": [
                        {
                          "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                          "step_id": 7
                        },
                        {
                          "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                          "step_id": 10
                        }
                      ],
                      "screenshots": [
                        "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/shots/0007.png"
                      ]
                    },
                    "repro_steps": [
                      {
                        "action": "navigate",
                        "target": "https://example.com/signup",
                        "expect": "sign-up form visible"
                      },
                      {
                        "action": "fill",
                        "target": "form#signup",
                        "expect": "a password containing a space"
                      },
                      {
                        "action": "click",
                        "target": "button[type=submit]",
                        "expect": "a signed-in page"
                      }
                    ],
                    "preconditions": [
                      "viewport 1440x900",
                      "auth_state anonymous"
                    ],
                    "baseline_status": "new",
                    "confidence": 0.92
                  },
                  "reading": {
                    "issue_id": "iss_signup_submit_500",
                    "may_file_ticket_alone": true,
                    "subjective_fields": [
                      "persona_rationale"
                    ],
                    "advisory": "SUBJECTIVE. `persona_rationale` is persona-voiced narration from an affected session, not a statement of observed behaviour. Quote it as the persona's words; the objective claim is `description`."
                  },
                  "evidence_refs": {
                    "screenshots": [
                      "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/shots/0007.png"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/issues/{issue_id}/retest": {
      "post": {
        "tags": [
          "artifact"
        ],
        "summary": "Retest Issue",
        "description": "Replay one issue's recorded path again, now, and keep the answer beside the run's own.\n\nThe same deterministic replay the run's reproduction ran: no persona and no model walks the path, two\nattempts, the target's partition reset before each. When the failure does happen again, the claim check\nthe run's reproduction asks is asked once more, on the run's own reproduction provider. The issue and the\nartifact keep what the run found; this record is where the new answer goes. A replay's steps are never\nbilled. Poll `GET .../retests/{retest_id}` until `status` is `completed` or `failed`.",
        "operationId": "retest_issue",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "issue_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Issue Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/RetestRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetestRecord"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/issues/{issue_id}/retests": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "List Retests",
        "description": "Every retest asked for on one issue, newest first, each with its status and, once it has one, its\nresult. Empty for an issue nobody has retested.",
        "operationId": "list_retests",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "issue_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Issue Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetestList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/issues/{issue_id}/retests/{retest_id}": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Retest",
        "description": "One retest of one issue: poll it after `POST .../retest` until `status` is `completed` or `failed`.\n`result` then says whether the failure happened again, and the issue itself keeps what the run found.",
        "operationId": "get_retest",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          },
          {
            "name": "issue_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Issue Id"
            }
          },
          {
            "name": "retest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Retest Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetestRecord"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/verdicts": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Verdicts",
        "description": "Each persona's verdict at the end of its session - its answer to the run's verdict question, quoted\nfrom its own diary - and the cohort's tally. Persona feedback: subjective, to be attributed to the\npersona, and never a ticket on its own. An empty list with the run's `status` until the run is done.",
        "operationId": "get_verdicts",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerdictList"
                },
                "example": {
                  "run_id": "run_2026-09-26_4f1c2a",
                  "status": "completed",
                  "summary": {
                    "positive": 1,
                    "negative": 1,
                    "undecided": 0,
                    "top_reasons": [
                      "The sign-up form failed without saying why.",
                      "Signing up was fast."
                    ]
                  },
                  "verdicts": [
                    {
                      "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
                      "persona_id": "dana_first_timer@1",
                      "mission_id": "sign_up",
                      "result": "abandoned",
                      "verdict_prompt": "Would you use this product again after today, and what decided it?",
                      "verdict_prompt_source": "mission",
                      "verdict": {
                        "decision": "negative",
                        "decision_text": "No. If I can't even make an account, I'm not trusting it with my work.",
                        "reason": "I pressed the button four times and nothing happened",
                        "reason_paraphrase": "The sign-up form failed without saying why.",
                        "quotes": [
                          "GAVE UP: I pressed the button four times and nothing happened, so I assumed the site was broken. No. If I can't even make an account, I'm not trusting it with my work."
                        ],
                        "step_refs": [
                          10,
                          18
                        ]
                      }
                    },
                    {
                      "session_id": "run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer",
                      "persona_id": "marcus_fast_skimmer@1",
                      "mission_id": "sign_up",
                      "result": "completed",
                      "verdict_prompt": "Would you use this product again after today, and what decided it?",
                      "verdict_prompt_source": "mission",
                      "verdict": {
                        "decision": "positive",
                        "decision_text": "Yes - I was in and looking at a dashboard in under a minute.",
                        "reason": "I was in and looking at a dashboard in under a minute",
                        "reason_paraphrase": "Signing up was fast.",
                        "quotes": [
                          "FINISHED: Yes - I was in and looking at a dashboard in under a minute."
                        ],
                        "step_refs": [
                          9
                        ]
                      }
                    }
                  ],
                  "advisory": "ADVISORY, SUBJECTIVE. A verdict is the persona's own answer at the end of its session, grounded in verbatim diary quotes. It is feedback, not a defect: it never files a ticket on its own and must be presented as the persona's opinion, never as an observed fact.",
                  "subjective_fields": [
                    "verdict"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/opportunities": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Opportunities",
        "description": "What the personas' behaviour says the product could do better, or already does well - unmet\nexpectations, missing capabilities, value gaps, delight - scored by how many sessions share it. Persona\nfeedback, never a ticket on its own. `display` says which clear the cohort floors and which are\nsingle-persona notes. An empty list with the run's `status` until the run is done.",
        "operationId": "get_opportunities",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpportunityList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/runs/{run_id}/tester-errors": {
      "get": {
        "tags": [
          "artifact"
        ],
        "summary": "Get Tester Errors",
        "description": "The findings this run attributed to the persona's own action rather than to the product.\n\nA mistyped credential, the wrong element, placeholder text read as a value: issue-shaped records that are\nnever issues - not in `/issues`, not in `issues_total`, not reproduced, never a ticket. Read them for why\na session stalled and for what to fix in the test setup. Empty with the run's `status` until the artifact\nis assembled, like the other views; `classified` tells an artifact that never asked from one that found\nnone.",
        "operationId": "get_tester_errors",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TesterErrorList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "tags": [
          "usage"
        ],
        "summary": "Get Usage",
        "description": "This tenant's usage for one calendar month: the meters, the runs behind them, and the totals.\n\nYour own tenant and no other - the ledger is filtered by the tenant the request's key belongs to.\nA month you ran nothing in is an empty report, not a 404.",
        "operationId": "get_usage",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The calendar month to report, YYYY-MM, in UTC. Omitted, it is the month it is now. A month you ran nothing in answers with zero totals, not a 404.",
              "title": "Period"
            },
            "description": "The calendar month to report, YYYY-MM, in UTC. Omitted, it is the month it is now. A month you ran nothing in answers with zero totals, not a 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageReport"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/usage.csv": {
      "get": {
        "tags": [
          "usage"
        ],
        "summary": "Get Usage CSV",
        "description": "The same month as a CSV: each run oldest first with a row per session under it - its Usage, whose\nfailure ended it and what was charged and refunded for it - then a `TOTAL` row, and below it the month's\ngrants, top-ups, expiries and adjustments. What an invoice is written from by hand - the per-role split is\nin the JSON above.",
        "operationId": "get_usage_csv",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The calendar month to report, YYYY-MM, in UTC. Omitted, it is the month it is now. A month you ran nothing in answers with zero totals, not a 404.",
              "title": "Period"
            },
            "description": "The calendar month to report, YYYY-MM, in UTC. Omitted, it is the month it is now. A month you ran nothing in answers with zero totals, not a 404."
          }
        ],
        "responses": {
          "200": {
            "description": "One header row; each run oldest first, followed by one row per session of that run; then a TOTAL row. Columns: period, tenant, run_id, session_id, persona_id, persona_label, caller, occurred_at, then the run's meters - steps, sessions, browser_seconds, model_usd_hosted, model_usd_byo - then the session's persona_steps, persona_seconds (the time its steps took, without setup and teardown), usage, fault (none, saram, provider or target), usage_usd (net) and refunded_usd, then billable_usd. A run's row leaves the session columns empty and a session's row leaves the run's empty, so every column sums to its TOTAL. A session that failed on Saram's side (fault saram) reads usage_usd 0.0000 with its whole debit in refunded_usd; usage_usd and refunded_usd are empty where the prepaid ledger moved no money for the session. Then one empty line and a second table, always with its header: period, tenant, kind (grant, top_up, expiry or adjustment), bucket (included or purchased), amount_usd (signed, to four places), created_at and stripe_ref (the Stripe invoice behind a top-up or a Pro grant) - each credit that came into or lapsed from the balance that month, oldest first. An expiry is in the month whose credit lapsed.",
            "content": {
              "application/json": {
                "schema": {}
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "period is not a calendar month (YYYY-MM)"
          }
        }
      }
    },
    "/api/v1/billing/portal": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Portal",
        "description": "A link into Stripe's hosted customer portal for this tenant: cards, invoices, receipts.\n\nSaram holds no payment instrument and renders no payment form - what it holds is the usage the\ninvoice is derived from, which is the two routes above. The Stripe customer is created here if this\ntenant has never had one (`billing.ensure_customer`), so a tenant that has not run yet can still put\na card on file.\n\nMinted per call and never cached: a portal session is short-lived and single-use, and a cached link\nwould hand the second person the first person's session.",
        "operationId": "billing_portal",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingPortal"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, so there is no billing account to open"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing": {
      "get": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Overview",
        "description": "This tenant's plan and its prices, the Pro subscription, prepaid balance, card on file, auto-refill and\nrecent top-ups.\n\nRead from Saram's ledger, which is the balance (section 3.2): Stripe collects money and holds no copy of\nit. Answers without a Stripe key too, because the ledger is this deployment's own. The subscription is\nStripe's, mirrored as its events arrive; a Pro subscription nothing has mirrored yet is read from Stripe\nonce, here.",
        "operationId": "billing_overview",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingOverview"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing/auto-refill": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Auto Refill",
        "description": "Set auto-refill and the monthly budget. Turning auto-refill on wants a threshold, an amount and a card\non file (`POST /billing/setup`); if the balance is already under the threshold, the first refill starts\nnow rather than at the next run.",
        "operationId": "billing_auto_refill",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutoRefill"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingOverview"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing/subscription": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Upgrade",
        "description": "Upgrade this tenant to Saram Pro: an `incomplete` subscription whose first invoice the Payment Element\npays with `client_secret`. Asked again before paying, it answers the same checkout; asked while Pro is\nset to cancel, it takes the cancellation back. The plan becomes `pro` when Stripe reports it active.",
        "operationId": "billing_upgrade",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingSubscription"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          },
          "409": {
            "description": "the tenant is already on Saram Pro or on a subscription an operator moves; or the top-up would pass the tenant's own monthly budget"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "delete": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Cancel",
        "description": "Cancel Saram Pro at the end of the month already paid for; the tenant then moves to Free. The month's\nincluded credit stays until it expires, because the month's fee bought it.",
        "operationId": "billing_cancel",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingSubscription"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          },
          "409": {
            "description": "the tenant is already on Saram Pro or on a subscription an operator moves; or the top-up would pass the tenant's own monthly budget"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing/setup": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Setup",
        "description": "A SetupIntent to put a card on file, or change it (`stripe.confirmSetup`). The card becomes the one\ntop-ups and auto-refill charge when Stripe reports the intent succeeded.",
        "operationId": "billing_setup",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingSetup"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing/top-ups": {
      "post": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Top Up",
        "description": "Buy prepaid credit: a one-off invoice for `amount_usd` x $1.00, answered with the client secret the\nPayment Element pays it with (section 3.3).\n\nA `TopUpWorkflow` makes and finalizes the invoice, and credits the balance once when Stripe says it is\npaid - on the webhook, or on its own read ten minutes later if the webhook never comes. The browser's\nword is never enough. An invoice left unpaid for a day is voided. Refused with a 409 when it would take\nthis month's charges past the monthly budget. Usually answers `open` with `client_secret`; if the invoice\nis slow to make it answers `pending`, and `GET /billing/top-ups/{id}` carries the secret once it exists.",
        "operationId": "billing_top_up",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopUpWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopUpRecord"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          },
          "409": {
            "description": "the tenant is already on Saram Pro or on a subscription an operator moves; or the top-up would pass the tenant's own monthly budget"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/billing/top-ups/{topup_id}": {
      "get": {
        "tags": [
          "usage"
        ],
        "summary": "Billing Top Up Status",
        "description": "One of this tenant's top-ups, with its client secret while it is still open. Another tenant's id is\nthe 404 an unknown one gets.",
        "operationId": "billing_top_up_status",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "topup_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Topup Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopUpRecord"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not manage billing: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "an amount out of bounds, or auto-refill turned on without a threshold, amount or card"
          },
          "502": {
            "description": "Stripe refused the request or could not be reached; the detail is Stripe's own message"
          },
          "503": {
            "description": "this deployment has no Stripe key configured, or its Stripe account is not set up for what was asked, so nothing is started and nothing is charged"
          }
        }
      }
    },
    "/api/v1/providers/codex/login": {
      "post": {
        "tags": [
          "providers"
        ],
        "summary": "Codex Login",
        "description": "Start connecting your ChatGPT subscription, so your runs' models can bill it: open `verification_url`,\ntype `user_code`, then poll `GET /api/v1/providers/codex` until `state` is `connected`.\n\nA login serves only the caller that made it, and only runs that caller starts. A 409 is a login already\nin flight; a 403 is a deployment that does not offer ChatGPT logins to this caller.",
        "operationId": "codex_login",
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodexLoginStarted"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/providers/codex": {
      "get": {
        "tags": [
          "providers"
        ],
        "summary": "Codex Status",
        "description": "Your ChatGPT login's state - `none`, `pending`, `connected`, `expired` or `error` - with its plan,\nwhen it was connected and when it expires, and the subscription's rate-limit use. Never any part of the\nlogin itself.",
        "operationId": "codex_status",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodexStatus"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/providers/codex/disconnect": {
      "post": {
        "tags": [
          "providers"
        ],
        "summary": "Codex Disconnect",
        "description": "Disconnect your ChatGPT login: Saram revokes it at OpenAI and forgets it, and no run can bill that\nsubscription any more. The revocation is best effort: if OpenAI cannot be reached the login is still\nforgotten, and then stops serving within a minute and at OpenAI only when its token expires. Your run\nspecs keep their `providers` entry, so connecting again is all a later run needs. A 404 when there is no\nlogin to disconnect.",
        "operationId": "codex_disconnect",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodexDisconnected"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/credentials": {
      "get": {
        "tags": [
          "credentials"
        ],
        "summary": "List Credentials",
        "description": "This tenant's live credentials, oldest first. Metadata only; the values never come back out.",
        "operationId": "list_credentials",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CredentialList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "post": {
        "tags": [
          "credentials"
        ],
        "summary": "Create Credential",
        "description": "This tenant's own provider key, test-account login, webhook key or GitHub token, written to Secrets\nManager and named by an `awssm://` ref.\n\nThe ref that comes back is what goes into a run spec's `providers.entries[].credential_ref` - or, for\nkind `account`, into `target.seed.accounts`, for `webhook` into `connectors.webhook.secret_ref`, and for\n`github` into `connectors.github.token_ref` - and only this tenant's specs may name it: another\ntenant's spec naming it is refused at `POST /runs` with a 422. The value is never returned, logged,\nput on a span, or stored anywhere but the secret.\n\nWhat is refused: a name that is not one readable segment, a kind no worker resolves, an empty or\noversized value, and a value of the wrong shape for its kind - JSON on a kind whose credential is one\nstring, a non-object on `google-cloud`, or an `account` that is not a valid secret-store entry (422\neach); a name this tenant already has, or whose secret\nalready exists (409). There is no replace: rotation is `DELETE` then `POST`, so a mistyped name cannot\nretire a working key.",
        "operationId": "create_credential",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CredentialWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CredentialRecord"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the credentials:write scope, or the request carries no tenant"
          },
          "409": {
            "description": "this tenant already has a credential by that name"
          },
          "422": {
            "description": "bad name, a kind no worker resolves, or a value of the wrong shape for its kind"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/credentials/{name}": {
      "delete": {
        "tags": [
          "credentials"
        ],
        "summary": "Delete Credential",
        "description": "Force-delete the secret and free the name. A name this tenant does not have is a 404 - the same\n404 whether it belongs to nobody or to another tenant, because the lookup never leaves this one.",
        "operationId": "delete_credential",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Name"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CredentialRecord"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the credentials:write scope, or the request carries no tenant"
          },
          "404": {
            "description": "this tenant has no credential by that name"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "tags": [
          "identity"
        ],
        "summary": "Who Am I",
        "description": "Who this credential is, which tenant it spends and reads on, and what it may do.\n\nThe dashboard's first call, and deliberately the one route with no scope of its own: every credential\nmay ask what it is, including one whose role reads nothing else, and including an ungated request -\nwhich answers as the `fleet` tenant, because that is whose runs an open dev API serves.\n\n`scopes` is the *effective* answer, not the stored one: a person's role expanded through\n`tenancy.ROLE_SCOPES`, a minted key's own list, and the full set for a credential that carries none.\nA caller deciding which buttons to draw should never have to know that an empty list meant everything.",
        "operationId": "who_am_i",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "tags": [
          "identity"
        ],
        "summary": "List API Keys",
        "description": "This tenant's live keys, oldest first. Metadata only - no secret and no hash comes back out.\n\n`last_used_at` is the field this list exists for: it answers \"is anything still using this\" before a\nrevocation, and it is stamped at most once every few minutes per key, not on every request.",
        "operationId": "list_api_keys",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not mint keys: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "post": {
        "tags": [
          "identity"
        ],
        "summary": "Create API Key",
        "description": "Mint one machine key for your own tenant. The secret is in this response and in nothing else.\n\nWhat it is for: a person signs in to the dashboard and needs a credential for their CI, their laptop\nor their coding agent. The key carries *scopes* and no role, so a CI key minted `runs:write,runs:read`\nstarts and reads runs and can neither mint another key nor upload a provider credential.\n\nOnly `sha256` of the secret is stored, so there is no route that shows it again: losing it means\nminting another and revoking this one, which is the same motion as a rotation and is meant to be\ncheap. The tenant is the one your own credential belongs to and cannot be named in the request.",
        "operationId": "create_api_key",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMinted"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not mint keys: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "422": {
            "description": "a name the table cannot hold, or a scope that is not one of ours"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/api-keys/{key_id}": {
      "delete": {
        "tags": [
          "identity"
        ],
        "summary": "Revoke API Key",
        "description": "Revoke one of this tenant's keys. The row stays and `revoked_at` is set; the key stops at the gate.\n\nNever deleted, because `runs.created_by` points at it: a revoked key that vanished would leave every\nrun it started attributed to nothing. An id belonging to another tenant is the same 404 an id nobody\never minted gets, so this route cannot be used to learn which key ids are real.",
        "operationId": "revoke_api_key",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Key Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeySummary"
                }
              }
            }
          },
          "403": {
            "description": "this credential may not mint keys: an API key without the keys:admin scope, or a person whose role in this tenant is not owner or admin"
          },
          "404": {
            "description": "no key of this tenant's has that id - deliberately the same answer whether the id belongs to nobody or to another tenant, so that a 404 cannot be used to enumerate key ids"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/github/installations": {
      "get": {
        "tags": [
          "credentials"
        ],
        "summary": "List GitHub Installations",
        "description": "The Saram GitHub App's installations linked to this tenant, by account: where the App writes for it.",
        "operationId": "list_github_installations",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GitHubInstallationList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "post": {
        "tags": [
          "credentials"
        ],
        "summary": "Link GitHub Installation",
        "description": "Link the Saram GitHub App's installation to this tenant, from the setup URL GitHub sent the installer to.\n\nPost the `installation_id` and `code` from that URL with the signed-in person's token. Saram trades the code\nfor the GitHub user's own token, links the installation only if that GitHub user may reach it, and then\nforgets the token. From then on the App writes to repositories on that account for this tenant, and for no\nother. Installing again on the same account replaces the link. Refused with a 409 when the installation is\nanother tenant's.",
        "operationId": "link_github_installation",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GitHubInstallationWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GitHubInstallationRecord"
                }
              }
            }
          },
          "403": {
            "description": "not a person signed in to a tenant with credentials:write, or the GitHub user who authorized cannot reach the installation"
          },
          "409": {
            "description": "the installation, or the account it is on, is linked to another tenant"
          },
          "422": {
            "description": "GitHub refused the code: used already, expired, or not this App's"
          },
          "502": {
            "description": "GitHub could not be asked, or answered something no link can be decided on"
          },
          "503": {
            "description": "this deployment has no OAuth client for the Saram GitHub App"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "x-internal": true
      }
    },
    "/api/v1/files": {
      "get": {
        "tags": [
          "files"
        ],
        "summary": "List Files",
        "description": "This tenant's files, newest first. Metadata only: the API never serves an uploaded file back.",
        "operationId": "list_files",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      },
      "post": {
        "tags": [
          "files"
        ],
        "summary": "Upload File",
        "description": "One file, multipart, into this tenant's own prefix of the evidence store.\n\nThe answer is a `saram-file://<tenant_id>/<file_id>` ref, which is the only form a run spec may name a\nfile by - in a run started by any key or member of the same tenant, and by nobody else's. What is\nrefused, and why each: more bytes than the per-file limit, 25 MiB unless the deployment sets another\n(413, and the body is cut off at the cap rather than read and then measured); a name that is not one path\nsegment, a media type the deployment does not accept, or magic bytes that disagree with the declared type\n(422 each); and a caller that may not own files (403). Accepted by default: PNG, JPEG, WebP, GIF, PDF,\nCSV, plain text, JSON, and Word and Excel documents (`.docx`, `.xlsx`); not HTML, SVG, scripts,\narchives or executables.",
        "operationId": "upload_file",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The bytes, with the part's Content-Type declaring the media type. One file per request."
                  },
                  "name": {
                    "type": "string",
                    "description": "What to store the file as; overrides the part's filename. One path segment: A-Z, a-z, 0-9, space, '.', '_' or '-', no leading dot."
                  }
                },
                "type": "object",
                "required": [
                  "file"
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileRecord"
                }
              }
            }
          },
          "403": {
            "description": "this caller may not own files, or the API key lacks the files:write scope"
          },
          "413": {
            "description": "larger than the most one file may be on this deployment; the detail names the limit"
          },
          "422": {
            "description": "bad name, a media type off the allowlist, or bytes that disagree with it"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/files/{file_id}": {
      "get": {
        "tags": [
          "files"
        ],
        "summary": "Get File",
        "description": "One uploaded file's metadata: its ref, name, size, media type and when it expires. Never its bytes.",
        "operationId": "get_file",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "file_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "File Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileRecord"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/files/{file_id}/delete": {
      "post": {
        "tags": [
          "files"
        ],
        "summary": "Delete File",
        "description": "Delete one uploaded file. A run started afterwards that names it is refused; its ref stays in the\nrecords of runs that already used it.",
        "operationId": "delete_file",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "file_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "File Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileDeleted"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/personas": {
      "get": {
        "tags": [
          "personas"
        ],
        "summary": "List Personas",
        "description": "The personas a run can name: each by its `id@version` address, which is what a run spec's `personas`\nlists, with its name, pack and tags.",
        "operationId": "list_personas",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonaList"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/personas/{address}": {
      "get": {
        "tags": [
          "personas"
        ],
        "summary": "Get Persona",
        "description": "One persona in full, by `id@version` or by `id` for its latest version: its goals, priors, tolerance,\nconstraints, voice and verdict question - everything that drives how it behaves in a run.",
        "operationId": "get_persona",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "address",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Address"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Persona"
                }
              }
            }
          },
          "403": {
            "description": "the API key lacks the required scope"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/blobs": {
      "get": {
        "tags": [
          "blobs"
        ],
        "summary": "Get Blob",
        "description": "Evidence bytes, streamed from the store a chunk at a time, with `Range`, `HEAD` and conditional requests.\n\nNothing here holds the object: `get_blob` returns a handle whose `size` answers `Content-Length` and\n`Content-Range`, and the body is a `StreamingResponse` over `Blob.open`, which reads `CHUNK_SIZE`\nat a time and asks the store for the requested byte range only. So a dashboard playing three session\nvideos costs three chunks in this process, and seeking to the last second of a recording transfers a\nsecond of it. `HEAD` answers those headers and no body, which is how a caller (the MCP server's\n`get_evidence_url`) learns a blob's size without fetching it.\n\nExcept a step stream: an `.ndjson` blob is read whole and served with its keystrokes withheld - a surface\nhalf's keyboard input reads `[secret]` unless that step's agent half says it was not a credential\n(`blobs.withhold_keystrokes`) - because a pool without `type.secret` writes a typed password into its\n`surface.ndjson` verbatim, and so does a replay. Its `Content-Length`, ranges and `ETag` describe the bytes\nserved, not the bytes stored.\n\nEvery answer with bytes carries the blob's `ETag` and `Last-Modified`, from the metadata read that gave\nits size, and the route answers the conditional requests a browser cache and a media player make with\nthem (RFC 9110 section 13): `If-None-Match` or `If-Modified-Since` that still match are a 304 with the\n`ETag` and no body, and a `Range` under an `If-Range` that no longer matches is ignored for the whole\nblob with a 200, so a player never splices a stale half onto a new one. The 304 is decided before the\nrange, which is the order RFC 9110 gives, and is the same for `HEAD`.\n\nThe generator handed to `StreamingResponse` is deliberately synchronous: Starlette pulls a sync\niterator through its threadpool, so the store's blocking read never sits on the event loop, and the\n`with` inside it closes the file handle or the S3 body even when the client disconnects mid-stream.\n\nSigned S3 URLs would be the other way to get the bytes out of this process, and are refused for now:\na presigned URL is bearer authority on the object, so it leaves both the API key gate and the\nrun-ownership check behind and cannot be narrowed to the caller who asked.",
        "operationId": "get_blob",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "ref",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "a `file://` or `s3://` evidence URI as recorded in a trace",
              "title": "Ref"
            },
            "description": "a `file://` or `s3://` evidence URI as recorded in a trace"
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Range"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated.",
              "title": "If-None-Match"
            },
            "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated."
          },
          {
            "name": "If-Modified-Since",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date.",
              "title": "If-Modified-Since"
            },
            "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date."
          },
          {
            "name": "If-Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200.",
              "title": "If-Range"
            },
            "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/octet-stream": {}
            },
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Partial Content"
          },
          "304": {
            "description": "Not Modified: If-None-Match or If-Modified-Since matched, so the caller's copy is current",
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "a ref this API never serves: outside the evidence store, or a bucket that is not its run's"
          },
          "404": {
            "description": "nothing here for this key: the blob is gone, the ref names no run, or it names a run belonging to another tenant - deliberately one answer, so that a 403 can never confirm a run id exists"
          },
          "416": {
            "description": "Range Not Satisfiable"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      },
      "head": {
        "tags": [
          "blobs"
        ],
        "summary": "Head Blob",
        "description": "Evidence bytes, streamed from the store a chunk at a time, with `Range`, `HEAD` and conditional requests.\n\nNothing here holds the object: `get_blob` returns a handle whose `size` answers `Content-Length` and\n`Content-Range`, and the body is a `StreamingResponse` over `Blob.open`, which reads `CHUNK_SIZE`\nat a time and asks the store for the requested byte range only. So a dashboard playing three session\nvideos costs three chunks in this process, and seeking to the last second of a recording transfers a\nsecond of it. `HEAD` answers those headers and no body, which is how a caller (the MCP server's\n`get_evidence_url`) learns a blob's size without fetching it.\n\nExcept a step stream: an `.ndjson` blob is read whole and served with its keystrokes withheld - a surface\nhalf's keyboard input reads `[secret]` unless that step's agent half says it was not a credential\n(`blobs.withhold_keystrokes`) - because a pool without `type.secret` writes a typed password into its\n`surface.ndjson` verbatim, and so does a replay. Its `Content-Length`, ranges and `ETag` describe the bytes\nserved, not the bytes stored.\n\nEvery answer with bytes carries the blob's `ETag` and `Last-Modified`, from the metadata read that gave\nits size, and the route answers the conditional requests a browser cache and a media player make with\nthem (RFC 9110 section 13): `If-None-Match` or `If-Modified-Since` that still match are a 304 with the\n`ETag` and no body, and a `Range` under an `If-Range` that no longer matches is ignored for the whole\nblob with a 200, so a player never splices a stale half onto a new one. The 304 is decided before the\nrange, which is the order RFC 9110 gives, and is the same for `HEAD`.\n\nThe generator handed to `StreamingResponse` is deliberately synchronous: Starlette pulls a sync\niterator through its threadpool, so the store's blocking read never sits on the event loop, and the\n`with` inside it closes the file handle or the S3 body even when the client disconnects mid-stream.\n\nSigned S3 URLs would be the other way to get the bytes out of this process, and are refused for now:\na presigned URL is bearer authority on the object, so it leaves both the API key gate and the\nrun-ownership check behind and cannot be narrowed to the caller who asked.",
        "operationId": "head_blob",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "ref",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "a `file://` or `s3://` evidence URI as recorded in a trace",
              "title": "Ref"
            },
            "description": "a `file://` or `s3://` evidence URI as recorded in a trace"
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Range"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated.",
              "title": "If-None-Match"
            },
            "description": "ETags the caller already holds, or *. If one matches the blob's (weak comparison), the answer is 304 with no body. When present, If-Modified-Since is not evaluated."
          },
          {
            "name": "If-Modified-Since",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date.",
              "title": "If-Modified-Since"
            },
            "description": "An HTTP-date. If the blob has not been written since, the answer is 304 with no body. Ignored when If-None-Match is present, or when it is not a valid date."
          },
          {
            "name": "If-Range",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200.",
              "title": "If-Range"
            },
            "description": "An ETag or an HTTP-date the caller's partial copy was fetched under. If it still matches the blob (strong comparison), Range is honoured; if not, Range is ignored and the whole blob comes back with a 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/octet-stream": {}
            },
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "when the blob was last written, as an HTTP-date; send it back as If-Modified-Since",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Partial Content"
          },
          "304": {
            "description": "Not Modified: If-None-Match or If-Modified-Since matched, so the caller's copy is current",
            "headers": {
              "ETag": {
                "description": "a strong entity tag for the blob's bytes; send it back as If-None-Match or If-Range",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "a ref this API never serves: outside the evidence store, or a bucket that is not its run's"
          },
          "404": {
            "description": "nothing here for this key: the blob is gone, the ref names no run, or it names a run belonging to another tenant - deliberately one answer, so that a 403 can never confirm a run id exists"
          },
          "416": {
            "description": "Range Not Satisfiable"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ops/kpis": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "Get Ops KPIs",
        "description": "Saram's own growth counts across every tenant: sign-ups, activations and homepage demo runs.\n\nFor Saram's operators only. Every other credential gets one 403, whatever its scopes or role. The answer\nis counted when it is asked for and carries `Cache-Control: no-store`, so no proxy serves an old count.",
        "operationId": "get_ops_kpis",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Also count from this instant to as_of: ISO 8601 with its offset, as in 2026-10-01T00:00:00Z. One with no offset is refused rather than guessed at.",
              "title": "Since"
            },
            "description": "Also count from this instant to as_of: ISO 8601 with its offset, as in 2026-10-01T00:00:00Z. One with no offset is refused rather than guessed at."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsKpis"
                }
              }
            }
          },
          "403": {
            "description": "this credential is not an operator's: every tenant's key and every person's sign-in, whatever its scopes or role, and a request to an API serving without a gate"
          },
          "422": {
            "description": "since has no offset, is after as_of, or is before 2026"
          },
          "429": {
            "description": "too many requests from this credential; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "this deployment's database cannot count across tenants"
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/ops/kpis/series": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "Get Ops KPI Series",
        "description": "Saram's weekly growth counts once a day over the last `days` days, the last point at `as_of`.\n\nFor Saram's operators only, like `GET /api/v1/ops/kpis`, whose seven-day counts the last point is. Every\npoint is counted in one statement when the series is asked for, and the answer carries\n`Cache-Control: no-store`.",
        "operationId": "get_ops_kpi_series",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 90,
              "minimum": 1,
              "description": "How many points: as_of and the whole days before it, 1 to 90.",
              "default": 7,
              "title": "Days"
            },
            "description": "How many points: as_of and the whole days before it, 1 to 90."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsKpiSeries"
                }
              }
            }
          },
          "403": {
            "description": "this credential is not an operator's: every tenant's key and every person's sign-in, whatever its scopes or role, and a request to an API serving without a gate"
          },
          "422": {
            "description": "days is not a whole number from 1 to 90"
          },
          "429": {
            "description": "too many requests from this credential; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "this deployment's database cannot count across tenants"
          }
        },
        "x-internal": true
      }
    },
    "/api/v1/ops/tenants": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "Get Ops Tenants",
        "description": "Every tenant of this deployment, newest first, with its plan, status, run counts and the operator's\nmarks: ids, codes, times, counts and flags, never a name, a person or a price.\n\nFor Saram's operators only, and only with a key that may list (`ops:list`): a key that reads the counts\nalone gets the same 403 as every tenant's. Read when it is asked for, and the answer carries\n`Cache-Control: no-store`.",
        "operationId": "get_ops_tenants",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 2000,
              "minimum": 1,
              "description": "At most this many tenants, the newest; 1 to 2000.",
              "default": 2000,
              "title": "Limit"
            },
            "description": "At most this many tenants, the newest; 1 to 2000."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsTenantList"
                }
              }
            }
          },
          "403": {
            "description": "this credential is not an operator's that may list tenants: every tenant's key and every person's sign-in, whatever its scopes or role, an operator key that may read the counts and not list (ops:read without ops:list), and a request to an API serving without a gate"
          },
          "422": {
            "description": "limit is not a whole number from 1 to 2000"
          },
          "429": {
            "description": "too many requests from this credential; Retry-After says when to ask again",
            "headers": {
              "Retry-After": {
                "description": "seconds",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "this deployment's database cannot list across tenants"
          }
        },
        "x-internal": true
      }
    }
  },
  "components": {
    "schemas": {
      "A": {
        "properties": {
          "step_id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Step Id"
          },
          "session_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Session Id"
          },
          "text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Text"
          }
        },
        "type": "object",
        "title": "A",
        "description": "The first of the two statements that conflict: what the product said (`text`), and the session and step where it said it."
      },
      "AbandonBehavior": {
        "type": "string",
        "enum": [
          "silent",
          "narrates_reason"
        ],
        "title": "AbandonBehavior",
        "description": "What the persona's diary says when they give up, on one task or on the whole session. `narrates_reason` (the default): they say why before they stop. `silent`: they simply stop."
      },
      "AccessibilityEnum": {
        "type": "string",
        "enum": [
          "screen_reader",
          "keyboard_only",
          "low_vision",
          "reduced_motion",
          "color_blind"
        ],
        "title": "AccessibilityEnum",
        "description": "One accessibility need the persona has."
      },
      "ActionPolicy": {
        "properties": {
          "reading": {
            "$ref": "#/components/schemas/Reading"
          },
          "input": {
            "$ref": "#/components/schemas/Input"
          },
          "exploration": {
            "$ref": "#/components/schemas/Exploration"
          },
          "trust_defaults": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TrustDefaults"
              },
              {
                "type": "null"
              }
            ]
          },
          "help_seeking": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/HelpSeeking"
              },
              {
                "type": "null"
              }
            ]
          },
          "error_response": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorResponse"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "reading",
          "input",
          "exploration"
        ],
        "title": "ActionPolicy",
        "description": "How the persona works a page: how much of it they read, what they work it with, whether they wander or head straight for the goal, and how they treat defaults, help and errors. Each value becomes a line of the persona's briefing."
      },
      "ActionStep": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "step_id": {
            "type": "integer",
            "title": "Step Id"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "session_id",
          "step_id"
        ],
        "title": "ActionStep",
        "description": "The step the claim is about and the replay is judged at - the action that failed, not the look that saw it or the retry after it. Written by reproduction, which picks it (`_judged_path`); absent when nothing replayed this issue."
      },
      "AlignedStep": {
        "properties": {
          "step_id": {
            "type": "integer",
            "title": "Step Id"
          },
          "has_agent": {
            "type": "boolean",
            "title": "Has Agent"
          },
          "has_surface": {
            "type": "boolean",
            "title": "Has Surface"
          },
          "terminal": {
            "type": "boolean",
            "title": "Terminal"
          },
          "ts_agent": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts Agent"
          },
          "action_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Action Type"
          },
          "target_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Description"
          },
          "action_args": {
            "additionalProperties": true,
            "type": "object",
            "title": "Action Args"
          },
          "narration": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Narration",
            "description": "SUBJECTIVE: the persona's diary line said before this action. Quote it as the persona's, never as a\nfinding."
          },
          "reasoning_summary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reasoning Summary"
          },
          "perceived_state": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Perceived State"
          },
          "observation_mode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Observation Mode"
          },
          "tolerance_state": {
            "additionalProperties": true,
            "type": "object",
            "title": "Tolerance State"
          },
          "latency_ms": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latency Ms"
          },
          "tokens": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/StepTokens"
              },
              {
                "type": "null"
              }
            ]
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model"
          },
          "end_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "End Reason"
          },
          "result": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Result",
            "description": "On the terminal step, the artifact result it implies (`completed`, `abandoned`, `budget_exhausted`)."
          },
          "field": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Field",
            "description": "The focused text-entry control after this step, as the persona was told it (`agent_trace.field`): its\nkind, accessible name, the value it really holds (cut at 200 characters), the value's full length, and the\nhint it shows while empty - never part of the value. A password field, and any field holding one of the\nsession's credentials, is `masked`: the length is there and the value is not. Present on a `click`, `type`\nor `key` step whose focus landed in a text control, on a pool that reports it; absent otherwise. On the\nagent half, so it is readable on an `in_flight` page."
          },
          "ts_surface": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts Surface"
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url"
          },
          "route": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Route"
          },
          "viewport": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Viewport"
          },
          "inputs": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Inputs",
            "description": "What the body dispatched at this step (`surface_trace.inputs_dispatched`): pointer and keyboard input,\nwith the selector and text each resolved onto. On a `type` the persona made with the session's credential\n(`action_args.secret`), every keyboard input's `detail.text` and `resolved_text` is `[secret]`, whatever\nthe pool recorded: the password is on no step a reader is served."
          },
          "files_attached": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Files Attached",
            "description": "Files the body handed to the page at this step (`surface_trace.files_attached`): name, size, media\ntype, hash and whether it went through the file picker or straight into a file input - never a path. Empty\non every step but an `upload`."
          },
          "dialogs": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Dialogs",
            "description": "Native dialogs the page opened, or that were answered, at this step (`surface_trace.dialogs`): type,\nmessage verbatim, and how the step left it - `pending` on the step that opened one, `accept`, `dismiss` or\n`auto_accepted_beforeunload` on the step that answered it. A dialog floats above the page and is in no\nscreenshot, so this is the only place a reader sees it. Empty when the pool saw none."
          },
          "downloads": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Downloads",
            "description": "Files the browser saved at this step (`surface_trace.downloads`): filename, media type, size, hash and\nthe stored copy's ref when the pool kept the bytes - never a path. A download moves no pixel and no url, so\nwithout this the step reads as a click that did nothing. Empty when the pool saw none."
          },
          "network_failures": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Network Failures"
          },
          "console_errors": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Console Errors"
          },
          "dom_mutation_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dom Mutation Count"
          },
          "timing": {
            "$ref": "#/components/schemas/StepTiming"
          },
          "screenshot_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Screenshot Ref"
          },
          "screenshot_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Screenshot Url"
          },
          "video_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Ref"
          },
          "video_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Url"
          },
          "ts_start_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts Start S",
            "description": "Where this step starts in the session video, seconds from its first frame."
          },
          "ts_end_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts End S"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "step_id",
          "has_agent",
          "has_surface",
          "terminal"
        ],
        "title": "AlignedStep",
        "description": "One step, both halves: what the persona decided (the agent half) and what the browser did (the surface\nhalf), joined on `step_id`. A half that is missing is flagged (`has_agent`, `has_surface`), not invented; the\ntrailing `abandon`/`complete` step is agent-only by design (`terminal`).\n\nOn an `in_flight` page (`StepsPage.status`) both halves are there for every step the browser has answered.\nWhat waits for the session to end is the video, which is stored when the browser closes: a live step carries\nits `video_ref` and window but no `video_url` yet."
      },
      "ApiKeyList": {
        "properties": {
          "api_keys": {
            "items": {
              "$ref": "#/components/schemas/ApiKeySummary"
            },
            "type": "array",
            "title": "Api Keys",
            "description": "This tenant's live keys, oldest first. A revoked one is not listed."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "api_keys"
        ],
        "title": "ApiKeyList",
        "description": "The tenant's API keys, as metadata: never a key's secret, which is shown once, when it is minted."
      },
      "ApiKeyMinted": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Scopes"
          },
          "created_by": {
            "type": "string",
            "title": "Created By",
            "description": "usr_... for a person who minted it from the dashboard, operator for the CLI."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At",
            "description": "When this key last got through the gate, to a few minutes: it answers whether anything is still using the key before you revoke it, which does not need the last second."
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "revoked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At",
            "description": "Set on the record DELETE answers with. The row stays; the key stops working."
          },
          "secret": {
            "type": "string",
            "title": "Secret",
            "description": "sk_saram_<env>_<40 characters>. Shown once, here, and never again by any route: only its hash is stored. Treat it as you would a password - it carries this tenant's authority until it is revoked."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "scopes",
          "created_by",
          "created_at",
          "secret"
        ],
        "title": "ApiKeyMinted",
        "description": "What `POST /api/v1/api-keys` answers: the row, and the one and only sight of the secret.\n\n`secret` is not stored anywhere - `api_keys` holds `sha256` of it - so a caller who does not keep this\nresponse mints another and revokes this one. That is the same motion as a rotation, and it is meant to be."
      },
      "ApiKeySummary": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Scopes"
          },
          "created_by": {
            "type": "string",
            "title": "Created By",
            "description": "usr_... for a person who minted it from the dashboard, operator for the CLI."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At",
            "description": "When this key last got through the gate, to a few minutes: it answers whether anything is still using the key before you revoke it, which does not need the last second."
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "revoked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At",
            "description": "Set on the record DELETE answers with. The row stays; the key stops working."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "scopes",
          "created_by",
          "created_at"
        ],
        "title": "ApiKeySummary",
        "description": "One of this tenant's machine keys, as the list and the revocation answer it. The secret is not here."
      },
      "ApiKeyWrite": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Which of this tenant's credentials this is: lowercase letters, digits, underscore and dash, up to 64 characters - ci, dana-laptop. Not unique, because a rotation is two live keys under one name until the caller has moved."
          },
          "scopes": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scopes",
            "description": "A subset of runs:write, runs:read, files:write, credentials:write, keys:admin. Omitted, the key gets runs:write and runs:read, which is what a CI key needs and nothing more."
          },
          "expires_in_days": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires In Days",
            "description": "Refuse this key at the gate after this many days. Omitted, it does not expire on its own."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name"
        ],
        "title": "ApiKeyWrite",
        "description": "`POST /api/v1/api-keys`: what a person asks for when they mint one of their tenant's machine keys."
      },
      "Attribution": {
        "type": "string",
        "title": "Attribution",
        "description": "Whose failure this is, asked before an issue is a product finding: could the persona's own action explain it? `product`: no - the failure is the product's to own. `tester`: yes - a mistyped credential, the wrong element, placeholder text read as a value; the record moves to `tester_errors[]` with a `tester_error` block, is never a ticket and never counted as an issue. `undetermined`: the harness could not tell (a persona handed stale credentials by the mission author typed exactly what it was told; an attribution call that failed), and a human must; it caps `status` at `marker_matched`. Absent means never asked - every issue before this release - and a reader treats absent as `undetermined`. A classification: nothing acts on it directly, the producer derives `status` from it, so a later member (`setup`) is additive and an older reader buckets it as `unknown`.",
        "examples": [
          "product",
          "tester",
          "undetermined"
        ],
        "x-classification": true
      },
      "AuthState": {
        "type": "string",
        "enum": [
          "anonymous",
          "new_user",
          "returning_user",
          "admin"
        ],
        "title": "AuthState",
        "description": "The kind of visitor the persona arrives as: `anonymous` (the default), a `new_user`, a `returning_user` or an `admin`. Descriptive, and listed in each issue's `preconditions`; it signs nobody in. A session is signed in by the account it is handed from the run spec's `target.seed.accounts`."
      },
      "AutoRefill": {
        "properties": {
          "auto_refill": {
            "type": "boolean",
            "title": "Auto Refill"
          },
          "refill_below_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Refill Below Usd"
          },
          "refill_amount_usd": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 500.0,
                "minimum": 10.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Refill Amount Usd"
          },
          "monthly_budget_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Monthly Budget Usd",
            "description": "The most Saram may charge the card in a calendar month; null is no budget."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "auto_refill"
        ],
        "title": "AutoRefill",
        "description": "The tenant's auto-refill setting and monthly budget (`POST /api/v1/billing/auto-refill`, and on\n`GET /api/v1/billing`).\n\nWith `auto_refill` on, whenever the balance less what running runs hold falls below `refill_below_usd`, Saram\ncharges the card on file `refill_amount_usd` - never past `monthly_budget_usd` in a calendar month, manual\ntop-ups included. A declined charge, or one the bank wants authenticated, turns auto-refill off."
      },
      "AutoRefillState": {
        "properties": {
          "auto_refill": {
            "type": "boolean",
            "title": "Auto Refill"
          },
          "refill_below_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Refill Below Usd"
          },
          "refill_amount_usd": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 500.0,
                "minimum": 10.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Refill Amount Usd"
          },
          "monthly_budget_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Monthly Budget Usd",
            "description": "The most Saram may charge the card in a calendar month; null is no budget."
          },
          "last_skipped": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RefillSkip"
              },
              {
                "type": "null"
              }
            ],
            "description": "The latest refill that was due and not charged, so a screen can say 'Auto-refill is on, but this month's budget is spent'. Null once a refill has been started since, once the setting has been changed (by POST /billing/auto-refill, or by auto-refill turning itself off after a declined card), and for a monthly_budget skip once its calendar month is over, because the new month's budget has room again."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "auto_refill"
        ],
        "title": "AutoRefillState",
        "description": "The auto-refill setting as `GET /api/v1/billing` reads it: the setting, and the refill it last did not\ncharge. Only the setting is written back (`POST /api/v1/billing/auto-refill` takes `AutoRefill`)."
      },
      "B": {
        "properties": {
          "step_id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Step Id"
          },
          "session_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Session Id"
          },
          "text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Text"
          }
        },
        "type": "object",
        "title": "B",
        "description": "The second of the two statements that conflict: what the product said (`text`), and the session and step where it said it."
      },
      "BaselineStatus": {
        "type": "string",
        "title": "BaselineStatus",
        "description": "How this finding compares with the run named as the baseline. `new`: the baseline reached the same page and did not have it (for an issue, only one that is `reproducible`). `persisting`: the baseline has it too. `unknown`: nothing could say - no baseline was compared, the baseline never reached the page, or an issue the baseline does not have was not verified. `resolved` is never stamped on a finding of this run: a baseline issue this run no longer has is counted in `summary.issues_resolved_vs_baseline`. More values may be added; read one you do not know as `unknown`.",
        "examples": [
          "new",
          "persisting",
          "resolved",
          "unknown"
        ],
        "x-classification": true
      },
      "BillingOverview": {
        "properties": {
          "plan": {
            "type": "string",
            "title": "Plan"
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits",
            "description": "What your plan allows, from the plan's row in force now."
          },
          "monthly_fee_usd": {
            "type": "number",
            "title": "Monthly Fee Usd",
            "description": "Your plan's monthly fee, before tax: $0 on Free, $20 on Pro. From the plan's row now.",
            "default": 0
          },
          "included_credit_usd": {
            "type": "number",
            "title": "Included Credit Usd",
            "description": "The credit your plan includes each month, which expires at month end: $1 on Free, $10 on Pro. What this month has left of it is included_usd.",
            "default": 0
          },
          "usage_price_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Price Usd",
            "description": "USD per Usage on your plan now: available_usd divided by this is 'about N Usage'. Absent on a plan billed in steps, which has no Usage price."
          },
          "subscription": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SubscriptionState"
              },
              {
                "type": "null"
              }
            ],
            "description": "Your Saram Pro subscription, when there is one: whether it renews or ends, and when. Absent on a tenant that never started a Pro checkout."
          },
          "members_used": {
            "type": "integer",
            "title": "Members Used",
            "description": "How many people are in your workspace now, every role counted: see limits.max_members.",
            "default": 0
          },
          "billing_status": {
            "type": "string",
            "title": "Billing Status"
          },
          "included_usd": {
            "type": "number",
            "title": "Included Usd"
          },
          "purchased_usd": {
            "type": "number",
            "title": "Purchased Usd"
          },
          "balance_usd": {
            "type": "number",
            "title": "Balance Usd"
          },
          "held_usd": {
            "type": "number",
            "title": "Held Usd"
          },
          "available_usd": {
            "type": "number",
            "title": "Available Usd"
          },
          "card_on_file": {
            "type": "boolean",
            "title": "Card On File"
          },
          "auto_refill": {
            "$ref": "#/components/schemas/AutoRefillState"
          },
          "budget_used_usd": {
            "type": "number",
            "title": "Budget Used Usd",
            "description": "This month's top-ups, paid or still in flight."
          },
          "top_ups": {
            "items": {
              "$ref": "#/components/schemas/TopUpRecord"
            },
            "type": "array",
            "title": "Top Ups",
            "description": "The newest twenty, newest first. No client secrets."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "plan",
          "billing_status",
          "included_usd",
          "purchased_usd",
          "balance_usd",
          "held_usd",
          "available_usd",
          "card_on_file",
          "auto_refill",
          "budget_used_usd",
          "top_ups"
        ],
        "title": "BillingOverview",
        "description": "`GET /api/v1/billing`: the tenant's plan, prepaid balance, card on file, auto-refill and recent top-ups.\n\nThe balance is Saram's ledger, not Stripe's: `included` is the plan's monthly credit and expires at month end,\n`purchased` is top-ups and does not; `held` is what runs in flight have reserved, and `available` is what a\nnew run can hold against."
      },
      "BillingPortal": {
        "properties": {
          "url": {
            "type": "string",
            "title": "Url",
            "description": "Where to send the customer. Short-lived and single-use; mint a new one per click."
          },
          "return_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Return Url",
            "description": "Where Stripe sends them back to afterwards, when this deployment names one."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "url"
        ],
        "title": "BillingPortal",
        "description": "A one-use link into Stripe's hosted customer portal (`POST /api/v1/billing/portal`).\n\nWhere a customer manages the card Saram never sees: Saram stores no payment instrument, renders no\npayment form and is not in the flow at all. The link is short-lived and single-use by Stripe's design, so it\nis minted per click and never cached; the invoices, receipts and the subscription behind it are Stripe's\nscreens, and what this API serves is the usage the invoice is derived from (`GET /api/v1/usage`)."
      },
      "BillingSetup": {
        "properties": {
          "setup_intent_id": {
            "type": "string",
            "title": "Setup Intent Id"
          },
          "client_secret": {
            "type": "string",
            "title": "Client Secret",
            "description": "What the Payment Element confirms in the browser (stripe.confirmPayment / confirmSetup), where SCA happens. Present only while there is something to confirm. Saram never sees the card."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "setup_intent_id",
          "client_secret"
        ],
        "title": "BillingSetup",
        "description": "`POST /api/v1/billing/setup`: a SetupIntent for a card Saram may charge off-session (auto-refill). The card\nbecomes this tenant's card on file when Stripe reports the intent succeeded."
      },
      "BillingSubscription": {
        "properties": {
          "subscription_id": {
            "type": "string",
            "title": "Subscription Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "Stripe's: `incomplete` until the first invoice is paid, then `active`; `past_due` when a renewal failed; `canceled` once it has ended."
          },
          "client_secret": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Secret",
            "description": "What the Payment Element confirms in the browser (stripe.confirmPayment / confirmSetup), where SCA happens. Present only while there is something to confirm. Saram never sees the card."
          },
          "cancel_at_period_end": {
            "type": "boolean",
            "title": "Cancel At Period End",
            "description": "Pro ends when the month already paid for does; the tenant then moves to Free."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "subscription_id",
          "status",
          "cancel_at_period_end"
        ],
        "title": "BillingSubscription",
        "description": "`POST` and `DELETE /api/v1/billing/subscription`: the Saram Pro subscription and, while its first invoice\nis unpaid, the secret the Payment Element pays it with.\n\nThe tenant's plan becomes `pro` when Stripe reports the subscription active, not when this answers: an\nabandoned checkout leaves the tenant where it was, and expires at Stripe within a day."
      },
      "Check": {
        "properties": {
          "verdict": {
            "$ref": "#/components/schemas/Verdict1"
          },
          "quote": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Quote"
          },
          "quote_step": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuoteStep"
              },
              {
                "type": "null"
              }
            ]
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason"
          },
          "checked_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CheckedBy"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "verdict"
        ],
        "title": "Check",
        "description": "The claim check: one narrow question asked of the evidence at the cited steps after a replay matched a marker - does this evidence show the product doing what the title says? Descriptive: nothing acts on it directly; the producer derives `status` from `verdict` and `attribution`. Absent when the check never ran, which is every issue before this release and every issue no marker matched."
      },
      "CheckedBy": {
        "type": "string",
        "title": "CheckedBy",
        "description": "Which instrument answered: `judge` for the observer's checker model call under its own instructions, `code` for a mechanical rule, `manual` for a human. A classification.",
        "examples": [
          "judge",
          "code",
          "manual"
        ],
        "x-classification": true
      },
      "Claim": {
        "properties": {
          "action_step": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ActionStep"
              },
              {
                "type": "null"
              }
            ]
          },
          "expected": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expected"
          },
          "observed": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Observed"
          },
          "verification": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verification"
              },
              {
                "type": "null"
              }
            ]
          },
          "attribution": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Attribution"
              },
              {
                "type": "null"
              }
            ]
          },
          "check": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Check"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Claim",
        "description": "What this issue asserts, and what checked it. A replay matching a failure marker at a step does not say the marker was the thing the title is about, and in practice it often is not; since 0.5 that match is `verification.status: marker_matched`, and `verified` is reserved for a claim a second check (`check`) found supported by a verbatim quote from the cited steps and whose failure is the product's (`attribution`). An issue with no `claim` block, or with none of `verification`, is `unverified`: never read the absence of this object as a confirmation."
      },
      "CodexDisconnected": {
        "properties": {
          "caller": {
            "type": "string",
            "title": "Caller"
          },
          "state": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "connected",
              "expired",
              "error"
            ],
            "title": "State"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "caller",
          "state"
        ],
        "title": "CodexDisconnected",
        "description": "A ChatGPT login disconnected: this caller's `state` is now `none`."
      },
      "CodexGateway": {
        "properties": {
          "login": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Login"
          },
          "primary_used_percent": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Primary Used Percent"
          },
          "primary_reset_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Primary Reset At"
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Error"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "CodexGateway",
        "description": "What the ChatGPT gateway reports about this caller's slot: whether it can serve, and the consumer rate\nlimit's state from the last `x-codex-*` headers it saw. Never a token and never a routing key."
      },
      "CodexLoginStarted": {
        "properties": {
          "caller": {
            "type": "string",
            "title": "Caller"
          },
          "user_code": {
            "type": "string",
            "title": "User Code"
          },
          "verification_url": {
            "type": "string",
            "title": "Verification Url"
          },
          "expires_in": {
            "type": "integer",
            "title": "Expires In"
          },
          "status_url": {
            "type": "string",
            "title": "Status Url"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "caller",
          "user_code",
          "verification_url",
          "expires_in",
          "status_url"
        ],
        "title": "CodexLoginStarted",
        "description": "`POST /api/v1/providers/codex/login`: open `verification_url`, type `user_code`, then poll `status_url`."
      },
      "CodexPending": {
        "properties": {
          "user_code": {
            "type": "string",
            "title": "User Code"
          },
          "verification_url": {
            "type": "string",
            "title": "Verification Url"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "user_code",
          "verification_url",
          "expires_at"
        ],
        "title": "CodexPending",
        "description": "A device login in flight. The `device_auth_id` that redeems it is not here and never leaves the pod."
      },
      "CodexProviders": {
        "properties": {
          "entries": {
            "additionalProperties": {
              "$ref": "#/components/schemas/CodexProvidersEntry"
            },
            "type": "object",
            "title": "Entries"
          },
          "roles": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Roles"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "entries",
          "roles"
        ],
        "title": "CodexProviders",
        "description": "The `providers` block to paste into a run spec, secret name included, so the caller never has to type\none. The ref is the routing key's name, never its value."
      },
      "CodexProvidersEntry": {
        "properties": {
          "model": {
            "type": "string",
            "title": "Model"
          },
          "base_url": {
            "type": "string",
            "title": "Base Url"
          },
          "credential_ref": {
            "type": "string",
            "title": "Credential Ref"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "model",
          "base_url",
          "credential_ref"
        ],
        "title": "CodexProvidersEntry",
        "description": "One `providers` entry of a run spec, routed through this caller's ChatGPT login."
      },
      "CodexStatus": {
        "properties": {
          "caller": {
            "type": "string",
            "title": "Caller"
          },
          "state": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "connected",
              "expired",
              "error"
            ],
            "title": "State"
          },
          "pending": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CodexPending"
              },
              {
                "type": "null"
              }
            ]
          },
          "account_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Account Id"
          },
          "plan_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Plan Type"
          },
          "connected_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Connected At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "last_refresh": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Refresh"
          },
          "error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error",
            "description": "Why the last login attempt failed, when `state` is `error` and no gateway call was involved. The OAuth\n`error_description`, which cannot carry a token."
          },
          "gateway": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CodexGateway"
              },
              {
                "type": "null"
              }
            ]
          },
          "providers": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CodexProviders"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "caller",
          "state"
        ],
        "title": "CodexStatus",
        "description": "`GET /api/v1/providers/codex`. Metadata about a login; never any part of one."
      },
      "Constraints": {
        "properties": {
          "device": {
            "$ref": "#/components/schemas/Device"
          },
          "viewport": {
            "type": "string",
            "pattern": "^[0-9]+x[0-9]+$",
            "title": "Viewport"
          },
          "network": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Network"
              },
              {
                "type": "null"
              }
            ],
            "default": "fast"
          },
          "accessibility": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/AccessibilityEnum"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accessibility"
          },
          "locale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Locale",
            "default": "en-US"
          },
          "auth_state": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/AuthState"
              },
              {
                "type": "null"
              }
            ],
            "default": "anonymous"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "device",
          "viewport"
        ],
        "title": "Constraints",
        "description": "The browser the persona uses: the kind of device, the screen size, the connection, the language, accessibility needs and the signed-in state they start in. Most of these set up the browser session rather than the persona's briefing. A replay of an issue runs with the same ones, and the issue lists them in `preconditions`."
      },
      "Contradiction": {
        "properties": {
          "a": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/A"
              },
              {
                "type": "null"
              }
            ]
          },
          "b": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/B"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Contradiction",
        "description": "Populated for type=contradiction: the two statements/surfaces that conflict, each pinned to a step."
      },
      "Conversation": {
        "properties": {
          "phrasing": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Phrasing"
              },
              {
                "type": "null"
              }
            ]
          },
          "asks_before_hunting": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Asks Before Hunting"
          },
          "pushes_back_on_generic": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pushes Back On Generic"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Conversation",
        "description": "How the persona talks to an in-product assistant or chat surface. Matters when the target is itself an AI product."
      },
      "CostCeiling": {
        "properties": {
          "usd": {
            "type": "number",
            "title": "Usd",
            "description": "The most Saram can bill for this run on your plan, in US dollars: a ceiling, since every step is counted at the most it can be charged. A normal run bills well under it (a typical 5-second step is about 1 Usage, against the 3.5 a step can reach). 0 when your plan bills nothing. Model calls on your own provider are never in it: model_calls says who bills those."
          },
          "billed_in": {
            "type": "string",
            "enum": [
              "usage",
              "steps",
              "nothing"
            ],
            "title": "Billed In",
            "description": "What your plan bills: usage (prepaid Usage, the Free and Pro plans), steps (persona steps, the older per-step plans), or nothing."
          },
          "plan": {
            "type": "string",
            "title": "Plan",
            "description": "The plan the figure is priced on."
          },
          "price_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Price Usd",
            "description": "Dollars per unit of billed_in: per Usage, or per persona step. Absent when billed_in is nothing."
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "description": "Sessions the spec plans, one per persona per mission."
          },
          "steps_per_session": {
            "type": "integer",
            "title": "Steps Per Session"
          },
          "wall_clock_s_per_session": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Wall Clock S Per Session",
            "description": "The spec's own wall-clock budget, which bounds the seconds a session's steps are charged for. Absent when it\nnames none."
          },
          "usage_per_session": {
            "type": "number",
            "title": "Usage Per Session",
            "description": "The most Usage one session can use."
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "The run's Usage ceiling, sessions x usage_per_session, whatever your plan bills in."
          },
          "basis": {
            "type": "string",
            "title": "Basis",
            "description": "One line saying how usd was reached: the sessions, the steps, the price, and what is included. Quote it with usd."
          },
          "model_calls": {
            "type": "string",
            "title": "Model Calls",
            "description": "Who bills the run's model calls, per model role: your own provider key (billed to you by that provider), your ChatGPT login (within that subscription), or Saram's credential (not billed separately). Never part of usd."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "usd",
          "billed_in",
          "plan",
          "sessions",
          "steps_per_session",
          "usage_per_session",
          "usage",
          "basis",
          "model_calls"
        ],
        "title": "CostCeiling",
        "description": "What this run would cost: the most it can bill on your plan, as a number and the arithmetic behind it.\n\nA ceiling, not a forecast. Given on every plan and whatever the run gate is doing, because \"what would it\ncost?\" has an answer on every plan: the most Usage the spec's own budgets allow, priced the way your plan\nbills."
      },
      "CredentialList": {
        "properties": {
          "credentials": {
            "items": {
              "$ref": "#/components/schemas/CredentialRecord"
            },
            "type": "array",
            "title": "Credentials",
            "description": "This tenant's live credentials, oldest first. A deleted one is not listed."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "credentials"
        ],
        "title": "CredentialList",
        "description": "The tenant's own credentials, as metadata: never a value, which the API stores and cannot read back."
      },
      "CredentialRecord": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "kind": {
            "type": "string",
            "title": "Kind"
          },
          "secret_ref": {
            "type": "string",
            "title": "Secret Ref",
            "description": "The awssm:// ref to paste into a run spec as providers.entries[].credential_ref, for an account into target.seed.accounts, as connectors.webhook.secret_ref for a webhook key, or as connectors.github.token_ref for a github token. Only this tenant's specs may name it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "deleted_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deleted At",
            "description": "Set on the record `DELETE` answers with, absent on a live one: the secret is gone and the name is free."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "kind",
          "secret_ref",
          "created_at"
        ],
        "title": "CredentialRecord",
        "description": "One of this tenant's credentials, a provider key or a test account, as all three routes answer it.\nMetadata only."
      },
      "CredentialWrite": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name",
            "description": "What to call this credential: lowercase letters, digits, underscore and dash, up to 64 characters. The last segment of the secret's name and part of the ref you will paste into a run spec, so pick something a person can read - anthropic, openai-judge."
          },
          "kind": {
            "type": "string",
            "title": "Kind",
            "description": "Which provider this key is for, as the model string's prefix names it: anthropic, bedrock, openai, openrouter, azure, google, google-cloud, litellm. A run spec entry naming this credential must be on the same kind. Or account: a test-account login for a persona, named in a run spec's target.seed.accounts (or target.auth.secret_ref) instead. Or webhook: the key a run spec's connectors.webhook is signed with, named there as secret_ref. Or github: a token that may write issues on one repository, named as connectors.github.token_ref and used only where the Saram GitHub App is not installed on that repository."
          },
          "value": {
            "type": "string",
            "format": "password",
            "title": "Value",
            "description": "The provider key itself. Written to Secrets Manager and never returned, logged or echoed; for google-cloud it is the service-account JSON document, and for account it is one secret-store entry as a JSON object (email, password, inbox, notes, cookies, basic, headers).",
            "writeOnly": true
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name",
          "kind",
          "value"
        ],
        "title": "CredentialWrite",
        "description": "`POST /api/v1/credentials`: the one request body in this API that carries a secret.\n\nThe API stores `value` once and never answers it back: no response anywhere has a `value` field. To change a\ncredential, delete it and write it again."
      },
      "DecidedBy": {
        "type": "string",
        "title": "DecidedBy",
        "description": "`code`: a mechanical comparison decided it (the credential check). `judge`: the observer's attribution model call under its own instructions decided it. A classification.",
        "examples": [
          "code",
          "judge"
        ],
        "x-classification": true
      },
      "Decision": {
        "type": "string",
        "enum": [
          "positive",
          "negative",
          "undecided"
        ],
        "title": "Decision",
        "description": "Would keep paying / would recommend / would text 'it works' = positive"
      },
      "DemoRefusal": {
        "properties": {
          "status": {
            "type": "string",
            "const": "refused",
            "title": "Status"
          },
          "reason": {
            "type": "string",
            "enum": [
              "invalid_url",
              "private_target",
              "denied_host",
              "bot_check",
              "address_limit",
              "target_limit",
              "daily_ceiling",
              "busy",
              "demo_off"
            ],
            "title": "Reason",
            "description": "`invalid_url`, `private_target` (the message then carries the tunnel recipe when the address is the\nvisitor's own machine), `denied_host`, `bot_check`, `address_limit`, `target_limit`, `daily_ceiling`, `busy` or\n`demo_off`."
          },
          "message": {
            "type": "string",
            "title": "Message",
            "description": "One sentence a person can read, in English."
          },
          "retry_after_s": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retry After S",
            "description": "Where waiting helps: seconds until the next UTC midnight for a day's limit, or until a place should be free\nfor `busy`. Also the response's `Retry-After`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status",
          "reason",
          "message"
        ],
        "title": "DemoRefusal",
        "description": "A demo that will not run, and why, in a code the landing words for itself."
      },
      "DemoRunWrite": {
        "properties": {
          "url": {
            "type": "string",
            "title": "Url",
            "description": "The site to try, as pasted: `https://example.com/pricing`, or a bare `example.com`, read as `https://`. Opened\nas origin and path on its usual port; the query and the fragment are dropped before anything is stored."
          },
          "turnstile": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Turnstile",
            "description": "The token the Turnstile widget on the page gave this visitor. Single-use: get a new one for each POST."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "url"
        ],
        "title": "DemoRunWrite",
        "description": "The body of `POST /api/v1/demo/runs`: the address a visitor pasted, and the Turnstile widget's token."
      },
      "DemoStarted": {
        "properties": {
          "status": {
            "type": "string",
            "const": "pending",
            "title": "Status"
          },
          "demo_token": {
            "type": "string",
            "title": "Demo Token",
            "description": "dmo_ plus 32 letters and digits: the one credential for this demo's status, for 24 hours. Send it in\n`X-Saram-Demo`; never put it in a URL."
          },
          "poll_after_s": {
            "type": "integer",
            "title": "Poll After S",
            "description": "How long to wait between polls."
          },
          "steps_cap": {
            "type": "integer",
            "title": "Steps Cap",
            "description": "The most steps Rosa takes: the end of the progress bar."
          },
          "url": {
            "type": "string",
            "title": "Url",
            "description": "The address Rosa opens: what was pasted, as origin and path. A hash-routed app opens at its root."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status",
          "demo_token",
          "poll_after_s",
          "steps_cap",
          "url"
        ],
        "title": "DemoStarted",
        "description": "A demo that is on its way: poll `GET /api/v1/demo/runs/status` with `demo_token` until it is done."
      },
      "DemoStatus": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "done",
              "failed",
              "refused"
            ],
            "title": "Status"
          },
          "steps_taken": {
            "type": "integer",
            "title": "Steps Taken",
            "description": "How many steps Rosa has taken: the progress bar, against `steps_cap`."
          },
          "steps_cap": {
            "type": "integer",
            "title": "Steps Cap"
          },
          "share_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Share Url",
            "description": "The replay, on `done`: a 7-day public link to the session's recording, steps and narration. Every `done`\nanswer carries it, so a reload loses nothing."
          },
          "share_expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Share Expires At",
            "description": "When that link stops playing."
          },
          "reason": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "invalid_url",
                  "private_target",
                  "denied_host",
                  "bot_check",
                  "address_limit",
                  "target_limit",
                  "daily_ceiling",
                  "busy",
                  "demo_off"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason",
            "description": "On `refused`: `busy` when no browser came free in time and the demo was called off, `demo_off` when the demo\nhas closed since."
          },
          "message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Message",
            "description": "On `failed` and `refused`: one sentence a person can read."
          },
          "retry_after_s": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retry After S",
            "description": "On `refused` with `busy`: about when a place should be free again."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status",
          "steps_taken",
          "steps_cap"
        ],
        "title": "DemoStatus",
        "description": "Where one demo has got to: `pending` until Rosa has a browser, `running` while she acts, then `done` with\nthe replay's link, `failed`, or `refused`."
      },
      "Device": {
        "type": "string",
        "enum": [
          "desktop",
          "mobile",
          "tablet"
        ],
        "title": "Device",
        "description": "The kind of device the browser presents as. `mobile` and `tablet` open a mobile, touch-enabled browser; `desktop` a desktop one. The screen size is `viewport`, set separately."
      },
      "DisplaySplit": {
        "properties": {
          "note": {
            "type": "string",
            "title": "Note"
          },
          "min_consensus": {
            "type": "number",
            "title": "Min Consensus"
          },
          "min_affected_sessions": {
            "type": "integer",
            "title": "Min Affected Sessions"
          },
          "strong_consensus": {
            "type": "number",
            "title": "Strong Consensus"
          },
          "shown": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Shown"
          },
          "folded": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Folded"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "note",
          "min_consensus",
          "min_affected_sessions",
          "strong_consensus",
          "shown",
          "folded"
        ],
        "title": "DisplaySplit",
        "description": "The cohort floors, and which of the run's opportunities clear them.\n\nThe split is the whole run's, so every surface - here, on GitHub, in the MCP answer - leads with the same\nrecord. Nothing is dropped: `folded` is a single-persona note, one click away."
      },
      "DomainLiteracy": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high"
        ],
        "title": "DomainLiteracy",
        "description": "How fluent the persona is in the language of the product's industry. `low`: does not know its terms and does not pretend to. `medium`: knows enough to follow along. `high`: fluent, and notices when a term is used loosely."
      },
      "ErrorResponse": {
        "type": "string",
        "enum": [
          "retry_same",
          "retry_different",
          "back_out",
          "abandon"
        ],
        "title": "ErrorResponse",
        "description": "First reaction to an error"
      },
      "Evidence": {
        "properties": {
          "step_refs": {
            "items": {
              "$ref": "#/components/schemas/StepRef"
            },
            "type": "array",
            "minItems": 1,
            "title": "Step Refs"
          },
          "quotes": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Quotes"
          },
          "screenshots": {
            "anyOf": [
              {
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "format": "uri"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Screenshots"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "step_refs"
        ],
        "title": "Evidence",
        "description": "What the feedback rests on: the steps where personas raised it (`step_refs`), lines from their diaries at those steps, each checked to appear there verbatim (`quotes`), and screenshots of what they were looking at."
      },
      "Evidence1": {
        "properties": {
          "step_refs": {
            "items": {
              "$ref": "#/components/schemas/StepRef"
            },
            "type": "array",
            "minItems": 1,
            "title": "Step Refs"
          },
          "screenshots": {
            "anyOf": [
              {
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "format": "uri"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Screenshots"
          },
          "video": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Video"
              },
              {
                "type": "null"
              }
            ]
          },
          "dom_snapshot": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "format": "uri"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dom Snapshot"
          },
          "network": {
            "anyOf": [
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Network"
          },
          "console": {
            "anyOf": [
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Console"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "step_refs"
        ],
        "title": "Evidence1",
        "description": "What the issue rests on: the steps where sessions met it (`step_refs`) and, when the run kept them, screenshots, a clip of the session's video, a snapshot of the page, and the network requests and console messages around it."
      },
      "EvidenceRefs": {
        "properties": {
          "screenshots": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Screenshots"
          },
          "dom_snapshot": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dom Snapshot"
          },
          "video": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "EvidenceRefs",
        "description": "The store URIs on one issue, gathered where a consumer that wants to *fetch* evidence looks for them.\n\n`screenshots` are what the MCP `get_screenshot` tool accepts. The other two are not - it serves images\nonly - and are fetched over `GET /api/v1/blobs?ref=` with the same key, which is why they are named here\nrather than left for a caller to dig out of `issue.evidence`."
      },
      "Exploration": {
        "type": "string",
        "enum": [
          "explores",
          "beeline"
        ],
        "title": "Exploration",
        "description": "Wanders vs. heads straight for the goal"
      },
      "FileDeleted": {
        "properties": {
          "ref": {
            "type": "string",
            "title": "Ref"
          },
          "deleted": {
            "type": "boolean",
            "title": "Deleted",
            "default": true
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "ref"
        ],
        "title": "FileDeleted",
        "description": "An uploaded file deleted: a run started from now on that names `ref` is refused."
      },
      "FileList": {
        "properties": {
          "files": {
            "items": {
              "$ref": "#/components/schemas/FileRecord"
            },
            "type": "array",
            "title": "Files",
            "description": "This tenant's files, newest first."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "files"
        ],
        "title": "FileList",
        "description": "The files this tenant has uploaded and not deleted, as metadata."
      },
      "FileRecord": {
        "properties": {
          "ref": {
            "type": "string",
            "title": "Ref",
            "description": "`saram-file://<tenant_id>/<file_id>`: what a run spec's `files[].ref` names, and the only handle a caller\never gets on the object. A file uploaded before files were a tenant's keeps the caller name it was uploaded\nunder in that segment."
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "size_bytes": {
            "type": "integer",
            "title": "Size Bytes"
          },
          "media_type": {
            "type": "string",
            "title": "Media Type"
          },
          "sha256": {
            "type": "string",
            "title": "Sha256"
          },
          "uploaded_at": {
            "type": "string",
            "title": "Uploaded At"
          },
          "expires_at": {
            "type": "string",
            "title": "Expires At",
            "description": "When the file is deleted: its upload time plus how long this deployment keeps uploads. A run naming a file\npast this is refused at `POST /runs`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "ref",
          "name",
          "size_bytes",
          "media_type",
          "sha256",
          "uploaded_at",
          "expires_at"
        ],
        "title": "FileRecord",
        "description": "One file this tenant uploaded, as `POST /api/v1/files` and the two read routes answer it.\n\nMetadata only, and deliberately: the API never serves an uploaded file's bytes back. Nothing needs them -\nthe dashboard shows names - and `GET /api/v1/blobs` stays evidence-only, which is what keeps a `files/`\nkey unreachable there. The tenant is not a field of its own: it is the ref's first segment, and it is\nalways the tenant of whoever asked."
      },
      "GitHubInstallationList": {
        "properties": {
          "installations": {
            "items": {
              "$ref": "#/components/schemas/GitHubInstallationRecord"
            },
            "type": "array",
            "title": "Installations",
            "description": "By account. Empty until somebody links one: the App then writes for this tenant nowhere."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "installations"
        ],
        "title": "GitHubInstallationList",
        "description": "The Saram GitHub App's installations linked to this tenant."
      },
      "GitHubInstallationRecord": {
        "properties": {
          "installation_id": {
            "type": "integer",
            "title": "Installation Id",
            "description": "GitHub's id for the installation."
          },
          "account": {
            "type": "string",
            "title": "Account",
            "description": "The GitHub user or organisation the App is installed on, lowercased: the owner half of a run spec's\n`connectors.github.repo` that this installation writes to."
          },
          "linked_by": {
            "type": "string",
            "title": "Linked By",
            "description": "Who linked it: the person and the GitHub user who came back from installing it, or Saram's operator."
          },
          "linked_at": {
            "type": "string",
            "format": "date-time",
            "title": "Linked At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "installation_id",
          "account",
          "linked_by",
          "linked_at"
        ],
        "title": "GitHubInstallationRecord",
        "description": "One installation of the Saram GitHub App linked to this tenant: the App writes there for this tenant only."
      },
      "GitHubInstallationWrite": {
        "properties": {
          "installation_id": {
            "type": "integer",
            "maximum": 9.223372036854776e+18,
            "exclusiveMinimum": 0.0,
            "title": "Installation Id",
            "description": "The installation_id GitHub put on the setup URL: the App's installation just made or updated."
          },
          "code": {
            "type": "string",
            "maxLength": 512,
            "minLength": 1,
            "format": "password",
            "title": "Code",
            "description": "The code GitHub put on the setup URL beside installation_id, when the App asks for user authorization during installation. Good once, for ten minutes; never stored.",
            "writeOnly": true
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "installation_id",
          "code"
        ],
        "title": "GitHubInstallationWrite",
        "description": "`POST /api/v1/github/installations`: what GitHub put on the App's setup URL after an installation.\n\nBoth values come off the URL GitHub sent the installing browser to, untouched. `code` is what proves who that\nbrowser's GitHub user is; the id alone proves nothing."
      },
      "Goals": {
        "properties": {
          "primary": {
            "type": "string",
            "title": "Primary"
          },
          "secondary": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Secondary"
          },
          "intensity": {
            "$ref": "#/components/schemas/Intensity"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "primary",
          "intensity"
        ],
        "title": "Goals",
        "description": "What the persona wants out of the visit, in their own terms: the `primary` goal, anything `secondary` they also want if they get that far, and how much they want it (`intensity`). All three go into the persona's briefing beside the mission's goal; what counts as success for the run is the mission's `success_criteria`."
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "HelpSeeking": {
        "type": "string",
        "enum": [
          "never",
          "after_error",
          "eager"
        ],
        "title": "HelpSeeking",
        "description": "Uses tooltips, help links, search"
      },
      "Input": {
        "type": "string",
        "enum": [
          "mouse",
          "keyboard",
          "touch"
        ],
        "title": "Input",
        "description": "What the persona works the page with. `mouse`: a computer with a mouse and a keyboard. `keyboard`: tabs between fields and presses Enter rather than reach for the mouse. `touch`: a phone, tapping and swiping. This shapes the persona's habits; whether the browser itself is a touch device is `constraints.device`."
      },
      "Intensity": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high"
        ],
        "title": "Intensity",
        "description": "How much they want it; interacts with tolerance"
      },
      "Issue": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "type": {
            "$ref": "#/components/schemas/Type"
          },
          "severity": {
            "$ref": "#/components/schemas/Severity"
          },
          "reproducible": {
            "type": "boolean",
            "title": "Reproducible"
          },
          "reproduction": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Reproduction1"
              },
              {
                "type": "null"
              }
            ]
          },
          "persona_specific": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Specific"
          },
          "claim": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Claim"
              },
              {
                "type": "null"
              }
            ]
          },
          "tester_error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TesterError"
              },
              {
                "type": "null"
              }
            ]
          },
          "affected_sessions": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Affected Sessions"
          },
          "affected_personas": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Affected Personas"
          },
          "affected_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Affected Count"
          },
          "affected_fraction": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 1.0,
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Affected Fraction"
          },
          "reached_sessions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reached Sessions"
          },
          "first_seen_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "First Seen Step"
          },
          "location": {
            "$ref": "#/components/schemas/Location1"
          },
          "title": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "persona_rationale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Rationale"
          },
          "contradiction": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Contradiction"
              },
              {
                "type": "null"
              }
            ]
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence1"
          },
          "repro_steps": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/ReproStep"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Steps"
          },
          "preconditions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Preconditions"
          },
          "baseline_status": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BaselineStatus"
              },
              {
                "type": "null"
              }
            ]
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence"
          },
          "suggested_owner": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Suggested Owner"
          },
          "connector_refs": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Connector Refs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "type",
          "severity",
          "reproducible",
          "affected_sessions",
          "location",
          "description",
          "evidence",
          "confidence"
        ],
        "title": "Issue",
        "description": "A product defect the run found: what the product did wrong, where, in how many of the sessions that got there, with the evidence and the steps that reproduce it. `claim.verification.status` says what checked it, and only a `verified` issue - `reproducible: true` - may be filed as a ticket on its own. `description` is objective; `persona_rationale` is the persona's voice."
      },
      "IssueDetail": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "issue": {
            "$ref": "#/components/schemas/Issue"
          },
          "reading": {
            "$ref": "#/components/schemas/IssueReading"
          },
          "evidence_refs": {
            "$ref": "#/components/schemas/EvidenceRefs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "issue",
          "reading",
          "evidence_refs"
        ],
        "title": "IssueDetail",
        "description": "One issue with its reading and its evidence refs beside it: `GET /runs/{id}/issues/{issue_id}` and the MCP\n`get_issue` answer.\n\n`issue` is the contract object verbatim and `reading` is the entry `IssueList.reading[]` carries for the same\nissue, computed by the same function, so a surface that shows one issue - an issue page, or an agent that\ncalls `get_issue` first - reads whether it may be filed alone, and how to read its persona-voiced fields,\nwithout a second call and cannot disagree with the list about them."
      },
      "IssueList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "The run's status. An empty `issues` is only a result once this is terminal."
          },
          "issues": {
            "items": {
              "$ref": "#/components/schemas/Issue"
            },
            "type": "array",
            "title": "Issues"
          },
          "reading": {
            "items": {
              "$ref": "#/components/schemas/IssueReading"
            },
            "type": "array",
            "title": "Reading",
            "description": "One entry per issue, in the same order and naming its `issue_id`. Always present, so a consumer reads\nthe gate rather than re-deriving it."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "issues",
          "reading"
        ],
        "title": "IssueList",
        "description": "A run's issues: product defects the run found, each with its evidence, and beside them how each may be\nacted on (`reading`). Only an issue whose reading says `may_file_ticket_alone` is a confirmed bug."
      },
      "IssueReading": {
        "properties": {
          "issue_id": {
            "type": "string",
            "title": "Issue Id"
          },
          "may_file_ticket_alone": {
            "type": "boolean",
            "title": "May File Ticket Alone",
            "description": "True only when reproduction cleared this issue to be filed as a confirmed bug on its own. False means\nreport it with its evidence and mark it unconfirmed."
          },
          "subjective_fields": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Subjective Fields",
            "description": "Fields on the issue that are the persona's voice rather than observed behaviour. Quote, never state."
          },
          "advisory": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Advisory",
            "description": "How to read those fields, present only when there are any."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "issue_id",
          "may_file_ticket_alone",
          "subjective_fields"
        ],
        "title": "IssueReading",
        "description": "How one issue of `IssueList.issues` may be acted on: whether it may be filed as a ticket on its own, and\nwhich of its fields are the persona's voice rather than observed behaviour."
      },
      "JudgeDecision": {
        "type": "string",
        "enum": [
          "positive",
          "negative",
          "undecided"
        ],
        "title": "JudgeDecision",
        "description": "The decision the judge returned, before the gate discarded it. The judge's and not the persona's: no diary line at the steps it cites supports it. Named apart from `verdict.decision` so that nothing reading a row by field name takes it for the persona's."
      },
      "Kind": {
        "type": "string",
        "title": "Kind",
        "description": "What sort of feedback this is. `unmet_expectation`: the product did not do what the personas expected of it. `missing_capability`: something they looked for is not there. `value_gap`: they judged what it offers not worth what it asks of them. `trust_gap`: something made them hesitate to trust it. `delight`: a moment that worked well. More kinds may be added; read one you do not know as unknown.",
        "examples": [
          "unmet_expectation",
          "missing_capability",
          "value_gap",
          "trust_gap",
          "delight"
        ],
        "x-classification": true
      },
      "Kind1": {
        "type": "string",
        "title": "Kind1",
        "description": "`mistyped_credential`: typed an email or password that differs from the one the account gave it (decided by code, comparing the typed text with the account; no secret enters a prompt or this document). `mistyped_input`: typed something other than what the brief or the screen asked for. `wrong_element`: acted on an element other than the one the title is about, or a non-interactive one. `placeholder_as_value`: read placeholder text as a pre-filled value and tried to clear or fix it. `ignored_instruction`: skipped something the screen visibly asked for first. `other`: something else the persona did that explains it. Not a tester error, by rule: a persona misreading the product's intent when a reasonable customer would misread it the same way - that is `confusion`, and it is the product's. A classification: a later kind is additive.",
        "examples": [
          "mistyped_credential",
          "mistyped_input",
          "wrong_element",
          "placeholder_as_value",
          "ignored_instruction",
          "other"
        ],
        "x-classification": true
      },
      "LiveStatus": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status"
          },
          "paused": {
            "type": "boolean",
            "title": "Paused",
            "default": false
          },
          "paused_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Paused Reason",
            "description": "Why: `signal` for a human pause, `budget_cost` when `budget.total_cost_usd` was reached. Either way\nonly a `resume` releases the run."
          },
          "sessions": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Sessions",
            "description": "Session count per state while the run is open: pending (the run has not started it yet), running,\nwaiting_for_seat, and each artifact result. A session the run has started is counted in running once it has a\nbrowser seat and in waiting_for_seat while it is queued for one - taken from each session's own\n`waiting_for_seat`, so the two add up to the sessions in flight. A session whose live state cannot be read\nis counted running."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status"
        ],
        "title": "LiveStatus",
        "description": "The run's state as its workflow reports it while the run is open. Absent on a closed run."
      },
      "Location": {
        "properties": {
          "route": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Route"
          },
          "element_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Element Description"
          }
        },
        "type": "object",
        "title": "Location",
        "description": "Where in the product the feedback is about: the page (`route`) and, when the personas pointed at one, the element in words (`element_description`)."
      },
      "Location1": {
        "properties": {
          "route": {
            "type": "string",
            "title": "Route"
          },
          "selector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Selector"
          },
          "element_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Element Description"
          },
          "viewport": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Viewport"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "route"
        ],
        "title": "Location1",
        "description": "Where the defect is: the page (`route`), the element in words (`element_description`) or as a selector (`selector`), and the screen size it was seen at (`viewport`, WIDTHxHEIGHT)."
      },
      "Me": {
        "properties": {
          "subject": {
            "type": "string",
            "title": "Subject",
            "description": "What runs.created_by will record for this credential: usr_... for a person, key_... for a minted key, operator for a break-glass entry or an ungated request."
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "The human-readable label this credential is logged and attributed under: an email or the provider subject for a person, cookai/ci for a minted key, the bare name for a break-glass entry. Absent only when no gate is configured at all."
          },
          "tenant": {
            "$ref": "#/components/schemas/MeTenant"
          },
          "role": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Role",
            "description": "owner, admin, member or viewer for a person; absent for a machine credential, which carries scopes instead."
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Scopes",
            "description": "What this credential may actually do, already resolved: a person's role expanded to the scopes it allows, a minted key's own scopes, and the full set for a credential that carries none (a break-glass entry or an ungated request). Never an empty list - read it as the answer, not as what was stored. An operator key minted for Saram's own operations carries operator scopes instead, ops:read and on the operator page's key ops:list, which no tenant can hold, and nothing beside them."
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At",
            "description": "When this minted key stops getting through the gate, the expires_at it was minted with. Absent for a key minted without one, for a person, whose sign-in the identity provider ends, and for a break-glass entry or an ungated request. Read here because a key that may not list keys, an operator key among them, can still ask when its own lapses."
          },
          "user": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MeUser"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "subject",
          "tenant",
          "scopes"
        ],
        "title": "Me",
        "description": "Who this credential is, which tenant it spends and reads on, and what it may do.\n\nThe first call a dashboard shell makes: it decides which tenant's name is in the header, whether the\nmembers and keys screens are offered, and whether the \"start a run\" button is there at all. It is\ndeliberately answerable by *every* credential - a person's token, a minted key, a break-glass entry, an\nungated request - so that one screen works against every way of reaching this API."
      },
      "MeTenant": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "slug": {
            "type": "string",
            "title": "Slug"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "plan": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Plan"
          },
          "status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Status"
          },
          "house_models": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "House Models",
            "description": "True when Saram has agreed to carry this tenant's model spend: your runs may name a Saram-held credential for the actor, the judge and the reproduction check even though your plan is a bring-your-own-keys one, and your steps still bill at your plan's price. It is set by Saram per tenant and by no request; absent means the tenant row was not read."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "slug"
        ],
        "title": "MeTenant",
        "description": "The tenant the request's credential belongs to, as `GET /api/v1/me` reports it.\n\nEverything but `id` and `slug` is optional because the gate can answer without the row: a break-glass\nentry and an ungated request are the `fleet` tenant by construction, and on a machine with no database\nthere is nothing to read the name and the plan out of."
      },
      "MeUser": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Email",
            "description": "Absent until the identity provider tells us one: an AuthKit access token carries no email claim unless the deployment adds one through a JWT template, and the address otherwise arrives with the user webhook."
          },
          "display_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Display Name"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id"
        ],
        "title": "MeUser",
        "description": "The person behind an access token. Absent from `GET /api/v1/me` when the credential is a machine's."
      },
      "Method": {
        "type": "string",
        "title": "Method",
        "description": "Which check ran, when one did; absent when `status` is `unverified`. `marker`: a replay compared the failure marker at `action_step` - name it in `markers`. A marker matching is evidence the step failed again, not that it failed for the reason the title gives, so on its own it produces `marker_matched`, and `verified` only with `check.verdict: supported` and `attribution: product`. `assertion`: a state assertion checked the claim itself (reserved; not produced yet). `manual`: a human checked it. A classification: nothing acts on it - every surface shows `status` - so a reader on an older release buckets an instrument it does not know as `unknown`, and adding one is additive.",
        "examples": [
          "marker",
          "assertion",
          "manual"
        ],
        "x-classification": true
      },
      "MonthlyCap": {
        "properties": {
          "period": {
            "type": "string",
            "title": "Period",
            "description": "The calendar month the cap is counted over, YYYY-MM, in UTC."
          },
          "billed_usd": {
            "type": "number",
            "title": "Billed Usd",
            "description": "What this month has billed so far, to the cent: the billable_usd GET /api/v1/usage answers."
          },
          "cap_usd": {
            "type": "number",
            "title": "Cap Usd",
            "description": "The tenant's monthly cap, which billed_usd has reached: no run starts until it is raised or the month turns."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "period",
          "billed_usd",
          "cap_usd"
        ],
        "title": "MonthlyCap",
        "description": "The monthly cap a run was refused by: the month, what it has billed, and the cap it has reached."
      },
      "Network": {
        "type": "string",
        "enum": [
          "fast",
          "regular_4g",
          "slow_3g",
          "offline_flaky"
        ],
        "title": "Network",
        "description": "The connection the browser runs on. `fast` (the default): not throttled. `regular_4g` and `slow_3g`: bandwidth limited and latency added, like those mobile connections. `offline_flaky` is not supported yet, and a session that asks for it does not start."
      },
      "Opportunity": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "kind": {
            "$ref": "#/components/schemas/Kind"
          },
          "title": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "persona_rationale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Rationale"
          },
          "suggested_change": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Suggested Change"
          },
          "affected_sessions": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Affected Sessions"
          },
          "affected_personas": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Affected Personas"
          },
          "reached_sessions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reached Sessions"
          },
          "consensus": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Consensus"
          },
          "solicited_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Solicited By"
          },
          "location": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Location"
              },
              {
                "type": "null"
              }
            ]
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          },
          "baseline_status": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BaselineStatus"
              },
              {
                "type": "null"
              }
            ]
          },
          "connector_refs": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Connector Refs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "kind",
          "description",
          "affected_sessions",
          "consensus",
          "evidence"
        ],
        "title": "Opportunity",
        "description": "Behavior-grounded persona feedback that is not a defect: an unmet expectation, a missing capability, a value or trust gap, or a moment of delight. Scored by consensus across the sessions that reached the area, never by reproduction, and never a ticket on its own. `description` is the objective account; `persona_rationale` and `suggested_change` are opinion."
      },
      "OpportunityList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "The run's status. An empty `opportunities` is only a result once this is terminal."
          },
          "opportunities": {
            "items": {
              "$ref": "#/components/schemas/Opportunity"
            },
            "type": "array",
            "title": "Opportunities"
          },
          "advisory": {
            "type": "string",
            "title": "Advisory",
            "description": "How to read this whole answer: behaviour-grounded persona feedback, consensus-scored, never a ticket on its\nown."
          },
          "subjective_fields": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Subjective Fields",
            "description": "Fields on each opportunity that are opinion. `description` is deliberately not among them."
          },
          "display": {
            "$ref": "#/components/schemas/DisplaySplit",
            "description": "The run's shown/folded split, present even when the run raised nothing: the floors are the answer to\n\"why is this list short\", and they are the same whether or not anything cleared them."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "opportunities",
          "advisory",
          "subjective_fields",
          "display"
        ],
        "title": "OpportunityList",
        "description": "A run's opportunities: behaviour-grounded persona feedback that is not a defect - unmet expectations,\nmissing capabilities, value gaps, delight - scored by how many personas share it. Never a ticket on its own."
      },
      "OpsKpiPoint": {
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "title": "At",
            "description": "The moment counted at, in UTC, to the second: `as_of` less a whole number of days."
          },
          "signups_7d": {
            "type": "integer",
            "title": "Signups 7D",
            "description": "Accounts created in the seven days before `at`."
          },
          "activated_signups_7d": {
            "type": "integer",
            "title": "Activated Signups 7D",
            "description": "Accounts whose first run started in the seven days before `at` and within seven days of the account's\ncreation."
          },
          "demo_runs_7d": {
            "type": "integer",
            "title": "Demo Runs 7D",
            "description": "Runs of the homepage demo started in the seven days before `at` that replayed."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "at",
          "signups_7d",
          "activated_signups_7d",
          "demo_runs_7d"
        ],
        "title": "OpsKpiPoint",
        "description": "The three seven-day counts as they stood at `at`: each one over the seven days before `at`, as `OpsKpis`\ncounts them at `as_of`."
      },
      "OpsKpiSeries": {
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "title": "As Of",
            "description": "The moment counted at, in UTC, to the second: the last point's `at`."
          },
          "environment": {
            "type": "string",
            "title": "Environment",
            "description": "Which deployment counted: dev on the dev API."
          },
          "points": {
            "items": {
              "$ref": "#/components/schemas/OpsKpiPoint"
            },
            "type": "array",
            "title": "Points",
            "description": "As many as `days` asked for, a day apart, oldest first."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "as_of",
          "environment",
          "points"
        ],
        "title": "OpsKpiSeries",
        "description": "Saram's weekly growth counts as they stood once a day, for Saram's operators: one point at `as_of` and one\nat each whole day before it, oldest first. Counts only, like `OpsKpis`, and the last point is the seven-day\ncounts `OpsKpis` answers at the same `as_of`.\n\nEvery point is counted in one statement, so the points never disagree with each other over a sign-up made while\nthey were read. A past point is counted from today's rows: it differs from what was read that day only where a\ndemo run was deleted since, or an account was taken out of the counts since. Answered fresh on every request\nand never cached."
      },
      "OpsKpis": {
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "title": "As Of",
            "description": "The moment counted at, in UTC, to the second: the end of every window, and not included in it."
          },
          "environment": {
            "type": "string",
            "title": "Environment",
            "description": "Which deployment counted: dev on the dev API. Two deployments' counts are never one number."
          },
          "signups_7d": {
            "type": "integer",
            "title": "Signups 7D",
            "description": "Accounts created in the seven days before `as_of`."
          },
          "activated_signups_7d": {
            "type": "integer",
            "title": "Activated Signups 7D",
            "description": "Accounts whose first run started in the seven days before `as_of` and within seven days of the account's\ncreation. Each account activates once: deleting its first run does not make the next one a first."
          },
          "demo_runs_7d": {
            "type": "integer",
            "title": "Demo Runs 7D",
            "description": "Runs of the homepage demo started in the seven days before `as_of` that replayed: at least one session took\na step and left a recording."
          },
          "since": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Since",
            "description": "The `since` asked for, in UTC. Absent when none was, and so are the two counts that use it."
          },
          "signups_since": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Signups Since",
            "description": "Accounts created at or after `since` and before `as_of`."
          },
          "demo_runs_since": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Demo Runs Since",
            "description": "Homepage demo runs that replayed, started at or after `since` and before `as_of`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "as_of",
          "environment",
          "signups_7d",
          "activated_signups_7d",
          "demo_runs_7d"
        ],
        "title": "OpsKpis",
        "description": "Saram's own growth counts across every tenant, for Saram's operators. Counts only: no tenant, person, run\nor address is named, and none can be worked out from them.\n\nCounted at `as_of`, over the seven days before it, and over `[since, as_of)` when a `since` was asked for.\nAn account is a tenant somebody signed up for, other than Saram's own and other than one whose first member\nsigned up from an address of Saram's or its testers; a suspended account still counts, because a sign-up is\nan event. Answered fresh on every request and never cached."
      },
      "OpsTenant": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "The tenant's id, `ten_...`."
          },
          "slug": {
            "type": "string",
            "title": "Slug",
            "description": "The tenant's slug: for a sign-up, made from its organisation's id at the identity provider, not its name."
          },
          "plan": {
            "type": "string",
            "title": "Plan",
            "description": "The plan the tenant is on: `free`, `pro`, `pilot`, `byo`, `hosted`, `byo_team` or `hosted_team`."
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "`active` or `suspended`."
          },
          "signup": {
            "type": "boolean",
            "title": "Signup",
            "description": "Made by somebody signing up, rather than by an operator."
          },
          "kpi_account": {
            "type": "boolean",
            "title": "Kpi Account",
            "description": "Counted as an account by `GET /api/v1/ops/kpis`: of the tenants created in the seven days before `as_of`,\nthe ones with this set are its `signups_7d`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At",
            "description": "When the tenant was made, in UTC."
          },
          "first_run_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "First Run At",
            "description": "When its first run was started, in UTC; stamped once. Absent until it starts one."
          },
          "last_run_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Run At",
            "description": "When its newest run there is was started, in UTC. Absent when it has none."
          },
          "run_count": {
            "type": "integer",
            "title": "Run Count",
            "description": "How many runs it has: a run it deleted is not counted."
          },
          "runs_7d": {
            "type": "integer",
            "title": "Runs 7D",
            "description": "How many of those were started in the seven days before `as_of`."
          },
          "run_source": {
            "type": "string",
            "title": "Run Source",
            "description": "What its runs are recorded as: `demo` for the homepage demo's tenant, `api` for every other."
          },
          "kpi_excluded": {
            "type": "boolean",
            "title": "Kpi Excluded",
            "description": "Marked by an operator as Saram's own, so the counts never count it."
          },
          "public_shares": {
            "type": "boolean",
            "title": "Public Shares",
            "description": "An operator lets it mint public share links."
          },
          "house_models": {
            "type": "boolean",
            "title": "House Models",
            "description": "An operator lets it run on Saram's own model keys."
          },
          "private_targets": {
            "type": "boolean",
            "title": "Private Targets",
            "description": "An operator lets its runs reach private addresses."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "slug",
          "plan",
          "status",
          "signup",
          "kpi_account",
          "created_at",
          "run_count",
          "runs_7d",
          "run_source",
          "kpi_excluded",
          "public_shares",
          "house_models",
          "private_targets"
        ],
        "title": "OpsTenant",
        "description": "One tenant on the operator's list: ids, codes, times, counts and flags only. Never the organisation's name,\na person, an email, a billing reference or a price."
      },
      "OpsTenantList": {
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "title": "As Of",
            "description": "The moment listed at, in UTC, to the second: the end of every `runs_7d` window."
          },
          "environment": {
            "type": "string",
            "title": "Environment",
            "description": "Which deployment listed: dev on the dev API."
          },
          "tenants": {
            "items": {
              "$ref": "#/components/schemas/OpsTenant"
            },
            "type": "array",
            "title": "Tenants",
            "description": "Newest first."
          },
          "truncated": {
            "type": "boolean",
            "title": "Truncated",
            "description": "There were more tenants than `limit`, and the oldest are left out."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "as_of",
          "environment",
          "tenants",
          "truncated"
        ],
        "title": "OpsTenantList",
        "description": "Every tenant of this deployment, for Saram's operators: newest first, at most `limit` of them. Read when it\nis asked for and never cached."
      },
      "PaymentRequired": {
        "properties": {
          "detail": {
            "type": "string",
            "title": "Detail",
            "description": "Why, in a sentence: the month's billing against the monthly cap on a plan that bills steps, or the run's Usage ceiling against the balance on a plan that prices Usage, with what to change."
          },
          "usage_ceiling": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UsageCeiling"
              },
              {
                "type": "null"
              }
            ],
            "description": "The numbers behind the detail when the balance does not cover the run's Usage ceiling: the usage_ceiling POST /api/v1/runs/validate answers for the same spec at the same moment, with covered false. Absent when the refusal is the monthly cap's."
          },
          "monthly_cap": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MonthlyCap"
              },
              {
                "type": "null"
              }
            ],
            "description": "The numbers behind the detail when the month has billed the tenant's monthly cap, on a plan that bills steps: the month, what it has billed and the cap. Absent when the refusal is the run gate's."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "detail"
        ],
        "title": "PaymentRequired",
        "description": "No run is started, because the tenant's plan cannot pay for it: `POST /runs`' 402 body, and the structured\ncontent of the `start_run` tool's refusal for the same reason."
      },
      "Persona": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9_]+$",
            "title": "Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1.0,
            "title": "Version"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "pack": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pack"
          },
          "tags": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tags"
          },
          "backstory": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Backstory"
          },
          "vocabulary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Vocabulary"
              },
              {
                "type": "null"
              }
            ]
          },
          "facts": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Facts"
          },
          "conversation": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Conversation"
              },
              {
                "type": "null"
              }
            ]
          },
          "verdict_prompt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Verdict Prompt"
          },
          "goals": {
            "$ref": "#/components/schemas/Goals"
          },
          "priors": {
            "$ref": "#/components/schemas/Priors"
          },
          "action_policy": {
            "$ref": "#/components/schemas/ActionPolicy"
          },
          "tolerance": {
            "$ref": "#/components/schemas/Tolerance"
          },
          "constraints": {
            "$ref": "#/components/schemas/Constraints"
          },
          "voice": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Voice"
              },
              {
                "type": "null"
              }
            ]
          },
          "provenance": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Provenance"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "version",
          "goals",
          "priors",
          "action_policy",
          "tolerance",
          "constraints"
        ],
        "title": "Persona",
        "description": "A structured persona: a sampling strategy over the UI state space plus a tolerance function. Two kinds of fields: a structured core the run enforces and the dashboard diffs (`tolerance`, `constraints`, `action_policy`), and prose behavior fields the model reads (`backstory`, `vocabulary`, `facts`, `conversation`, `verdict_prompt`). Both drive what the persona does. `voice` affects narration only."
      },
      "PersonaList": {
        "properties": {
          "personas": {
            "items": {
              "$ref": "#/components/schemas/PersonaSummary"
            },
            "type": "array",
            "title": "Personas"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "personas"
        ],
        "title": "PersonaList",
        "description": "The personas a run can name. `GET /api/v1/personas/{address}` answers one in full."
      },
      "PersonaSummary": {
        "properties": {
          "address": {
            "type": "string",
            "title": "Address",
            "description": "`id@version`, what a run spec and a `sessions.persona_id` carry."
          },
          "id": {
            "type": "string",
            "title": "Id"
          },
          "version": {
            "type": "integer",
            "title": "Version"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Name"
          },
          "pack": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pack"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "address",
          "id",
          "version"
        ],
        "title": "PersonaSummary",
        "description": "One persona a run can name, by its address, with its name, pack and tags."
      },
      "Phrasing": {
        "type": "string",
        "enum": [
          "plain_speech",
          "commands",
          "mixed"
        ],
        "title": "Phrasing",
        "description": "Asks like they would ask a person, vs. writes instructions"
      },
      "PlanLimits": {
        "properties": {
          "max_concurrent_sessions": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Concurrent Sessions",
            "description": "How many sessions of one run your plan runs at the same time: 2 on Free, 8 on Pro. A run whose budget.max_concurrent_sessions asks for more runs this many at once, and a run that names none runs as many as the browser pool has seats, up to this. Null when your plan sets no limit."
          },
          "evidence_retention_days": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Evidence Retention Days",
            "description": "How many days your plan keeps a run's evidence - its videos, screenshots and step traces - counted from the run's creation: 14 on Free, 90 on Pro. A run keeps the longer of this and what your plan kept when the run was created, and says the date it applies as evidence_expires_at; the run's findings are kept after it. Null when your plan sets no number, and then this deployment's own number applies."
          },
          "max_members": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Members",
            "description": "How many people your workspace holds, every role counted: 2 on Free, 10 on Pro. An invitation accepted once members_used has reached it does not become a member, and the person is told why when they sign in; nobody already in is removed. Null when your plan sets no limit."
          },
          "automation": {
            "type": "boolean",
            "title": "Automation",
            "description": "Whether your plan includes Saram's built-in automation: filing a run's issues on GitHub and commenting on the pull request (connectors.github), the run-finished webhook (connectors.webhook), and the CI action that posts a run spec with them. False on Free and true on Pro. On a plan without it, a run spec naming either connector is refused when it is validated or started, never run without it; the API and MCP are open on every plan.",
            "default": true
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "PlanLimits",
        "description": "What the tenant's plan allows, as the plan stands now.\n\nEach limit is `null` when the plan sets none, and then the platform's own bound applies instead."
      },
      "PlannedSession": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id",
            "description": "`<run_id>.sess_NN_<persona slug>`. When the spec names no run, the prefix reads `<run_id>` literally -\n`POST /runs` is what mints the run id, and it is part of the session id. That is the same case as\n`SpecValidation.run_id` being absent."
          },
          "persona_id": {
            "type": "string",
            "title": "Persona Id"
          },
          "mission_id": {
            "type": "string",
            "title": "Mission Id"
          },
          "entry_url": {
            "type": "string",
            "title": "Entry Url",
            "description": "The mission's entry URL (or the target's) with this session's partition on it (`?saram_sid=`)."
          },
          "account_secret_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Account Secret Ref",
            "description": "The seed account this session would be handed. Absent when the target's seed accounts ran out, and then the\npersona is told they have no login."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "session_id",
          "persona_id",
          "mission_id",
          "entry_url"
        ],
        "title": "PlannedSession",
        "description": "One session `POST /runs` would create for this spec, planned the way the run itself plans, so the dry run\ncannot promise sessions the run would not make."
      },
      "Priors": {
        "properties": {
          "product_familiarity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProductFamiliarity"
              },
              {
                "type": "null"
              }
            ]
          },
          "domain_literacy": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DomainLiteracy"
              },
              {
                "type": "null"
              }
            ]
          },
          "technical_literacy": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TechnicalLiteracy"
              },
              {
                "type": "null"
              }
            ]
          },
          "mental_models": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mental Models"
          },
          "prior_instructions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Prior Instructions"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Priors",
        "description": "What the persona knows before they arrive: how well they know this product, the language of its industry and technology in general, the expectations they bring from other products, and what they were told beforehand. Each goes into the persona's briefing and shapes what they understand, expect and notice."
      },
      "ProductFamiliarity": {
        "type": "string",
        "enum": [
          "none",
          "low",
          "medium",
          "high"
        ],
        "title": "ProductFamiliarity",
        "description": "How well the persona knows this product before the session. `none`: never used it and not sure what it is. `low`: seen it once and remembers almost nothing. `medium`: knows this kind of product, not this one in particular. `high`: knows it well and expects things where they left them."
      },
      "Provenance": {
        "properties": {
          "generator": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Generator"
          },
          "sources": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sources"
          },
          "generated_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Generated At"
          },
          "notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notes"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Provenance",
        "description": "How this persona was grounded. Generators fill this."
      },
      "ProviderRun": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "model_usd_byo": {
            "type": "number",
            "title": "Model Usd Byo"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "model_usd_byo"
        ],
        "title": "ProviderRun",
        "description": "One run's model spend on your own provider key, estimated at list price."
      },
      "ProviderSpend": {
        "properties": {
          "model_usd_byo": {
            "type": "number",
            "title": "Model Usd Byo",
            "description": "Model spend on your own provider keys, estimated at the provider's list price: their invoice, not a charge from Saram. A ChatGPT login's calls are included in that plan and are not priced here.",
            "default": 0
          },
          "complete": {
            "type": "boolean",
            "title": "Complete",
            "description": "Whether it covers every model caller in a run: the persona loop, the judge and the reproduction check, each under its own role. A run metered before the judge was priced (2026-09-25) carries the persona loop's spend alone, so a month reaching back past that is a floor for those runs.",
            "default": false
          },
          "runs": {
            "items": {
              "$ref": "#/components/schemas/ProviderRun"
            },
            "type": "array",
            "title": "Runs",
            "description": "Per run with any spend, oldest first."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "ProviderSpend",
        "description": "What your model provider billed you for this month's runs, estimated: never Saram's charge, and never to be\nadded to it (section 2.5). The heading a screen puts over it is \"Your model provider (billed by them, not by\nSaram)\"."
      },
      "QuoteStep": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "step_id": {
            "type": "integer",
            "title": "Step Id"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "session_id",
          "step_id"
        ],
        "title": "QuoteStep",
        "description": "The step whose ledger block the quote came from."
      },
      "Reading": {
        "type": "string",
        "enum": [
          "reads",
          "skims",
          "ignores"
        ],
        "title": "Reading",
        "description": "How much on-screen text they consume"
      },
      "RefillSkip": {
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "title": "At",
            "description": "When the latest skipped refill was decided. Each skip moves it."
          },
          "reason": {
            "type": "string",
            "enum": [
              "monthly_budget",
              "no_card"
            ],
            "title": "Reason",
            "description": "`monthly_budget`: the refill would take this month's top-ups, manual and automatic, past monthly_budget_usd, so it waits for a higher budget or the next calendar month. `no_card`: there is no card on file to charge, so it waits for one to be saved through POST /billing/setup."
          },
          "detail": {
            "type": "string",
            "title": "Detail",
            "description": "The same in a sentence: for the budget, what was already charged and what would still fit."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "at",
          "reason",
          "detail"
        ],
        "title": "RefillSkip",
        "description": "A refill that was due and was not charged: the balance less what runs hold was under `refill_below_usd`\nwith auto-refill on, and Saram did not charge the card. Nothing is retried until the next settlement or run\nfinds a refill due again, and once the balance is gone new runs are refused."
      },
      "ReproStep": {
        "properties": {
          "action": {
            "type": "string",
            "title": "Action"
          },
          "target": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target"
          },
          "value": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value"
          },
          "replace": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Replace"
          },
          "expect": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expect"
          }
        },
        "type": "object",
        "required": [
          "action"
        ],
        "title": "ReproStep",
        "description": "One step of a replay a person or a coding agent can run without the persona, in order from an `open` of the entry page. `action` is a browser action such as `click`, `type`, `key`, `scroll`, `back`, `reload`, `upload` or `wait`; `target` is what it acts on (the address for `open`, `x,y` screen coordinates for a `click`); `value` is what it types, presses or scrolls by; `expect` is what should hold after it, such as the address the page should be on."
      },
      "Reproduction": {
        "properties": {
          "candidates": {
            "type": "integer",
            "title": "Candidates"
          },
          "merged": {
            "type": "integer",
            "title": "Merged"
          },
          "evaluated": {
            "type": "integer",
            "title": "Evaluated"
          },
          "confirmed": {
            "type": "integer",
            "title": "Confirmed"
          },
          "inconclusive": {
            "type": "integer",
            "title": "Inconclusive"
          },
          "excluded": {
            "type": "integer",
            "title": "Excluded"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "candidates",
          "merged",
          "evaluated",
          "confirmed",
          "inconclusive",
          "excluded"
        ],
        "title": "Reproduction",
        "description": "The counts behind `confirmation_rate`, so a reader can see its denominator instead of trusting it. Funnel stages and readings of one set of verdicts, not a partition: `candidates` are the per-session findings the judge raised, merging folds them into `merged`, reproduction ruled on `evaluated` of those and confirmed `confirmed`. The two overlaps are deliberate. A `flaky` finding is in `evaluated` (the experiment ran) and in `inconclusive` (it settled nothing). `excluded` is a subset of the unconfirmed - the ones dropped for having no persona voice left once the replay did not confirm them - and they are exactly the findings this artifact no longer carries, so `merged - excluded` is `issues_total`."
      },
      "Reproduction1": {
        "properties": {
          "attempted": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Attempted"
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Result1"
              },
              {
                "type": "null"
              }
            ]
          },
          "attempts": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Attempts"
          },
          "notes": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Notes"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Reproduction1",
        "description": "Whether Saram replayed the issue without the persona, and what the replay showed: whether one was `attempted`, its `result`, how many `attempts` it took, and `notes` on it. A replay that saw the failure again is not on its own enough to file a ticket; `reproducible` says whether this issue may be."
      },
      "Result1": {
        "type": "string",
        "title": "Result1",
        "description": "What the replay showed. `reproduced`: the failure happened again. `not_reproduced`: it did not. `flaky`: the attempts disagreed. `skipped`: no replay ran. More values may be added; read one you do not know as unknown.",
        "examples": [
          "reproduced",
          "not_reproduced",
          "flaky",
          "skipped"
        ],
        "x-classification": true
      },
      "RetestAttempt": {
        "properties": {
          "attempt_id": {
            "type": "string",
            "title": "Attempt Id"
          },
          "issue_id": {
            "type": "string",
            "title": "Issue Id"
          },
          "session_id": {
            "type": "string",
            "title": "Session Id",
            "description": "The throwaway body session the attempt ran in: `<run_id>.<retest_id>_<issue_id>_a<n>`, never one the run's\nown reproduction used, so its evidence sits beside the original's rather than over it."
          },
          "outcome": {
            "type": "string",
            "title": "Outcome",
            "description": "`reproduced`, `not_reproduced`, `path_diverged`, `target_moved` or `error`."
          },
          "expected": {
            "additionalProperties": true,
            "type": "object",
            "title": "Expected",
            "description": "The outcome signature the run recorded at the judged step: `url_after` and the failure `markers`."
          },
          "observed": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Observed"
          },
          "notes": {
            "type": "string",
            "title": "Notes",
            "default": ""
          },
          "surface_trace_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Surface Trace Ref"
          },
          "screenshot_refs": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Screenshot Refs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "attempt_id",
          "issue_id",
          "session_id",
          "outcome",
          "expected"
        ],
        "title": "RetestAttempt",
        "description": "One replay attempt of a retest: the session it ran in, what it expected at the judged step, and what it\nsaw."
      },
      "RetestList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "issue_id": {
            "type": "string",
            "title": "Issue Id"
          },
          "retests": {
            "items": {
              "$ref": "#/components/schemas/RetestRecord"
            },
            "type": "array",
            "title": "Retests",
            "description": "Newest first."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "issue_id",
          "retests"
        ],
        "title": "RetestList",
        "description": "Every retest asked for on one issue, newest first."
      },
      "RetestRecord": {
        "properties": {
          "retest_id": {
            "type": "string",
            "title": "Retest Id"
          },
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "issue_id": {
            "type": "string",
            "title": "Issue Id"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "completed",
              "failed"
            ],
            "title": "Status",
            "description": "`pending` until the replay has a pool seat (it queues for one like any session), `running` while it\nreplays, then `completed` with a `result`, or `failed` with an `error` and no result."
          },
          "entry_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Entry Url",
            "description": "The deployment this replay opened, when it was not the run's own."
          },
          "requested_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Requested By",
            "description": "The subject of the credential that asked: `key_...`, a person's user id, or `operator`."
          },
          "workflow_id": {
            "type": "string",
            "title": "Workflow Id"
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Result1"
              },
              {
                "type": "null"
              }
            ],
            "description": "What this replay concluded, in `issue.reproduction.result`'s words. `not_reproduced`: the failure the run saw\nat the judged step did not happen again on either attempt - the answer to \"is it fixed?\" that is yes as far as\nthis replay can tell, and not a proof the product works. `reproduced`: it happened again on both. `flaky`: on\none of two. `skipped`: the replay could not rule (no browser pool, a session it could not open, a control that\nmoved). Absent until `status` is `completed`."
          },
          "verification": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verification"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `claim.verification` block this replay would have written onto the issue: `verified` only when the\nfailure happened again and the claim check supported the title."
          },
          "check": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Check"
              },
              {
                "type": "null"
              }
            ],
            "description": "The claim check, present only when a marker matched and it was asked."
          },
          "attempts": {
            "items": {
              "$ref": "#/components/schemas/RetestAttempt"
            },
            "type": "array",
            "title": "Attempts"
          },
          "gate_discards": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gate Discards",
            "description": "How many claims this replay's gate threw away: claim-check quotes the cited evidence does not contain, each\nof which leaves `check` `ungrounded`, and attempts whose path diverged before the judged step.\nZero is the expected value. Absent until `status` is `completed`, and on retests completed before this was\nrecorded."
          },
          "gate_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gate Ref",
            "description": "The records behind `gate_discards`, fetched through `GET /api/v1/blobs`: `{\"run_id\", \"retest_id\",\n\"issue_id\", \"discards\"}`, one discard per claim or attempt with its `session_id`, `kind`, `reason` and a\n`payload` naming the `issue_id` and the `quote` or the step the path diverged at. This retest's own, never the\nrun's reproduction's. Written even when empty, so an absent ref is a retest that has not completed, never\n\"nothing thrown away\"."
          },
          "error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error",
            "description": "Why a `failed` retest has no result, in a sentence for the caller: the target's reset hook failing or\nrefused before a replay, quoted with `target: ` in front; that Saram could not read the target's credential;\nor that Saram could not start or finish the retest. Never a finding about the product. Absent on every other\nstatus."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "retest_id",
          "run_id",
          "issue_id",
          "status",
          "workflow_id",
          "created_at"
        ],
        "title": "RetestRecord",
        "description": "One retest of one issue: a later replay a person asked for, kept beside what the run concluded.\n\nThe issue itself - `GET /runs/{id}/issues/{issue_id}` - still says what the run's own reproduction found,\nand so does the artifact: a retest never rewrites the run's record. Every retest is its own record."
      },
      "RetestRequest": {
        "properties": {
          "entry_url": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2048,
                "pattern": "^https?://[^/?#\\s]+"
              },
              {
                "type": "null"
              }
            ],
            "title": "Entry Url",
            "description": "Where the replay opens instead of the run's own entry url - a new deployment of the same app, such as a preview. Every url the run recorded on its own origin is replayed on this url's origin, path for path, so the replay compares like with like; a path the new deployment no longer has reads as the path diverging, not as a fix. Absent replays against the run's own target."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "RetestRequest",
        "description": "The optional body of `POST /runs/{id}/issues/{issue_id}/retest`."
      },
      "RunDeleted": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id",
            "description": "The run that was deleted."
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "title": "Deleted At",
            "description": "When it was deleted: the first time it was asked, however often since."
          },
          "objects_deleted": {
            "type": "integer",
            "title": "Objects Deleted",
            "description": "How many stored objects went with it - traces, screenshots, video, checkpoints and the run's own documents. 0 for a run that had not recorded anything."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "deleted_at",
          "objects_deleted"
        ],
        "title": "RunDeleted",
        "description": "A run its owner deleted: its evidence, its recordings, and its record - the run, its sessions, issues,\nverdicts, opportunities, retests and artifact - are gone, and every route now answers the run with the 404\nan unknown run gets. Asking again answers this same body, so a retried delete is safe.\n\nWhat is kept is what the run was billed: the usage ledger outlives a deleted run as it outlives expired\nevidence, so a month's usage still counts it."
      },
      "RunDetail": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "`pending`, `running`, `observing`, then one terminal value. `completed` means at least one session\nreached an ending its persona got to (`completed`, `abandoned`, `blocked`, `budget_exhausted`), so there\nare results to read. `failed` covers both a run that broke - a rejected spec, a reset hook or the\nobserver - and a run whose every session `errored`, which came back with no persona outcome at all;\n`status_reason` says which. `cancelled` is a `cancel` signal. Poll until the value is terminal, and treat\nonly `completed` as results-ready."
          },
          "status_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Status Reason",
            "description": "Why a run ended `failed` or `cancelled`, in a sentence for the caller: the spec's refusal or the target's\nreset hook's, quoted; that Saram could not start the run, or could not finish judging its sessions; that every\nsession errored; or `cancelled by signal`. Absent on a `completed` run."
          },
          "target": {
            "type": "string",
            "title": "Target"
          },
          "target_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Name"
          },
          "baseline_run_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Baseline Run Id"
          },
          "tenant_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tenant Id",
            "description": "Who owns this run, and so who may read it: every route filters by the tenant the request's key belongs\nto, and a run outside it is a 404 rather than a 403. Always your own tenant, which is what makes it safe to\npublish - a response can only ever carry the id you are already authenticated as. Absent on runs created\nbefore runs recorded their tenant; those are readable by this deployment's own fleet tenant and by nobody\nelse."
          },
          "created_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By",
            "description": "Which credential started it: `key_...` for an API key, `operator` for a break-glass entry or a run an\noperator started in-process. The id of the key rather than its name, so rotating a key leaves the runs it\nstarted still pointing at it."
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "Who started this run, in words: the caller name the API key gate identified at `POST /runs` - the\noperator's own name for a break-glass entry, a name like `acme/ci` for an API key. Absent when nobody was\nidentified, which is the gate being off, a run an operator started in-process, or a run created before runs\nrecorded their caller; the three are deliberately indistinguishable rather than dressed up with an invented\nname. Attribution, beside the authorization `tenant_id` carries."
          },
          "confirmation_rate": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confirmation Rate",
            "description": "Confirmed issues over the issues reproduction ruled on. Available as soon as reproduction finishes, which\nis before `summary.confirmation_rate` exists; the two are the same number computed from the same lineage."
          },
          "repro_attempts": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Attempts"
          },
          "repro_gate_discards": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Gate Discards",
            "description": "How many things reproduction's gate threw away, of two kinds: claim-check quotes the cited evidence does\nnot contain, each of which left its issue `ungrounded`, and replay attempts whose path diverged\nbefore the judged step, each of which ruled on nothing and left its issue `skipped`. Read beside\n`confirmation_rate`: that says how much of what the judge said survived the replay, this says how often the\nreplay's own checker cited loosely or its path stopped being walkable. Zero is the expected value. Absent\nuntil reproduction has run, and on runs reproduced before this was recorded."
          },
          "repro_gate_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Gate Ref",
            "description": "The records behind `repro_gate_discards`, fetched through `GET /api/v1/blobs`: `{\"run_id\", \"discards\"}`,\none discard each with its `session_id`, `kind`, `reason` and a `payload` naming the `issue_id`. A\n`claim_quote` payload carries the `quote`; a `path_diverged` one carries the `attempt`, the\n`attempt_session_id`, the original `step_id` it diverged at (null for the entry url), that step's `action`\nand `expect`, and the `url` the replay was on instead. Written even when empty, so an absent ref is\nreproduction that has not run, never \"nothing thrown away\"."
          },
          "billed_steps": {
            "type": "integer",
            "title": "Billed Steps",
            "description": "What this run is charged for: the steps its personas took - one browser action each - never the\nreproduction replay's, which are included at no charge. LIVE: a session is recorded when\nit ends, so while the run is open this is the finished sessions' counts plus each unfinished\nsession's last `heartbeat_step`, and it climbs until the run does end.",
            "default": 0
          },
          "billable_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Billable Usd",
            "description": "What this run costs you, to the cent: `billed_steps` at your plan's step price - or your own agreed\nprice - in force when the run ran. This is Saram's charge; `cost_usd` beside it is model spend and is not.\nLIVE with `billed_steps` while the run is open, so it is what the run has cost you so far. Absent means\nnobody has priced your tenant, which is not the same as free - never read a missing price as $0. Your plan's\nincluded steps and monthly fee are not in it: they are properties of a month, and the invoice line with the\nallowance applied is this run's `billable_usd` on `GET /api/v1/usage`."
          },
          "cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cost Usd",
            "description": "What this run's model calls cost: `actor_cost_usd` and `judge_cost_usd` together. NOT what Saram charges\nyou - `billable_usd` is that: on your own keys this is your provider's bill, and on Saram's models it is Saram's\nown cost, shown so your Saram usage and your provider's invoice add up. Written when the run finishes. Absent\nmeans nothing priced the models, which is not the same as free. On a run finished before the judge was priced\nit is the persona loop's alone, and `judge_cost_usd` is absent beside it."
          },
          "actor_cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Actor Cost Usd",
            "description": "Of `cost_usd`, the persona loop's model calls: the sum of the priced sessions' `cost_usd`. Absent when no\nsession's model had a price."
          },
          "judge_cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Judge Cost Usd",
            "description": "Of `cost_usd`, the observer's model calls: the judge's reading of each session, attribution, the merge,\nand the reproduction's claim check and tie-break - a failed attempt included, because it was paid for. On\ntoday's models this is often the larger half. A retest you ask for later is not in it. Absent when none of\nthose calls had a price, and on a run finished before they were priced."
          },
          "usage": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage",
            "description": "This run's Usage: persona steps weighted by how long each took, summed over its sessions, to 0.001. LIVE\nwhile `usage_state` is `live`: settled sessions read what the ledger debited, and a session in flight its last\nheartbeat. Final once `usage_state` is `settled` or `refunded`, when it is the credit ledger's own figure.\nAbsent on a finished run none of whose sessions was measured, which is not the same as none used."
          },
          "usage_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Usd",
            "description": "What this run's Usage costs you in dollars, at your plan's Usage price when each session ran: charged from\nyour prepaid balance, refunds already taken off - so a run that failed on Saram's side reads 0 and the money\ngiven back is `refunded_usd`. Moves with `usage` while the run is live. This is Saram's charge on a plan that\nprices Usage (Free, Pro); your model provider's bill is never in it. Absent when nothing prices this run's\nUsage - a plan billed in steps, where `billable_usd` is the charge - which is not the same as free."
          },
          "refunded_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Refunded Usd",
            "description": "Of this run's Usage, the dollars given back because a session failed on Saram's side (the pool, a lost\nworker, a setup step in Saram's code: fault `saram`). Not in `usage_usd`. Absent wherever `usage_usd` is."
          },
          "held_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Held Usd",
            "description": "What this run still reserves from your balance: the most its spec's budgets can use (the Usage ceiling),\nless each finished session's share. Released when the run's last session is settled. Absent when the run\nholds nothing - it has finished, or the run gate did not hold for it."
          },
          "usage_state": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "live",
                  "held",
                  "settled",
                  "refunded"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage State",
            "description": "How final `usage` and `usage_usd` are. `held`: the run is open and holds `held_usd`, and no step is\nmeasured yet. `live`: the run is open, or has just finished and is being settled - the figures are so far and\nstill rise. `settled`: every session is settled and the figures are the credit ledger's. `refunded`: settled,\nand every dollar of its Usage was given back because it failed on Saram's side."
          },
          "sessions_per_hour": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sessions Per Hour",
            "description": "Sessions that ran a step, per hour of the execution phase (run start to last session finish)."
          },
          "artifact_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Artifact Ref"
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Summary"
              },
              {
                "type": "null"
              }
            ],
            "description": "The artifact's `summary` block, once the artifact is assembled."
          },
          "session_counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Session Counts",
            "description": "Session count per status for this run: pending, running, the artifact results, or cancelled."
          },
          "packs": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Packs",
            "description": "The distinct persona packs this run's sessions were drawn from, sorted: the cohorts it covers. A run whose\npersonas have no pack lists none rather than an empty cohort."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "evidence_expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Evidence Expires At",
            "description": "When this run's step-by-step evidence is deleted: `created_at` plus the days its plan keeps evidence - 14 on\nFree, 90 on Pro, this deployment's default on a plan that sets none - or the days its plan kept when the run\nwas created, whichever is longer, so a plan change never moves this date earlier and an upgrade moves it later.\nThe screenshots, the video, the agent and surface step-event streams, the checkpoints and the per-session\nobservation documents go at this date, in every bucket they live in. Earlier when they went earlier: a run\nwhose recordings the storage had already removed is found by the retention sweep, which takes the rest of its\nevidence and records when, and that moment is this date. The run's *record* is kept and has no\nexpiry: this response, its issues, its verdicts, its opportunities, the spec it ran and the assembled\nartifact. A reader that wants a screenshot after that date must keep the bytes, not the ref."
          },
          "evidence_expired": {
            "type": "boolean",
            "title": "Evidence Expired",
            "description": "Whether that date has passed. `GET /api/v1/blobs` on this run's session evidence answers 404 once it\nhas, and that 404 means \"expired\", not \"never existed\" - which is why the date is published beside it.",
            "default": false
          },
          "workflow_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Workflow Id"
          },
          "budgets": {
            "additionalProperties": true,
            "type": "object",
            "title": "Budgets"
          },
          "run_spec_ref": {
            "type": "string",
            "title": "Run Spec Ref"
          },
          "files": {
            "items": {
              "$ref": "#/components/schemas/RunFile"
            },
            "type": "array",
            "title": "Files",
            "description": "The files the run hands its personas, in the spec's order: its `files[]`, without the rest of the spec.\nEmpty for a run that names none."
          },
          "live": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LiveStatus"
              },
              {
                "type": "null"
              }
            ]
          },
          "sessions": {
            "items": {
              "$ref": "#/components/schemas/SessionRow"
            },
            "type": "array",
            "title": "Sessions"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "target",
          "created_at",
          "evidence_expires_at",
          "run_spec_ref"
        ],
        "title": "RunDetail",
        "description": "One run in full: everything `RunSummary` carries, plus its budgets, the files it hands its personas, its\nlive state while it is open, and every session."
      },
      "RunFile": {
        "properties": {
          "ref": {
            "type": "string",
            "title": "Ref",
            "description": "`saram-file://<tenant_id>/<file_id>`, as `FileRecord.ref`. The file may since have been deleted or have\nexpired: this is what the run was handed, not what the tenant still has."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "What the persona calls it, and the filename the target receives. Unique within the run."
          },
          "description": {
            "type": "string",
            "title": "Description",
            "description": "What the persona was told about it, verbatim."
          },
          "assign": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RunFileAssign"
              },
              {
                "type": "null"
              }
            ],
            "description": "Which sessions carry it. Absent means every session."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "ref",
          "name",
          "description"
        ],
        "title": "RunFile",
        "description": "One file a run hands its personas: the run spec's `files[]` entry as the run was started with it.\n\nServed on `RunDetail` so the run page can list a run's handouts without the spec document itself, which also\ncarries its providers, connectors and seed-account refs. Read from the run's own row, never from the store:\nthe spec is immutable once the run is created, and the run row keeps the copy that was validated."
      },
      "RunFileAssign": {
        "properties": {
          "personas": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Personas",
            "description": "Persona addresses, as the spec's `personas` names them. Absent means any persona."
          },
          "missions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Missions",
            "description": "Mission ids. Absent means any mission."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "RunFileAssign",
        "description": "Which of a run's sessions carry a file: those matching every list given (`files[].assign` in the run spec)."
      },
      "RunList": {
        "properties": {
          "runs": {
            "items": {
              "$ref": "#/components/schemas/RunSummary"
            },
            "type": "array",
            "title": "Runs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "runs"
        ],
        "title": "RunList",
        "description": "Your runs, newest first."
      },
      "RunSignalled": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id",
            "description": "The run the signal was delivered to."
          },
          "signal": {
            "type": "string",
            "title": "Signal",
            "description": "The signal delivered: pause, resume or cancel."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "signal"
        ],
        "title": "RunSignalled",
        "description": "A pause, resume or cancel delivered to a run."
      },
      "RunSummary": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "`pending`, `running`, `observing`, then one terminal value. `completed` means at least one session\nreached an ending its persona got to (`completed`, `abandoned`, `blocked`, `budget_exhausted`), so there\nare results to read. `failed` covers both a run that broke - a rejected spec, a reset hook or the\nobserver - and a run whose every session `errored`, which came back with no persona outcome at all;\n`status_reason` says which. `cancelled` is a `cancel` signal. Poll until the value is terminal, and treat\nonly `completed` as results-ready."
          },
          "status_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Status Reason",
            "description": "Why a run ended `failed` or `cancelled`, in a sentence for the caller: the spec's refusal or the target's\nreset hook's, quoted; that Saram could not start the run, or could not finish judging its sessions; that every\nsession errored; or `cancelled by signal`. Absent on a `completed` run."
          },
          "target": {
            "type": "string",
            "title": "Target"
          },
          "target_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Name"
          },
          "baseline_run_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Baseline Run Id"
          },
          "tenant_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tenant Id",
            "description": "Who owns this run, and so who may read it: every route filters by the tenant the request's key belongs\nto, and a run outside it is a 404 rather than a 403. Always your own tenant, which is what makes it safe to\npublish - a response can only ever carry the id you are already authenticated as. Absent on runs created\nbefore runs recorded their tenant; those are readable by this deployment's own fleet tenant and by nobody\nelse."
          },
          "created_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created By",
            "description": "Which credential started it: `key_...` for an API key, `operator` for a break-glass entry or a run an\noperator started in-process. The id of the key rather than its name, so rotating a key leaves the runs it\nstarted still pointing at it."
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "Who started this run, in words: the caller name the API key gate identified at `POST /runs` - the\noperator's own name for a break-glass entry, a name like `acme/ci` for an API key. Absent when nobody was\nidentified, which is the gate being off, a run an operator started in-process, or a run created before runs\nrecorded their caller; the three are deliberately indistinguishable rather than dressed up with an invented\nname. Attribution, beside the authorization `tenant_id` carries."
          },
          "confirmation_rate": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confirmation Rate",
            "description": "Confirmed issues over the issues reproduction ruled on. Available as soon as reproduction finishes, which\nis before `summary.confirmation_rate` exists; the two are the same number computed from the same lineage."
          },
          "repro_attempts": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Attempts"
          },
          "repro_gate_discards": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Gate Discards",
            "description": "How many things reproduction's gate threw away, of two kinds: claim-check quotes the cited evidence does\nnot contain, each of which left its issue `ungrounded`, and replay attempts whose path diverged\nbefore the judged step, each of which ruled on nothing and left its issue `skipped`. Read beside\n`confirmation_rate`: that says how much of what the judge said survived the replay, this says how often the\nreplay's own checker cited loosely or its path stopped being walkable. Zero is the expected value. Absent\nuntil reproduction has run, and on runs reproduced before this was recorded."
          },
          "repro_gate_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Repro Gate Ref",
            "description": "The records behind `repro_gate_discards`, fetched through `GET /api/v1/blobs`: `{\"run_id\", \"discards\"}`,\none discard each with its `session_id`, `kind`, `reason` and a `payload` naming the `issue_id`. A\n`claim_quote` payload carries the `quote`; a `path_diverged` one carries the `attempt`, the\n`attempt_session_id`, the original `step_id` it diverged at (null for the entry url), that step's `action`\nand `expect`, and the `url` the replay was on instead. Written even when empty, so an absent ref is\nreproduction that has not run, never \"nothing thrown away\"."
          },
          "billed_steps": {
            "type": "integer",
            "title": "Billed Steps",
            "description": "What this run is charged for: the steps its personas took - one browser action each - never the\nreproduction replay's, which are included at no charge. LIVE: a session is recorded when\nit ends, so while the run is open this is the finished sessions' counts plus each unfinished\nsession's last `heartbeat_step`, and it climbs until the run does end.",
            "default": 0
          },
          "billable_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Billable Usd",
            "description": "What this run costs you, to the cent: `billed_steps` at your plan's step price - or your own agreed\nprice - in force when the run ran. This is Saram's charge; `cost_usd` beside it is model spend and is not.\nLIVE with `billed_steps` while the run is open, so it is what the run has cost you so far. Absent means\nnobody has priced your tenant, which is not the same as free - never read a missing price as $0. Your plan's\nincluded steps and monthly fee are not in it: they are properties of a month, and the invoice line with the\nallowance applied is this run's `billable_usd` on `GET /api/v1/usage`."
          },
          "cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cost Usd",
            "description": "What this run's model calls cost: `actor_cost_usd` and `judge_cost_usd` together. NOT what Saram charges\nyou - `billable_usd` is that: on your own keys this is your provider's bill, and on Saram's models it is Saram's\nown cost, shown so your Saram usage and your provider's invoice add up. Written when the run finishes. Absent\nmeans nothing priced the models, which is not the same as free. On a run finished before the judge was priced\nit is the persona loop's alone, and `judge_cost_usd` is absent beside it."
          },
          "actor_cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Actor Cost Usd",
            "description": "Of `cost_usd`, the persona loop's model calls: the sum of the priced sessions' `cost_usd`. Absent when no\nsession's model had a price."
          },
          "judge_cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Judge Cost Usd",
            "description": "Of `cost_usd`, the observer's model calls: the judge's reading of each session, attribution, the merge,\nand the reproduction's claim check and tie-break - a failed attempt included, because it was paid for. On\ntoday's models this is often the larger half. A retest you ask for later is not in it. Absent when none of\nthose calls had a price, and on a run finished before they were priced."
          },
          "usage": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage",
            "description": "This run's Usage: persona steps weighted by how long each took, summed over its sessions, to 0.001. LIVE\nwhile `usage_state` is `live`: settled sessions read what the ledger debited, and a session in flight its last\nheartbeat. Final once `usage_state` is `settled` or `refunded`, when it is the credit ledger's own figure.\nAbsent on a finished run none of whose sessions was measured, which is not the same as none used."
          },
          "usage_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Usd",
            "description": "What this run's Usage costs you in dollars, at your plan's Usage price when each session ran: charged from\nyour prepaid balance, refunds already taken off - so a run that failed on Saram's side reads 0 and the money\ngiven back is `refunded_usd`. Moves with `usage` while the run is live. This is Saram's charge on a plan that\nprices Usage (Free, Pro); your model provider's bill is never in it. Absent when nothing prices this run's\nUsage - a plan billed in steps, where `billable_usd` is the charge - which is not the same as free."
          },
          "refunded_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Refunded Usd",
            "description": "Of this run's Usage, the dollars given back because a session failed on Saram's side (the pool, a lost\nworker, a setup step in Saram's code: fault `saram`). Not in `usage_usd`. Absent wherever `usage_usd` is."
          },
          "held_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Held Usd",
            "description": "What this run still reserves from your balance: the most its spec's budgets can use (the Usage ceiling),\nless each finished session's share. Released when the run's last session is settled. Absent when the run\nholds nothing - it has finished, or the run gate did not hold for it."
          },
          "usage_state": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "live",
                  "held",
                  "settled",
                  "refunded"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage State",
            "description": "How final `usage` and `usage_usd` are. `held`: the run is open and holds `held_usd`, and no step is\nmeasured yet. `live`: the run is open, or has just finished and is being settled - the figures are so far and\nstill rise. `settled`: every session is settled and the figures are the credit ledger's. `refunded`: settled,\nand every dollar of its Usage was given back because it failed on Saram's side."
          },
          "sessions_per_hour": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sessions Per Hour",
            "description": "Sessions that ran a step, per hour of the execution phase (run start to last session finish)."
          },
          "artifact_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Artifact Ref"
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Summary"
              },
              {
                "type": "null"
              }
            ],
            "description": "The artifact's `summary` block, once the artifact is assembled."
          },
          "session_counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Session Counts",
            "description": "Session count per status for this run: pending, running, the artifact results, or cancelled."
          },
          "packs": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Packs",
            "description": "The distinct persona packs this run's sessions were drawn from, sorted: the cohorts it covers. A run whose\npersonas have no pack lists none rather than an empty cohort."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "evidence_expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Evidence Expires At",
            "description": "When this run's step-by-step evidence is deleted: `created_at` plus the days its plan keeps evidence - 14 on\nFree, 90 on Pro, this deployment's default on a plan that sets none - or the days its plan kept when the run\nwas created, whichever is longer, so a plan change never moves this date earlier and an upgrade moves it later.\nThe screenshots, the video, the agent and surface step-event streams, the checkpoints and the per-session\nobservation documents go at this date, in every bucket they live in. Earlier when they went earlier: a run\nwhose recordings the storage had already removed is found by the retention sweep, which takes the rest of its\nevidence and records when, and that moment is this date. The run's *record* is kept and has no\nexpiry: this response, its issues, its verdicts, its opportunities, the spec it ran and the assembled\nartifact. A reader that wants a screenshot after that date must keep the bytes, not the ref."
          },
          "evidence_expired": {
            "type": "boolean",
            "title": "Evidence Expired",
            "description": "Whether that date has passed. `GET /api/v1/blobs` on this run's session evidence answers 404 once it\nhas, and that 404 means \"expired\", not \"never existed\" - which is why the date is published beside it.",
            "default": false
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "target",
          "created_at",
          "evidence_expires_at"
        ],
        "title": "RunSummary",
        "description": "A run as `GET /api/v1/runs` lists it: where it stands, what it found in summary, and what it cost and\nbilled. `GET /api/v1/runs/{run_id}` answers the same fields and more (`RunDetail`)."
      },
      "SessionList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "sessions": {
            "items": {
              "$ref": "#/components/schemas/SessionRow"
            },
            "type": "array",
            "title": "Sessions"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "sessions"
        ],
        "title": "SessionList",
        "description": "A run's sessions, one per persona per mission."
      },
      "SessionRow": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "persona_id": {
            "type": "string",
            "title": "Persona Id"
          },
          "pack": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pack",
            "description": "The persona's pack as the registry had it when the run was snapshotted - the session's cohort. Absent\nfor a persona that declares none, and for a session recorded before sessions carried one."
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Tags",
            "description": "The persona's tags at snapshot time, beside `pack`."
          },
          "mission_id": {
            "type": "string",
            "title": "Mission Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "`pending`, `running`, an artifact result, or `cancelled`. Nothing reports `running` today: a session is\nrecorded once, when it ends, so a session in flight still reads `pending` - see `heartbeat_step`, and\n`waiting_for_seat` for one that is queued for a browser seat rather than on a step."
          },
          "result": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Result",
            "description": "The artifact result once the loop reported; absent before that and on a session that never started."
          },
          "end_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "End Reason",
            "description": "How the session ended: the loop's word for an ending the persona reached (`success_criteria_met`,\n`patience_exhausted`, `budget_steps`), and on an `errored` one a sentence that says what kind of failure it\nwas - the target's, the model provider's (with the provider's own message), or Saram's - with `fault` beside\nit."
          },
          "fault": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "none",
                  "saram",
                  "provider",
                  "target"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Fault",
            "description": "Whose failure ended the session. `none` for every ending the persona or the loop reached, and for a\nsession a person cancelled; on an `errored` one, `saram` (the harness: the pool, a setup step, a lost\nworker), `provider` (the model API: an HTTP error or a transport failure after the retries, or a reply the\nloop could not act on) or `target` (the product: its entry page did not load, or its reset hook refused the\nsession). What decides whether an errored session is charged. `null` while the session is open, and on a\nsession recorded before this was classified - never read it as `none`."
          },
          "steps": {
            "type": "integer",
            "title": "Steps"
          },
          "end_step": {
            "type": "integer",
            "title": "End Step"
          },
          "abandon_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Abandon Step"
          },
          "tokens_in": {
            "type": "integer",
            "title": "Tokens In",
            "description": "The whole prompt plus the reply: `tokens_in` already includes both cache halves below."
          },
          "tokens_out": {
            "type": "integer",
            "title": "Tokens Out"
          },
          "tokens_cache_read": {
            "type": "integer",
            "title": "Tokens Cache Read",
            "description": "Of `tokens_in`, read from the prompt cache - billed at a tenth of the input rate on Anthropic.",
            "default": 0
          },
          "tokens_cache_write": {
            "type": "integer",
            "title": "Tokens Cache Write",
            "description": "Of `tokens_in`, written to the prompt cache.",
            "default": 0
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model"
          },
          "cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cost Usd",
            "description": "What this session's model calls cost. Absent when nothing prices its model; never 0 to mean \"unknown\"."
          },
          "price_source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Price Source",
            "description": "Which price table produced `cost_usd`: this deployment's own, `env` (a bare model id's row) or `env/<kind>`\n(a row for the model on one provider kind), else `genai-prices/<version>/<provider>`."
          },
          "wall_clock_s": {
            "type": "number",
            "title": "Wall Clock S"
          },
          "uncertain_steps": {
            "anyOf": [
              {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Uncertain Steps",
            "description": "The steps of this session whose effect on the product nothing can attest: the call was dispatched, the\nresponse was lost, and the resume reconciled against the pool's step id rather than re-issuing it. `null`\nmeans nobody looked - a session recorded before the harness produced this, or one whose activity was lost\nbefore it or its checkpoint could report - and `[]` means the loop checked and every step is attested.\nNever read `null` as \"none\": it is the same three-way reading as `artifact.outcomes[].uncertain_steps`."
          },
          "resumed_at_steps": {
            "anyOf": [
              {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resumed At Steps",
            "description": "The first step of each resumed browser execution, one per resume: the re-orienting `look` the loop\ntakes when its activity is retried. `null` and `[]` read as above. A browser checkpoint carries a url and\na storage state and nothing else, so each of these is where a new execution picked the experiment up, not\nwhere the same page carried on."
          },
          "verdict_decision": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Verdict Decision",
            "description": "The judge's verdict decision once it has read the session: positive, negative, or undecided."
          },
          "outcome_read": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Outcome Read",
            "description": "What the judge read the session's outcome as, independent of what the loop reported."
          },
          "observer_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Observer Error",
            "description": "Why nobody read this session, when nobody did, in Saram's words: the judge failure that left the session read\nas `unclear` - the model provider's status and own message when the provider refused, the `providers` entry's\nrefusal when it did not resolve, Saram's sentence otherwise. Absent on every session a judge pass read,\nincluding one it found nothing in - which is the only way a reader tells a clean session from one no model ever\nsaw."
          },
          "verdict_discarded": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/VerdictDiscarded"
              },
              {
                "type": "null"
              }
            ],
            "description": "NOT A VERDICT. The answer the judge gave about this session that the quote gate threw away, and why: set only\nwhen `verdict_decision` is absent on a session a judge read. Absent when the judge returned no verdict, which is\nthe difference it is here for - both leave no decision. The decision is the judge's with no line of the persona's\ndiary behind it, so it is never the persona's verdict and is never counted as one. Absent on a session observed\nbefore the control plane recorded it, whose gate ledger is the only record."
          },
          "heartbeat_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Heartbeat Step",
            "description": "LIVE. The `step_id` the persona last reported, read live: a session is recorded once, when it ends, so\nthis is the only field that says where a session in flight has got to. Best effort - absent on a finished\nsession, and on a running one whose live state cannot be read. Compare it with the steps page's\n`last_step_id` to see how far the readable trace lags the browser: the trace records a step after the browser\nanswers, so a step in flight shows here first."
          },
          "last_heartbeat_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Heartbeat At",
            "description": "When that heartbeat reached the server: how stale `heartbeat_step` is. Absent whenever it is."
          },
          "heartbeat_usage": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Heartbeat Usage",
            "description": "LIVE. This session's Usage so far, to 0.001, from the step times on its last heartbeat: persona steps\nweighted by how long each one took, measured the way the session's Usage will be when it ends. A report, not a\ncharge. Best effort, as `heartbeat_step` - and also absent for a session on a worker that does not report step\ntimes."
          },
          "usage": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage",
            "description": "This session's Usage, to 0.001: the credit ledger's figure once it is settled, measured from its recorded\nstep times before that, and on a session in flight its last heartbeat (`heartbeat_usage`). Absent on a session\nthat has not taken a step, and on one recorded without step times."
          },
          "usage_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Usd",
            "description": "What this session's Usage costs you, net of any refund (`refunded_usd`). Absent when nothing prices it: a\nplan billed in steps."
          },
          "refunded_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Refunded Usd",
            "description": "What was given back for this session because it failed on Saram's side (fault `saram`): its whole debit. 0\notherwise. Absent wherever `usage_usd` is."
          },
          "waiting_for_seat": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting For Seat",
            "description": "LIVE. `true` while this session is queued for a browser seat: the run has started it, but every seat in the\nbrowser pool is taken, so it has no browser and takes no steps until one frees - a run with more sessions than\nthe pool has seats shows this on the rest, and they start in turn without spending an attempt. `false` while\nthe session is not waiting: setting up, on a seat, or paused. Read live, best effort as `heartbeat_step` -\nabsent on a finished session, on one the run has not started yet (its `max_concurrent_sessions` holds it\nback), and on one whose live state cannot be read. A session resuming after a\npause or a pool failure queues the same way, so it can be `true` beside a `heartbeat_step`."
          },
          "waiting_since": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting Since",
            "description": "When the session was queued for the seat it is waiting for: how long it has waited. Absent whenever\n`waiting_for_seat` is not `true`."
          },
          "workflow_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Workflow Id"
          },
          "browser_session_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Browser Session Id"
          },
          "agent_trace_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Agent Trace Ref"
          },
          "surface_trace_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Surface Trace Ref"
          },
          "video_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Ref"
          },
          "video_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Url"
          },
          "recorded_under_mask": {
            "type": "boolean",
            "title": "Recorded Under Mask",
            "description": "Whether the browser pool recorded this session under its mask: every field the persona typed into, every\none-time-code field and every `data-saram-mask` element painted over in every screenshot and video frame it\nstored. Only such a session of yours can be shared by link while `ShareList.mask_required` is `true`: a share of\nany other is refused with a 409, so read the two together before offering one. `false` for every session\nrecorded before the pool masked, and by a pool that does not.",
            "default": false
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "session_id",
          "persona_id",
          "mission_id",
          "status",
          "steps",
          "end_step",
          "tokens_in",
          "tokens_out",
          "wall_clock_s",
          "created_at"
        ],
        "title": "SessionRow",
        "description": "One session of a run: one persona on one mission, in its own browser. What it ended as and whose fault\nan error was, the tokens, cost and Usage it took, where it has got to while it runs, and the refs to its\nstep-by-step traces and video."
      },
      "Severity": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high",
          "critical"
        ],
        "title": "Severity",
        "description": "How much the defect costs the people who meet it, from least to most: `low`, `medium`, `high`, `critical`. Rated by the judge from the sessions, and lowered one step when the replay did not reproduce the issue and it stands as persona-specific friction. The GitHub connector files only issues at or above its `min_severity_for_issue`."
      },
      "ShareCreated": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "shr_...: the link's id, for listing and revoking it. Not the link itself."
          },
          "url": {
            "type": "string",
            "title": "Url",
            "description": "The public link. Shown once, here, and never again by any route. Anyone who has it can watch this session until it expires or is revoked, with no sign-in."
          },
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "created_by": {
            "type": "string",
            "title": "Created By",
            "description": "key_... for an API key, usr_... for a person, operator for the operator."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At"
          },
          "capped_by_evidence": {
            "type": "boolean",
            "title": "Capped By Evidence",
            "description": "True when the run's evidence lifetime, and not the days asked for, set expires_at: the link ends with the recording."
          },
          "masked": {
            "type": "boolean",
            "title": "Masked",
            "description": "True when the browser pool recorded the session under its mask: the value of every field the persona typed into, every one-time-code field and every data-saram-mask element are painted over in every frame. False only for a session of Saram's own, which is shared unmasked."
          },
          "warning": {
            "type": "string",
            "title": "Warning",
            "description": "What anyone holding the link will see, what in the recording is masked and what is not, and how to revoke it. Show it to whoever posts the link."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "url",
          "run_id",
          "session_id",
          "created_at",
          "created_by",
          "expires_at",
          "capped_by_evidence",
          "masked",
          "warning"
        ],
        "title": "ShareCreated",
        "description": "A new public link to one session's replay, and the one and only sight of its URL.\n\nAnyone holding `url` can watch the session's recording, which hides only what the browser pool masked when it\nrecorded it (`masked`): whatever else the browser showed is in it. Only a hash of the link's token is stored, so\nno route shows the URL again; a lost link is replaced by minting another and revoking this one with\n`POST /api/v1/shares/{id}/revoke`."
      },
      "ShareList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "public_shares": {
            "type": "boolean",
            "title": "Public Shares",
            "description": "Whether your tenant may publish sessions at all. Off, a new link is refused with a 403 and no existing link plays; their rows are kept and they play again, until their own expiry, once it is back on."
          },
          "mask_required": {
            "type": "boolean",
            "title": "Mask Required",
            "description": "Whether a session of yours is shared only when the browser pool recorded it under its mask (SessionRow.recorded_under_mask): a share of any other is refused with a 409. True for every customer; false only for Saram's own sessions, which are shared unmasked."
          },
          "shares": {
            "items": {
              "$ref": "#/components/schemas/ShareSummary"
            },
            "type": "array",
            "title": "Shares"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "public_shares",
          "mask_required",
          "shares"
        ],
        "title": "ShareList",
        "description": "A run's public links, revoked and expired ones included, newest first."
      },
      "ShareSummary": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "token_hint": {
            "type": "string",
            "title": "Token Hint",
            "description": "The link's last four characters, to tell your links apart."
          },
          "state": {
            "type": "string",
            "enum": [
              "live",
              "expired",
              "revoked"
            ],
            "title": "State",
            "description": "live plays, expired and revoked do not. Read it beside ShareList.public_shares: while your tenant's sharing is off no link plays, whatever its state."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "created_by": {
            "type": "string",
            "title": "Created By"
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "Who minted it, in words, when the credential had a name."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At"
          },
          "revoked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "revoked_by": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked By",
            "description": "Who revoked it: any principal of your tenant that may write runs can."
          },
          "view_count": {
            "type": "integer",
            "title": "View Count",
            "description": "How many times the replay was opened. Nothing about who opened it is kept."
          },
          "last_viewed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Viewed At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "run_id",
          "session_id",
          "token_hint",
          "state",
          "created_at",
          "created_by",
          "expires_at",
          "view_count"
        ],
        "title": "ShareSummary",
        "description": "One public link to a session's replay, as the list and the revocation answer it. Never the link itself."
      },
      "ShareWrite": {
        "properties": {
          "expires_in_days": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 90.0,
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires In Days",
            "description": "How many days the link plays, 1 to 90; 30 when omitted. Never past the run's evidence_expires_at, whatever you ask for: a link ends when the recording it plays is deleted. There is no link that does not expire, and no extending one: mint a new link instead."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "ShareWrite",
        "description": "The optional body of `POST /runs/{run_id}/sessions/{session_id}/shares`."
      },
      "SharedMission": {
        "properties": {
          "goal": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Goal",
            "description": "The mission's goal, in the persona's own terms, as the run's spec stated it."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "SharedMission",
        "description": "What the persona was sent to do."
      },
      "SharedPersona": {
        "properties": {
          "label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label",
            "description": "The persona's display name, as the registry had it when the run started."
          },
          "pack": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pack",
            "description": "The persona pack (cohort) it came from."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "SharedPersona",
        "description": "Who the session's persona is, by name: never its id."
      },
      "SharedSession": {
        "properties": {
          "persona": {
            "$ref": "#/components/schemas/SharedPersona"
          },
          "mission": {
            "$ref": "#/components/schemas/SharedMission"
          },
          "target": {
            "$ref": "#/components/schemas/SharedTarget"
          },
          "result": {
            "type": "string",
            "title": "Result",
            "description": "How the session ended: `completed`, `abandoned`, `blocked`, `budget_exhausted` or `errored` (`cancelled` for\none the run was cancelled before it started)."
          },
          "duration_s": {
            "type": "number",
            "title": "Duration S",
            "description": "How long the session ran, in seconds of wall clock."
          },
          "step_count": {
            "type": "integer",
            "title": "Step Count",
            "description": "How many steps `steps` holds."
          },
          "video": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SharedVideo"
              },
              {
                "type": "null"
              }
            ],
            "description": "The recording; absent when the session has none."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At",
            "description": "When this link stops playing, unless it is revoked first."
          },
          "steps": {
            "items": {
              "$ref": "#/components/schemas/SharedStep"
            },
            "type": "array",
            "title": "Steps"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "persona",
          "mission",
          "target",
          "result",
          "duration_s",
          "step_count",
          "expires_at",
          "steps"
        ],
        "title": "SharedSession",
        "description": "One session's replay as a public share link shows it: the persona, the mission, the result, the recording and\neach step. Nothing else about the run, its tenant or its other sessions."
      },
      "SharedStep": {
        "properties": {
          "step_id": {
            "type": "integer",
            "title": "Step Id",
            "description": "The step's number within the session, from 0; the order of the replay."
          },
          "action_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Action Type",
            "description": "The body tool the persona called: `click`, `type`, `scroll`, `look`, `abandon`, `complete` and so on."
          },
          "target_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Description",
            "description": "SUBJECTIVE: what the persona said it was acting on, in its own words."
          },
          "narration": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Narration",
            "description": "SUBJECTIVE: the persona's diary line said before this action. Quote it as the persona's, never as a\nfinding."
          },
          "page": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page",
            "description": "The page the browser was on after the step: origin and path, the query and fragment dropped."
          },
          "ts_start_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts Start S",
            "description": "Where this step starts in the recording, seconds from its first frame."
          },
          "ts_end_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts End S",
            "description": "Where it ends."
          },
          "terminal": {
            "type": "boolean",
            "title": "Terminal",
            "description": "True on the step that ended the session (`abandon` or `complete`), which the persona took without the\nbrowser."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "step_id",
          "terminal"
        ],
        "title": "SharedStep",
        "description": "One step of the replay: what the persona did and said, on which page, and where it is in the video."
      },
      "SharedTarget": {
        "properties": {
          "origin": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Origin",
            "description": "The scheme, host and port the mission started on. Never a path, a query or a credential."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "SharedTarget",
        "description": "Which product the session was run against."
      },
      "SharedVideo": {
        "properties": {
          "path": {
            "type": "string",
            "const": "video",
            "title": "Path",
            "description": "Where the share host serves the recording, relative to the link: `<link>/video`.",
            "default": "video"
          },
          "content_type": {
            "type": "string",
            "title": "Content Type",
            "description": "The recording's media type: `video/webm`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "content_type"
        ],
        "title": "SharedVideo",
        "description": "The session's recording, served by `GET /api/v1/shared/session/video` with the same token."
      },
      "SpecValidation": {
        "properties": {
          "run_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Run Id",
            "description": "The run id the spec carries. Absent means `POST /runs` would assign one."
          },
          "spec": {
            "additionalProperties": true,
            "type": "object",
            "title": "Spec",
            "description": "The spec as the run would store it."
          },
          "sessions": {
            "items": {
              "$ref": "#/components/schemas/PlannedSession"
            },
            "type": "array",
            "title": "Sessions"
          },
          "warnings": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Warnings",
            "description": "What the dry run can say about the spec but does not refuse it for, one sentence each, the ones about your ChatGPT login first. Today: providers entries on a ChatGPT login whose access token has lapsed (the gateway refreshes it on the run's first model call, and the run fails if that refresh is refused) or whose gateway this API could not ask; a providers entry whose model this platform's price table has no price for, which is either a model too new for the table or a misspelt id that fails at the run's first model call; and a budget.max_concurrent_sessions above what your plan runs at once, which the run honours by running fewer sessions at a time."
          },
          "cost_ceiling": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CostCeiling"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the run would cost: the most it can bill on your plan (usd), with a one-line basis and who bills its model calls. A ceiling, not a forecast. On every plan; absent only for a caller this deployment has no tenant for."
          },
          "usage_ceiling": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UsageCeiling"
              },
              {
                "type": "null"
              }
            ],
            "description": "The most Usage this run can use and the balance it is held against. Absent when this deployment's run gate is off, or your plan is not billed in Usage."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "spec",
          "sessions"
        ],
        "title": "SpecValidation",
        "description": "`POST /api/v1/runs/validate`: what `POST /runs` would make of this spec, without making it.\n\nEverything `POST /runs` would refuse is the same 422 here: the contract, the cross-references\ninside `providers`, and the caller's own model provider - including a ChatGPT login the gateway no longer\nholds. A 200 means the spec is accepted as written."
      },
      "StartedRun": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id",
            "description": "The run's id: the handle every other run route and tool takes."
          },
          "workflow_id": {
            "type": "string",
            "title": "Workflow Id",
            "description": "The id of the workflow executing the run, for support requests."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "workflow_id"
        ],
        "title": "StartedRun",
        "description": "A run accepted and started. Follow it by its `run_id`: `GET /api/v1/runs/{run_id}`, or the MCP tools\n`get_run_status` and `wait_for_run`."
      },
      "Status": {
        "type": "string",
        "enum": [
          "verified",
          "marker_matched",
          "inconclusive",
          "unverified"
        ],
        "title": "Status",
        "description": "Best-supported first. `verified`: a replay matched the failure marker at `action_step` AND the claim check answered `supported` with a verbatim quote from the cited steps (`check`) AND the failure is attributed to the product (`attribution`) - or `method` is `assertion` or `manual` and the same two conditions hold. This and only this may file a ticket alone, and `reproducible` is true exactly when this reads `verified`. `marker_matched` (0.5): a replay matched the failure marker at `action_step`; the claim as titled is unchecked, unsupported, or the check could not quote. The step failed again; whether it failed for the stated reason is not established, so it never files alone. `inconclusive`: a check ran and settled nothing (the marker did not appear again, the two attempts disagreed) - not a refutation. `unverified`: nothing checked it, because there was nothing to check, reproduction never ruled, or the finding is a tester error. A wrong total or a select that resets is `unverified` by construction, not disproven. Default and safe reading when the block is absent. This enum drives an action - it is the ticket-filing gate, like `issue.severity` - so it stays closed for readers too and adding a member is a breaking change: a gate must refuse a value it cannot interpret rather than guess at one, which is why `marker_matched` made 0.5 a new family."
      },
      "StepRef": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "step_id": {
            "type": "integer",
            "title": "Step Id"
          }
        },
        "type": "object",
        "required": [
          "session_id",
          "step_id"
        ],
        "title": "StepRef",
        "description": "One step of one session: the session's id and the step's number within it. A finding points at the steps it rests on, and that session's step events say what happened at each."
      },
      "StepTiming": {
        "properties": {
          "action_to_settle_ms": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Action To Settle Ms"
          },
          "longest_task_ms": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Longest Task Ms"
          },
          "navigation_ms": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Navigation Ms"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "StepTiming",
        "description": "How long the browser took over one step, in milliseconds. Each is absent when the browser did not\nmeasure it."
      },
      "StepTokens": {
        "properties": {
          "input": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Input"
          },
          "output": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output"
          },
          "cached": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cached"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "StepTokens",
        "description": "The model tokens behind one step's decision: prompt, reply, and the part of the prompt read from the\ncache. Each is absent when the provider did not report it."
      },
      "StepsPage": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "persona_id": {
            "type": "string",
            "title": "Persona Id"
          },
          "mission_id": {
            "type": "string",
            "title": "Mission Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "`in_flight` while the session has not been recorded yet: the steps below are what the persona and the\nbrowser have written so far, both halves joined, and more will follow. The one thing a live page lacks is the\nsession video, which is stored when the browser closes: `video_ref` and each step's window are there,\n`video_url` is absent until the page is final. A step whose surface half is missing on a live page is one\nthe browser has not answered yet or a real gap, as on a final one. Otherwise the session's own\nstatus (`completed`, `abandoned`, `blocked`, `errored`, `budget_exhausted`, `cancelled`), and the page is\nfinal."
          },
          "heartbeat_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Heartbeat Step",
            "description": "LIVE. The `step_id` the persona last reported: where the browser has got to, which is at or one ahead of\n`last_step_id` while a step is in flight - a step is written after the browser answers, so a step in flight\nshows here first. Only asked for while `status` is `in_flight`: absent on a final page, and on a live one\nwhose live state cannot be read."
          },
          "last_heartbeat_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Heartbeat At",
            "description": "When that heartbeat reached the server: how stale `heartbeat_step` is. Absent whenever it is."
          },
          "waiting_for_seat": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting For Seat",
            "description": "LIVE. `true` while the session is queued for a browser seat, which is why a live page can have no steps at\nall: every seat in the pool is taken, and it takes no steps until one frees. A session resuming after a pause\nor a pool failure queues the same way, so it can be `true` on a page that has steps. Only asked for while\n`status` is `in_flight`: absent on a final page, and on a live one whose live state cannot be read."
          },
          "waiting_since": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Waiting Since",
            "description": "When the session was queued for the seat it is waiting for: how long it has waited. Absent whenever\n`waiting_for_seat` is not `true`."
          },
          "workflow_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Workflow Id",
            "description": "The session's workflow id, as `SessionRow.workflow_id`: what `heartbeat_step` was read off."
          },
          "from_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "From Step",
            "description": "The inclusive lower bound in effect, whether it came from `from` or `after` + 1."
          },
          "to_step": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "To Step"
          },
          "total_steps": {
            "type": "integer",
            "title": "Total Steps",
            "description": "Steps in the whole trace, before the `from`/`to` window - not the count of `steps`."
          },
          "first_step_id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "First Step Id"
          },
          "last_step_id": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Step Id",
            "description": "The last step in the whole trace. On an `in_flight` page this is what a tailing caller passes back as\n`after` to get the next batch; absent means the session has written no step yet."
          },
          "agent_trace_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Agent Trace Ref"
          },
          "surface_trace_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Surface Trace Ref"
          },
          "video_ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Ref"
          },
          "video_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Video Url"
          },
          "steps": {
            "items": {
              "$ref": "#/components/schemas/AlignedStep"
            },
            "type": "array",
            "title": "Steps"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "session_id",
          "persona_id",
          "mission_id",
          "status",
          "total_steps",
          "steps"
        ],
        "title": "StepsPage",
        "description": "A session's steps, aligned. Readable while the session runs, not only after it ends.\n\nOne step is written as the persona takes it, so a caller polling `?after=<last_step_id>` tails a live\nsession: each call returns the steps written since the last one, and `status` says whether more are coming.\n`heartbeat_step` beside it is where the browser has got to, which is at or one ahead of `last_step_id` while\na step is in flight."
      },
      "SubscriptionState": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "Stripe's: `incomplete` until the first invoice is paid, then `active`; `past_due` when a renewal failed; `canceled` once it has ended, `incomplete_expired` for a checkout nobody paid."
          },
          "cancel_at_period_end": {
            "type": "boolean",
            "title": "Cancel At Period End",
            "description": "True after Pro was cancelled (DELETE /billing/subscription): it stays Pro until current_period_end, then the tenant moves to Free. POST /billing/subscription takes the cancellation back."
          },
          "current_period_end": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Current Period End",
            "description": "When the month already paid for ends: the renewal date, or the date Pro ends when cancel_at_period_end is true. Absent until Stripe has said."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status",
          "cancel_at_period_end"
        ],
        "title": "SubscriptionState",
        "description": "The tenant's Saram Pro subscription as Stripe last reported it, mirrored so the billing screen can say\n\"Pro renews on Oct 1\" or \"Pro ends on Oct 1\" without asking Stripe on every read."
      },
      "Summary": {
        "properties": {
          "personas_run": {
            "type": "integer",
            "title": "Personas Run"
          },
          "missions": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Missions"
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions"
          },
          "completed": {
            "type": "integer",
            "title": "Completed"
          },
          "abandoned": {
            "type": "integer",
            "title": "Abandoned"
          },
          "blocked": {
            "type": "integer",
            "title": "Blocked"
          },
          "errored": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Errored"
          },
          "budget_exhausted": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Budget Exhausted",
            "default": 0
          },
          "issues_total": {
            "type": "integer",
            "title": "Issues Total"
          },
          "issues_reproducible": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Issues Reproducible"
          },
          "issues_marker_matched": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Issues Marker Matched"
          },
          "tester_errors": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tester Errors"
          },
          "issues_new_vs_baseline": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Issues New Vs Baseline"
          },
          "issues_resolved_vs_baseline": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Issues Resolved Vs Baseline"
          },
          "issues_persisting_vs_baseline": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Issues Persisting Vs Baseline"
          },
          "opportunities_total": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Opportunities Total"
          },
          "verdicts": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verdicts"
              },
              {
                "type": "null"
              }
            ]
          },
          "confirmation_rate": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 1.0,
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Confirmation Rate"
          },
          "reproduction": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Reproduction"
              },
              {
                "type": "null"
              }
            ]
          },
          "gate_discards": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Gate Discards",
            "default": 0
          },
          "path_diversity": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 1.0,
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Path Diversity"
          },
          "cost_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cost Usd"
          },
          "actor_cost_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Actor Cost Usd"
          },
          "judge_cost_usd": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Judge Cost Usd"
          },
          "wall_clock_s": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Wall Clock S"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "personas_run",
          "sessions",
          "completed",
          "abandoned",
          "blocked",
          "issues_total"
        ],
        "title": "Summary",
        "description": "Counts over the run. The result buckets (completed, abandoned, blocked, errored, budget_exhausted) partition `sessions`: one bucket per value of outcomes[].result, every session in exactly one, so they sum to `sessions`. `verdicts` has the same denominator."
      },
      "TechnicalLiteracy": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high"
        ],
        "title": "TechnicalLiteracy",
        "description": "How comfortable the persona is with technology. `low`: not technical; keys, tokens and APIs mean nothing to them. `medium`: finds their way around an app's settings without being technical. `high`: technical, and notices when a key or a token is handled badly."
      },
      "TesterError": {
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/Kind1"
          },
          "quote": {
            "type": "string",
            "title": "Quote"
          },
          "quote_step": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuoteStep"
              },
              {
                "type": "null"
              }
            ]
          },
          "decided_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecidedBy"
              },
              {
                "type": "null"
              }
            ]
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "kind",
          "quote"
        ],
        "title": "TesterError",
        "description": "What the persona did that explains this failure. Present exactly when `claim.attribution` is `tester`, and such an issue is in `tester_errors[]`, never in `issues[]`. The quote is the evidence line that shows the persona's action - the input echo, the step head with its args, a diary line, a browser line - copied exactly from the ledger and gated the same way a diary quote is; a `tester` answer whose quote does not gate falls back to `undetermined` and this block is not written."
      },
      "TesterErrorList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "The run's status. An empty `tester_errors` is only a result once this is terminal."
          },
          "classified": {
            "type": "boolean",
            "title": "Classified",
            "description": "Whether the run's artifact sorts findings by whose failure they are at all. Empty with this false is an\nartifact from before the classification (contracts 0.5), or no artifact yet: nobody asked. Empty with this\ntrue is a run that asked and found none."
          },
          "tester_errors": {
            "items": {
              "$ref": "#/components/schemas/Issue"
            },
            "type": "array",
            "title": "Tester Errors",
            "description": "The artifact's `tester_errors[]`, each the contract's own issue object with its `tester_error` block."
          },
          "reading": {
            "items": {
              "$ref": "#/components/schemas/TesterErrorReading"
            },
            "type": "array",
            "title": "Reading",
            "description": "One entry per record, in the same order and naming its `tester_error_id`."
          },
          "advisory": {
            "type": "string",
            "title": "Advisory",
            "description": "How to read this whole answer: harness-attributed, never a defect. What it may and may not be used\nfor."
          },
          "subjective_fields": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Subjective Fields",
            "description": "Fields on each record that are somebody's words rather than an observation: the persona's narration, and\n`tester_error.reason`, the harness's sentence about its own persona."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "classified",
          "tester_errors",
          "reading",
          "advisory",
          "subjective_fields"
        ],
        "title": "TesterErrorList",
        "description": "The run's tester errors: findings the harness attributed to its own persona, never to the product.\n\nIssue-shaped - the same id, evidence and screenshots an issue carries - and never an issue: not in\n`IssueList`, not in `issues_total`, not reproduced, never a ticket. They are served so a customer can see\nwhy a session stalled on their own test setup (a mistyped credential, the wrong element) instead of reading\nthe stall as the product's."
      },
      "TesterErrorReading": {
        "properties": {
          "tester_error_id": {
            "type": "string",
            "title": "Tester Error Id"
          },
          "label": {
            "type": "string",
            "title": "Label",
            "description": "What the persona did, in the words every surface uses: the sentence the local MCP server, the GitHub PR\ncomment and the dashboard all show for this kind."
          },
          "may_file_ticket_alone": {
            "type": "boolean",
            "const": false,
            "title": "May File Ticket Alone",
            "description": "False for every record in this array, by construction. Stated rather than left to be inferred from an\nabsent field, because a reader that has to work it out is a reader that might not.",
            "default": false
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "tester_error_id",
          "label"
        ],
        "title": "TesterErrorReading",
        "description": "How one record of `TesterErrorList.tester_errors` reads: what the persona did, and that it is never a\nticket."
      },
      "Tolerance": {
        "properties": {
          "patience_steps": {
            "type": "integer",
            "minimum": 1.0,
            "title": "Patience Steps"
          },
          "error_threshold": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Error Threshold"
          },
          "latency_sensitivity_ms": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Latency Sensitivity Ms"
          },
          "confusion_threshold": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Confusion Threshold"
          },
          "attempts_per_subtask": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Attempts Per Subtask"
          },
          "abandon_behavior": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/AbandonBehavior"
              },
              {
                "type": "null"
              }
            ],
            "default": "narrates_reason"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "patience_steps",
          "error_threshold"
        ],
        "title": "Tolerance",
        "description": "How much the persona puts up with before giving up. Saram counts, step by step, the steps without visible progress, the errors and slow responses, and the moments with no clear next action, and ends the session `abandoned` the moment one crosses its threshold here; the model playing the persona cannot argue with the count. The persona is also told these limits in their own terms."
      },
      "TopUpRecord": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "amount_usd": {
            "type": "number",
            "title": "Amount Usd"
          },
          "origin": {
            "type": "string",
            "enum": [
              "manual",
              "auto"
            ],
            "title": "Origin"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "open",
              "paid",
              "failed",
              "requires_action",
              "void"
            ],
            "title": "Status"
          },
          "invoice_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Invoice Id"
          },
          "client_secret": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Secret",
            "description": "What the Payment Element confirms in the browser (stripe.confirmPayment / confirmSetup), where SCA happens. Present only while there is something to confirm. Saram never sees the card."
          },
          "failure_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Failure Code",
            "description": "Why a `failed`, `requires_action` or `void` top-up did not go through, for a screen to branch on. When the card was refused it is the bank's reason as Stripe names it - the `decline_code` (`generic_decline`, `insufficient_funds`, ...) or, when there is none, Stripe's error `code` (`card_declined`, `expired_card`, ...): another card may go through. Otherwise it is one of Saram's own: `requires_action` (the bank wants the person to confirm an automatic refill, which only a top-up by hand can do), `payment_failed` (an automatic refill's payment did not complete), `expired` (unpaid after a day), `voided` (the invoice was cancelled before it was paid) or `not_started` (the payment could not be started; try again later). Nothing is charged for any of them. Absent while in flight and once paid."
          },
          "failure_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Failure Message",
            "description": "The same, in a sentence written to be shown to the person: the bank's own words as Stripe gives them for the cardholder ('Your card was declined.'), or Saram's for its own codes."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "amount_usd",
          "origin",
          "status",
          "created_at"
        ],
        "title": "TopUpRecord",
        "description": "One top-up: where it stands, and while it is open, the secret the Payment Element pays it with.\n\n`pending` (the invoice is being made), `open` (awaiting payment), then `paid` (credited to the balance),\n`failed` (declined, or no invoice could be made), `requires_action` (an off-session charge the bank wants the\nperson to authenticate) or `void` (unpaid after a day). Credit is added once, when Stripe says the invoice is\npaid - never on the browser's word. A top-up that ended unpaid says why in `failure_code` and\n`failure_message`."
      },
      "TopUpWrite": {
        "properties": {
          "amount_usd": {
            "type": "integer",
            "maximum": 500.0,
            "minimum": 10.0,
            "title": "Amount Usd",
            "description": "Whole dollars of prepaid credit: the invoice reads 'Saram usage credit, N x $1.00', with tax on top where it applies. Counts against the monthly budget, manual and automatic top-ups together."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "amount_usd"
        ],
        "title": "TopUpWrite",
        "description": "`POST /api/v1/billing/top-ups`: how much credit to buy, in whole dollars."
      },
      "TrustDefaults": {
        "type": "string",
        "enum": [
          "low",
          "medium",
          "high"
        ],
        "title": "TrustDefaults",
        "description": "Accepts prefilled/default choices vs. checks everything"
      },
      "Type": {
        "type": "string",
        "title": "Type",
        "description": "Defect classes only. A moment that worked well is an opportunity of kind `delight`, never an issue: it has no failure for reproduction to replay.",
        "examples": [
          "blocker",
          "friction",
          "confusion",
          "bug",
          "contradiction"
        ],
        "x-classification": true
      },
      "UsageCeiling": {
        "properties": {
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "description": "Sessions the spec plans, one per persona per mission."
          },
          "steps_per_session": {
            "type": "integer",
            "title": "Steps Per Session"
          },
          "wall_clock_s_per_session": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Wall Clock S Per Session",
            "description": "The spec's own wall-clock budget. Absent when it names none, and then only the step budget bounds a\nsession: every step is counted at the most seconds a step is charged for."
          },
          "usage_per_session": {
            "type": "number",
            "title": "Usage Per Session",
            "description": "The most Usage one session can use: every step at its most, or the wall clock spent in full."
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "`sessions x usage_per_session`: the run's ceiling, which `POST /runs` holds against the balance."
          },
          "usage_usd": {
            "type": "number",
            "title": "Usage Usd",
            "description": "The ceiling in dollars, at the plan's price per Usage."
          },
          "usage_price_usd": {
            "type": "number",
            "title": "Usage Price Usd"
          },
          "available_usd": {
            "type": "number",
            "title": "Available Usd",
            "description": "The balance less what the tenant's runs in flight already hold."
          },
          "covered": {
            "type": "boolean",
            "title": "Covered",
            "description": "Whether `available_usd` covers `usage_usd`."
          },
          "steps_per_session_that_fit": {
            "type": "integer",
            "title": "Steps Per Session That Fit",
            "description": "The largest `budget.steps_per_session` that `available_usd` covers with the rest of the spec as written;\n0 when not even one step per session does."
          },
          "enforced": {
            "type": "boolean",
            "title": "Enforced",
            "description": "Whether `POST /runs` refuses this spec with a 402 when `covered` is false. False while the gate runs in\nshadow, and always for Saram's own fleet."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "sessions",
          "steps_per_session",
          "usage_per_session",
          "usage",
          "usage_usd",
          "usage_price_usd",
          "available_usd",
          "covered",
          "steps_per_session_that_fit",
          "enforced"
        ],
        "title": "UsageCeiling",
        "description": "The most Usage a run on this spec can use, and the balance `POST /runs` would hold it against.\n\nComputed the way `POST /runs` holds the ceiling, at the same moment's balance, so a run this says is covered\nis one that starts - unless another run takes the balance between the two calls."
      },
      "UsageChargeRun": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "Who started it, as GET /runs reports it. Absent when nobody was identified."
          },
          "settled_at": {
            "type": "string",
            "format": "date-time",
            "title": "Settled At",
            "description": "When the run's last session was settled this month: the moment its Usage was charged."
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "description": "Its persona sessions settled this month."
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "The Usage its settled sessions used, refunded ones included, to 0.001."
          },
          "usage_usd": {
            "type": "number",
            "title": "Usage Usd",
            "description": "What that Usage cost you, net: refunded_usd is already taken off."
          },
          "refunded_usd": {
            "type": "number",
            "title": "Refunded Usd",
            "description": "Given back for its sessions that failed on Saram's side; each one is also in refunds."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "settled_at",
          "sessions",
          "usage",
          "usage_usd",
          "refunded_usd"
        ],
        "title": "UsageChargeRun",
        "description": "One run's Usage in the month, from the credit ledger: what it used and what that cost, refunds apart."
      },
      "UsageCharges": {
        "properties": {
          "usage_price_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Price Usd",
            "description": "USD per Usage on your plan, in force at the end of the month. Absent on a plan billed in steps. A session is charged at the price in force when it ran."
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "The month's Usage, refunded sessions included, to 0.001.",
            "default": 0
          },
          "usage_usd": {
            "type": "number",
            "title": "Usage Usd",
            "description": "What the month's Usage cost you, net of refunds: the total a screen prints as Saram's charge. Your model provider's bill is never in it (see provider).",
            "default": 0
          },
          "refunded_usd": {
            "type": "number",
            "title": "Refunded Usd",
            "description": "Given back this month for sessions that failed on Saram's side.",
            "default": 0
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "description": "Persona sessions settled this month.",
            "default": 0
          },
          "runs": {
            "items": {
              "$ref": "#/components/schemas/UsageChargeRun"
            },
            "type": "array",
            "title": "Runs",
            "description": "Per run, oldest settlement first, then by run_id."
          },
          "personas": {
            "items": {
              "$ref": "#/components/schemas/UsagePersona"
            },
            "type": "array",
            "title": "Personas",
            "description": "Per persona, most Usage first, then by persona_id; deleted personas included."
          },
          "refunds": {
            "items": {
              "$ref": "#/components/schemas/UsageRefund"
            },
            "type": "array",
            "title": "Refunds",
            "description": "Every session refunded this month, oldest refund first, then by run_id and session_id."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "UsageCharges",
        "description": "The month's prepaid Usage, from Saram's credit ledger: what your personas used and what it cost you, by run\nand by persona, and the sessions that were not charged.\n\nOnly a plan that prices Usage (Free, Pro) has rows here; a plan billed in steps reports zeros, and its charge\nis `billable_usd` beside this. A session is in the month it was settled in, which is the month it finished."
      },
      "UsageMeter": {
        "properties": {
          "meter": {
            "type": "string",
            "title": "Meter",
            "description": "steps (one body tool call, the platform fee's unit), sessions (persona sessions that ran a step), browser_seconds (pool time), model_usd_hosted (model spend on a Saram-held credential), model_usd_byo (the same on your own provider key, which the invoice charges at $0) or usage (persona steps weighted by how long each took, recorded per session; reported, not billed)."
          },
          "role": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Role",
            "description": "Which model caller the quantity is attributed to: actor, judge or reproduction. The counting meters are the actor's; a model meter is written per role that had anything priced."
          },
          "quantity": {
            "type": "number",
            "title": "Quantity",
            "description": "In the meter's own unit: a count, seconds, or USD."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "meter",
          "quantity"
        ],
        "title": "UsageMeter",
        "description": "One line of the table of meters: how much of one meter one model caller used in the period."
      },
      "UsagePersona": {
        "properties": {
          "persona_id": {
            "type": "string",
            "title": "Persona Id",
            "description": "The persona's address as the run named it: id, or id@version."
          },
          "persona_label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Label",
            "description": "Its display name as recorded when its latest session this month was settled - not as it reads now. Absent for a persona that declared no name; show persona_id then."
          },
          "deleted": {
            "type": "boolean",
            "title": "Deleted",
            "description": "True when the persona no longer exists: checked when this report is read, so a line can say 'Dana (deleted)' and still carry every dollar its sessions cost."
          },
          "runs": {
            "type": "integer",
            "title": "Runs",
            "description": "How many runs it took part in this month."
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "description": "Its sessions settled this month."
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "The Usage its sessions used, refunded ones included, to 0.001."
          },
          "usage_usd": {
            "type": "number",
            "title": "Usage Usd",
            "description": "What that cost you, net of refunds."
          },
          "refunded_usd": {
            "type": "number",
            "title": "Refunded Usd",
            "description": "Given back for its sessions that failed on Saram's side."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "persona_id",
          "deleted",
          "runs",
          "sessions",
          "usage",
          "usage_usd",
          "refunded_usd"
        ],
        "title": "UsagePersona",
        "description": "One persona's Usage in the month, under the name it had when its runs ran (section 2.3).\n\nGrouped by `persona_id`, which carries the version: `dana_roofer_novice@1` and `@2` are two lines. A persona\nthat was renamed keeps the name its sessions were settled under; one that has since been deleted keeps its line\nand says so, because the money it used is still money you spent."
      },
      "UsageRefund": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "persona_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Id"
          },
          "persona_label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Persona Label",
            "description": "As recorded when the session was settled."
          },
          "fault": {
            "type": "string",
            "const": "saram",
            "title": "Fault",
            "description": "Whose failure it was. Always saram: the harness - the browser pool, a lost worker, a setup step in Saram's own code. A session your model provider or your product ended is charged, and is not here.",
            "default": "saram"
          },
          "usage": {
            "type": "number",
            "title": "Usage",
            "description": "The Usage the session used before it failed, to 0.001."
          },
          "amount_usd": {
            "type": "number",
            "title": "Amount Usd",
            "description": "What was given back: the whole of the session's debit."
          },
          "refunded_at": {
            "type": "string",
            "format": "date-time",
            "title": "Refunded At"
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Note",
            "description": "The refund's reason as the ledger recorded it."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "session_id",
          "usage",
          "amount_usd",
          "refunded_at"
        ],
        "title": "UsageRefund",
        "description": "One session that was not charged because it failed on Saram's side (section 2.4): debited, then refunded\nwhole, and both kept, so the Usage it would have cost stays visible."
      },
      "UsageReport": {
        "properties": {
          "period": {
            "type": "string",
            "title": "Period",
            "description": "The calendar month these figures cover, YYYY-MM, in UTC."
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "title": "From",
            "description": "First moment of the period, inclusive (UTC)."
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "title": "To",
            "description": "First moment of the next period, exclusive (UTC)."
          },
          "tenant_id": {
            "type": "string",
            "title": "Tenant Id"
          },
          "tenant_slug": {
            "type": "string",
            "title": "Tenant Slug"
          },
          "tenant_name": {
            "type": "string",
            "title": "Tenant Name"
          },
          "plan": {
            "type": "string",
            "title": "Plan",
            "description": "pilot, byo or hosted: the price list this usage is invoiced under."
          },
          "house_models": {
            "type": "boolean",
            "title": "House Models",
            "description": "True when Saram has agreed to carry this tenant's model spend, so runs on a bring-your-own-keys plan may sign their model calls with a Saram-held credential. It changes no figure here - the steps bill at the plan's price either way - and it is what explains a model_usd_hosted total that is not zero on such a plan.",
            "default": false
          },
          "meters": {
            "items": {
              "$ref": "#/components/schemas/UsageMeter"
            },
            "type": "array",
            "title": "Meters",
            "description": "Per meter and role, summed over the period's runs."
          },
          "totals": {
            "$ref": "#/components/schemas/UsageTotals",
            "description": "Per meter, roles collapsed."
          },
          "runs": {
            "items": {
              "$ref": "#/components/schemas/UsageRun"
            },
            "type": "array",
            "title": "Runs",
            "description": "The runs metered in the period, oldest first."
          },
          "model_usd_complete": {
            "type": "boolean",
            "title": "Model Usd Complete",
            "description": "Whether the model meters cover every model caller in a run: the persona loop under role actor, the judge and the reproduction check under their own roles. A run metered before the judge was priced (2026-09-25) carries the persona loop's spend alone, so a month reaching back past that is a floor for those runs. The step meter is complete.",
            "default": false
          },
          "billed_steps": {
            "type": "integer",
            "title": "Billed Steps",
            "description": "The month's billed steps: the persona loop's (role actor), never the reproduction replay's. This, not totals.steps, is what billable_usd is a multiplication of.",
            "default": 0
          },
          "step_price_usd": {
            "type": "number",
            "title": "Step Price Usd",
            "description": "USD per billed step under your plan, or your own agreed price where one was set. The price in force at the end of this period; a run is billed at the price in force when it ran, so across a mid-month price change this figure times billed_steps is not billable_usd, and the per-run billable_usd figures say why.",
            "default": 0
          },
          "included_steps": {
            "type": "integer",
            "title": "Included Steps",
            "description": "Billed steps the plan's monthly fee covers before the step price applies.",
            "default": 0
          },
          "monthly_fee_usd": {
            "type": "number",
            "title": "Monthly Fee Usd",
            "description": "The plan's monthly fee, included in billable_usd.",
            "default": 0
          },
          "monthly_cap_usd": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Monthly Cap Usd",
            "description": "On a plan that bills steps (pilot): what this month may bill before a new run is refused with a 402. Absent means no cap. The refusal compares billable_usd below against this figure. Not a limit on a plan that prices Usage (free, pro), whose runs are limited by the available balance instead."
          },
          "billable_usd": {
            "type": "number",
            "title": "Billable Usd",
            "description": "What Saram bills you for this month, to the cent: billed steps at the price in force when each run ran, less included_steps, plus monthly_fee_usd. Model spend on your own keys is not in it.",
            "default": 0
          },
          "usage": {
            "$ref": "#/components/schemas/UsageCharges",
            "description": "The month's prepaid Usage by run and by persona, with its refunds: Saram's charge on a plan that prices Usage. Read from the credit ledger, apart from the step meters above."
          },
          "provider": {
            "$ref": "#/components/schemas/ProviderSpend",
            "description": "Your model provider's estimated bill for the month: shown apart, never added to usage.usage_usd."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "period",
          "from",
          "to",
          "tenant_id",
          "tenant_slug",
          "tenant_name",
          "plan"
        ],
        "title": "UsageReport",
        "description": "One month of your tenant's usage: the meters, the runs behind them, and the totals (`GET /api/v1/usage`).\n\nYour own tenant and no other: the ledger is filtered by the tenant the request's key belongs to, so a\nneighbour's run is not in these figures and cannot be. `GET /api/v1/usage.csv` is the same report as the\nfile an invoice is written from.\n\n**One meter is billed and four are reported.** `billable_usd` is the month's bill: billed steps at\n`step_price_usd`, less `included_steps`, plus `monthly_fee_usd`. The model spend, the session count and\nthe browser seconds are here so you can see what your keys spent and how long your personas were in your\nproduct; none of them is multiplied by anything. The arithmetic is served rather than left to the\nclient, so a screen and an invoice cannot disagree."
      },
      "UsageRun": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "caller": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Caller",
            "description": "Who started it, as GET /runs reports it. Absent when nobody was identified."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "title": "Occurred At",
            "description": "When the run finished, which is the moment its usage is dated."
          },
          "steps": {
            "type": "integer",
            "title": "Steps",
            "default": 0
          },
          "billed_steps": {
            "type": "integer",
            "title": "Billed Steps",
            "description": "The steps this run is billed for: the persona loop's (role actor), never the reproduction replay's, which are included. Equal to steps until replay steps are counted.",
            "default": 0
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "default": 0
          },
          "browser_seconds": {
            "type": "number",
            "title": "Browser Seconds",
            "default": 0
          },
          "model_usd_hosted": {
            "type": "number",
            "title": "Model Usd Hosted",
            "default": 0
          },
          "model_usd_byo": {
            "type": "number",
            "title": "Model Usd Byo",
            "default": 0
          },
          "billable_usd": {
            "type": "number",
            "title": "Billable Usd",
            "description": "What this run adds to the month's bill: its billed steps, less any covered by the plan's included allowance, at the step price in force when it ran. The plan's monthly fee is not in it - that is on the month, not on a run.",
            "default": 0
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "occurred_at"
        ],
        "title": "UsageRun",
        "description": "One run's line on the invoice: its meters with the roles collapsed, because a line is money per run."
      },
      "UsageTotals": {
        "properties": {
          "steps": {
            "type": "integer",
            "title": "Steps",
            "default": 0
          },
          "sessions": {
            "type": "integer",
            "title": "Sessions",
            "default": 0
          },
          "browser_seconds": {
            "type": "number",
            "title": "Browser Seconds",
            "default": 0
          },
          "model_usd_hosted": {
            "type": "number",
            "title": "Model Usd Hosted",
            "default": 0
          },
          "model_usd_byo": {
            "type": "number",
            "title": "Model Usd Byo",
            "default": 0
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "UsageTotals",
        "description": "The period's totals per meter, roles collapsed: the figures a price list is applied to."
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      },
      "Verbosity": {
        "type": "string",
        "enum": [
          "terse",
          "normal",
          "verbose"
        ],
        "title": "Verbosity",
        "description": "How long the persona's diary entries run. `terse`: short. `normal`: as much as the moment deserves. `verbose`: in full, with the detail they noticed."
      },
      "Verdict": {
        "properties": {
          "decision": {
            "$ref": "#/components/schemas/Decision"
          },
          "decision_text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Decision Text"
          },
          "reason": {
            "type": "string",
            "title": "Reason"
          },
          "reason_paraphrase": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason Paraphrase"
          },
          "quotes": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Quotes"
          },
          "step_refs": {
            "items": {
              "type": "integer"
            },
            "type": "array",
            "minItems": 1,
            "title": "Step Refs"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "decision",
          "reason",
          "step_refs"
        ],
        "title": "Verdict",
        "description": "SUBJECTIVE. The persona's answer at session end to the run's verdict prompt - the run spec's `mission.verdict_prompt` when it has one, `persona.verdict_prompt` otherwise - grounded in diary quotes. A persona pack's question is about the product the pack was written for, so a run that points a pack at another target sets the mission's and the pack's is not applicable for that run. Which of the two this answers, and its text, are on the row: `outcomes[].verdict_prompt_source` and `outcomes[].verdict_prompt`, which sit there rather than here so that a session with no verdict still names the question that went unanswered. Consumers must tag it as subjective. `decision_text`, `reason` and `quotes` are quotation, verbatim from the diary at `step_refs`; `reason_paraphrase` is the only field written in the judge's voice, and a consumer that shows it must label it as the judge's, never as the persona's words."
      },
      "Verdict1": {
        "type": "string",
        "title": "Verdict1",
        "description": "`supported`: the checker answered yes with a quote that gates against the ledger text of the cited steps - the only verdict that lets `status` read `verified`. `unsupported`: the checker answered no; the title claims more than the quoted line shows. `ungrounded`: the checker answered yes but its quote is not in the ledger, so the answer is discarded the way an ungrounded diary quote is and recorded in the gate ledger. `skipped`: no marker matched, or the check did not run. A classification: a later verdict is additive.",
        "examples": [
          "supported",
          "unsupported",
          "ungrounded",
          "skipped"
        ],
        "x-classification": true
      },
      "VerdictDiscarded": {
        "properties": {
          "judge_decision": {
            "$ref": "#/components/schemas/JudgeDecision"
          },
          "gate_reason": {
            "type": "string",
            "title": "Gate Reason"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "judge_decision",
          "gate_reason"
        ],
        "title": "VerdictDiscarded",
        "description": "NOT A VERDICT. The answer a judge gave about this session that the verbatim-quote gate threw away because it could not ground itself in the persona's words: none of its quotes is in the diary at the steps it cites, or none of the steps it cites is a step of the session. Present only on a row a judge read that has no `verdict`, so never beside `verdict` or `observer_error`. It is what tells a judge that answered and was not grounded apart from a persona who gave no answer: both rows carry no `verdict` and both count `undecided` in `summary.verdicts`, which counts grounded verdicts alone, and only this one says an answer was given. The decision is the judge's reading with nothing in the diary to support it, so a consumer never shows it as the persona's verdict, never counts it and never acts on it; what it points a reader at is the judge's citations. Nothing the judge quoted is carried here, because none of it is verifiably the persona's: the records, the dropped quotes among them, are in the session's gate ledger beside its observation, and the discard is counted in `summary.gate_discards`. Absent on a row with no `verdict` means nothing was discarded: the judge returned no answer, no judge read the session (`observer_error`), or no question was in play (`verdict_prompt_source` absent). An artifact written before 0.6.3 carries none either, and `contracts_version` is what tells those apart."
      },
      "VerdictList": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "The run's status. An empty `verdicts` is only a result once this is terminal."
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verdicts"
              },
              {
                "type": "null"
              }
            ],
            "description": "The artifact's cohort read (`summary.verdicts`)."
          },
          "verdicts": {
            "items": {
              "$ref": "#/components/schemas/VerdictRow"
            },
            "type": "array",
            "title": "Verdicts"
          },
          "advisory": {
            "type": "string",
            "title": "Advisory",
            "description": "How to read this whole answer: persona feedback, not defects. Never a ticket on its own."
          },
          "subjective_fields": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Subjective Fields",
            "description": "Fields on each row that are the persona's opinion - for a verdict, the verdict itself."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "run_id",
          "status",
          "verdicts",
          "advisory",
          "subjective_fields"
        ],
        "title": "VerdictList",
        "description": "A run's persona verdicts: each session's answer to the run's verdict question, grounded in the persona's\nown diary quotes, and the cohort's tally. Persona feedback, never a ticket on its own."
      },
      "VerdictPromptSource": {
        "type": "string",
        "title": "VerdictPromptSource",
        "description": "Whose question `verdict` answers: the run spec's `mission.verdict_prompt`, or the persona pack's `persona.verdict_prompt` when the mission carried none. `mission` is also the record that the pack's own question was not applicable for this run, which is what makes an absent `verdict` beside it readable: the diary did not answer a question about the product actually under test, which is a run-spec problem, not a judge that could not ground itself (that one names its discarded answer in `verdict_discarded`). Absent means no verdict question was in play: neither the mission nor the pack carried one, so `verdict` is absent by construction and `summary.verdicts` counts the session `undecided` without that being anything the judge failed at. An artifact written before this release carries none either, and `contracts_version` is what tells the two apart. Absent beside `observer_error` is neither: no judge ran, so nobody resolved which question was in play.",
        "examples": [
          "mission",
          "persona"
        ],
        "x-classification": true
      },
      "VerdictRow": {
        "properties": {
          "session_id": {
            "type": "string",
            "title": "Session Id"
          },
          "persona_id": {
            "type": "string",
            "title": "Persona Id"
          },
          "mission_id": {
            "type": "string",
            "title": "Mission Id"
          },
          "result": {
            "type": "string",
            "title": "Result"
          },
          "verdict_prompt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Verdict Prompt",
            "description": "The verdict question this session was asked, verbatim: the one `verdict` answers. Absent when no question\nwas in play for the session, and on every row of an artifact written before the outcome carried it (0.4.1)."
          },
          "verdict_prompt_source": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/VerdictPromptSource"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whose question `verdict` answers: the run spec's `mission.verdict_prompt`, or the persona pack's when the\nmission carried none. Present exactly when `verdict_prompt` is; `mission` beside an absent\n`verdict` is a persona who was asked the run's question and did not answer it - unless `verdict_discarded`\nsays the judge answered for them and could not ground the answer."
          },
          "verdict": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verdict"
              },
              {
                "type": "null"
              }
            ]
          },
          "verdict_discarded": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/VerdictDiscarded"
              },
              {
                "type": "null"
              }
            ],
            "description": "NOT A VERDICT. The answer the judge gave that the quote gate threw away because none of it could be grounded in\nthe persona's diary at the steps it cited, and why (`artifact.outcomes[].verdict_discarded`). Set only on a row\nwith no `verdict`; absent there means nothing was discarded - the judge returned no answer, or no judge read the\nsession (`observer_error`). Never show `judge_decision` as the persona's verdict and never count it: the cohort's\n`summary` counts this session `undecided`. Absent on every row of an artifact written before contracts 0.6.3."
          },
          "observer_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Observer Error",
            "description": "Why no judge read this session, when none did, in Saram's words (`artifact.outcomes[].observer_error`): its\njudge pass failed after its retries, so the absent `verdict` is a question nobody asked a model, not one the\npersona left unanswered. The model provider's status and own message when the provider refused, the `providers`\nentry's refusal when it did not resolve, Saram's sentence otherwise. Absent on every session a judge read, and\non every row of an artifact written before contracts 0.6.1; one written before 0.6.3 carries the failure as it\nwas recorded."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "session_id",
          "persona_id",
          "mission_id",
          "result"
        ],
        "title": "VerdictRow",
        "description": "SUBJECTIVE: one session's verdict as the artifact's `outcomes[]` carries it. Persona feedback, never a\nticket on its own."
      },
      "Verdicts": {
        "properties": {
          "positive": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Positive"
          },
          "negative": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Negative"
          },
          "undecided": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Undecided"
          },
          "top_reasons": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Top Reasons"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Verdicts",
        "description": "Cohort-level read of persona verdicts. Subjective, derived from outcomes[].verdict. positive + negative + undecided = sessions: a session with no grounded verdict is undecided, not absent. That includes a session whose judge answered and had the answer discarded by the quote gate: its row names it in `outcomes[].verdict_discarded`, and it is still counted here as undecided, because this tally counts only verdicts grounded in the persona's words."
      },
      "Verification": {
        "properties": {
          "status": {
            "$ref": "#/components/schemas/Status"
          },
          "method": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Method"
              },
              {
                "type": "null"
              }
            ]
          },
          "markers": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Markers"
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Note"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status"
        ],
        "title": "Verification",
        "description": "Whether the claim was checked, and by what. The two are separate questions and were conflated once already: `status` is the answer a reader acts on, `method` only says which instrument produced it. Consumers show `status`."
      },
      "Video": {
        "properties": {
          "ref": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ref"
          },
          "ts_start_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts Start S"
          },
          "ts_end_s": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ts End S"
          }
        },
        "type": "object",
        "title": "Video",
        "description": "A clip of a session's video that shows the issue: where the video is stored (`ref`), and where the clip starts and ends, in seconds from the start of the recording."
      },
      "Vocabulary": {
        "properties": {
          "knows": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Knows"
          },
          "unknown": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unknown"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Vocabulary",
        "description": "Terms the persona does and does not understand. More specific than the literacy enums in `priors`; the renderer tells the actor to say so when an unknown term appears rather than guess."
      },
      "Voice": {
        "properties": {
          "style": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Style"
          },
          "verbosity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Verbosity"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "title": "Voice",
        "description": "Narration style only. It never influences what the persona does."
      },
      "WebhookAck": {
        "properties": {
          "received": {
            "type": "boolean",
            "title": "Received",
            "default": true
          },
          "applied": {
            "type": "string",
            "title": "Applied"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "applied"
        ],
        "title": "WebhookAck",
        "description": "What each vendor's webhook - `/api/v1/webhooks/workos`, `/stripe` and `/github` - answers a delivery with.\n\nA body at all, rather than a bare 200, because the vendor's delivery log is where an operator debugs a\nmirror that is not mirroring: `applied` says what the event actually did here - including `ignored`, which\nis the honest answer for an event this deployment has no reading for, for an organisation nobody has made a\ntenant, and for an installation of the GitHub App nobody has linked."
      },
      "RunSpec": {
        "title": "RunSpec",
        "description": "Input to the control plane. A run = target x persona set x missions x budget x baseline.",
        "type": "object",
        "required": [
          "target",
          "personas",
          "missions",
          "budget"
        ],
        "additionalProperties": false,
        "properties": {
          "run_id": {
            "type": "string",
            "description": "Assigned by the control plane if absent"
          },
          "trigger": {
            "type": "object",
            "description": "Who or what started the run, and against which code: a record kept with the run, which changes nothing about what it does but in two places. A run billed to a ChatGPT subscription login is refused when `kind` is `ci`, and `pr` and `commit` are copied onto the artifact's `target`, where `pr` is the pull request the GitHub connector's comment goes on.",
            "additionalProperties": false,
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "manual",
                  "api",
                  "ci"
                ],
                "description": "How the run was started: `manual`, by a person at a keyboard; `api`, through the API or a coding agent's MCP tools, which is what `start_run` writes when the spec leaves it out; `ci`, by a CI job with nobody watching. The caller's own claim, recorded as given. `ci` is refused on a run billed to a ChatGPT subscription login, which serves only runs a person starts."
              },
              "actor": {
                "type": "string",
                "description": "User, token, or CI job that triggered the run"
              },
              "pr": {
                "type": "integer"
              },
              "commit": {
                "type": "string"
              },
              "diff_ref": {
                "type": "string",
                "description": "Where the PR diff can be fetched, for diff-aware targeting. Accepted and recorded; nothing reads it yet."
              }
            }
          },
          "target": {
            "type": "object",
            "description": "The product under test: the URL every session opens on (a mission's own `entry_url` overrides it), a name for it, the tunnel to it when it runs on your own machine (`tunnel`), and, when it needs them, how a session gets past a gate in front of the whole deployment (`auth`) and how the product's state is reset and its test accounts handed out between sessions (`seed`).",
            "required": [
              "entry_url"
            ],
            "additionalProperties": false,
            "properties": {
              "entry_url": {
                "type": "string",
                "format": "uri"
              },
              "name": {
                "type": "string"
              },
              "tunnel": {
                "type": "string",
                "pattern": "^tun_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9a-f]{8}$",
                "description": "For a product running on your own machine: the id of the tunnel you opened to it (`tun_...`), which Saram's browsers reach it through and nothing else can. Not available yet: until Saram opens and binds tunnels, every spec naming one is refused with a 422 (`tunnels to your own machine are not available yet`) by `POST /api/v1/runs`, `POST /api/v1/runs/validate` and the `validate_spec` and `start_run` tools, before any run exists. Naming it binds that tunnel to this run, and the spec is refused unless the tunnel is one the tenant starting the run opened, is still open and is bound to no other run, and every URL the run opens on the target - `entry_url`, each mission's `entry_url` and `seed.reset_hook` - is on the tunnel's hostname. Saram puts the tunnel's own door in front of the run, so a spec naming a tunnel carries no `auth` of its own. The tunnel closes when the run ends."
              },
              "auth": {
                "type": "object",
                "x-closed": true,
                "description": "How a session gets past the target's own door: the gate in front of the whole deployment, not the product's sign-in. `auth` and `seed.accounts` are one mechanism seen from two ends and both resolve `secret-store` entries; which end a credential belongs at is decided by whether it is the target's or the persona's. **The target's** (one for the run, the same for every session, nothing a person would be shown): `basic` on a staging host, a deployment-protection bypass `header` on a Vercel or Cloudflare Access preview, a `cookie` the whole run shares. That is this object: the pool receives `secret_ref` as `session_create.target_auth_secret_ref` and applies the entry's `basic` and `headers` to requests to the target's site (`secret-store` `entry.basic`) and its `cookies` to the browser context, and the control plane sends the same entry's `basic`, `headers` and the `cookies` scoped to the hook on its POST to `seed.reset_hook`, which is behind the same door. **The persona's** (one per session, and a persona may be asked to type it): `seed.accounts`, one ref per session, where the pool applies the entry's `cookies` and the prompt renderer reads `email`, `password`, `inbox` and `notes`. A run can carry both - a staging host behind basic auth whose personas each sign in with their own account - and where the two name the same credential the account's wins. Closed by design: credentials arrive only as `secret_ref`, so a field this object does not list is an inline secret and every reader rejects it, not just the producer.",
                "additionalProperties": false,
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "none",
                      "basic",
                      "cookie",
                      "header",
                      "login_flow"
                    ],
                    "description": "What the door is, for a reader of the spec. It selects nothing: the pool applies whatever the entry behind `secret_ref` carries, so `basic` and `header` on one entry are both applied whichever of the two this says. `none` is a target with no gate (`secret_ref` then says nothing and is omitted); `login_flow` is the one kind that is not a gate at all but a form the persona fills in, which is `seed.accounts` and `login_url`."
                  },
                  "secret_ref": {
                    "type": "string",
                    "description": "Reference into the secret store; never inline credentials. Carried to the pool as `session_create.target_auth_secret_ref`, once per session, unchanged; the pool resolves it and applies `basic` and `headers` to requests to the target's site (`secret-store` `entry.basic`) and `cookies` on the context. The same ref for every session of the run - a per-session credential is `seed.accounts`, not this."
                  },
                  "login_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the product's own sign-in form is, with `kind: login_flow`. Descriptive only: the persona is given the account behind `seed.accounts` and finds the form itself, which is the behaviour under test, so nothing navigates here."
                  }
                }
              },
              "seed": {
                "type": "object",
                "x-closed": true,
                "additionalProperties": false,
                "description": "Environment provisioning. Every e2e product bleeds here; make it explicit. Closed by design (x-closed) for the same reason as `auth`: `accounts` are secret refs and nothing inline may ride beside them.",
                "properties": {
                  "reset_hook": {
                    "type": "string",
                    "format": "uri",
                    "description": "Endpoint to reset state before each session. The control plane POSTs it once per run with `{\"feature_flags\": ...}` (the whole target), before each session with `session_id` added (that session's partition), and before each reproduction replay attempt the same way. It sits behind the target's own door like every other url on the host, so the POST carries the entry behind `auth.secret_ref`: its `basic` as HTTP basic auth, its `headers` verbatim and, as one `Cookie` header, those of its `cookies` the browser would send to the hook's url - the same credential the pool opens the browser's door with. A cookie goes when its scope matches the hook as Playwright scopes it in the pool: a `url` names its host alone, that url's path up to its last `/`, and https only; otherwise `domain` (with a leading dot, every subdomain too) and `path` (a prefix ending at a `/`) do, and `secure` keeps it off plain http. A cookie scoped to another host or path is not sent, and neither is any cookie when `headers` already name `Cookie`. A hook that answers anything but 2xx fails the reset."
                  },
                  "accounts": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Pool of test account secret_refs, one per session, handed out in session order; sessions past the end of the list get none and the persona is told they have no login. The other half of `auth` (see its description for the split): the persona's credential rather than the target's. Each ref reaches the pool as `session_create.auth_secret_ref`, which applies the entry's `cookies` (and `basic`/`headers`, for the rare account that overrides the target's own), while the renderer tells the persona which credentials they 'have' (email, password) and which they do not (inbox access). Never inline."
                  },
                  "feature_flags": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "personas": {
            "type": "array",
            "minItems": 1,
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "description": "persona id@version from the registry, e.g. novice_mobile_skimmer@2"
                },
                {
                  "$ref": "#/components/schemas/Persona"
                }
              ]
            }
          },
          "missions": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/RunSpecMission"
            }
          },
          "budget": {
            "type": "object",
            "description": "How far each session may go and how much the run may spend. A session ends at the first of its caps it reaches: `steps_per_session` steps (`end_reason` `budget_steps`), `tokens_per_session` model tokens billed, which is the prompt not read from cache plus the reply (`budget_tokens`), or `wall_clock_s_per_session` seconds (`budget_wall_clock`). `max_concurrent_sessions` is the most of the run's sessions that run at once, and your plan's own limit bounds it. `total_cost_usd` pauses the run once the sessions it has recorded have cost that much: no further session starts until it is resumed, and the ones already running finish.",
            "required": [
              "steps_per_session"
            ],
            "additionalProperties": false,
            "properties": {
              "steps_per_session": {
                "type": "integer",
                "minimum": 1
              },
              "tokens_per_session": {
                "type": "integer",
                "minimum": 1
              },
              "wall_clock_s_per_session": {
                "type": "integer",
                "minimum": 1
              },
              "max_concurrent_sessions": {
                "type": "integer",
                "minimum": 1
              },
              "total_cost_usd": {
                "type": "number",
                "minimum": 0,
                "description": "Soft cap; run pauses when reached"
              }
            }
          },
          "baseline_run_id": {
            "type": "string",
            "description": "Run to diff against, typically the last run on main"
          },
          "providers": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "entries"
            ],
            "description": "Which model, on which provider or router, with which credential, every model call in this run uses. `entries` are named; `default` names the entry every role uses unless `roles` says otherwise; `roles` binds the three callers (the actor loop, the observer's judge for session judgment and run merge, the reproduction model check) to entry names. A role that resolves to no entry is a producer error. When this block is present it wins over `actor.actor_model` and `actor.judge_model`; when absent, those two and the worker's own process credential apply, which is every run written before 0.4.1. Resolved by the worker that makes the call, at the start of its activity, never by the API or a workflow.",
            "properties": {
              "entries": {
                "type": "object",
                "minProperties": 1,
                "propertyNames": {
                  "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
                },
                "additionalProperties": {
                  "$ref": "#/components/schemas/RunSpecProvider"
                }
              },
              "default": {
                "type": "string",
                "description": "Entry name. Absent with exactly one entry means that entry."
              },
              "roles": {
                "type": "object",
                "description": "Which entry, by name, each of the run's three model callers uses: `actor`, the model that plays the persona; `judge`, the judge that reads each session and merges the run's findings; `reproduction`, the model check made when an issue is replayed. A role left out uses `default`.",
                "additionalProperties": false,
                "properties": {
                  "actor": {
                    "type": "string"
                  },
                  "judge": {
                    "type": "string"
                  },
                  "reproduction": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "actor": {
            "type": "object",
            "additionalProperties": false,
            "description": "How the persona sees the page and which models drive it: the observation mode, the persona and judge models, when a screenshot is taken, and whether the session is recorded on video.",
            "properties": {
              "observation_mode": {
                "type": "string",
                "enum": [
                  "vision",
                  "tree",
                  "dom_diff",
                  "hybrid"
                ],
                "default": "vision",
                "description": "How the persona perceives the page. `vision`, the default and the only mode supported today: the persona sees a screenshot of the page when it chooses to look. `tree`, `dom_diff` and `hybrid` are reserved for modes that read the page's structure instead of, or beside, a screenshot, and nothing runs them yet: a spec naming one is refused with a 422 by `POST /api/v1/runs`, `POST /api/v1/runs/validate` and the `validate_spec` and `start_run` tools, before any run exists."
              },
              "actor_model": {
                "type": "string",
                "description": "Model id for navigation steps"
              },
              "judge_model": {
                "type": "string",
                "description": "Model id for the observer"
              },
              "screenshot_policy": {
                "type": "string",
                "enum": [
                  "on_look",
                  "every_step",
                  "decision_points",
                  "on_escalation"
                ],
                "default": "on_look",
                "description": "`on_look`: the persona sees a screenshot only when it chooses to look at the page. `every_step`: a screenshot after every action is also kept in the session's trace for the judge."
              },
              "record_video": {
                "type": "boolean",
                "default": true
              }
            }
          },
          "connectors": {
            "type": "object",
            "description": "Where the run's results are delivered once its artifact is assembled: `github` files issues and comments on a pull request in your repository, and `webhook` sends one signed POST to a URL of yours. Both are optional; a run with neither is read through the API, the dashboard and the MCP tools.",
            "additionalProperties": false,
            "properties": {
              "github": {
                "type": "object",
                "description": "Deliver the run to the GitHub repository `repo` (`owner/name`). `create_issues` files a GitHub issue for each issue that reproduced, is not specific to one persona and is rated at least `min_severity_for_issue`; `pr_comment` posts one comment on the run on the pull request named in `trigger.pr`, created once and updated after that. Both are off by default, so a block that turns neither on writes nothing. `check_run` is accepted and nothing posts a check run yet. Delivery writes as Saram's GitHub App where it is installed on `repo`, and otherwise with `token_ref`.",
                "additionalProperties": false,
                "properties": {
                  "repo": {
                    "type": "string"
                  },
                  "create_issues": {
                    "type": "boolean",
                    "default": false
                  },
                  "pr_comment": {
                    "type": "boolean",
                    "default": false
                  },
                  "check_run": {
                    "type": "boolean",
                    "default": false
                  },
                  "min_severity_for_issue": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ],
                    "default": "medium",
                    "description": "The least severe issue `create_issues` files, from least to most: `low`, `medium` (the default), `high`, `critical`. An issue rated below it stays in the run's artifact and is not filed."
                  },
                  "token_ref": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9+.-]*://.+$",
                    "description": "Reference to a GitHub token that may write issues and comments on `repo`, never the token: `awssm://<secret name>` under the requesting tenant's own prefix (a credential of kind `github`, POST /api/v1/credentials), or `env://NAME` on an ungated local stack. The fallback, not the connector's identity: delivery authenticates as the deployment's GitHub App installation on `repo` and reads this only when the App is not installed there. With neither, every record the delivery would have written fails with that reason; no other credential is tried, so a run never writes to its repo as somebody else. Resolved by the worker that delivers, at delivery."
                  }
                }
              },
              "webhook": {
                "type": "object",
                "x-closed": true,
                "additionalProperties": false,
                "required": [
                  "url",
                  "secret_ref"
                ],
                "description": "One signed POST when the run has finished and its artifact is assembled: a small JSON summary (run id, status, session and issue counts, links to the run and the artifact), so a CI job or a deploy pipeline can act on a run without polling. Sent once, after the run's row is closed, by an activity that never fails the run: a receiver that answers 5xx, 429 or nothing is retried a bounded number of times and then given up on, and the outcome is recorded on the run. A run that is cancelled, or that fails before an artifact exists, sends nothing. Closed by design: the signing key arrives only as `secret_ref`, so a field this object does not list is an inline secret and every reader rejects it.",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the POST goes. `https` only; the control plane refuses a URL naming a loopback, private, link-local or otherwise non-public address, whether written as one or resolved to one at delivery, unless the deployment allows private hosts (a local stack), and then `http` is accepted for a loopback host too. Redirects are not followed."
                  },
                  "secret_ref": {
                    "type": "string",
                    "pattern": "^[a-z][a-z0-9+.-]*://.+$",
                    "description": "Reference to the HMAC-SHA256 key the body is signed with, never the key: `awssm://<secret name>` under the requesting tenant's own prefix (a credential of kind `webhook`, POST /api/v1/credentials), or `env://NAME` on an ungated local stack. Resolved by the worker that delivers, at delivery. The signature is `Saram-Signature: t=<unix seconds>,v1=<hex>`, the digest being HMAC-SHA256 over `<t>.<raw body>` - the scheme Stripe uses - so a receiver verifies the raw bytes it received and refuses a timestamp outside its own tolerance."
                  }
                }
              },
              "webhook_url": {
                "type": "string",
                "format": "uri",
                "description": "Never read. Reserved since 0.3 and superseded by `webhook` (0.6.0), which carries the signing key a bare URL has no room for; kept so a spec that sets it still validates."
              }
            }
          },
          "files": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "ref",
                "name",
                "description"
              ],
              "description": "One file a persona brings with them: the uploaded file (`ref`), what the persona and the target call it (`name`), what the persona knows about it (`description`), and which sessions carry it (`assign`).",
              "properties": {
                "ref": {
                  "type": "string",
                  "pattern": "^saram-file://[a-z0-9][a-z0-9_-]{0,63}/f_[a-z2-7]{26}$",
                  "description": "A file uploaded through POST /api/v1/files by the tenant starting the run: saram-file://<tenant_id>/<file_id>, where the owner segment is that tenant's id. A file uploaded before files were a tenant's carries the name of the caller that uploaded it there instead, and only that caller may name it."
                },
                "name": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$",
                  "description": "What the persona calls it and what the target receives as the filename. Unique within the run."
                },
                "description": {
                  "type": "string",
                  "minLength": 1,
                  "description": "What the persona knows about it, in one sentence, briefed verbatim."
                },
                "assign": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "personas": {
                      "type": "array",
                      "minItems": 1,
                      "items": {
                        "type": "string"
                      }
                    },
                    "missions": {
                      "type": "array",
                      "minItems": 1,
                      "items": {
                        "type": "string",
                        "pattern": "^[a-z0-9_]+$"
                      }
                    }
                  },
                  "description": "Which sessions carry it: those matching every list given. Absent means every session."
                }
              }
            },
            "description": "Files the personas bring with them. Bytes never travel in the spec: upload each file with POST /api/v1/files first and name it here by the `ref` that answers. A file belongs to the tenant that uploaded it, so any key or member of that tenant may name it in a run and nobody else may."
          }
        },
        "examples": [
          {
            "target": {
              "entry_url": "https://example.com"
            },
            "personas": [
              "dana_first_timer@1"
            ],
            "missions": [
              {
                "id": "sign_up",
                "goal": "create an account",
                "success_criteria": [
                  {
                    "kind": "judge_assertion",
                    "value": "the persona reached a signed-in page"
                  }
                ]
              }
            ],
            "budget": {
              "steps_per_session": 30
            }
          }
        ]
      },
      "RunSpecMission": {
        "type": "object",
        "description": "One thing each persona is sent to do: a goal in the persona's own terms, how to tell it was achieved, and optionally where to start, what the judge should watch for and the question the persona answers at the end.",
        "required": [
          "id",
          "goal",
          "success_criteria"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9_]+$"
          },
          "name": {
            "type": "string"
          },
          "entry_url": {
            "type": "string",
            "format": "uri",
            "description": "Overrides target.entry_url"
          },
          "goal": {
            "type": "string",
            "description": "Stated in terms a persona would understand, e.g. 'buy one of the blue shirts'"
          },
          "success_criteria": {
            "type": "array",
            "minItems": 1,
            "description": "What reaching the goal looks like. The persona is never shown these: it is given `goal` and `brief` and decides for itself when it is finished. Only a `url_matches` criterion is checked by machine; every other kind is context for the judge. See `kind`.",
            "items": {
              "type": "object",
              "required": [
                "kind",
                "value"
              ],
              "description": "One way to tell the goal was reached: what sort of evidence it is (`kind`) and what to look for (`value`). Only a `url_matches` criterion is checked by machine; see `kind`.",
              "additionalProperties": false,
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "url_matches",
                    "text_visible",
                    "element_visible",
                    "network_call",
                    "judge_assertion"
                  ],
                  "description": "What the criterion states and who checks it. `url_matches` is the only kind checked by machine: `value` is part of a URL, e.g. `/order/confirmed`, and when the persona says it is finished the session ends with `end_reason` `success_criteria_met` if `value` occurs in the last URL the browser was on, or `claimed_finished_unverified` if it does not (always, for a mission with no `url_matches` criterion). Nothing checks the other four: the judge that reads each session afterwards is shown every criterion as text, `<kind>=<value>`, beside the mission's goal, and nothing decides whether one was met. `text_visible`: text the page shows once the goal is reached. `element_visible`: an element on that page, in words or as a selector. `network_call`: a request the product makes when the goal is reached, e.g. `POST /api/orders`. `judge_assertion`: a plain-language statement about how the session ended, e.g. `the persona reached a signed-in page`. For a verified ending, give a mission a `url_matches` criterion beside any of these."
                },
                "value": {
                  "type": "string",
                  "description": "Read according to `kind`: part of a URL for `url_matches`, otherwise text for the judge."
                }
              }
            }
          },
          "watch_list": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Specific concerns the judge should look for, e.g. 'is the new discount widget discoverable'. The only place free opinions may be solicited: the persona itself is never asked to critique the product."
          },
          "authored_by": {
            "type": "string",
            "enum": [
              "human",
              "doppelganger",
              "planner"
            ],
            "default": "human",
            "description": "Who wrote the mission: `human`, the default, for one a person wrote; `doppelganger` and `planner` for one an agent wrote on someone's behalf. A label only: it is kept with the run and changes nothing about how the mission is run."
          },
          "brief": {
            "type": "string",
            "description": "Optional context from the mission author to the persona (what they were told before starting)"
          },
          "verdict_prompt": {
            "type": "string",
            "description": "The question about THIS product that the persona answers in their final diary entry, asked after the persona's own `persona.verdict_prompt`. Set it whenever the personas were written for a different product than the one under test: a roofing persona asked 'is this going to bring you roofing jobs' has nothing to answer with after a shirt-shop session, and the verdict comes back null. When present, this is the question the judge answers and `persona.verdict_prompt` is not applicable for the run."
          }
        }
      },
      "RunSpecProvider": {
        "type": "object",
        "x-closed": true,
        "additionalProperties": false,
        "required": [
          "model"
        ],
        "description": "One model on one provider or router, in pydantic-ai's own terms: `model` is `<provider>:<model>` and the prefix decides how the rest is read. Closed by design: a credential arrives only as `credential_ref`, so a field this object does not list is an inline secret and every reader rejects it.",
        "properties": {
          "model": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]*:.+$",
            "description": "pydantic-ai model string: anthropic:claude-opus-5, bedrock:us.anthropic.claude-opus-5, openai:gpt-5.6, openrouter:anthropic/claude-opus-5, azure:<deployment>, google:gemini-3-pro, google-cloud:gemini-3-pro, litellm:<model>."
          },
          "credential_ref": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9+.-]*://.+$",
            "description": "Reference to the credential, never the credential: `env://NAME`, `awssm://<secret name>`. Absent means the provider's ambient credential (IRSA or instance role on bedrock, Application Default Credentials on google-cloud); for prefixes that have no ambient path, absent is a producer error at resolution. One secret holds one string; what the string is (API key, bearer token, service-account JSON) is fixed by the prefix."
          },
          "base_url": {
            "type": "string",
            "format": "uri",
            "description": "Endpoint override: a gateway (LiteLLM, a private OpenAI-compatible URL), an Azure resource endpoint, a Bedrock VPC endpoint. Absent means the provider's public endpoint."
          },
          "region": {
            "type": "string",
            "description": "bedrock: the AWS region the model is invoked in. Absent means AWS_REGION / AWS_DEFAULT_REGION on the worker."
          },
          "project": {
            "type": "string",
            "description": "google-cloud: the GCP project. Absent means the one Application Default Credentials carry."
          },
          "location": {
            "type": "string",
            "description": "google-cloud: the Vertex location, e.g. us-central1 or global."
          },
          "api_version": {
            "type": "string",
            "description": "azure: the Azure OpenAI api-version. Absent means OPENAI_API_VERSION on the worker."
          }
        }
      }
    },
    "securitySchemes": {
      "HTTPBearer": {
        "type": "http",
        "description": "a Saram API key (`sk_saram_<env>_...`) or a person's AuthKit access token, as `Authorization: Bearer <credential>`. Both are told apart by shape and produce the same principal.",
        "scheme": "bearer"
      }
    }
  },
  "tags": [
    {
      "name": "runs",
      "description": "Start a run from a run spec, check a spec without starting one, list and read runs, and pause, resume or cancel one while it executes.",
      "x-displayName": "Runs"
    },
    {
      "name": "sessions",
      "description": "One session is one persona on one mission. Read a run's sessions, and each session's steps: what the persona saw, did and said, aligned with what the browser recorded, readable while it runs.",
      "x-displayName": "Sessions"
    },
    {
      "name": "shares",
      "description": "Publish one finished session's replay by an unlisted link anyone holding it can open with no sign-in, list a run's links, and revoke one. The recording is not redacted. Off until Saram turns it on for your tenant.",
      "x-displayName": "Share links"
    },
    {
      "name": "artifact",
      "description": "What a run found: the artifact, its reproduced issues, the personas' verdicts and opportunities, and retests that replay an issue again to answer whether it is fixed.",
      "x-displayName": "Results"
    },
    {
      "name": "personas",
      "description": "The personas a run spec can name, by `id` or `id@version`, and each one's full document.",
      "x-displayName": "Personas"
    },
    {
      "name": "files",
      "description": "Files a persona can bring into a run - a document to upload, an image to attach. Uploaded once, named in a run spec by their `ref`, never served back.",
      "x-displayName": "Files"
    },
    {
      "name": "blobs",
      "description": "Evidence bytes - screenshots, videos, traces - streamed by the ref a step or an issue records, with `Range` for seeking.",
      "x-displayName": "Evidence"
    },
    {
      "name": "credentials",
      "description": "Your own model provider keys and the test-account logins your personas sign in with. Stored as secrets and named in a run spec by `secret_ref`; a value is never returned.",
      "x-displayName": "Credentials"
    },
    {
      "name": "providers",
      "description": "Connect your own ChatGPT subscription as a model provider for the runs you start yourself: a device login, its status, and disconnecting it.",
      "x-displayName": "ChatGPT connection"
    },
    {
      "name": "identity",
      "description": "Who a credential is - its tenant, role and scopes - and the API keys that tenant holds.",
      "x-displayName": "Identity and API keys"
    },
    {
      "name": "usage",
      "description": "What your runs have used and cost, by month, and billing: your plan, prepaid balance, top-ups, auto-refill and the card on file.",
      "x-displayName": "Usage and billing"
    },
    {
      "name": "ops",
      "description": "Saram's own operations, for Saram's operators: growth counts across every tenant. No customer credential is admitted.",
      "x-displayName": "Operations"
    },
    {
      "name": "demo",
      "description": "The free demo on saram.sh: a visitor pastes a site and watches one persona try it, with no account. Called by that page alone, with no credential; limited per address, per site and per day.",
      "x-displayName": "Homepage demo"
    },
    {
      "name": "health",
      "description": "Whether the API is up. The one route that needs no credential.",
      "x-displayName": "Health"
    }
  ]
}
