openapi: 3.1.0 info: title: PreyLab Headless Laboratory version: 0.1.0 description: Authenticated finite simulation jobs using the shared PreyLab model. Browser Observatory sessions use a secure cookie; assistants use a scoped Bearer credential. servers: - url: https://nysa.quest/thefairiesquest/preylab/lab-api security: - labBearer: [] paths: /health: get: security: [] summary: Process health, active slot and queue depth responses: '200': {description: Health readout, content: {application/json: {schema: {type: object}}}} /session: post: security: [] summary: Exchange an existing scoped credential for an HttpOnly Observatory session requestBody: {required: true, content: {application/json: {schema: {type: object, required: [token], properties: {token: {type: string}}}}}} responses: '200': {description: Scoped session established; Secure SameSite cookie returned} '401': {description: Credential rejected} /capabilities: get: summary: Versions, finite limits and capture format responses: '200': {description: Capabilities, content: {application/json: {schema: {type: object}}}} /experiments/validate: post: summary: Validate without creating or advancing a dish requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/Experiment'}}}} responses: '200': {description: Valid with initial resource and placement preview} '422': {description: Validation errors with paths} /experiments: post: summary: Create a paused finite dish; requestKey is idempotent requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/Experiment'}}}} responses: '201': {description: Paused run and initial preview} '409': {description: Request key reused with different content} '422': {description: Invalid experiment} /handoff-browser: post: summary: Transfer a guarded browser archive checkpoint to the runner requestBody: {required: true, content: {application/json: {schema: {type: object, required: [dishId, writeToken, durationMs, speed, requestKey], properties: {dishId: {type: string, format: uuid}, writeToken: {type: string}, durationMs: {type: integer, minimum: 1000, maximum: 3600000}, speed: {type: integer, enum: [1, 2]}, requestKey: {type: string, minLength: 8, maxLength: 100}}}}}} responses: '201': {description: Runner-owned paused run} '409': {description: Stale browser writer} /runs: get: summary: List authorized in-memory headless runs responses: '200': {description: Bounded run directory} /runs/{runId}: get: summary: Inspect one owned run parameters: [{$ref: '#/components/parameters/runId'}] responses: '200': {description: Run status, ownership, revision and accounting} /runs/{runId}/telemetry: get: summary: Read bounded history and recent events without advancing biology parameters: [{$ref: '#/components/parameters/runId'}] responses: '200': {description: Up to 120 history points and 40 events} /runs/{runId}/preview: get: summary: Read a cached, simplified SVG position capture parameters: [{$ref: '#/components/parameters/runId'}] responses: '200': description: Image; X-Preylab-Revision, X-Preylab-Simulated-Ms and X-Preylab-Captured-At provide freshness content: {image/svg+xml: {schema: {type: string}}} /runs/{runId}/checkpoint: get: summary: Export the last durable authorized continuing state parameters: [{$ref: '#/components/parameters/runId'}] responses: '200': {description: Full durable dish and revision; keep private} /runs/{runId}/start: post: summary: Start or queue a paused run parameters: [{$ref: '#/components/parameters/runId'}] requestBody: {$ref: '#/components/requestBodies/Command'} responses: {'200': {description: Accepted command ID, run revision and polling hint}} /runs/{runId}/pause: post: summary: Pause and checkpoint a running or queued run parameters: [{$ref: '#/components/parameters/runId'}] requestBody: {$ref: '#/components/requestBodies/Command'} responses: {'200': {description: Accepted command ID, run revision and polling hint}} /runs/{runId}/cancel: post: summary: Stop a finite run and retain its dish parameters: [{$ref: '#/components/parameters/runId'}] requestBody: {$ref: '#/components/requestBodies/Command'} responses: {'200': {description: Accepted command ID and retained completed run}} /runs/{runId}/speed: post: summary: Select 1x or 2x requested scheduling parameters: [{$ref: '#/components/parameters/runId'}] requestBody: {required: true, content: {application/json: {schema: {type: object, required: [requestKey, speed], properties: {requestKey: {type: string, minLength: 8, maxLength: 100}, speed: {type: integer, enum: [1, 2]}}}}}} responses: {'200': {description: Accepted command ID and current speed}} /runs/{runId}/takeover: post: summary: Fence runner and transfer one checkpoint to microscope authority parameters: [{$ref: '#/components/parameters/runId'}] requestBody: {$ref: '#/components/requestBodies/Command'} responses: '200': {description: Dish ID, revision, age and private browser write token} '409': {description: Ownership changed before takeover} /mcp: post: summary: Streamable HTTP MCP endpoint with scoped PreyLab tools description: Standard JSON-RPC MCP transport. Send an MCP protocol version header and Bearer credential. requestBody: {required: true, content: {application/json: {schema: {type: object}}}} responses: {'200': {description: MCP JSON-RPC response}} components: securitySchemes: labBearer: {type: http, scheme: bearer} parameters: runId: {name: runId, in: path, required: true, schema: {type: string, format: uuid}} requestBodies: Command: {required: true, content: {application/json: {schema: {type: object, required: [requestKey], properties: {requestKey: {type: string, minLength: 8, maxLength: 100}}}}}} schemas: Experiment: type: object additionalProperties: false required: [format, schemaVersion, name, durationMs, speed] properties: format: {const: preylab.lab-experiment} schemaVersion: {const: 1} name: {type: string, minLength: 1, maxLength: 80} requestKey: {type: string, minLength: 8, maxLength: 100, description: Required when creating} seed: {type: string, minLength: 1, maxLength: 80} durationMs: {type: integer, minimum: 1000, maximum: 3600000} speed: {type: integer, enum: [1, 2]} foundationTrial: {type: object, required: [id], properties: {id: {type: string}, variant: {type: string}}} catalogPreset: type: object additionalProperties: false required: [id] description: Complete catalog recipe; do not combine with foundationTrial, environment, protocol or inoculum. properties: id: {type: string, enum: [feature-menagerie, dark-scent-hunt-v2, tidal-scent-reef]} volume: {type: integer, minimum: 1, maximum: 1000, description: Defaults to 50 for feature-menagerie and 200 for dark-scent-hunt-v2 and tidal-scent-reef; tidal-scent-reef requires 200} maxCells: {type: integer, minimum: 6, maximum: 512, description: Defaults to 160 with minimum 51 for feature-menagerie; 32 with minimum 6 for dark-scent-hunt-v2; 250 with minimum 40 for tidal-scent-reef} environment: {type: object, description: Existing portable Environment v1 record} protocol: {type: object, description: Existing Experiment Protocol v2 record} inoculum: {type: array, maxItems: 32, items: {type: object, required: [dna, x, y, z], properties: {dna: {type: object, description: Exact PreyLab DNA v4 record}, x: {type: number, minimum: 0, maximum: 1}, y: {type: number, minimum: 0, maximum: 1}, z: {type: number, minimum: 0, maximum: 1}}}}