openapi: 3.0.3
info:
    title: Aiqaramba API
    version: 1.0.0
    description: |
        The authenticated Aiqaramba REST API. Bearer tokens are API keys created
        in the Aiqaramba application.
servers:
    -   url: /
security:
    -   bearerAuth: []
tags:
    -   name: Projects
        description: >-
            Projects are containers that group together your tests, agents, schedules, and other resources. They are simple organizational buckets with no behavior of their own.
    -   name: Models
        description: >-
            Models are the LLM configurations that power agents. These are managed by Aiqaramba and this endpoint lets you query which models are currently available.
    -   name: Tunnels
        description: >-
            Tunnels expose a local development server to Aiqaramba's agents and browsers through an encrypted connection, without making it reachable from the public internet. Open a tunnel with `aiqa tunnel <port>`; the assigned hostname stays reachable only from inside Aiqaramba's network. A tunnel lives exactly as long as its connection: closing the CLI closes the tunnel.
    -   name: Mailboxes
        description: >-
            A mailbox is a real email address your agents can read. Every message delivered to `{name}@agents.aiqaramba.com` is stored against the matching mailbox and exposed via the API. Attach a mailbox to a test or a test run to grant that run read access to the inbox, useful for magic links, verification codes, and signup confirmations. Mailboxes are read-only: there is no API to send mail.
    -   name: Findings
        description: >-
            Findings are issues discovered by agents during test runs, discoveries, and test plans. Each finding includes severity, evidence linking back to specific agent trace steps, and a lifecycle status (open, regressed, resolved, dismissed).
    -   name: Mobile Apps
        description: >-
            Upload and manage Android APK files used by mobile tests and mobile discoveries. Mobile apps are immutable artifacts; upload each build separately.
    -   name: Files
        description: >-
            Manage tenant-scoped files that can be attached to agent runs. Upload files to your tenant store, then reference them when creating agents or running tests to copy them into the agent workspace.
    -   name: Roles
        description: >-
            Roles are reusable prompt injection templates. A role's system prompt is injected into the agent's system prompt, shaping how it behaves during execution. Roles can carry attached knowledge documents (reference material) that the agent can consult while running.
    -   name: Agents
        description: >-
            An agent carries out a set of instructions in Aiqaramba. It uses a browser or mobile device to complete the task and records what happened. The API lets you follow the run and inspect its result.
    -   name: Tests
        description: >-
            A test saves the instructions and settings for a task you want to run more than once. Running a test spawns an agent with that setup. Aiqaramba links each run to the test so you can review results over time.
    -   name: Discoveries
        description: >-
            Discoveries automatically explore a website or Android application by spawning multiple agents across several phases. Starting from a URL (web) or an APK (Android), agents map pages/screens, forms, and interactions, then the system identifies gaps and sends agents to explore further.
    -   name: Test Plans
        description: >-
            Chain several tests into a directed acyclic graph (DAG): each step runs one test under a named browser profile and declares the outputs it records. Every value recorded by a step's ancestors is provided to that step's agent automatically — prompts refer to them in plain language (e.g. "log in with the credentials from the register step"), no template syntax needed. The runner walks the graph in dependency order, runs independent steps in parallel, and shares one logged-in browser across all steps that name the same profile.
    -   name: Recordings
        description: >-
            Recordings capture user flows from the Aiqaramba Recorder browser extension. Each recording contains rrweb DOM events (clicks, typing, scrolling, navigation) and optionally tab video with audio. Recordings can be converted into tests for automated replay.
    -   name: Schedules
        description: >-
            Schedules automate recurring test execution. Assign tests to a schedule, set a daily run time, and the system runs them on a rotating basis, most-overdue first.
paths:
    /api/v1/schedules:
        post:
            operationId: createSchedule
            x-cli-name: create
            tags: [Schedules]
            summary: Create a schedule
            requestBody:
                description: Automation timing, targets, variables, and digest settings.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/CreateScheduleRequest'}}}
            responses:
                '201': {description: Schedule created., content: {application/json: {schema: {$ref: '#/components/schemas/CreateScheduleResponse'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listSchedules
            x-cli-name: list
            tags: [Schedules]
            summary: List schedules
            parameters:
                - {name: project_id, in: query, description: Restrict schedules to one project., schema: {type: string, format: uuid}}
                - {name: limit, in: query, description: Maximum schedules to return., schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
                - {name: after, in: query, description: Continue below this schedule ID., schema: {type: string, format: uuid}}
            responses:
                '200': {description: Tenant schedules., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}:
        parameters: [{$ref: '#/components/parameters/ScheduleID'}]
        get:
            operationId: getSchedule
            x-cli-name: get
            tags: [Schedules]
            summary: Get a schedule
            responses:
                '200': {description: Schedule with items and recent runs., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleDetail'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        patch:
            operationId: updateSchedule
            x-cli-name: update
            tags: [Schedules]
            summary: Update a schedule
            requestBody:
                description: Timing, enablement, variable, or digest changes.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/UpdateScheduleRequest'}}}
            responses:
                '200': {description: Updated schedule., content: {application/json: {schema: {$ref: '#/components/schemas/Schedule'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteSchedule
            x-cli-name: delete
            tags: [Schedules]
            summary: Delete a schedule
            responses:
                '204': {description: Schedule deleted.}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/items:
        post:
            operationId: addScheduleItem
            x-cli-name: add-item
            tags: [Schedules]
            summary: Add a schedule target
            parameters: [{$ref: '#/components/parameters/ScheduleID'}]
            requestBody:
                description: Exactly one test or test-plan target.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/AddScheduleItemRequest'}}}
            responses:
                '201': {description: Target added., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleItem'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/items/{itemId}:
        delete:
            operationId: deleteScheduleItem
            x-cli-name: remove-item
            tags: [Schedules]
            summary: Remove a schedule target
            parameters:
                -   $ref: '#/components/parameters/ScheduleID'
                - {name: itemId, in: path, required: true, description: Schedule item ID., schema: {type: string, format: uuid}}
            responses:
                '204': {description: Target removed.}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/runs:
        get:
            operationId: listScheduleRuns
            x-cli-name: list-runs
            tags: [Schedules]
            summary: List schedule runs
            parameters:
                -   $ref: '#/components/parameters/ScheduleID'
                - {name: limit, in: query, description: Maximum runs to return., schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
            responses:
                '200': {description: Recent schedule-launched agents., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleRunList'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/trigger:
        post:
            operationId: triggerSchedule
            x-cli-name: trigger
            tags: [Schedules]
            summary: Trigger a schedule now
            parameters: [{$ref: '#/components/parameters/ScheduleID'}]
            requestBody:
                description: Optional one-fire variable overrides.
                content: {application/json: {schema: {$ref: '#/components/schemas/TriggerScheduleRequest'}}}
            responses:
                '200': {description: Trigger result., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleTriggerResponse'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/next-fires:
        get:
            operationId: getScheduleNextFires
            x-cli-name: next-fires
            tags: [Schedules]
            summary: Preview schedule fire times
            parameters:
                -   $ref: '#/components/parameters/ScheduleID'
                - {name: n, in: query, description: Number of future fires., schema: {type: integer, minimum: 1, maximum: 25, default: 5}}
            responses:
                '200': {description: Upcoming fire moments., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleNextFires'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/schedules/{id}/digest:
        get:
            operationId: getScheduleDigest
            x-cli-name: get-digest
            tags: [Schedules]
            summary: Preview a schedule digest
            parameters: [{$ref: '#/components/parameters/ScheduleID'}]
            responses:
                '200': {description: Structured digest preview., content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleDigestResponse'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        post:
            operationId: sendScheduleDigest
            x-cli-name: send-digest
            tags: [Schedules]
            summary: Send a schedule digest now
            parameters: [{$ref: '#/components/parameters/ScheduleID'}]
            responses:
                '200': {description: Digest queued., content: {application/json: {schema: {$ref: '#/components/schemas/SendScheduleDigestResponse'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/recordings:
        post:
            operationId: createRecording
            x-cli-name: create
            tags: [Recordings]
            summary: Create a recording
            description: Stores rrweb events and returns a direct video upload URL when video is expected.
            x-ogen-json-streaming: true
            requestBody:
                description: Recording metadata, events, navigation history, and optional audio narration.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/CreateRecordingRequest'}}}
            responses:
                '201': {description: Recording created., content: {application/json: {schema: {$ref: '#/components/schemas/CreateRecordingResponse'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listRecordings
            x-cli-name: list
            tags: [Recordings]
            summary: List recordings
            parameters:
                - {name: limit, in: query, description: Maximum recordings to return., schema: {type: integer, minimum: 1, maximum: 100, default: 25}}
                - {name: cursor, in: query, description: Continue below this recording ID., schema: {type: string, format: uuid}}
                - {name: project_id, in: query, description: Restrict recordings to one project., schema: {type: string, format: uuid}}
                - {name: status, in: query, description: Restrict recordings to one processing status., schema: {type: string, enum: [uploading, processing, ready, failed]}}
                - {name: source_type, in: query, description: Restrict recordings to one source., schema: {type: string, enum: [extension, upload, posthog]}}
            responses:
                '200': {description: Tenant recordings., content: {application/json: {schema: {$ref: '#/components/schemas/RecordingList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/recordings/{id}:
        parameters: [{$ref: '#/components/parameters/RecordingID'}]
        get:
            operationId: getRecording
            x-cli-name: get
            tags: [Recordings]
            summary: Get a recording
            responses:
                '200': {description: Recording found., content: {application/json: {schema: {$ref: '#/components/schemas/Recording'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteRecording
            x-cli-name: delete
            tags: [Recordings]
            summary: Delete a recording
            responses:
                '204': {description: Recording deleted.}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/recordings/{id}/events:
        get:
            operationId: getRecordingEvents
            x-cli-name: list-events
            tags: [Recordings]
            summary: Stream recording events
            description: Streams the decompressed rrweb JSON event array.
            parameters: [{$ref: '#/components/parameters/RecordingID'}]
            responses:
                '200':
                    description: Decompressed rrweb event JSON.
                    content:
                        application/octet-stream:
                            schema: {type: string, format: binary}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/recordings/{id}/video-complete:
        post:
            operationId: completeRecordingVideo
            x-cli-name: video-complete
            tags: [Recordings]
            summary: Confirm recording video upload
            parameters:
                -   $ref: '#/components/parameters/RecordingID'
                - {name: skip_video, in: query, description: Process events without a video object., schema: {type: boolean, default: false}}
            responses:
                '200': {description: Processing started., content: {application/json: {schema: {$ref: '#/components/schemas/RecordingProcessingResponse'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/recordings/{id}/reproduce:
        post:
            operationId: reproduceRecording
            x-cli-name: reproduce
            tags: [Recordings]
            summary: Reproduce a recording
            description: Runs an existing generated test or queues test generation from stored events.
            parameters: [{$ref: '#/components/parameters/RecordingID'}]
            requestBody:
                description: Optional finding used to ground the generated test.
                content: {application/json: {schema: {$ref: '#/components/schemas/ReproduceRecordingRequest'}}}
            responses:
                '202': {description: Reproduction queued., content: {application/json: {schema: {$ref: '#/components/schemas/ReproduceRecordingResponse'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '402': {$ref: '#/components/responses/PaymentRequired'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plans:
        post:
            operationId: createTestPlan
            x-cli-name: create
            tags: [Test Plans]
            summary: Create a test plan
            requestBody:
                description: Plan metadata, variables, and initial graph.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/CreateTestPlanRequest'}}}
            responses:
                '201': {description: Plan created., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listTestPlans
            x-cli-name: list
            tags: [Test Plans]
            summary: List test plans
            parameters:
                - {name: project_id, in: query, required: true, description: Owning project ID., schema: {type: string, format: uuid}}
            responses:
                '200': {description: Project plan summaries., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plans/{id}:
        parameters: [{$ref: '#/components/parameters/TestPlanID'}]
        get:
            operationId: getTestPlan
            x-cli-name: get
            tags: [Test Plans]
            summary: Get a hydrated test plan
            responses:
                '200': {description: Hydrated plan., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        put:
            operationId: replaceTestPlan
            x-cli-name: replace
            tags: [Test Plans]
            summary: Replace a test plan
            requestBody:
                description: Complete replacement metadata and graph.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/ReplaceTestPlanRequest'}}}
            responses:
                '200': {description: Replaced plan., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteTestPlan
            x-cli-name: delete
            tags: [Test Plans]
            summary: Delete a test plan
            responses:
                '204': {description: Plan deleted.}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plans/{id}/steps:
        post:
            operationId: addTestPlanStep
            x-cli-name: add-step
            tags: [Test Plans]
            summary: Add a plan step
            parameters: [{$ref: '#/components/parameters/TestPlanID'}]
            requestBody:
                description: Complete new step definition.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanStepInput'}}}
            responses:
                '201': {description: Updated hydrated plan., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plans/{id}/steps/{stepId}:
        parameters:
            -   $ref: '#/components/parameters/TestPlanID'
            - {name: stepId, in: path, required: true, description: Stable plan step ID., schema: {type: string, format: uuid}}
        patch:
            operationId: updateTestPlanStep
            x-cli-name: update-step
            tags: [Test Plans]
            summary: Replace a plan step
            requestBody:
                description: Complete replacement step definition.
                required: true
                content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanStepInput'}}}
            responses:
                '200': {description: Plan with replaced step., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteTestPlanStep
            x-cli-name: delete-step
            tags: [Test Plans]
            summary: Delete a plan step
            responses:
                '200': {description: Plan after step removal., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlan'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plans/{id}/runs:
        parameters: [{$ref: '#/components/parameters/TestPlanID'}]
        post:
            operationId: startTestPlanRun
            x-cli-name: start-run
            tags: [Test Plans]
            summary: Start a test plan run
            requestBody:
                description: Optional plan variable values and tunnel hostname.
                content: {application/json: {schema: {$ref: '#/components/schemas/StartTestPlanRunRequest'}}}
            responses:
                '201': {description: Run started., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanRun'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '503': {$ref: '#/components/responses/ServiceUnavailable'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listTestPlanRuns
            x-cli-name: list-runs
            tags: [Test Plans]
            summary: List test plan runs
            responses:
                '200': {description: Plan runs., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanRunList'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plan-runs/{id}:
        get:
            operationId: getTestPlanRun
            x-cli-name: get-run
            tags: [Test Plans]
            summary: Get a test plan run
            parameters: [{$ref: '#/components/parameters/TestPlanRunID'}]
            responses:
                '200': {description: Run with node runs., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanRun'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/test-plan-runs/{id}/stop:
        post:
            operationId: stopTestPlanRun
            x-cli-name: stop-run
            tags: [Test Plans]
            summary: Stop a test plan run
            parameters: [{$ref: '#/components/parameters/TestPlanRunID'}]
            responses:
                '202': {description: Stop accepted., content: {application/json: {schema: {$ref: '#/components/schemas/TestPlanRun'}}}}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/discoveries:
        post:
            operationId: createDiscovery
            x-cli-name: create
            tags: [Discoveries]
            summary: Start a discovery
            description: >
                Creates a discovery against the given entry URL and returns it in the pending state; a background coordinator then runs the pipeline — an optional login stage using the supplied credentials, a deterministic crawl, and a review producing suggested tests and findings. Poll GET /discoveries/{id} to follow progress. Credentials are write-only and never returned; supply non-sensitive test credentials only — they are shared with an LLM during login.
            requestBody:
                description: Target application and crawl configuration.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/CreateDiscoveryRequest'}
            responses:
                '201': {description: Discovery created and queued., content: {application/json: {schema: {$ref: '#/components/schemas/Discovery'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listDiscoveries
            x-cli-name: list
            tags: [Discoveries]
            summary: List discoveries
            parameters:
                - {name: limit, in: query, description: Maximum discoveries to return., schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
                - {name: cursor, in: query, description: Continue below this discovery ID., schema: {type: string, format: uuid}}
                - {name: project_id, in: query, description: Restrict discoveries to one project., schema: {type: string, format: uuid}}
            responses:
                '200': {description: Tenant discoveries., content: {application/json: {schema: {$ref: '#/components/schemas/DiscoveryList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/discoveries/{id}:
        parameters:
            -   $ref: '#/components/parameters/DiscoveryID'
        get:
            operationId: getDiscovery
            x-cli-name: get
            tags: [Discoveries]
            summary: Get a discovery
            responses:
                '200': {description: Discovery found., content: {application/json: {schema: {$ref: '#/components/schemas/Discovery'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteDiscovery
            x-cli-name: delete
            tags: [Discoveries]
            summary: Delete a discovery
            responses:
                '204': {description: Discovery deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/discoveries/{id}/cancel:
        post:
            operationId: cancelDiscovery
            x-cli-name: cancel
            tags: [Discoveries]
            summary: Cancel a discovery
            description: >
                Requests cancellation of a non-terminal discovery. Cancellation is asynchronous: the run stops at its next safe point and moves to the canceled state. The response echoes the discovery with cancel_requested set; poll GET /discoveries/{id} for the terminal state.
            parameters:
                -   $ref: '#/components/parameters/DiscoveryID'
            responses:
                '200': {description: Discovery canceled., content: {application/json: {schema: {$ref: '#/components/schemas/Discovery'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/discoveries/{id}/map:
        get:
            operationId: getDiscoveryMap
            x-cli-name: map
            tags: [Discoveries]
            summary: Get the crawled map
            description: >
                Returns the state-flow graph discovered so far: one node per distinct UI state, one edge per observed transition. Available while the crawl is still running — nodes and edges appear as they are discovered.
            parameters:
                -   $ref: '#/components/parameters/DiscoveryID'
            responses:
                '200': {description: Crawled state-flow graph., content: {application/json: {schema: {$ref: '#/components/schemas/DiscoveryMap'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/discoveries/{id}/suggestions:
        get:
            operationId: listDiscoverySuggestions
            x-cli-name: suggestions
            tags: [Discoveries]
            summary: List a discovery's suggestions
            description: >
                Returns the reviewer's drafted tests and findings for a discovery. Populated once the review stage completes; empty before then. These are drafts — accepting one into a real test or finding is a separate action.
            parameters:
                -   $ref: '#/components/parameters/DiscoveryID'
            responses:
                '200': {description: Discovery suggestions., content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySuggestionList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/tests:
        post:
            operationId: createTest
            x-cli-name: create
            tags: [Tests]
            summary: Create a test
            requestBody:
                description: Reusable test configuration and attached mailbox names.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/CreateTestRequest'}
            responses:
                '201': {description: Test created., content: {application/json: {schema: {$ref: '#/components/schemas/Test'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listTests
            x-cli-name: list
            tags: [Tests]
            summary: List tests
            parameters:
                - {name: limit, in: query, description: Maximum tests to return., schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
                - {name: cursor, in: query, description: Return tests with IDs lower than this test ID., schema: {type: string, format: uuid}}
                - {name: project_id, in: query, description: Filter by project ID., schema: {type: string, format: uuid}}
            responses:
                '200': {description: Tenant tests., content: {application/json: {schema: {$ref: '#/components/schemas/TestList'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/tests/{id}:
        parameters:
            -   $ref: '#/components/parameters/TestID'
        get:
            operationId: getTest
            x-cli-name: get
            tags: [Tests]
            summary: Get a test
            responses:
                '200': {description: Test found., content: {application/json: {schema: {$ref: '#/components/schemas/Test'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        patch:
            operationId: updateTest
            x-cli-name: update
            tags: [Tests]
            summary: Update a test
            requestBody:
                description: Fields to change; omitted fields remain unchanged.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/UpdateTestRequest'}
            responses:
                '200': {description: Test updated., content: {application/json: {schema: {$ref: '#/components/schemas/Test'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteTest
            x-cli-name: delete
            tags: [Tests]
            summary: Delete a test
            responses:
                '204': {description: Test deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/tests/{id}/run:
        post:
            operationId: runTest
            x-cli-name: run
            tags: [Tests]
            summary: Run a test
            description: Creates and queues an agent using the saved test configuration and optional overrides.
            parameters:
                -   $ref: '#/components/parameters/TestID'
            requestBody:
                description: Optional run overrides and inline workspace files.
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/RunTestRequest'}
            responses:
                '201': {description: Test run created and queued., content: {application/json: {schema: {$ref: '#/components/schemas/Agent'}}}}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '402': {$ref: '#/components/responses/PaymentRequired'}
                '404': {$ref: '#/components/responses/NotFound'}
                '503': {$ref: '#/components/responses/ServiceUnavailable'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents:
        post:
            operationId: createAgent
            x-cli-name: create
            tags: [Agents]
            summary: Create an agent
            description: Creates and queues an agent from inline instructions.
            requestBody:
                description: Inline run configuration and optional attached resources.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/CreateAgentRequest'}
            responses:
                '201':
                    description: Agent created and queued.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Agent'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '402': {$ref: '#/components/responses/PaymentRequired'}
                '404': {$ref: '#/components/responses/NotFound'}
                '503': {$ref: '#/components/responses/ServiceUnavailable'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listAgents
            x-cli-name: list
            tags: [Agents]
            summary: List agents
            description: Returns tenant-owned runs in descending ID order.
            parameters:
                -   name: limit
                    in: query
                    description: Maximum agents to return.
                    schema: {type: integer, minimum: 1, maximum: 100, default: 20}
                -   name: cursor
                    in: query
                    description: Continue agent pagination below this run ID.
                    schema: {type: string, format: uuid}
                -   name: project_id
                    in: query
                    description: Restrict runs to one owning project.
                    schema: {type: string, format: uuid}
                -   name: source
                    in: query
                    description: Filter by run source.
                    schema: {type: string}
                -   name: failure_source
                    in: query
                    description: Filter by classified failure source.
                    schema: {type: string}
            responses:
                '200':
                    description: Tenant agents.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/AgentList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents/{id}/stop:
        post:
            operationId: stopAgent
            x-cli-name: stop
            tags: [Agents]
            summary: Stop an agent
            description: Cancels active jobs and atomically finalizes a pending, running, or waiting agent as stopped.
            parameters:
                -   $ref: '#/components/parameters/AgentID'
            responses:
                '200':
                    description: Agent stopped.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Agent'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents/{id}/clarification:
        post:
            operationId: clarifyAgent
            x-cli-name: clarify
            tags: [Agents]
            summary: Respond to an agent clarification
            description: Appends a function response and resumes an agent waiting for human clarification.
            parameters:
                -   $ref: '#/components/parameters/AgentID'
            requestBody:
                description: Human response to the agent's pending clarification request.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/ClarifyAgentRequest'}
            responses:
                '200':
                    description: Clarification accepted and agent queued to resume.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Agent'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '402': {$ref: '#/components/responses/PaymentRequired'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents/{id}/files:
        post:
            operationId: uploadAgentFiles
            x-cli-name: upload-files
            tags: [Agents]
            summary: Upload files to an agent workspace
            description: Delivers files to a pending, running, or waiting agent workspace.
            parameters:
                -   $ref: '#/components/parameters/AgentID'
            requestBody:
                description: Workspace path prefix and one or more files.
                required: true
                content:
                    multipart/form-data:
                        schema: {$ref: '#/components/schemas/UploadAgentFilesRequest'}
            responses:
                '200':
                    description: Files delivered.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/UploadAgentFilesResponse'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '413': {$ref: '#/components/responses/ContentTooLarge'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents/{id}/retry:
        post:
            operationId: retryAgent
            x-cli-name: retry
            tags: [Agents]
            summary: Retry an agent
            description: Clones a terminal or recoverable run and queues the clone while preserving the original.
            parameters:
                -   $ref: '#/components/parameters/AgentID'
            requestBody:
                description: Optional configuration overrides; omitted fields inherit from the parent.
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/RetryAgentRequest'}
            responses:
                '201':
                    description: Cloned agent created and queued.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Agent'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '402': {$ref: '#/components/responses/PaymentRequired'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/agents/{id}:
        parameters:
            -   $ref: '#/components/parameters/AgentID'
        get:
            operationId: getAgent
            x-cli-name: get
            tags: [Agents]
            summary: Get an agent
            responses:
                '200':
                    description: Agent found.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Agent'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteAgent
            x-cli-name: delete
            tags: [Agents]
            summary: Delete an agent
            responses:
                '204': {description: Agent deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/roles:
        post:
            operationId: createRole
            x-cli-name: create
            tags: [Roles]
            summary: Create a role
            description: Creates a tenant role, or a global role when requested by support.
            requestBody:
                description: Role behavior and optional human-facing description.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/CreateRoleRequest'}
            responses:
                '201':
                    description: Role created.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Role'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '403': {$ref: '#/components/responses/Forbidden'}
                '409': {$ref: '#/components/responses/Conflict'}
                '502': {$ref: '#/components/responses/BadGateway'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        get:
            operationId: listRoles
            x-cli-name: list
            tags: [Roles]
            summary: List visible roles
            description: Returns tenant-owned and platform-global roles in descending ID order.
            parameters:
                -   name: limit
                    in: query
                    description: Maximum roles to return.
                    schema: {type: integer, minimum: 1, maximum: 100, default: 20}
                -   name: cursor
                    in: query
                    description: Return roles with IDs lower than this role ID.
                    schema: {type: string, format: uuid}
            responses:
                '200':
                    description: Visible roles.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/RoleList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/roles/{id}:
        parameters:
            -   $ref: '#/components/parameters/RoleID'
        get:
            operationId: getRole
            x-cli-name: get
            tags: [Roles]
            summary: Get a visible role
            responses:
                '200':
                    description: Role found.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Role'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        put:
            operationId: updateRole
            x-cli-name: update
            tags: [Roles]
            summary: Replace a role
            description: Replaces role behavior fields; global roles require support access.
            requestBody:
                description: Complete replacement role fields.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/UpdateRoleRequest'}
            responses:
                '200':
                    description: Role updated.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/Role'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '502': {$ref: '#/components/responses/BadGateway'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteRole
            x-cli-name: delete
            tags: [Roles]
            summary: Delete a role
            description: Deletes the role after removing its stored knowledge documents.
            responses:
                '204': {description: Role deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/roles/{id}/knowledge:
        parameters:
            -   $ref: '#/components/parameters/RoleID'
        get:
            operationId: listRoleKnowledge
            x-cli-name: list-knowledge
            tags: [Roles]
            summary: List role knowledge documents
            responses:
                '200':
                    description: Attached knowledge documents.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/RoleKnowledgeList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        post:
            operationId: uploadRoleKnowledge
            x-cli-name: upload-knowledge
            tags: [Roles]
            summary: Upload role knowledge
            description: Stores one reference document and queues asynchronous indexing.
            requestBody:
                description: Multipart body containing one reference document.
                required: true
                content:
                    multipart/form-data:
                        schema: {$ref: '#/components/schemas/UploadRoleKnowledgeRequest'}
            responses:
                '202':
                    description: Document stored and indexing queued.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/RoleKnowledgeDocument'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '413': {$ref: '#/components/responses/ContentTooLarge'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteRoleKnowledge
            x-cli-name: delete-knowledge
            tags: [Roles]
            summary: Delete all role knowledge
            responses:
                '204': {description: Knowledge deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/roles/{id}/knowledge/{documentID}:
        delete:
            operationId: deleteRoleKnowledgeDocument
            x-cli-name: delete-knowledge-document
            tags: [Roles]
            summary: Delete a role knowledge document
            parameters:
                -   $ref: '#/components/parameters/RoleID'
                -   name: documentID
                    in: path
                    required: true
                    description: Knowledge document ID.
                    schema: {type: string, format: uuid}
            responses:
                '204': {description: Knowledge document deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/files:
        get:
            operationId: listFiles
            x-cli-name: list
            tags: [Files]
            summary: List directory entries
            description: Lists non-placeholder files and directories under a tenant-relative path.
            parameters:
                -   name: path
                    in: query
                    description: Relative directory path; omitted for the tenant root.
                    schema: {type: string}
            responses:
                '200':
                    description: Directory entries.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/FileList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteFile
            x-cli-name: delete
            tags: [Files]
            summary: Delete a file or directory
            description: Deletes a file or an empty directory. Deleting a directory that still contains files requires recursive=true and otherwise fails with 409.
            parameters:
                -   $ref: '#/components/parameters/RequiredFilePath'
                -   name: recursive
                    in: query
                    required: false
                    schema: {type: boolean, default: false}
                    description: Also delete a non-empty directory together with everything under it.
            responses:
                '204': {description: File or directory deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {description: The directory is not empty and recursive was not set., content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/files/download:
        get:
            operationId: downloadFile
            x-cli-name: download
            tags: [Files]
            summary: Download a file
            description: Streams one tenant file as an octet stream.
            parameters:
                -   $ref: '#/components/parameters/RequiredFilePath'
            responses:
                '200':
                    description: Raw file content.
                    headers:
                        Content-Disposition:
                            description: Attachment disposition containing the stored base filename.
                            schema: {type: string}
                        Content-Length:
                            description: File size in bytes.
                            schema: {type: integer, format: int64}
                    content:
                        application/octet-stream:
                            schema: {type: string, format: binary}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/files/upload:
        post:
            operationId: uploadFiles
            x-cli-name: upload
            tags: [Files]
            summary: Upload files
            description: Stores one or more multipart files under an optional relative directory prefix.
            requestBody:
                description: Multipart files and destination prefix.
                required: true
                content:
                    multipart/form-data:
                        schema: {$ref: '#/components/schemas/UploadFilesRequest'}
            responses:
                '200':
                    description: Stored files.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/UploadedFileList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '413': {$ref: '#/components/responses/ContentTooLarge'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/files/move:
        post:
            operationId: moveFile
            x-cli-name: move
            tags: [Files]
            summary: Move or rename a file
            requestBody:
                description: Existing and destination tenant-relative paths.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/MoveFileRequest'}
            responses:
                '200':
                    description: File moved.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MutationSuccess'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/files/mkdir:
        post:
            operationId: createDirectory
            x-cli-name: mkdir
            tags: [Files]
            summary: Create a directory
            description: Creates a storage placeholder for an otherwise-empty directory.
            requestBody:
                description: Tenant-relative directory path.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/CreateDirectoryRequest'}
            responses:
                '200':
                    description: Directory created.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MutationSuccess'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/mobile-apps:
        get:
            operationId: listMobileApps
            x-cli-name: list
            tags: [Mobile Apps]
            summary: List mobile apps
            description: Returns a cursor-paginated page of the tenant's uploaded Android apps.
            parameters:
                -   name: limit
                    in: query
                    description: Maximum mobile apps to return.
                    schema:
                        type: integer
                        minimum: 1
                        maximum: 100
                        default: 20
                -   name: cursor
                    in: query
                    description: Return mobile apps older than this artifact.
                    schema: {type: string, format: uuid}
            responses:
                '200':
                    description: Tenant mobile apps.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MobileAppList'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/mobile-apps/{id}:
        parameters:
            -   name: id
                in: path
                required: true
                description: Mobile app ID.
                schema: {type: string, format: uuid}
        get:
            operationId: getMobileApp
            x-cli-name: get
            tags: [Mobile Apps]
            summary: Get a mobile app
            responses:
                '200':
                    description: Mobile app found.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MobileAppEnvelope'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
        delete:
            operationId: deleteMobileApp
            x-cli-name: delete
            tags: [Mobile Apps]
            summary: Delete a mobile app
            description: Deletes an app unless queued or executing runs still use it.
            responses:
                '204': {description: Mobile app deleted.}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/mobile-apps/upload:
        post:
            operationId: uploadMobileApp
            x-cli-name: upload
            tags: [Mobile Apps]
            summary: Upload a mobile app
            description: Uploads and validates one Android APK of at most 500 MB.
            requestBody:
                description: Multipart body containing one APK in the file field.
                required: true
                content:
                    multipart/form-data:
                        schema:
                            $ref: '#/components/schemas/UploadMobileAppRequest'
            responses:
                '201':
                    description: Mobile app uploaded.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MobileAppEnvelope'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '413': {$ref: '#/components/responses/ContentTooLarge'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/mobile-apps/import:
        post:
            operationId: importMobileApp
            x-cli-name: import
            tags: [Mobile Apps]
            summary: Import a staged mobile app
            description: Consumes an APK from tenant Files or a trusted assistant workspace.
            requestBody:
                description: Staged APK source and relative path.
                required: true
                content:
                    application/json:
                        schema: {$ref: '#/components/schemas/ImportMobileAppRequest'}
            responses:
                '201':
                    description: Mobile app imported.
                    content:
                        application/json:
                            schema: {$ref: '#/components/schemas/MobileAppEnvelope'}
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '403': {$ref: '#/components/responses/Forbidden'}
                '404': {$ref: '#/components/responses/NotFound'}
                '413': {$ref: '#/components/responses/ContentTooLarge'}
                '422': {$ref: '#/components/responses/UnprocessableEntity'}
                '507': {$ref: '#/components/responses/InsufficientStorage'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings:
        get:
            operationId: listFindings
            x-cli-name: list
            tags: [Findings]
            summary: List findings
            description: Returns tenant findings visible to the authenticated principal. Defaults to active findings (open and regressed) when status is omitted. Follow next_cursor with the same filters and sort to read subsequent pages.
            parameters:
                -   $ref: '#/components/parameters/ProjectFilter'
                -   name: severity
                    in: query
                    description: Severity filter.
                    schema: {type: string}
                -   name: status
                    in: query
                    description: Comma-separated lifecycle status filter.
                    schema: {type: string}
                -   name: source
                    in: query
                    description: Finding source filter.
                    schema: {type: string}
                -   name: type
                    in: query
                    description: Finding type filter.
                    schema: {type: string}
                -   name: date_range
                    in: query
                    description: Recently observed findings window.
                    schema:
                        type: string
                        enum: [7d, 30d]
                -   name: day
                    in: query
                    description: Calendar day on which the finding was first seen.
                    schema:
                        type: string
                        format: date
                -   name: query
                    in: query
                    description: Case-insensitive title substring.
                    schema: {type: string}
                -   name: sort
                    in: query
                    description: Finding ordering.
                    schema:
                        type: string
                        enum: [recent, severity]
                        default: recent
                -   name: limit
                    in: query
                    description: Maximum findings to return.
                    schema:
                        type: integer
                        minimum: 1
                        maximum: 100
                        default: 20
                -   name: cursor
                    in: query
                    description: Continue pagination from the next_cursor of the previous page. Opaque; send it with the same sort and filters.
                    schema: {type: string}
                -   $ref: '#/components/parameters/FindingNotSeenDays'
                -   $ref: '#/components/parameters/FindingURLFilter'
                -   $ref: '#/components/parameters/FindingAgentFilter'
            responses:
                '200':
                    description: Matching findings.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/FindingList'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/summary:
        get:
            operationId: summarizeTenantFindings
            x-cli-name: summary
            tags: [Findings]
            summary: Summarize findings
            description: Returns counts of the tenant's visible findings by status, severity, type, and source. Defaults to all lifecycle states, unlike the findings list which defaults to open and regressed. Supply matching status filters when comparing the summary with the list.
            parameters:
                -   $ref: '#/components/parameters/ProjectFilter'
                -   name: severity
                    in: query
                    description: Comma-separated severity filter.
                    schema: {type: string}
                -   name: status
                    in: query
                    description: Comma-separated lifecycle status filter; every state when omitted.
                    schema: {type: string}
                -   name: source
                    in: query
                    description: Comma-separated source filter.
                    schema: {type: string}
                -   name: type
                    in: query
                    description: Comma-separated finding type filter.
                    schema: {type: string}
                -   name: date_range
                    in: query
                    description: Only findings observed within a recent window.
                    schema:
                        type: string
                        enum: [7d, 30d]
                -   $ref: '#/components/parameters/FindingNotSeenDays'
                -   $ref: '#/components/parameters/FindingURLFilter'
                -   $ref: '#/components/parameters/FindingAgentFilter'
            responses:
                '200':
                    description: Tenant findings summary.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/FindingsSummary'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/transition:
        post:
            operationId: transitionFindings
            x-cli-name: transition
            tags: [Findings]
            summary: Transition findings in bulk
            description: Applies one lifecycle action to up to 100 findings atomically with a shared reason. If any finding is missing, hidden, or cannot take the action from its current state, nothing changes.
            requestBody:
                description: The action, the findings, and why.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/FindingBulkTransitionRequest'
            responses:
                '200':
                    description: The action was applied to every listed finding.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/FindingBulkTransitionResult'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/{id}/resolve:
        post:
            operationId: resolveFinding
            x-cli-name: resolve
            tags: [Findings]
            summary: Resolve a finding
            description: Marks the finding resolved (fixed, or no longer reproducible) and records who did it and why in the finding history. Resolving an already resolved finding is a no-op and records nothing.
            parameters:
                -   $ref: '#/components/parameters/FindingID'
            requestBody:
                description: Why the finding is being resolved.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/FindingTransitionRequest'
            responses:
                '200':
                    description: The resolved finding.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Finding'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/{id}/dismiss:
        post:
            operationId: dismissFinding
            x-cli-name: dismiss
            tags: [Findings]
            summary: Dismiss a finding
            description: Marks the finding dismissed (not a bug, obsolete, or a test-fixture artifact) and records who did it and why in the finding history. Dismissing an already dismissed finding is a no-op and records nothing.
            parameters:
                -   $ref: '#/components/parameters/FindingID'
            requestBody:
                description: Why the finding is being dismissed.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/FindingTransitionRequest'
            responses:
                '200':
                    description: The dismissed finding.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Finding'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/{id}/reopen:
        post:
            operationId: reopenFinding
            x-cli-name: reopen
            tags: [Findings]
            summary: Reopen a finding
            description: Returns a resolved or dismissed finding to open and records who did it and why in the finding history. Reopening an open finding is a no-op and records nothing.
            parameters:
                -   $ref: '#/components/parameters/FindingID'
            requestBody:
                description: Why the finding is being reopened.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/FindingTransitionRequest'
            responses:
                '200':
                    description: The reopened finding.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Finding'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '409': {$ref: '#/components/responses/Conflict'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/findings/{id}:
        get:
            operationId: getFinding
            x-cli-name: get
            tags: [Findings]
            summary: Get a finding
            description: Returns a visible tenant finding with its evidence chain and its lifecycle history (every resolve, dismiss, reopen, and regress with who applied it and why).
            parameters:
                -   name: id
                    in: path
                    required: true
                    description: Finding ID.
                    schema: {type: string, format: uuid}
            responses:
                '200':
                    description: Finding and evidence.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/FindingDetail'
                '400': {$ref: '#/components/responses/BadRequest'}
                '401': {$ref: '#/components/responses/Unauthorized'}
                '404': {$ref: '#/components/responses/NotFound'}
                '500': {$ref: '#/components/responses/InternalServerError'}
                default: {$ref: '#/components/responses/InternalServerError'}
    /api/v1/mailboxes:
        post:
            operationId: createMailbox
            x-cli-name: create
            tags: [Mailboxes]
            summary: Create a mailbox
            description: Reserves a globally unique mailbox name and returns its deliverable address.
            requestBody:
                description: Mailbox name to normalize and reserve.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/CreateMailboxRequest'
            responses:
                '201':
                    description: Mailbox created.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Mailbox'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '409':
                    $ref: '#/components/responses/Conflict'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        get:
            operationId: listMailboxes
            x-cli-name: list
            tags: [Mailboxes]
            summary: List mailboxes
            parameters:
                -   $ref: '#/components/parameters/MailboxLimit'
            responses:
                '200':
                    description: The authenticated tenant's mailboxes, newest first.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/MailboxList'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/mailboxes/{name}:
        parameters:
            -   $ref: '#/components/parameters/MailboxName'
        get:
            operationId: getMailbox
            x-cli-name: get
            tags: [Mailboxes]
            summary: Get a mailbox
            responses:
                '200':
                    description: Mailbox found.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Mailbox'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        delete:
            operationId: deleteMailbox
            x-cli-name: delete
            tags: [Mailboxes]
            summary: Delete a mailbox
            description: Deletes the mailbox, its stored emails, and its agent and test attachments.
            responses:
                '204':
                    description: Mailbox deleted.
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/mailboxes/{name}/emails:
        parameters:
            -   $ref: '#/components/parameters/MailboxName'
        get:
            operationId: listMailboxEmails
            x-cli-name: list-emails
            tags: [Mailboxes]
            summary: List emails received by a mailbox
            description: Returns the most recently received messages first.
            parameters:
                -   $ref: '#/components/parameters/MailboxLimit'
            responses:
                '200':
                    description: Emails received by the mailbox.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/MailboxEmailList'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/tunnels:
        get:
            operationId: listTunnels
            x-cli-name: list
            tags: [Tunnels]
            summary: List live tunnels
            description: Returns the authenticated tenant's live tunnels, newest first.
            responses:
                '200':
                    description: Live tunnels and traffic statistics.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/TunnelList'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '503':
                    $ref: '#/components/responses/ServiceUnavailable'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/models:
        get:
            operationId: listModels
            x-cli-name: list
            tags: [Models]
            summary: List available models
            description: Returns the enabled LLM configurations currently available to agents.
            responses:
                '200':
                    description: Available model configurations.
                    content:
                        application/json:
                            schema:
                                type: array
                                items:
                                    $ref: '#/components/schemas/Model'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/projects:
        post:
            operationId: createProject
            x-cli-name: create
            tags: [Projects]
            summary: Create a project
            requestBody:
                description: Project fields.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/CreateProjectRequest'
            responses:
                '201':
                    description: Project created.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Project'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        get:
            operationId: listProjects
            x-cli-name: list
            tags: [Projects]
            summary: List projects
            description: Projects are returned in descending project ID order.
            parameters:
                -   $ref: '#/components/parameters/Limit'
                -   $ref: '#/components/parameters/Cursor'
            responses:
                '200':
                    description: Projects belonging to the authenticated tenant.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/ProjectList'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/projects/{id}:
        parameters:
            -   $ref: '#/components/parameters/ProjectID'
        get:
            operationId: getProject
            x-cli-name: get
            tags: [Projects]
            summary: Get a project
            responses:
                '200':
                    description: Project found.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Project'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        patch:
            operationId: updateProject
            x-cli-name: update
            tags: [Projects]
            summary: Update a project
            requestBody:
                description: Fields to replace on the project.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/UpdateProjectRequest'
            responses:
                '200':
                    description: Project updated.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Project'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        delete:
            operationId: deleteProject
            x-cli-name: delete
            tags: [Projects]
            summary: Delete a project
            responses:
                '204':
                    description: Project deleted.
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '409':
                    $ref: '#/components/responses/Conflict'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
    /api/v1/projects/{id}/webhook:
        parameters:
            -   $ref: '#/components/parameters/ProjectID'
        put:
            operationId: setProjectWebhook
            x-cli-name: set-webhook
            tags: [Projects]
            summary: Configure a project webhook
            requestBody:
                description: Webhook endpoint and optional signing secret.
                required: true
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/SetProjectWebhookRequest'
            responses:
                '204':
                    description: Webhook configured.
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        get:
            operationId: getProjectWebhook
            x-cli-name: get-webhook
            tags: [Projects]
            summary: Get a project webhook
            description: The webhook signing secret is never returned.
            responses:
                '200':
                    description: Webhook configuration.
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/ProjectWebhook'
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
        delete:
            operationId: deleteProjectWebhook
            x-cli-name: delete-webhook
            tags: [Projects]
            summary: Delete a project webhook
            responses:
                '204':
                    description: Webhook removed.
                '400':
                    $ref: '#/components/responses/BadRequest'
                '401':
                    $ref: '#/components/responses/Unauthorized'
                '404':
                    $ref: '#/components/responses/NotFound'
                '500':
                    $ref: '#/components/responses/InternalServerError'
                default:
                    $ref: '#/components/responses/InternalServerError'
components:
    securitySchemes:
        bearerAuth:
            type: http
            scheme: bearer
            bearerFormat: API key
            description: API key passed using the Bearer authentication scheme.
    parameters:
        ProjectID:
            name: id
            in: path
            required: true
            description: Project ID.
            schema:
                type: string
                format: uuid
        Limit:
            name: limit
            in: query
            description: Maximum number of projects to return.
            schema:
                type: integer
                minimum: 1
                maximum: 100
                default: 20
        Cursor:
            name: cursor
            in: query
            description: Return projects with IDs lower than this project ID.
            schema:
                type: string
                format: uuid
        MailboxName:
            name: name
            in: path
            required: true
            description: Globally unique mailbox name.
            schema:
                type: string
                minLength: 1
                maxLength: 40
                pattern: '^[a-z0-9](?:[a-z0-9.-]{0,38}[a-z0-9])?$'
        MailboxLimit:
            name: limit
            in: query
            description: Maximum number of results to return.
            schema:
                type: integer
                minimum: 1
                maximum: 200
                default: 50
        ProjectFilter:
            name: project_id
            in: query
            description: Restrict results to one project.
            schema:
                type: string
                format: uuid
        FindingID:
            name: id
            in: path
            required: true
            description: Finding ID.
            schema: {type: string, format: uuid}
        FindingNotSeenDays:
            name: not_seen_days
            in: query
            description: Only findings whose last recorded observation is older than N days. This does not establish that a relevant test ran or that the issue was fixed. Staleness alone is not evidence for resolving or dismissing a finding.
            schema: {type: integer, minimum: 1}
        FindingURLFilter:
            name: url
            in: query
            description: Case-insensitive fragment of the finding's normalized host and path.
            schema: {type: string}
        FindingAgentFilter:
            name: agent_id
            in: query
            description: Only findings with evidence observed by this agent run.
            schema: {type: string, format: uuid}
        RequiredFilePath:
            name: path
            in: query
            required: true
            description: Tenant-relative file path.
            schema:
                type: string
                minLength: 1
        RoleID:
            name: id
            in: path
            required: true
            description: Role ID.
            schema: {type: string, format: uuid}
        AgentID:
            name: id
            in: path
            required: true
            description: Agent ID.
            schema: {type: string, format: uuid}
        TestID:
            name: id
            in: path
            required: true
            description: Test ID.
            schema: {type: string, format: uuid}
        DiscoveryID:
            name: id
            in: path
            required: true
            description: Discovery ID.
            schema: {type: string, format: uuid}
        TestPlanID:
            name: id
            in: path
            required: true
            description: Test plan ID.
            schema: {type: string, format: uuid}
        TestPlanRunID:
            name: id
            in: path
            required: true
            description: Test plan run ID.
            schema: {type: string, format: uuid}
        RecordingID:
            name: id
            in: path
            required: true
            description: Recording ID.
            schema: {type: string, format: uuid}
        ScheduleID:
            name: id
            in: path
            required: true
            description: Schedule ID.
            schema: {type: string, format: uuid}
    responses:
        BadRequest:
            description: The request is malformed or failed validation.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        Unauthorized:
            description: Authentication is missing or invalid.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        NotFound:
            description: The requested resource does not exist.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        Forbidden:
            description: The authenticated principal is not authorized for this operation.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        Conflict:
            description: The request conflicts with the current resource state.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        InternalServerError:
            description: The server could not complete the request.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        ServiceUnavailable:
            description: The requested feature is not configured on this server.
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
        ContentTooLarge:
            description: The request body or uploaded artifact is too large.
            content:
                application/problem+json:
                    schema: {$ref: '#/components/schemas/Problem'}
        UnprocessableEntity:
            description: The request is structurally valid but cannot be processed.
            content:
                application/problem+json:
                    schema: {$ref: '#/components/schemas/Problem'}
        InsufficientStorage:
            description: The tenant does not have enough storage quota.
            content:
                application/problem+json:
                    schema: {$ref: '#/components/schemas/Problem'}
        BadGateway:
            description: A required upstream service could not complete the operation.
            content:
                application/problem+json:
                    schema: {$ref: '#/components/schemas/Problem'}
        PaymentRequired:
            description: The tenant has exhausted its run allowance.
            content:
                application/problem+json:
                    schema: {$ref: '#/components/schemas/Problem'}
    schemas:
        Project:
            type: object
            description: A tenant-scoped organizational container.
            required:
                - id
                - tenant_id
                - name
                - description
                - created_at
                - updated_at
            properties:
                id:
                    type: string
                    format: uuid
                tenant_id:
                    type: string
                    format: uuid
                name:
                    type: string
                description:
                    type: string
                webhook_url:
                    type: string
                    format: uri
                created_at:
                    type: string
                    format: date-time
                updated_at:
                    type: string
                    format: date-time
            additionalProperties: false
        ProjectList:
            type: object
            description: A cursor-paginated page of projects.
            required: [projects]
            properties:
                projects:
                    type: array
                    items:
                        $ref: '#/components/schemas/Project'
                next_cursor:
                    type: string
                    format: uuid
            additionalProperties: false
        CreateProjectRequest:
            type: object
            description: Fields required to create a project.
            required: [name]
            properties:
                name:
                    description: Project name
                    type: string
                    minLength: 1
                description:
                    description: Project description
                    type: string
                    default: ''
            additionalProperties: false
        UpdateProjectRequest:
            type: object
            description: Project fields to replace.
            minProperties: 1
            properties:
                name:
                    description: New project name
                    type: string
                    minLength: 1
                description:
                    description: New project description
                    type: string
            additionalProperties: false
        SetProjectWebhookRequest:
            type: object
            description: Webhook configuration for a project.
            required: [url]
            properties:
                url:
                    type: string
                    minLength: 1
                    description: Absolute HTTPS endpoint that receives project webhook requests.
                secret:
                    description: Optional HMAC signing secret
                    type: string
            additionalProperties: false
        ProjectWebhook:
            type: object
            description: Public project webhook configuration without its secret.
            required: [has_webhook]
            properties:
                has_webhook:
                    type: boolean
                url:
                    type: string
                    format: uri
            additionalProperties: false
        JSONValue:
            type: string
            description: Default textual value used when the caller omits a plan variable.
        SchedulePattern:
            type: object
            description: Weekly, monthly-weekday, or one-time fire pattern.
            required: [kind]
            properties:
                kind: {type: string, enum: [weekly, monthly_weekday, once]}
                days: {type: array, items: {type: integer, minimum: 1, maximum: 7}}
                ordinal: {type: string, enum: [first, second, third, fourth, last]}
                weekday: {type: integer, minimum: 1, maximum: 7}
                offset_days: {type: integer, minimum: -28, maximum: 28}
                date: {type: string, format: date}
            additionalProperties: false
        ScheduleDigestConfig:
            type: object
            description: Delivery and severity settings for schedule digests.
            required: [enabled, channels, notify_on]
            properties:
                enabled: {type: boolean}
                channels: {type: array, items: {type: string, enum: [email, slack]}}
                notify_on: {type: string, enum: [always, failures_only]}
                recipients: {type: array, items: {type: string}}
                min_severity: {type: string, enum: [critical, high, medium, low]}
            additionalProperties: false
        Schedule:
            type: object
            description: Recurring automation configuration.
            required: [id, tenant_id, project_id, name, run_at, run_days, enabled, pattern, variables, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                name: {type: string}
                run_at: {type: string}
                run_days: {type: array, items: {type: integer, minimum: 1, maximum: 7}}
                enabled: {type: boolean}
                digest_config: {$ref: '#/components/schemas/ScheduleDigestConfig'}
                last_tick_at: {type: string, format: date-time}
                pattern: {$ref: '#/components/schemas/SchedulePattern'}
                timezone: {type: string}
                variables: {type: object, additionalProperties: {type: string}}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        ScheduleItem:
            type: object
            description: One test or test-plan target attached to a schedule.
            required: [id, schedule_id, created_at]
            properties:
                id: {type: string, format: uuid}
                schedule_id: {type: string, format: uuid}
                test_id: {type: string, format: uuid}
                test_plan_id: {type: string, format: uuid}
                created_at: {type: string, format: date-time}
            additionalProperties: false
        ScheduleRun:
            type: object
            description: Agent launched by a schedule or scheduled plan run.
            required: [agent_id, triggered_at, status]
            properties:
                agent_id: {type: string, format: uuid}
                test_id: {type: string, format: uuid}
                plan_run_id: {type: string, format: uuid}
                triggered_at: {type: string, format: date-time}
                status: {type: string}
            additionalProperties: false
        CreateScheduleRequest:
            type: object
            description: New recurring automation and initial test targets.
            required: [project_id, name]
            properties:
                project_id:
                    description: Project ID
                    type: string
                    format: uuid
                name:
                    description: Schedule name
                    type: string
                    minLength: 1
                run_at:
                    description: Time of day the schedule fires, formatted HH:MM in the schedule timezone. Defaults to 09:00.
                    type: string
                    default: 09:00
                run_days:
                    description: 'ISO day numbers to run on (Mon=1..Sun=7). Default: all days. Example: [1,2,3,4,5] for weekdays. Compatibility alias for a weekly pattern.'
                    type: array
                    items: {maximum: 7, minimum: 1, type: integer}
                pattern:
                    $ref: '#/components/schemas/SchedulePattern'
                timezone:
                    description: IANA timezone for fire evaluation (e.g. Europe/Brussels). Empty = tenant timezone.
                    type: string
                variables:
                    description: 'Static string variables forwarded to scheduled test-plan runs, merged under the per-fire built-ins fire_date and fire_id. Applied only where the plan declares the variable. Example: {"environment": "staging"}.'
                    type: object
                    additionalProperties: {type: string}
                test_ids:
                    description: Test IDs to include
                    type: array
                    items: {format: uuid, type: string}
                digest_config:
                    $ref: '#/components/schemas/ScheduleDigestConfig'
            additionalProperties: false
        UpdateScheduleRequest:
            type: object
            description: Partial recurring automation changes.
            properties:
                name:
                    description: New name
                    type: string
                    minLength: 1
                run_at:
                    description: Time of day the schedule fires, formatted HH:MM in the schedule timezone.
                    type: string
                run_days:
                    description: ISO day numbers (Mon=1..Sun=7). Compatibility alias for a weekly pattern.
                    type: array
                    items: {maximum: 7, minimum: 1, type: integer}
                enabled:
                    description: Enable or disable
                    type: boolean
                pattern:
                    $ref: '#/components/schemas/SchedulePattern'
                timezone:
                    description: IANA timezone for fire evaluation. Empty string reverts to the tenant timezone.
                    type: string
                variables:
                    description: Static variables for scheduled test-plan runs. Empty object clears them.
                    type: object
                    additionalProperties: {type: string}
                digest_config:
                    description: Email digest settings (see Create for shape). Set to null to disable.
                    allOf: [{$ref: '#/components/schemas/ScheduleDigestConfig'}]
                    nullable: true
            additionalProperties: false
        CreateScheduleResponse:
            type: object
            description: Created schedule and initial targets.
            required: [schedule, items]
            properties:
                schedule: {$ref: '#/components/schemas/Schedule'}
                items: {type: array, items: {$ref: '#/components/schemas/ScheduleItem'}}
            additionalProperties: false
        ScheduleDetail:
            description: Schedule with attached targets and recent runs.
            allOf:
                -   $ref: '#/components/schemas/Schedule'
                -   type: object
                    required: [items, runs]
                    properties:
                        items: {type: array, items: {$ref: '#/components/schemas/ScheduleItem'}}
                        runs: {type: array, items: {$ref: '#/components/schemas/ScheduleRun'}}
        ScheduleList:
            type: object
            description: Cursor-paginated tenant schedules.
            required: [schedules]
            properties:
                schedules: {type: array, items: {$ref: '#/components/schemas/Schedule'}}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        AddScheduleItemRequest:
            type: object
            description: Exactly one runnable target identifier.
            properties:
                test_id:
                    description: Test to add. Mutually exclusive with test_plan_id.
                    type: string
                    format: uuid
                test_plan_id:
                    description: Test plan to add. Mutually exclusive with test_id.
                    type: string
                    format: uuid
            additionalProperties: false
        ScheduleRunList:
            type: object
            description: Recent agents launched by a schedule.
            required: [runs]
            properties:
                runs: {type: array, items: {$ref: '#/components/schemas/ScheduleRun'}}
            additionalProperties: false
        TriggerScheduleRequest:
            type: object
            description: One-fire variable overrides.
            properties:
                variables:
                    description: 'Extra per-fire variables for this trigger only, overriding schedule variables and built-ins. Example: {"run_tag": "release-42"}.'
                    type: object
                    additionalProperties: true
            additionalProperties: false
        ScheduleTriggerResponse:
            description: Schedule metadata and manual trigger counts.
            allOf:
                -   $ref: '#/components/schemas/Schedule'
                -   type: object
                    required: [launched, failed]
                    properties:
                        launched: {type: integer, minimum: 0}
                        failed: {type: integer, minimum: 0}
        ScheduleNextFires:
            type: object
            description: Future fire moments in the effective timezone.
            required: [schedule_id, timezone, description, next_fires]
            properties:
                schedule_id: {type: string, format: uuid}
                timezone: {type: string}
                description: {type: string}
                next_fires: {type: array, items: {type: string, format: date-time}}
            additionalProperties: false
        ScheduleDigestResponse:
            type: object
            description: Structured digest for the most recent trigger batch.
            required: [schedule_id, schedule_name, project_id, project_name, agent_ids]
            properties:
                schedule_id: {type: string, format: uuid}
                schedule_name: {type: string}
                project_id: {type: string, format: uuid}
                project_name: {type: string}
                agent_ids: {type: array, items: {type: string, format: uuid}}
                digest: {type: object, additionalProperties: true}
                reason: {type: string}
            additionalProperties: false
        SendScheduleDigestResponse:
            type: object
            description: Queued digest recipients represented by source agent IDs.
            required: [queued, agent_ids]
            properties:
                queued: {type: boolean}
                agent_ids: {type: array, items: {type: string, format: uuid}}
            additionalProperties: false
        Recording:
            type: object
            description: Public metadata for a captured or imported browser session.
            required: [id, tenant_id, name, source_type, status, event_count, has_video, has_audio, navigations, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                user_id: {type: string, format: uuid}
                name: {type: string}
                source_type: {type: string, enum: [extension, upload, posthog]}
                status: {type: string, enum: [uploading, processing, ready, failed]}
                started_at: {type: string, format: date-time}
                ended_at: {type: string, format: date-time}
                duration_ms: {type: integer, minimum: 0}
                event_count: {type: integer, minimum: 0}
                has_video: {type: boolean}
                has_audio: {type: boolean}
                video_mime_type: {type: string}
                video_bytes: {type: integer, format: int64, minimum: 0}
                navigations: {type: array, items: {type: object, additionalProperties: true}}
                uploaded_video_id: {type: string, format: uuid}
                test_id: {type: string, format: uuid}
                draft_test_plan_id: {type: string, format: uuid}
                error_message: {type: string}
                external_ref: {type: string}
                agent_id: {type: string, format: uuid}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        RecordingList:
            type: object
            description: Cursor-paginated tenant recordings.
            required: [recordings]
            properties:
                recordings: {type: array, items: {$ref: '#/components/schemas/Recording'}}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        CreateRecordingRequest:
            type: object
            description: Captured recording metadata and event payloads.
            required: [started_at, events]
            properties:
                name:
                    description: Recording name
                    type: string
                started_at:
                    description: When the recording started (ISO 8601)
                    type: string
                    format: date-time
                ended_at:
                    description: When the recording ended
                    type: string
                    format: date-time
                duration_ms:
                    description: Recording duration in milliseconds
                    type: integer
                    minimum: 0
                event_count:
                    description: Number of rrweb events captured
                    type: integer
                    minimum: 0
                events:
                    description: Array of rrweb event objects
                    type: array
                    items: {additionalProperties: true, type: object}
                navigations:
                    description: Array of navigation events with url and timestamp_ms
                    type: array
                    items: {additionalProperties: true, type: object}
                project_id:
                    description: Associate recording with a project
                    type: string
                    format: uuid
                has_video:
                    description: Whether video will be uploaded separately
                    type: boolean
                has_audio:
                    description: Whether the video includes audio
                    type: boolean
                audio_narration:
                    type: string
                    description: Base64 data URL containing microphone narration.
            additionalProperties: false
        CreateRecordingResponse:
            type: object
            description: Initial recording status and optional direct video upload URL.
            required: [id, status]
            properties:
                id: {type: string, format: uuid}
                status: {type: string, enum: [uploading, processing]}
                video_upload_url: {type: string, format: uri}
            additionalProperties: false
        RecordingProcessingResponse:
            type: object
            description: Recording processing transition result.
            required: [status]
            properties:
                status: {type: string, enum: [processing]}
            additionalProperties: false
        ReproduceRecordingRequest:
            type: object
            description: Optional finding context for generated reproduction tests.
            properties:
                finding_id:
                    description: 'Finding to ground the generated test in; the test prompt then ends with a Verify: line asserting the behavior the finding reports as broken'
                    type: string
                    format: uuid
            additionalProperties: false
        ReproduceRecordingResponse:
            type: object
            description: Queued reproduction identifiers.
            required: [queued]
            properties:
                queued: {type: boolean}
                recording_id: {type: string, format: uuid}
                test_id: {type: string, format: uuid}
                agent_id: {type: string, format: uuid}
            additionalProperties: false
        TestPlanVariableDefinition:
            type: object
            description: Declared caller-supplied plan variable.
            required: [description]
            properties:
                description: {type: string}
                type: {type: string}
                default: {$ref: '#/components/schemas/JSONValue'}
                order: {type: integer}
            additionalProperties: false
        TestPlanOutputDefinition:
            type: object
            description: Value a plan step records for descendants.
            required: [description]
            properties:
                description: {type: string}
                type: {type: string, enum: [string, number, boolean, object]}
                format: {type: string, enum: [uuid, url, email]}
            additionalProperties: false
        TestPlanStepInput:
            type: object
            description: Complete plan step definition with parent labels.
            required: [label, test_id, profile_name]
            properties:
                label:
                    description: Identifier-shaped label (^[A-Za-z_][A-Za-z0-9_]*$), unique within the plan. Descendant steps see this step's recorded outputs namespaced under this label, and prompts refer to the step by it.
                    type: string
                    pattern: ^[A-Za-z_][A-Za-z0-9_]*$
                test_id:
                    description: Test to run for this step. Must belong to the plan's project.
                    type: string
                    format: uuid
                profile_name:
                    description: 'Browser identity for the step (^[a-z0-9_-]+$). One profile per ACTOR, shared across that actor''s steps: steps sharing a profile_name reuse the same logged-in browser within a run (serialised, state carries over), so log in once in an early step and let descendants start from the logged-in state. Distinct profiles are separate sessions that may run in parallel — only use them for genuinely different actors.'
                    type: string
                    pattern: ^[a-z0-9_-]+$
                on_parent_failure:
                    description: What to do when a parent step reaches a non-success terminal state. One of skip (default — propagate failure to descendants) or run (run regardless once parents are terminal).
                    type: string
                    enum: [skip, run]
                    default: skip
                parents:
                    description: Sibling step labels this step depends on. Empty array for entry-point steps.
                    type: array
                    items: {type: string}
                outputs:
                    description: Map of output name → {description (required), type?, format?}. Names must match the label pattern. type ∈ {string, number, boolean, object}; format ∈ {uuid, url, email}. Values recorded here are automatically provided to every descendant step's agent.
                    type: object
                    additionalProperties: {$ref: '#/components/schemas/TestPlanOutputDefinition'}
                role_id:
                    description: Optional role to attach to the dispatched agent.
                    type: string
                    format: uuid
            additionalProperties: false
        TestPlanStep:
            description: Persisted plan step with stable ID.
            allOf:
                -   $ref: '#/components/schemas/TestPlanStepInput'
                -   type: object
                    required: [id]
                    properties:
                        id: {type: string, format: uuid}
        CreateTestPlanRequest:
            type: object
            description: New plan metadata and graph.
            required: [project_id, name]
            properties:
                project_id:
                    description: Project that owns the plan. Every referenced test must belong to the same project.
                    type: string
                    format: uuid
                name:
                    description: Plan name (required, ≤120 chars).
                    type: string
                    minLength: 1
                    maxLength: 120
                description:
                    description: Optional human description (≤500 chars).
                    type: string
                    maxLength: 500
                variables:
                    description: Plan-level variables the caller will supply at run time. Map of name → {description, type, default?}. Required variables are those without a default.
                    type: object
                    additionalProperties: {$ref: '#/components/schemas/TestPlanVariableDefinition'}
                steps:
                    description: Initial graph. May be empty; use POST /test-plans/{id}/steps to add steps incrementally.
                    type: array
                    items: {$ref: '#/components/schemas/TestPlanStepInput'}
            additionalProperties: false
        ReplaceTestPlanRequest:
            type: object
            description: Complete replacement plan metadata and graph.
            required: [name]
            properties:
                name:
                    description: Plan name (required, ≤120 chars).
                    type: string
                    minLength: 1
                    maxLength: 120
                description:
                    description: Optional human description (≤500 chars).
                    type: string
                    maxLength: 500
                variables:
                    description: Plan-level variables the caller will supply at run time. Map of name → {description, type, default?}. Required variables are those without a default.
                    type: object
                    additionalProperties: {$ref: '#/components/schemas/TestPlanVariableDefinition'}
                steps:
                    description: Initial graph. May be empty; use POST /test-plans/{id}/steps to add steps incrementally.
                    type: array
                    items: {$ref: '#/components/schemas/TestPlanStepInput'}
            additionalProperties: false
        TestPlan:
            type: object
            description: Hydrated test plan and complete graph.
            required: [id, tenant_id, project_id, name, description, variables, steps, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                name: {type: string}
                description: {type: string}
                variables: {type: object, additionalProperties: {$ref: '#/components/schemas/TestPlanVariableDefinition'}}
                steps: {type: array, items: {$ref: '#/components/schemas/TestPlanStep'}}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        TestPlanSummary:
            type: object
            description: Test plan metadata without hydrated steps.
            required: [id, tenant_id, project_id, name, description, variables, step_count, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                name: {type: string}
                description: {type: string}
                variables: {type: object, additionalProperties: {$ref: '#/components/schemas/TestPlanVariableDefinition'}}
                step_count: {type: integer, minimum: 0}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        TestPlanList:
            type: object
            description: Test plans owned by one project.
            required: [test_plans]
            properties:
                test_plans: {type: array, items: {$ref: '#/components/schemas/TestPlanSummary'}}
            additionalProperties: false
        StartTestPlanRunRequest:
            type: object
            description: Runtime plan variables and optional tunnel hostname.
            properties:
                variables:
                    description: Caller-supplied values for the plan's declared variables. Unknown variable names are rejected.
                    type: object
                    additionalProperties: true
                tunnel:
                    description: Hostname of a live tunnel owned by this tenant. Every agent in the run starts at its test's entry URL rewritten onto this host, and is instructed to substitute this host for any other URL its prompt mentions.
                    type: string
            additionalProperties: false
        TestPlanNodeRun:
            type: object
            description: Execution state for one frozen plan node.
            required: [id, node_id, status, attempt, outputs, created_at]
            properties:
                id: {type: string, format: uuid}
                node_id: {type: string, format: uuid}
                agent_id: {type: string, format: uuid}
                status: {type: string}
                attempt: {type: integer, minimum: 0}
                outputs: {type: object, additionalProperties: true}
                started_at: {type: string, format: date-time}
                finished_at: {type: string, format: date-time}
                created_at: {type: string, format: date-time}
            additionalProperties: false
        TestPlanRun:
            type: object
            description: Frozen execution of one test plan graph.
            required: [id, tenant_id, project_id, status, stopping, graph_snapshot, variables, node_runs, started_at, created_at]
            properties:
                id: {type: string, format: uuid}
                plan_id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                status: {type: string}
                stopping: {type: boolean}
                graph_snapshot: {type: object, additionalProperties: true}
                variables: {type: object, additionalProperties: true}
                tunnel_url: {type: string, format: uri}
                node_runs: {type: array, items: {$ref: '#/components/schemas/TestPlanNodeRun'}}
                started_at: {type: string, format: date-time}
                finished_at: {type: string, format: date-time}
                created_at: {type: string, format: date-time}
            additionalProperties: false
        TestPlanRunList:
            type: object
            description: Runs belonging to one test plan.
            required: [runs]
            properties:
                runs: {type: array, items: {$ref: '#/components/schemas/TestPlanRun'}}
            additionalProperties: false
        Discovery:
            type: object
            description: >
                One discovery run. Credentials supplied at creation are write-only and never appear here. The stats and app_description fields populate as the pipeline stages finish; the reviewer's tests and findings are served separately under /discoveries/{id}/suggestions.
            required: [id, tenant_id, project_id, entry_url, preset, settings, status, cancel_requested, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                entry_url: {type: string, format: uri}
                preset: {type: string, enum: [server, spa, custom]}
                settings: {$ref: '#/components/schemas/DiscoverySettings'}
                status:
                    description: >
                        Pipeline stage. pending/review_pending are waiting to be picked up; preparing/crawling/reviewing are running; completed/failed/canceled are terminal.
                    type: string
                    enum: [pending, preparing, crawling, review_pending, reviewing, completed, failed, canceled]
                cancel_requested: {type: boolean, description: A cancellation was requested; the run stops at its next safe point and moves to canceled.}
                failure_reason: {type: string, description: "Why the discovery failed, set exactly when status is failed."}
                stats: {$ref: '#/components/schemas/DiscoveryStats'}
                app_description: {type: string, description: Reviewer's best-effort description of the application.}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
                completed_at: {type: string, format: date-time}
            additionalProperties: false
        DiscoverySettings:
            type: object
            description: >
                Resolved crawler settings a discovery runs with. Returned on every discovery; accepted at creation only with the custom preset.
            required: [max_states, max_depth, max_duration_seconds, settle_navigate_ms, settle_click_ms, near_dup_threshold]
            properties:
                max_states: {type: integer, minimum: 1, maximum: 500, description: State budget of the crawl.}
                max_depth: {type: integer, minimum: 0, description: Maximum exploration depth from the entry state; 0 means unlimited.}
                max_duration_seconds: {type: integer, minimum: 60, maximum: 3600, description: Wall-clock budget of the crawl.}
                settle_navigate_ms: {type: integer, minimum: 0, maximum: 10000, description: Wait after a navigation before observing the page.}
                settle_click_ms: {type: integer, minimum: 0, maximum: 10000, description: Wait after a click before observing the page.}
                near_dup_threshold: {type: number, minimum: 0, maximum: 1, description: Near-duplicate distance threshold; 0 disables detection.}
            additionalProperties: false
        DiscoveryStats:
            type: object
            description: Summary of what the crawl did, written when the crawl ends.
            required: [nodes, edges, duration_seconds, actions_fired, failed_actions, lost, out_of_scope, skipped_known_hrefs, suppressed_near_dup]
            properties:
                nodes: {type: integer}
                edges: {type: integer}
                duration_seconds: {type: number}
                actions_fired: {type: integer}
                failed_actions: {type: integer}
                lost: {type: integer}
                out_of_scope: {type: integer}
                skipped_known_hrefs: {type: integer}
                suppressed_near_dup: {type: integer}
                exit_reason:
                    type: string
                    enum: [exhausted, max_states, max_duration, canceled]
                    description: Why the crawl ended. Absent while the crawl is still running — progress stats are reported before there is a reason.
                unreached_hrefs:
                    type: array
                    items: {type: string}
                    description: In-scope links the crawl saw on some page but never landed on (scheme, host and path). The crawl's own account of what it did not cover.
                failed_action_samples:
                    type: array
                    description: The first few actions the browser refused or the crawler could not return to, with the element and the driver's error, so a thin crawl can be diagnosed from the API alone.
                    items:
                        type: object
                        required: [from_node, url, tag, label, xpath, error, kind]
                        properties:
                            from_node: {type: integer}
                            url: {type: string}
                            tag: {type: string}
                            label: {type: string}
                            href: {type: string}
                            xpath: {type: string}
                            error: {type: string}
                            kind: {type: string, enum: [click, reposition, observe]}
                        additionalProperties: false
            additionalProperties: false
        DiscoveryMap:
            type: object
            description: The crawled state-flow graph.
            required: [nodes, edges]
            properties:
                nodes: {type: array, items: {$ref: '#/components/schemas/DiscoveryNode'}}
                edges: {type: array, items: {$ref: '#/components/schemas/DiscoveryEdge'}}
            additionalProperties: false
        DiscoveryNode:
            type: object
            description: >
                One distinct UI state. node_id is the crawler's discovery-order id (0 is the entry state). A node annotated with near_duplicate_of is a minor variant of that node (opened panel, lightbox), kept distinct but grouped in visualizations.
            required: [node_id, url, depth, candidates_count, discovered_at]
            properties:
                node_id: {type: integer}
                url: {type: string}
                depth: {type: integer}
                candidates_count: {type: integer, description: Number of clickable elements extracted from the state.}
                near_duplicate_of: {type: integer}
                near_dup_distance: {type: number}
                console_error_count: {type: integer, description: SEVERE console entries captured when the state was first observed.}
                discovered_at: {type: string, format: date-time}
            additionalProperties: false
        DiscoveryEdge:
            type: object
            description: One observed transition between two states.
            required: [from_node, to_node, tag, label]
            properties:
                from_node: {type: integer}
                to_node: {type: integer}
                tag: {type: string}
                label: {type: string}
            additionalProperties: false
        DiscoveryList:
            type: object
            description: Cursor-paginated tenant discoveries.
            required: [discoveries]
            properties:
                discoveries: {type: array, items: {$ref: '#/components/schemas/Discovery'}}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        CreateDiscoveryRequest:
            type: object
            description: >
                Target application and crawl configuration. Credentials are optional and write-only; a non-empty list makes the pipeline log in before crawling. Use non-sensitive test credentials only — they are shared with an LLM during the login stage and never echoed back.
            required: [project_id, entry_url]
            properties:
                project_id:
                    description: ID of the project this discovery belongs to
                    type: string
                    format: uuid
                entry_url:
                    description: Absolute http(s) URL the crawl starts from
                    type: string
                    format: uri
                credentials:
                    description: Write-only credentials for the login stage
                    type: array
                    items: {$ref: '#/components/schemas/DiscoveryCredential'}
                preset:
                    description: >
                        Crawler settings preset. server suits server-rendered applications, spa waits longer for client-side rendering, custom requires the settings object.
                    type: string
                    enum: [server, spa, custom]
                    default: server
                settings:
                    $ref: '#/components/schemas/DiscoverySettings'
            additionalProperties: false
        DiscoveryCredential:
            type: object
            description: >
                One write-only test credential for the discovery login stage. The type decides which fields are required: password needs username and password, secret needs value. Credentials never appear in any response.
            required: [type]
            properties:
                type:
                    type: string
                    enum: [password, secret]
                label:
                    description: Optional name so multi-credential logins stay unambiguous
                    type: string
                username:
                    description: Required for type password
                    type: string
                password:
                    description: Required for type password
                    type: string
                    format: password
                value:
                    description: Required for type secret
                    type: string
                    format: password
            additionalProperties: false
        DiscoverySuggestionList:
            type: object
            description: The reviewer's drafted tests and findings for a discovery.
            required: [suggestions]
            properties:
                suggestions: {type: array, items: {$ref: '#/components/schemas/DiscoverySuggestion'}}
            additionalProperties: false
        DiscoverySuggestion:
            type: object
            description: >
                One reviewer proposal for a discovery. kind selects which payload is populated: a test carries `test`, a finding carries `finding`. A suggestion is a draft until a user accepts it; it never becomes a real test or finding on its own.
            required: [id, kind, status, created_at]
            properties:
                id: {type: string, format: uuid}
                kind: {type: string, enum: [test, finding]}
                status: {type: string, enum: [suggested, dismissed, accepted]}
                created_at: {type: string, format: date-time}
                test: {$ref: '#/components/schemas/SuggestedTest'}
                finding: {$ref: '#/components/schemas/SuggestedFinding'}
            additionalProperties: false
        SuggestedTest:
            type: object
            description: >
                A machine-drafted test in the structured-prompt shape: instructions (what the agent should do) plus validations (what must check out).
            required: [name, description, importance, entry_url, instructions, validations]
            properties:
                name: {type: string}
                description: {type: string}
                importance: {type: string, enum: [critical, high, medium, low]}
                entry_url: {type: string}
                instructions: {type: string, description: The goal and what the agent cannot invent; the path is left to the agent.}
                validations: {type: array, items: {type: string}, description: The outcomes that must check out for the test to pass.}
                hints:
                    type: string
                    description: The path the reviewer observed, phrased as a dated observation the agent may find outdated.
                side_effects:
                    type: array
                    items: {type: string, enum: [none, creates_data, sends_email, payment, external_call, cost]}
                    description: What a run does beyond reading pages.
                surface:
                    type: string
                    enum: [customer, operator]
                    description: Whether end users or operators touch this flow.
                evidence_state_ids:
                    type: array
                    items: {type: integer}
                    description: Crawl states that show the flow exists.
            additionalProperties: false
        SuggestedFinding:
            type: object
            description: A machine-drafted finding observed during the crawl.
            required: [type, severity, title, description, state_id]
            properties:
                type: {type: string, enum: [bug, ux_issue, cosmetic, observation, accessibility]}
                severity: {type: string, enum: [critical, high, medium, low]}
                title: {type: string}
                description: {type: string}
                url: {type: string}
                element: {type: string}
                impact: {type: string}
                remediation: {type: string}
                state_id: {type: integer, description: The crawl state (node_id) that evidences this finding.}
            additionalProperties: false
        Test:
            type: object
            description: A reusable test configuration and its mailbox attachments.
            required: [id, tenant_id, project_id, name, description, entry_url, instructions, validations, status, file_paths, device, browser_type, mailbox_names, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                name: {type: string}
                description: {type: string}
                entry_url: {type: string, description: "The URL a browser agent starts at. Always empty for an android test."}
                instructions: {type: string, description: Plain-language brief of the actions the agent performs.}
                validations: {type: array, items: {type: string}, description: "The things the executing agent must explicitly resolve before it may complete. Empty on a legacy test that has not been edited since the structured prompt was introduced."}
                source: {type: string}
                status: {type: string, enum: [draft, active, archived]}
                importance: {type: string}
                external_ref: {type: string}
                file_paths: {type: array, items: {type: string}}
                issue_state: {type: string}
                issue_state_type: {type: string}
                role_id: {type: string, format: uuid}
                device: {type: string, enum: [browser, android]}
                browser_type: {type: string, enum: [chrome, firefox, edge]}
                app_artifact_id: {type: string, format: uuid}
                mailbox_names: {type: array, items: {type: string}}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        TestList:
            type: object
            description: Cursor-paginated tenant tests.
            required: [tests]
            properties:
                tests: {type: array, items: {$ref: '#/components/schemas/Test'}}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        CreateTestRequest:
            type: object
            description: "Fields for a new reusable test. The task is structured: entry_url (browser tests), instructions, and the validations the executing agent must explicitly resolve before it may complete."
            required: [project_id, name, instructions, validations]
            properties:
                project_id:
                    description: ID of the project this test belongs to
                    type: string
                    format: uuid
                name:
                    description: Test name
                    type: string
                    minLength: 1
                description:
                    description: Test description
                    type: string
                entry_url:
                    description: Absolute http(s) URL the agent starts at. Required for a browser test; must be omitted or empty for an android test.
                    type: string
                instructions:
                    description: Plain-language brief of the actions the agent performs. Supports {{ plan.variables.name }} references when the test runs inside a test plan.
                    type: string
                    minLength: 1
                    maxLength: 2000
                validations:
                    description: The concrete things the executing agent must verify and explicitly resolve (passed or failed) before it may complete. At least one; a run with a failed validation cannot complete successfully.
                    type: array
                    minItems: 1
                    items: {type: string, minLength: 1}
                source:
                    description: Origin of the test (e.g. 'discovery', 'manual')
                    type: string
                status:
                    description: 'Lifecycle status: draft (not eligible for regression/schedules), active (eligible), or archived. Defaults to active for manually created tests, draft for generated ones.'
                    type: string
                    enum: [draft, active, archived]
                    default: active
                importance:
                    description: Test importance level (e.g. 'critical', 'high', 'medium', 'low')
                    type: string
                file_paths:
                    description: Paths of tenant files to copy into the test run workspace when running this test
                    type: array
                    items: {type: string}
                mailbox_names:
                    description: Mailbox names to save on the test. Each run inherits these mailboxes unless the run request overrides them.
                    type: array
                    items: {minLength: 1, type: string}
                role_id:
                    description: Default role for this test. A run can override it.
                    type: string
                    format: uuid
                device:
                    description: 'Device type: browser or android'
                    type: string
                    enum: [browser, android]
                    default: browser
                browser_type:
                    description: 'Browser for browser tests: chrome, firefox, or edge'
                    type: string
                    enum: [chrome, firefox, edge]
                    default: chrome
                app_artifact_id:
                    description: Mobile app ID for Android tests
                    type: string
                    format: uuid
            additionalProperties: false
        UpdateTestRequest:
            type: object
            description: Partial changes to a reusable test.
            properties:
                name:
                    description: Test name
                    type: string
                    minLength: 1
                description:
                    description: Test description
                    type: string
                entry_url:
                    description: Absolute http(s) URL the agent starts at. Must be empty for an android test.
                    type: string
                instructions:
                    description: Plain-language brief of the actions the agent performs.
                    type: string
                    minLength: 1
                    maxLength: 2000
                validations:
                    description: Replaces the test's validations. At least one.
                    type: array
                    minItems: 1
                    items: {type: string, minLength: 1}
                source:
                    description: Origin of the test
                    type: string
                status:
                    description: 'Lifecycle status: draft, active, or archived'
                    type: string
                    enum: [draft, active, archived]
                importance:
                    description: Test importance level
                    type: string
                file_paths:
                    description: Paths of tenant files to copy into the test run workspace when running this test
                    type: array
                    items: {type: string}
                mailbox_names:
                    description: Replace the test's mailboxes. Omit this field to keep them. Send an empty array to remove them all.
                    type: array
                    items: {minLength: 1, type: string}
                role_id:
                    description: Default role for this test
                    type: string
                    format: uuid
                device:
                    description: 'Device type: browser or android'
                    type: string
                    enum: [browser, android]
                browser_type:
                    description: 'Browser for browser tests: chrome, firefox, or edge'
                    type: string
                    enum: [chrome, firefox, edge]
                app_artifact_id:
                    description: Mobile app ID for Android tests. Send null to clear it.
                    type: string
                    format: uuid
                    nullable: true
            additionalProperties: false
        RunTestFile:
            type: object
            description: One base64-encoded file delivered to the run workspace.
            required: [path, content]
            properties:
                path: {type: string, minLength: 1}
                content: {type: string, format: byte}
            additionalProperties: false
        RunTestRequest:
            type: object
            description: Optional execution overrides for a saved test.
            properties:
                role_id:
                    description: Role for this run. This value takes precedence over the role saved on the test.
                    type: string
                    format: uuid
                mailbox_names:
                    description: Override the test's attached mailboxes for this run. When omitted, the test's stored attachments are inherited.
                    type: array
                    items: {minLength: 1, type: string}
                model:
                    description: LLM model override for this run
                    type: string
                thinking_level:
                    description: 'Override the executor model''s reasoning depth for this run: MINIMAL, LOW, MEDIUM, or HIGH. Omit to use the model default.'
                    type: string
                    enum: [MINIMAL, LOW, MEDIUM, HIGH]
                auditor_thinking_level:
                    description: Reasoning depth for the auditor that reviews this run (MINIMAL, LOW, MEDIUM, HIGH).
                    type: string
                    enum: [MINIMAL, LOW, MEDIUM, HIGH]
                auditor_model:
                    description: LLM model for the auditor that reviews this run after it finishes. Lets you pick the review model independently of the run itself.
                    type: string
                max_iterations:
                    description: Max iterations override for this run
                    type: integer
                    minimum: 1
                files:
                    description: Inline files delivered into the run's workspace before the agent starts. Each entry is {path, content} with base64-encoded content. Max 20 files / 50MB combined per request.
                    type: array
                    maxItems: 20
                    items: {$ref: '#/components/schemas/RunTestFile'}
                tunnel:
                    description: Hostname of a live tunnel owned by this tenant. The agent starts at the test's entry URL rewritten onto this host, and is instructed to substitute this host for any other URL the prompt mentions.
                    type: string
            additionalProperties: false
        Agent:
            type: object
            description: One agent execution run and its current result metadata.
            required: [id, tenant_id, project_id, status, prompt, model, browser_type, device, kind, is_discovery, is_benchmark, iteration, max_iterations, tokens_used, cached_tokens, auditor_tokens_used, auditor_cached_tokens, source, created_by_automated, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                role_id: {type: string, format: uuid}
                status:
                    type: string
                    enum: [pending, running, waiting, auditing, completed, failed, stopped]
                name: {type: string}
                prompt: {type: string}
                model: {type: string}
                thinking_level: {type: string}
                auditor_thinking_level: {type: string}
                auditor_model: {type: string}
                browser_type: {type: string}
                device: {type: string}
                kind: {type: string}
                is_discovery: {type: boolean}
                is_benchmark: {type: boolean}
                messages:
                    type: array
                    items: {type: object, additionalProperties: true}
                iteration: {type: integer}
                max_iterations: {type: integer}
                tokens_used: {type: integer}
                cached_tokens: {type: integer}
                auditor_tokens_used: {type: integer}
                auditor_cached_tokens: {type: integer}
                browser_url: {type: string, format: uri}
                tunnel_url: {type: string, format: uri}
                session_id: {type: string}
                wait_condition: {type: object, additionalProperties: true}
                result: {type: object, additionalProperties: true}
                summary: {type: object, additionalProperties: true}
                test_id: {type: string, format: uuid}
                discovery_id: {type: string, format: uuid}
                variables:
                    type: object
                    additionalProperties: true
                source: {type: string}
                parent_agent_id: {type: string, format: uuid}
                test_plan_node_run_id: {type: string, format: uuid}
                schedule_id: {type: string, format: uuid}
                app_artifact_id: {type: string, format: uuid}
                issue_url: {type: string, format: uri}
                finding_id: {type: string, format: uuid}
                gh_owner: {type: string}
                gh_repo: {type: string}
                gh_base_branch: {type: string}
                gh_base_sha: {type: string}
                error_classification: {type: string}
                error_explanation: {type: string}
                failure_source: {type: string}
                completion_nudges:
                    type: object
                    additionalProperties: true
                created_by_automated: {type: boolean}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
                started_at: {type: string, format: date-time}
                completed_at: {type: string, format: date-time}
                interaction_url: {type: string, format: uri}
                warnings:
                    type: array
                    items: {type: string}
            additionalProperties: false
        CreateAgentRequest:
            type: object
            description: "Configuration for one inline agent run. The task is structured: entry_url (browser runs), instructions, and optional validations the agent must explicitly resolve before it may complete."
            required: [project_id, instructions]
            properties:
                project_id:
                    description: ID of the project this agent belongs to
                    type: string
                    format: uuid
                role_id:
                    description: Optional role for prompt injection context
                    type: string
                    format: uuid
                name:
                    description: Human-readable agent name
                    type: string
                entry_url:
                    description: Absolute http(s) URL the agent starts at. Required for a browser run; must be omitted or empty for an android run.
                    type: string
                instructions:
                    description: Plain-language brief of the task the agent performs.
                    type: string
                    minLength: 1
                    maxLength: 2000
                validations:
                    description: Optional list of things the agent must verify and explicitly resolve (passed or failed) before it may complete. A run with a failed validation cannot complete successfully. Omit for tasks with nothing to check off.
                    type: array
                    items: {type: string, minLength: 1}
                model:
                    description: LLM model to use
                    type: string
                thinking_level:
                    description: 'Override the executor model''s reasoning depth for this run: MINIMAL, LOW, MEDIUM, or HIGH. Higher = more reasoning tokens (more thorough, more expensive). Omit to use the model''s configured default.'
                    type: string
                    enum: [MINIMAL, LOW, MEDIUM, HIGH]
                auditor_thinking_level:
                    description: Same as thinking_level but for the auditor that reviews this run after it finishes (MINIMAL, LOW, MEDIUM, HIGH). Lets you give the review more or less reasoning independently of the run itself.
                    type: string
                    enum: [MINIMAL, LOW, MEDIUM, HIGH]
                auditor_model:
                    description: LLM model for the auditor that reviews this run after it finishes. Lets you pick the review model independently of the run itself.
                    type: string
                device:
                    description: 'Execution device: browser or android'
                    type: string
                    enum: [browser, android]
                    default: browser
                browser_type:
                    description: 'Browser to use for a browser agent: chrome, firefox, or edge'
                    type: string
                    enum: [chrome, firefox, edge]
                    default: chrome
                app_artifact_id:
                    description: Mobile app ID to install for an Android run
                    type: string
                    format: uuid
                max_iterations:
                    description: Maximum number of model iterations before the agent stops
                    type: integer
                    minimum: 1
                file_paths:
                    description: Paths of tenant files to copy into the agent workspace
                    type: array
                    items: {type: string, minLength: 1}
                mailbox_names:
                    description: Mailbox names to attach to this agent. The agent can read each attached inbox.
                    type: array
                    items: {type: string, minLength: 1}
                tunnel:
                    description: Hostname of a live tunnel owned by this tenant. The agent starts at the entry URL rewritten onto this host, and is instructed to substitute this host for any other URL the instructions mention.
                    type: string
            additionalProperties: false
        ClarifyAgentRequest:
            type: object
            description: Human response to a pending clarification.
            required: [response]
            properties:
                response:
                    description: The user's response to the clarification request
                    type: string
                    minLength: 1
            additionalProperties: false
        UploadAgentFilesRequest:
            type: object
            description: Multipart files delivered to an active agent workspace.
            required: [files]
            properties:
                path:
                    description: Directory prefix inside the workspace to place files under
                    type: string
                files:
                    description: 'One or more files (multipart form field name: ''files''). Max 50MB per request.'
                    type: array
                    minItems: 1
                    items: {type: string, format: binary}
            additionalProperties: false
        UploadedAgentFile:
            type: object
            description: One file written to an agent workspace.
            required: [path, size]
            properties:
                path: {type: string}
                size: {type: integer, format: int64, minimum: 0}
            additionalProperties: false
        UploadAgentFilesResponse:
            type: object
            description: Workspace paths and sizes written by an upload.
            required: [uploaded]
            properties:
                uploaded:
                    type: array
                    items: {$ref: '#/components/schemas/UploadedAgentFile'}
            additionalProperties: false
        RetryAgentRequest:
            type: object
            description: Optional overrides applied to a cloned run. The task itself is never overridable - a retry runs the parent's prompt and validations verbatim.
            properties:
                model:
                    description: Override LLM model
                    type: string
                    minLength: 1
                max_iterations:
                    description: Override max iterations
                    type: integer
                    minimum: 1
                browser_type:
                    description: Override browser (chrome, firefox, edge)
                    type: string
                    enum: [chrome, firefox, edge]
            additionalProperties: false
        AgentList:
            type: object
            description: Cursor-paginated tenant agents.
            required: [agents]
            properties:
                agents:
                    type: array
                    items: {$ref: '#/components/schemas/Agent'}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        Role:
            type: object
            description: A reusable agent behavior brief.
            required: [id, name, description, system_prompt, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                name: {type: string}
                description: {type: string}
                system_prompt: {type: string}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        RoleList:
            type: object
            description: Cursor-paginated visible roles.
            required: [roles]
            properties:
                roles:
                    type: array
                    items: {$ref: '#/components/schemas/Role'}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        CreateRoleRequest:
            type: object
            description: Fields required to create a role.
            required: [name, system_prompt]
            properties:
                name:
                    description: Role name
                    type: string
                    minLength: 1
                description:
                    description: Human-facing subtitle for the role list. When omitted the server auto-generates one from the name and system prompt.
                    type: string
                system_prompt:
                    description: Verbatim text injected into the agent's system prompt at the start of every run. This is the only field that influences runtime behavior.
                    type: string
                    minLength: 1
                global:
                    type: boolean
                    description: Create a platform-global role; support access is required.
            additionalProperties: false
        UpdateRoleRequest:
            type: object
            description: Complete replacement fields for a role.
            required: [name, system_prompt]
            properties:
                name:
                    description: Role name
                    type: string
                    minLength: 1
                description:
                    description: Human-facing subtitle. When omitted the server auto-generates a fresh one.
                    type: string
                system_prompt:
                    description: Verbatim text injected into the agent's system prompt at the start of every run
                    type: string
                    minLength: 1
            additionalProperties: false
        RoleKnowledgeDocument:
            type: object
            description: Public indexing state for one role reference document.
            required: [id, role_id, filename, mime_type, size_bytes, status, extraction, overview, chunk_count, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                role_id: {type: string, format: uuid}
                filename: {type: string}
                mime_type: {type: string}
                size_bytes: {type: integer, format: int64, minimum: 0}
                status:
                    type: string
                    enum: [processing, ready, failed]
                extraction:
                    type: string
                    enum: [ok, partial, empty]
                error_message: {type: string}
                overview:
                    type: object
                    additionalProperties: true
                chunk_count: {type: integer, minimum: 0}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        RoleKnowledgeList:
            type: object
            description: Knowledge documents attached to a role.
            required: [documents]
            properties:
                documents:
                    type: array
                    items: {$ref: '#/components/schemas/RoleKnowledgeDocument'}
            additionalProperties: false
        UploadRoleKnowledgeRequest:
            type: object
            description: Multipart role reference document upload.
            required: [file]
            properties:
                file:
                    description: 'The document to attach (multipart form field name: ''file'')'
                    type: string
                    format: binary
            additionalProperties: false
        FileEntry:
            type: object
            description: One file or directory immediately under a listed path.
            required: [name, is_dir, size]
            properties:
                name: {type: string}
                is_dir: {type: boolean}
                size: {type: integer, format: int64, minimum: 0}
            additionalProperties: false
        FileList:
            type: object
            description: Tenant directory entries.
            required: [files]
            properties:
                files:
                    type: array
                    items: {$ref: '#/components/schemas/FileEntry'}
            additionalProperties: false
        UploadFilesRequest:
            type: object
            description: Files and optional destination path for a multipart upload.
            required: [files]
            properties:
                path:
                    description: Directory prefix to place files under
                    type: string
                files:
                    description: 'One or more files (multipart form field name: ''files'')'
                    type: array
                    minItems: 1
                    items: {type: string, format: binary}
            additionalProperties: false
        UploadedFile:
            type: object
            description: Path and byte count of one stored upload.
            required: [path, size]
            properties:
                path: {type: string}
                size: {type: integer, format: int64, minimum: 0}
            additionalProperties: false
        UploadedFileList:
            type: object
            description: Files stored by a multipart upload.
            required: [uploaded]
            properties:
                uploaded:
                    type: array
                    items: {$ref: '#/components/schemas/UploadedFile'}
            additionalProperties: false
        MoveFileRequest:
            type: object
            description: Existing and destination file paths.
            required: [src_path, dst_path]
            properties:
                src_path:
                    description: Current path of the file
                    type: string
                    minLength: 1
                dst_path:
                    description: New path for the file
                    type: string
                    minLength: 1
            additionalProperties: false
        CreateDirectoryRequest:
            type: object
            description: Directory path to create.
            required: [path]
            properties:
                path:
                    description: Path of the directory to create
                    type: string
                    minLength: 1
            additionalProperties: false
        MutationSuccess:
            type: object
            description: Confirmation of a completed mutation.
            required: [success]
            properties:
                success: {type: boolean}
            additionalProperties: false
        MobileApp:
            type: object
            description: An immutable uploaded Android application artifact.
            required: [id, name, package_name, size_bytes, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                name: {type: string}
                package_name: {type: string}
                main_activity: {type: string}
                version_name: {type: string}
                version_code: {type: integer, format: int64}
                size_bytes: {type: integer, format: int64, minimum: 0}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        MobileAppEnvelope:
            type: object
            description: Response containing one mobile app.
            required: [mobile_app]
            properties:
                mobile_app: {$ref: '#/components/schemas/MobileApp'}
            additionalProperties: false
        MobileAppList:
            type: object
            description: Cursor-paginated tenant mobile apps.
            required: [mobile_apps]
            properties:
                mobile_apps:
                    type: array
                    items: {$ref: '#/components/schemas/MobileApp'}
                next_cursor: {type: string, format: uuid}
            additionalProperties: false
        UploadMobileAppRequest:
            type: object
            description: Multipart Android APK upload.
            required: [file]
            properties:
                file:
                    description: 'Android APK (multipart form field name: file)'
                    type: string
                    format: binary
            additionalProperties: false
        ImportMobileAppRequest:
            type: object
            description: A staged APK to consume into mobile-app storage.
            required: [source, path]
            properties:
                source:
                    description: tenant_files, or chat_workspace for embedded assistant calls
                    type: string
                    enum: [tenant_files, chat_workspace]
                path:
                    description: Relative APK path in the selected source
                    type: string
                    minLength: 1
            additionalProperties: false
        ActiveMobileAppRun:
            type: object
            description: An active test run preventing mobile-app deletion.
            required: [id, name, status]
            properties:
                id: {type: string, format: uuid}
                name: {type: string}
                status: {type: string}
            additionalProperties: false
        Finding:
            type: object
            description: A tenant finding and its current lifecycle state.
            required: [id, tenant_id, project_id, type, severity, title, description, url, element, source, origin, impact, remediation, status, status_changed_at, first_seen_at, last_seen_at, created_at, updated_at]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                type: {type: string}
                severity: {type: string}
                title: {type: string}
                description: {type: string}
                url: {type: string}
                element: {type: string}
                source: {type: string}
                origin: {type: string}
                impact: {type: string}
                remediation: {type: string}
                status: {$ref: '#/components/schemas/FindingStatus'}
                status_changed_at:
                    type: string
                    format: date-time
                    description: When the finding last changed state. Who changed it and why is in the history returned by getFinding.
                github_repo: {type: string}
                github_issue_number: {type: integer}
                github_issue_url: {type: string}
                github_issue_created_at: {type: string, format: date-time}
                github_pr_number: {type: integer}
                github_pr_url: {type: string}
                github_pr_created_at: {type: string, format: date-time}
                first_seen_at: {type: string, format: date-time}
                last_seen_at: {type: string, format: date-time}
                created_at: {type: string, format: date-time}
                updated_at: {type: string, format: date-time}
            additionalProperties: false
        FindingStatus:
            type: string
            description: Finding lifecycle state.
            enum: [open, regressed, resolved, dismissed]
        FindingEvidence:
            type: object
            description: One observation supporting a finding.
            required: [agent_id, observed_at]
            properties:
                agent_id: {type: string, format: uuid}
                agent_name: {type: string}
                test_id: {type: string, format: uuid}
                test_name: {type: string}
                trace_step_id: {type: string, format: uuid}
                trace_step_index: {type: integer}
                observed_at: {type: string, format: date-time}
            additionalProperties: false
        FindingDetail:
            type: object
            description: A finding, its evidence chain, and its lifecycle history.
            required: [finding, evidence, history]
            properties:
                finding: {$ref: '#/components/schemas/Finding'}
                evidence:
                    type: array
                    items: {$ref: '#/components/schemas/FindingEvidence'}
                history:
                    type: array
                    description: Every recorded status transition, oldest first. Empty for a finding that has never left open. The finding's current status equals the `to` of the last entry.
                    items: {$ref: '#/components/schemas/FindingStatusChange'}
            additionalProperties: false
        FindingStatusChange:
            type: object
            description: One lifecycle transition of a finding, who applied it, through which surface, and why.
            required: [occurred_at, action, from, to, channel, actor]
            properties:
                occurred_at: {type: string, format: date-time}
                action:
                    type: string
                    description: The lifecycle action that was applied. regress is only ever applied by the auditor.
                    enum: [resolve, dismiss, reopen, regress]
                from: {$ref: '#/components/schemas/FindingStatus'}
                to: {$ref: '#/components/schemas/FindingStatus'}
                reason:
                    type: string
                    description: Why the action was applied. Absent when the web UI applied it without a reason.
                channel:
                    type: string
                    description: The surface the action came through.
                    enum: [web, api, chat, auditor]
                actor: {$ref: '#/components/schemas/FindingStatusActor'}
            additionalProperties: false
        FindingStatusActor:
            type: object
            description: Who applied a lifecycle action. Exactly one id is present, matching the kind.
            required: [kind]
            properties:
                kind:
                    type: string
                    description: user is a signed-in person (web, chat, or a user-owned API key); api_key is a tenant-owned key with no person behind it; auditor is the audit worker acting on a run's outcome.
                    enum: [user, api_key, auditor]
                user_id: {type: string, format: uuid}
                api_key_id: {type: string, format: uuid}
                agent_id:
                    type: string
                    format: uuid
                    description: The run whose audit triggered the change, for the auditor.
            additionalProperties: false
        FindingListItem:
            type: object
            description: Finding fields and evidence context returned by list operations.
            required: [id, tenant_id, project_id, type, severity, title, description, url, element, source, origin, impact, remediation, status, created_at, first_seen_at, last_seen_at, evidence_count]
            properties:
                id: {type: string, format: uuid}
                tenant_id: {type: string, format: uuid}
                project_id: {type: string, format: uuid}
                type: {type: string}
                severity: {type: string}
                title: {type: string}
                description: {type: string}
                url: {type: string}
                element: {type: string}
                source: {type: string}
                origin: {type: string}
                impact: {type: string}
                remediation: {type: string}
                status: {$ref: '#/components/schemas/FindingStatus'}
                github_repo: {type: string}
                github_issue_number: {type: integer}
                github_issue_url: {type: string}
                github_issue_created_at: {type: string, format: date-time}
                created_at: {type: string, format: date-time}
                first_seen_at: {type: string, format: date-time}
                last_seen_at: {type: string, format: date-time}
                evidence_count: {type: integer}
                last_agent_id: {type: string, format: uuid}
                last_agent_name: {type: string}
                test_id: {type: string, format: uuid}
                test_name: {type: string}
            additionalProperties: false
        FindingList:
            type: object
            description: Cursor-paginated findings matching a tenant-scoped query.
            required: [findings, total]
            properties:
                findings:
                    type: array
                    items: {$ref: '#/components/schemas/FindingListItem'}
                total:
                    type: integer
                    description: Findings matching the filters across every page.
                next_cursor:
                    type: string
                    description: Opaque cursor for the next page; absent on the last page.
            additionalProperties: false
        FindingsSummary:
            type: object
            description: Counts of a tenant's visible findings by classification.
            required: [total, by_status, by_severity, by_type, by_source]
            properties:
                total: {type: integer}
                by_status: {$ref: '#/components/schemas/StringCountMap'}
                by_severity: {$ref: '#/components/schemas/StringCountMap'}
                by_type: {$ref: '#/components/schemas/StringCountMap'}
                by_source: {$ref: '#/components/schemas/StringCountMap'}
            additionalProperties: false
        FindingTransitionAction:
            type: string
            description: Lifecycle action a caller may apply to a finding.
            enum: [resolve, dismiss, reopen]
        FindingTransitionRequest:
            type: object
            description: Why a lifecycle action is being applied.
            required: [reason]
            properties:
                reason:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: Why this action is being applied. Recorded in the finding history so a scripted decision stays auditable.
            additionalProperties: false
        FindingBulkTransitionRequest:
            type: object
            description: One lifecycle action for several findings, applied atomically.
            required: [action, ids, reason]
            properties:
                action: {$ref: '#/components/schemas/FindingTransitionAction'}
                ids:
                    type: array
                    minItems: 1
                    maxItems: 100
                    items: {type: string, format: uuid}
                reason:
                    type: string
                    minLength: 1
                    maxLength: 2000
                    description: Why this action is being applied to every listed finding.
            additionalProperties: false
        FindingBulkTransitionResult:
            type: object
            description: Outcome of a bulk lifecycle transition.
            required: [action, updated, ids]
            properties:
                action: {$ref: '#/components/schemas/FindingTransitionAction'}
                updated:
                    type: integer
                    description: Findings the action was applied to.
                ids:
                    type: array
                    items: {type: string, format: uuid}
            additionalProperties: false
        StringCountMap:
            type: object
            description: Counts keyed by a finding classification value.
            additionalProperties:
                type: integer
        Mailbox:
            type: object
            description: A tenant-owned email inbox.
            required: [name, address, tenant_id, created_at]
            properties:
                name:
                    type: string
                address:
                    type: string
                tenant_id:
                    type: string
                    format: uuid
                created_at:
                    type: string
                    format: date-time
            additionalProperties: false
        MailboxList:
            type: object
            description: The authenticated tenant's mailboxes.
            required: [mailboxes]
            properties:
                mailboxes:
                    type: array
                    items:
                        $ref: '#/components/schemas/Mailbox'
            additionalProperties: false
        CreateMailboxRequest:
            type: object
            description: Fields required to create a mailbox.
            required: [name]
            properties:
                name:
                    type: string
                    minLength: 1
                    maxLength: 40
                    description: Name to normalize and reserve as the address local-part.
            additionalProperties: false
        MailboxEmail:
            type: object
            description: An inbound email stored against a mailbox.
            required:
                - id
                - sender
                - recipient
                - subject
                - body_text
                - body_html
                - received_at
                - created_at
            properties:
                id:
                    type: string
                    format: uuid
                sender:
                    type: string
                recipient:
                    type: string
                subject:
                    type: string
                body_text:
                    type: string
                body_html:
                    type: string
                received_at:
                    type: string
                    format: date-time
                created_at:
                    type: string
                    format: date-time
            additionalProperties: false
        MailboxEmailList:
            type: object
            description: Emails received by a mailbox.
            required: [emails]
            properties:
                emails:
                    type: array
                    items:
                        $ref: '#/components/schemas/MailboxEmail'
            additionalProperties: false
        Tunnel:
            type: object
            description: A point-in-time view of one live tunnel.
            required:
                - host
                - state
                - created_at
                - last_activity
                - active_connections
                - total_connections
                - bytes_in
                - bytes_out
                - client_address
            properties:
                host:
                    type: string
                state:
                    type: string
                    enum: [opening, open]
                created_at:
                    type: string
                    format: date-time
                last_activity:
                    type: string
                    format: date-time
                active_connections:
                    type: integer
                    format: int64
                    minimum: 0
                total_connections:
                    type: integer
                    format: int64
                    minimum: 0
                bytes_in:
                    type: integer
                    format: int64
                    minimum: 0
                bytes_out:
                    type: integer
                    format: int64
                    minimum: 0
                client_address:
                    type: string
            additionalProperties: false
        TunnelList:
            type: object
            description: The authenticated tenant's live tunnels.
            required: [tunnels]
            properties:
                tunnels:
                    type: array
                    items:
                        $ref: '#/components/schemas/Tunnel'
            additionalProperties: false
        Model:
            type: object
            description: An enabled LLM configuration available to agents.
            required:
                - id
                - provider
                - name
                - max_output_tokens
                - thinking_budget
                - thinking_level
                - tokens_per_minute
                - requests_per_minute
                - is_enabled
                - is_default
                - is_fast
                - is_coding
                - is_chat
                - is_auditor
            properties:
                id:
                    type: string
                provider:
                    type: string
                    enum: [anthropic, gemini]
                name:
                    type: string
                max_output_tokens:
                    type: integer
                thinking_budget:
                    type: integer
                thinking_level:
                    type: string
                tokens_per_minute:
                    type: integer
                requests_per_minute:
                    type: integer
                is_enabled:
                    type: boolean
                is_default:
                    type: boolean
                is_fast:
                    type: boolean
                is_coding:
                    type: boolean
                is_chat:
                    type: boolean
                is_auditor:
                    type: boolean
            additionalProperties: false
        Problem:
            type: object
            description: RFC 7807 problem details.
            required: [type, title, status]
            properties:
                type:
                    type: string
                    format: uri-reference
                title:
                    type: string
                status:
                    type: integer
                    minimum: 100
                    maximum: 599
                detail:
                    type: string
                instance:
                    type: string
                    format: uri-reference
                violations:
                    type: array
                    items:
                        $ref: '#/components/schemas/Violation'
                active_runs:
                    type: array
                    description: Active runs blocking a requested mobile-app deletion.
                    items:
                        $ref: '#/components/schemas/ActiveMobileAppRun'
            additionalProperties: false
        Violation:
            type: object
            description: A validation failure associated with one request field.
            required: [field, message]
            properties:
                field:
                    type: string
                message:
                    type: string
            additionalProperties: false
