{
  "openapi": "3.1.0",
  "info": {
    "title": "NMPortal API",
    "version": "0.1.0",
    "description": "The HTTP contract between the NMPortal backend and its clients \u2014 the browser\nUI and the MCP (Model Context Protocol) server.\n\n**This document is the source of truth.** Where a handler and this contract\ndisagree, the contract is right and the handler is the bug.\n\n## What is authored and what is generated\n\nAuthored here: everything under `paths/` except `paths/jobs/tools/`, and\neverything under `components/`.\n\nGenerated: `paths/jobs/tools/*.yaml` (one concrete path item per tool),\n`dist/openapi.json` (the bundle every tool consumes), and each dispatcher's\n`lambdas/*_api/tools/*.json`. Run `make generate`; CI fails if the committed\noutput is stale.\n\n## What this contract deliberately does not describe\n\nHow a job actually runs. The container image, the CLI wiring, the staging\nplan, and the job spec handed to a compute target are backend concerns and\nlive in `lambdas/*_api/runbooks/` \u2014 a client never needs them, and putting\nthem here would make every execution change a contract change.\n\nThe one exception is `target_id`: an execution concept that is client-facing\nbecause the user picks it.\n\nRoutes that are deployed but not yet described are listed, with an owner, in\n`contracts/route-exceptions.yaml`.\n"
  },
  "servers": [
    {
      "url": "/",
      "description": "Same-origin. CloudFront routes /api/* to the HTTP API, so the browser never\nmakes a cross-origin request and `apiPath` is always \"/api\".\n"
    }
  ],
  "security": [
    {
      "cognitoJwt": []
    }
  ],
  "tags": [
    {
      "name": "tools",
      "description": "Submitting work. One operation per tool."
    },
    {
      "name": "jobs",
      "description": "The job lifecycle \u2014 list, poll, and read results. Tool-agnostic."
    },
    {
      "name": "catalogs",
      "description": "Server-owned option lists. Clients populate pickers from these."
    },
    {
      "name": "lookup",
      "description": "Single records read in the request, by an identifier the caller already\nhas. Distinct from catalogs, which answer \"what may I choose?\".\n"
    },
    {
      "name": "global-data",
      "description": "Admin-curated reference datasets, readable by any authenticated caller."
    },
    {
      "name": "user-data",
      "description": "The caller's own durable file and dataset registry (\"My Data\")."
    },
    {
      "name": "uploads",
      "description": "Scratch uploads \u2014 write-once inputs the bucket expires on its own."
    },
    {
      "name": "compute",
      "description": "The API a registered compute host speaks. Authenticated by a host token\nissued at enrollment, not by a Cognito token \u2014 these are the only routes\nhere that identify a machine rather than a person.\n"
    },
    {
      "name": "account",
      "description": "The caller's own account settings. Scoped to the authenticated user;\nno operation here takes a user identifier.\n"
    },
    {
      "name": "support",
      "description": "Reporting a problem back to the people who run the portal. One operation,\nwrite-only: a report is filed and never read back, because it lands in a\nprivate repository the reporter has no access to.\n"
    }
  ],
  "paths": {
    "/api/global_data": {
      "get": {
        "tags": [
          "global-data"
        ],
        "operationId": "listGlobalData",
        "summary": "List the curated global-data catalog.",
        "description": "Every entry carries a short-lived presigned download URL. S3 keys are never\nreturned \u2014 the catalog is addressed by handle.\n",
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GlobalDataListing"
                },
                "example": {
                  "version": 2,
                  "datasets": [
                    {
                      "id": "swissprot-2024-01",
                      "name": "uniprot_sprot.fasta.gz",
                      "folder": "sequences/uniprot",
                      "size_bytes": 91234567,
                      "description": "Swiss-Prot release 2024_01, reviewed entries only.",
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/swissprot"
                    },
                    {
                      "id": "pfam-a-37",
                      "name": "Pfam-A.full.gz",
                      "folder": "alignments/pfam",
                      "size_bytes": 4123456789,
                      "description": "Pfam-A full alignments, release 37.0.",
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/pfam"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/user-data": {
      "get": {
        "tags": [
          "user-data"
        ],
        "operationId": "listUserData",
        "summary": "List the caller's My Data items and current quota usage.",
        "responses": {
          "200": {
            "description": "The caller's items, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDataListing"
                },
                "example": {
                  "items": [
                    {
                      "file_id": "8f14e45f-ceea-467a-9f6b-1c2d3e4f5a6b",
                      "display_name": "my_sequences.fasta",
                      "category": "sequences",
                      "description": "Kinase domain candidates, round 3.",
                      "uploaded_at": 1786100000.482,
                      "size_bytes": 51234,
                      "shared_with_teams": [],
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/mine"
                    },
                    {
                      "file_id": "c9f0f895-fb98-4b41-9b8d-7a6c5e4d3f2a",
                      "display_name": "esmfold run 12",
                      "category": "job-result",
                      "description": "Saved from job 41d2-8a7c (esmfold-predict)",
                      "uploaded_at": 1786013600.117,
                      "size_bytes": 8412300,
                      "shared_with_teams": [],
                      "kind": "dataset",
                      "source_job_id": "41d2-8a7c",
                      "source_tool_id": "esmfold-predict",
                      "output_subpath": "results/",
                      "object_count": 34,
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/dataset"
                    }
                  ],
                  "quota": {
                    "used_bytes": 8463534,
                    "max_bytes": 1073741824
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "user-data"
        ],
        "operationId": "createUserData",
        "summary": "Register a My Data item and get a presigned upload.",
        "description": "Two-step upload. This call reserves the record and returns a presigned POST;\nthe client then sends the bytes straight to S3, so they never transit the API.\n\nIf the upload then fails, `DELETE /api/user-data/{file_id}` cleans up the\norphaned record.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserDataRequest"
              },
              "example": {
                "display_name": "my_sequences.fasta",
                "size_bytes": 51234,
                "category": "sequences",
                "description": "Kinase domain candidates, round 3."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Record created. Upload the bytes to the returned URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUserDataResponse"
                },
                "example": {
                  "file_id": "8f14e45f-ceea-467a-9f6b-1c2d3e4f5a6b",
                  "upload_url": "https://example-outputs.s3.amazonaws.com/",
                  "upload_fields": {
                    "key": "user-data/dev-user-id/8f14e45f/file",
                    "policy": "eyJleHBpcmF0aW9uIjoi...",
                    "x-amz-signature": "4f2a1c9e8b7d6a5f"
                  },
                  "upload_method": "POST",
                  "expires_in_seconds": 3600
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The operation would push My Data usage past the caller's quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaExceededError"
                }
              }
            }
          }
        }
      }
    },
    "/api/user-data/{file_id}": {
      "delete": {
        "tags": [
          "user-data"
        ],
        "operationId": "deleteUserData",
        "summary": "Delete a My Data item and every object beneath it.",
        "description": "Idempotent and best-effort. Deleting an unknown `file_id` succeeds \u2014 the\nendpoint reports what it was asked to remove, not what it found.\n",
        "parameters": [
          {
            "name": "file_id",
            "in": "path",
            "required": true,
            "description": "A My Data item id.",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteUserDataResponse"
                },
                "example": {
                  "deleted": "8f14e45f-ceea-467a-9f6b-1c2d3e4f5a6b"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/user-data/from-job/{job_id}": {
      "post": {
        "tags": [
          "user-data"
        ],
        "operationId": "saveJobToUserData",
        "summary": "Copy a finished job's results into My Data as a dataset.",
        "description": "Server-side copy within the outputs bucket: the browsable results tree, the\nstatus document, and the downloadable archive. Quota-counted.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveJobToUserDataRequest"
              },
              "example": {
                "name": "esmfold run 12"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dataset created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SaveJobToUserDataResponse"
                },
                "example": {
                  "file_id": "c9f0f895-fb98-4b41-9b8d-7a6c5e4d3f2a",
                  "size_bytes": 8412300,
                  "object_count": 34
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The resource exists but is not in a state that permits this action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The operation would push My Data usage past the caller's quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaExceededError"
                }
              }
            }
          }
        }
      }
    },
    "/api/uploads": {
      "post": {
        "tags": [
          "uploads"
        ],
        "operationId": "createUpload",
        "summary": "Get a presigned upload for a one-shot job input.",
        "description": "Scratch, not storage. An upload is not quota-counted, not listed, and the\nbucket expires it on its own \u2014 it exists to be submitted as a job input once.\nDurable files belong in My Data instead.\n\nThe id is generated server-side, never accepted from the caller: it is what\nkeeps the object inside the caller's own prefix.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUploadRequest"
              },
              "example": {
                "size_bytes": 51234
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload the bytes to the returned URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadResponse"
                },
                "example": {
                  "upload_id": "3c59dc04-8e88-4506-a3a0-1c2d3e4f5a6b",
                  "upload_url": "https://example-outputs.s3.amazonaws.com/",
                  "upload_fields": {
                    "key": "uploads/dev-user-id/3c59dc04/file",
                    "policy": "eyJleHBpcmF0aW9uIjoi...",
                    "x-amz-signature": "9e8b7d6a5f4c3b2a"
                  },
                  "upload_method": "POST",
                  "expires_in_seconds": 3600,
                  "max_bytes": 104857600
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/{family}/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "listJobs",
        "summary": "List the caller's jobs.",
        "description": "The `{family}` segment is cosmetic \u2014 jobs from every family live in one\nplace and this handler is tool-agnostic. It is kept so the UI can group.\n",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "family",
            "in": "path",
            "required": true,
            "description": "The tool family. On the job read routes this segment is cosmetic \u2014 the handler\nis tool-agnostic and `jobs/<org_id>/<sub>/<job_id>/` holds every family's jobs\nalike \u2014\nbut it is kept so the frontend can group by family.\n\nThe enum is load-bearing, not documentation: the route-conformance test\nexpands it to check the templated path against the literal routes the\ndeployment registers. Adding a family here without deploying it, or the\nreverse, fails that test.\n",
            "schema": {
              "type": "string",
              "enum": [
                "align",
                "biom3",
                "bioparsers",
                "blast",
                "esmfold",
                "hmmer",
                "mysca",
                "project",
                "sbm",
                "tensor"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many jobs to return in one page.\n\nThis bounds server work, not just display: the handler sorts and slices\nbefore fetching, so `limit` decides how many objects are read per request.\nA client that omits it gets 10.\n\nOut-of-range values are **clamped, not rejected** \u2014 above 200 becomes 200,\nbelow 1 becomes 1, and an unparseable value falls back to 10. The handler\ndoes this deliberately so a stale frontend cannot break itself. No\n`minimum`/`maximum` is declared here because declaring them would make this\ncontract stricter than the API it describes.\n",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many jobs to skip, counting from the most recent. Defaults to 0;\nnegative and unparseable values clamp to 0.\n\nOffset paging is not stable across a mutating list: a job submitted between\ntwo requests shifts everything down, so a row can appear on both pages and\nanother be missed. Acceptable for a job table a human is reading; not\nsomething to build exactly-once processing on. Replacing this with a cursor\nis an open decision \u2014 see contracts/README.md.\n",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobListing"
                },
                "example": {
                  "total": 2,
                  "jobs": [
                    {
                      "job_id": "41d2-8a7c",
                      "job_name": "kinases-round-3",
                      "tool_id": "esmfold-predict",
                      "target_id": "spark-nm",
                      "status": "done",
                      "submitted_at": 1786013500.22,
                      "finished_at": 1786013740.91,
                      "submitted_by_email": "dev-user@localhost",
                      "result_size_bytes": 8412300,
                      "output_extension": "tar.gz"
                    },
                    {
                      "job_id": "7b3e-2f19",
                      "job_name": "swissprot-scan",
                      "tool_id": "blast-identify",
                      "target_id": "cloud-cpu",
                      "status": "running",
                      "step": "searching",
                      "submitted_at": 1786100200.05,
                      "finished_at": null,
                      "submitted_by_email": "dev-user@localhost"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/{family}/jobs/{job_id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "getJobStatus",
        "summary": "Poll one job's status.",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "family",
            "in": "path",
            "required": true,
            "description": "The tool family. On the job read routes this segment is cosmetic \u2014 the handler\nis tool-agnostic and `jobs/<org_id>/<sub>/<job_id>/` holds every family's jobs\nalike \u2014\nbut it is kept so the frontend can group by family.\n\nThe enum is load-bearing, not documentation: the route-conformance test\nexpands it to check the templated path against the literal routes the\ndeployment registers. Adding a family here without deploying it, or the\nreverse, fails that test.\n",
            "schema": {
              "type": "string",
              "enum": [
                "align",
                "biom3",
                "bioparsers",
                "blast",
                "esmfold",
                "hmmer",
                "mysca",
                "project",
                "sbm",
                "tensor"
              ]
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job's current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                },
                "examples": {
                  "running": {
                    "summary": "Still working \u2014 the poll loop keeps going.",
                    "value": {
                      "job_id": "7b3e-2f19",
                      "job_name": "swissprot-scan",
                      "tool_id": "blast-identify",
                      "target_id": "cloud-cpu",
                      "status": "running",
                      "step": "searching",
                      "submitted_at": 1786100200.05,
                      "finished_at": null,
                      "submitted_by_email": "dev-user@localhost"
                    }
                  },
                  "done": {
                    "summary": "Finished \u2014 the download link is live.",
                    "value": {
                      "job_id": "41d2-8a7c",
                      "job_name": "kinases-round-3",
                      "tool_id": "esmfold-predict",
                      "target_id": "spark-nm",
                      "status": "done",
                      "submitted_at": 1786013500.22,
                      "finished_at": 1786013740.91,
                      "submitted_by_email": "dev-user@localhost",
                      "result_size_bytes": 8412300,
                      "output_extension": "tar.gz",
                      "download_filename": "kinases-round-3.tar.gz",
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/archive",
                      "results_prefix": "jobs/dev-user-id/41d2-8a7c/results/",
                      "extras": {}
                    }
                  },
                  "failed": {
                    "summary": "Failed \u2014 drives the error UI.",
                    "value": {
                      "job_id": "55aa-9c31",
                      "job_name": "bad-input-run",
                      "tool_id": "blast-identify",
                      "target_id": "cloud-cpu",
                      "status": "failed",
                      "submitted_at": 1786099000.11,
                      "finished_at": 1786099062.4,
                      "submitted_by_email": "dev-user@localhost",
                      "error": "Input is not valid FASTA: line 3 has no header."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/jobs/{job_id}/cancel": {
      "post": {
        "tags": [
          "jobs"
        ],
        "operationId": "cancelJob",
        "summary": "Ask for one of the caller's jobs to be stopped.",
        "description": "Records a cancellation request against a job that has not finished, and\nreturns the job's status document with `status` set to `cancelling`.\n\nAddressed by `job_id` alone, like the result and file routes and unlike\nthe listing and status pair. Those two keep a cosmetic `{family}` segment\nso the UI can group by family; cancelling groups by nothing, needs no\nknowledge of what the tool produced, and is served by one handler for\nevery family \u2014 BioM3 included, which still serves its own listing and\nstatus elsewhere. Templating a family here would also have made the path\nambiguous against `/api/tools/jobs/{job_id}/results/{path}`.\n\n**This requests a stop; it does not perform one, and the response does not\nmean the job has ended.** The portal has no way to stop a job itself: a\nqueued job is a message on a compute target's SQS queue that this API\nholds no receipt handle for, and a running job is a container on a host it\ncannot reach. What it can do is write the request into the job's status\ndocument \u2014 which the supervisor already reads and writes on every\ntransition \u2014 and let the supervisor act on it. The supervisor writes the\nterminal `cancelled`, exactly as it writes `done` and `failed`, because a\ncomponent that cannot observe the container must not be the one to declare\nit dead.\n\nSo poll `getJobStatus` afterwards rather than treating the 200 as an\noutcome, and expect three possible endings: `cancelled` if the supervisor\nabandoned or killed it, `done` or `failed` if the job got there first. A\njob whose supervisor does not yet honour the request stays `cancelling`\nuntil it finishes on its own; that is visible rather than silent, which is\nthe reason the state is named for the request and not for the result.\n\nOwnership is structural, not checked: the job's storage key is built from\nthe caller's own subject, so another caller's `job_id` resolves to nothing\nand returns 404 \u2014 the same answer as an id that never existed, which is\nalso what stops this route from confirming that someone else's job exists.\n\nIdempotent. Cancelling a job already in `cancelling` succeeds and changes\nnothing.\n",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request was recorded. The job is now `cancelling`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "job_name": "swissprot-scan",
                  "tool_id": "blast-identify",
                  "target_id": "cloud-cpu",
                  "status": "cancelling",
                  "submitted_at": 1786100200.05,
                  "finished_at": null,
                  "submitted_by_email": "dev-user@localhost",
                  "cancel_requested_at": 1786100260.71
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The job has already finished. `done`, `failed` and `cancelled` are\nterminal, and a request to stop one is a mistake worth reporting\nrather than a no-op worth hiding \u2014 the caller is usually acting on a\nstale listing.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/jobs/{job_id}/results": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "listJobResults",
        "summary": "List the files in a job's results tree.",
        "description": "Addressed by `job_id` alone, deliberately not family-segmented: a caller\nnaming another job's artifact does not necessarily know which family made it.\n",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job's result files.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultListing"
                },
                "example": {
                  "truncated": false,
                  "files": [
                    {
                      "path": "result.tsv",
                      "size_bytes": 41233
                    },
                    {
                      "path": "logs/blast.log",
                      "size_bytes": 8120
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/jobs/{job_id}/results/{path}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "getJobResultFile",
        "summary": "Get a presigned download for one result file.",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "description": "Path of one file within the job's results tree.\n\nDeployed as a greedy `{path+}` parameter, which OpenAPI cannot express; the\nroute-conformance test normalizes the two spellings.\n",
            "x-nm-greedy": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A short-lived presigned GET for the file. Fetch `url` directly and\n**without** an Authorization header \u2014 the signature is in the URL, and\nan extra auth header makes S3 reject it.\n\nThe field is `url` here and `download_url` on the job status route.\nThe inconsistency is real and predates this contract; it is recorded\nrather than corrected because both readers\n(`site/js/workbench/api.js`, `site/js/tools/align/view.js`) and the\nhandler already agree on `url`, so changing it is a breaking change\nthat buys only tidiness.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url"
                  ],
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "Echoes the requested path, relative to results/."
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "size_bytes": {
                      "type": "integer",
                      "minimum": 0
                    }
                  }
                },
                "example": {
                  "path": "result.tsv",
                  "url": "https://example-outputs.s3.amazonaws.com/presigned/result-tsv",
                  "size_bytes": 41233
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/jobs/{job_id}/files": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "listJobFiles",
        "summary": "List every file in a job's folder.",
        "description": "The superset of `listJobResults`, which sees only the `results/` subtree.\nA job's folder also holds `job_metadata.json`, the submitted `input.*` and\n`results.tar.gz`, none of which the results listing can reach.\n\nA separate route rather than a scope flag on `listJobResults`: that route\nis what the workbench's file browser reads, and a default it could be\ntalked out of is a default that eventually flips. Here the narrower view\nstays narrow because it is a different URL, not because a parameter\ndefaulted correctly.\n",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every file in the job folder, `results/` included.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobFileListing"
                },
                "example": {
                  "job_id": "41d2-8a7c",
                  "truncated": false,
                  "files": [
                    {
                      "path": "input.fasta",
                      "size_bytes": 1204
                    },
                    {
                      "path": "job_metadata.json",
                      "size_bytes": 812
                    },
                    {
                      "path": "results/blast/hits.tsv",
                      "size_bytes": 41233
                    },
                    {
                      "path": "results.tar.gz",
                      "size_bytes": 8412300
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/jobs/{job_id}/files/{path}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "readJobFile",
        "summary": "Read one file from a job's folder.",
        "description": "Addressed from the job folder root, so it reaches `job_metadata.json` and\nthe submitted input as well as anything under `results/`.\n\nUnlike `getJobResultFile`, this returns small text **inline**. That is the\npoint of the route: an agent asking what a job produced should not have to\nmake a second, unauthenticated request to a presigned URL to find out. The\npresigned field remains for everything too large or not UTF-8, so the\nresponse shape is a superset rather than an alternative.\n",
        "parameters": [
          {
            "name": "X-NM-Org-Id",
            "in": "header",
            "required": false,
            "description": "The organization the caller is acting under. Job data is stored per\norganization, so a read has to know which one \u2014 and because a user can\nswitch organizations at any time, and may have two tabs open in two of them,\nthis is a property of the *request* rather than of the session or the token.\n\nIt selects a prefix; it does not grant anything. The caller's `sub` still\ncomes from the verified token and still scopes every key, so naming an\norganization you do not belong to returns nothing rather than someone\nelse's data \u2014 which is why this is accepted unvalidated.\n\nOptional today: when omitted, the server falls back to the organization\nsaved via `PUT /api/me/active-organization`, at the cost of a lookup per\nrequest. Clients should send it. The fallback is transitional and will be\nremoved once they all do.\n",
            "schema": {
              "type": "string",
              "example": "natural-machine"
            }
          },
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "description": "Path of one file within the job folder, relative to the folder root rather\nthan to `results/`. `job_metadata.json`, `input.fasta` and\n`results/blast/hits.tsv` are all addressable; `ResultPath` can only reach\nthe third.\n\nThe traversal rules are identical to `ResultPath` \u2014 no empty segment, no\n`.`, no `..`, no backslash \u2014 and ownership is structural either way, since\nthe caller's subject builds the key. The only difference is that the\n`results/` prefix is not forced on.\n\nDeployed as a greedy `{path+}` parameter, which OpenAPI cannot express; the\nroute-conformance test normalizes the two spellings.\n",
            "x-nm-greedy": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file inline, or a presigned URL when it cannot be.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobFileContent"
                },
                "examples": {
                  "inline": {
                    "summary": "Small UTF-8 text \u2014 returned directly.",
                    "value": {
                      "path": "job_metadata.json",
                      "size_bytes": 812,
                      "content_type": "application/json",
                      "content": "{\"tool_id\": \"blast-identify\"}"
                    }
                  },
                  "presigned": {
                    "summary": "Too large to inline \u2014 fetch the URL without auth.",
                    "value": {
                      "path": "results.tar.gz",
                      "size_bytes": 8412300,
                      "content_type": "application/gzip",
                      "download_url": "https://example-outputs.s3.amazonaws.com/presigned/archive",
                      "note": "File exceeds the 32768-byte inline cap; use download_url."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listTools",
        "summary": "Every tool that can be submitted, with its parameter schema.",
        "description": "One entry per tool. `params` is a JSON Schema for the flat submission form\n\u2014 the same object `POST /api/tools/{tool_id}` accepts under `params` \u2014 so a\nclient can render a form, validate a body, or generate a typed call from it\nwithout restating anything.\n\n`allowed_targets` is the tool's own static list. Which of them the *caller*\nmay use is `GET /api/tools/{family}/targets`, which has the caller's grants\nin scope to answer with.\n",
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolCatalog"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/{tool_id}": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "getTool",
        "summary": "One tool's definition.",
        "description": "The same entry `GET /api/tools` returns, for a single tool. Useful when a\nclient already knows the id and does not want the whole catalog.\n",
        "parameters": [
          {
            "name": "tool_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A tool id, e.g. `blast-identify`.",
            "example": "blast-identify"
          }
        ],
        "responses": {
          "200": {
            "description": "The tool's definition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolDefinition"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No tool with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/{family}/targets": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listToolTargets",
        "summary": "Which compute targets each tool in a family permits.",
        "description": "The intersection of what a tool declares and what the caller is granted.\nClients populate the target picker from this rather than hardcoding ids,\nand label it from the `names` in the same response rather than keeping a\ntable of their own.\n",
        "parameters": [
          {
            "name": "family",
            "in": "path",
            "required": true,
            "description": "The tool family. On the job read routes this segment is cosmetic \u2014 the handler\nis tool-agnostic and `jobs/<org_id>/<sub>/<job_id>/` holds every family's jobs\nalike \u2014\nbut it is kept so the frontend can group by family.\n\nThe enum is load-bearing, not documentation: the route-conformance test\nexpands it to check the templated path against the literal routes the\ndeployment registers. Adding a family here without deploying it, or the\nreverse, fails that test.\n",
            "schema": {
              "type": "string",
              "enum": [
                "align",
                "biom3",
                "bioparsers",
                "blast",
                "esmfold",
                "hmmer",
                "mysca",
                "project",
                "sbm",
                "tensor"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Allowed target ids, keyed by tool id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tools"
                  ],
                  "properties": {
                    "tools": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    },
                    "names": {
                      "type": "object",
                      "description": "Target id to display name, for every target listed above.\nSent with the ids for the same reason `capacity` is: a name\nis a property of the target, and a client that had to look\nit up somewhere else would be keeping a second copy that\ngoes stale \u2014 or fetching it separately and racing its own\nrender, which is what happened before this field existed.\n",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "capacity": {
                      "type": "object",
                      "description": "Per-target job limits, keyed by target id. Present only where\nthe family's dispatcher computes them.\n",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "max_nodes": {
                            "type": "integer"
                          },
                          "max_devices_per_node": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "tools": {
                    "blast-identify": [
                      "spark-nm",
                      "spark-nm-2",
                      "cloud-cpu"
                    ]
                  },
                  "names": {
                    "spark-nm": "NM DGX Spark 1",
                    "spark-nm-2": "NM DGX Spark 2",
                    "cloud-cpu": "AWS CPU host"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/blast/databases": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBlastDatabases",
        "summary": "Databases each blast tool may search.",
        "description": "Provisioned members only. An unprovisioned artifact stays out of the list\ndeliberately \u2014 a selectable option that fails inside the container minutes\nlater is the failure this endpoint exists to prevent.\n",
        "responses": {
          "200": {
            "description": "Selectable databases, keyed by tool id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseCatalog"
                },
                "example": {
                  "tools": {
                    "blast-identify": [
                      {
                        "id": "swissprot-blast",
                        "name": "Swiss-Prot (BLAST)",
                        "description": "Reviewed UniProtKB entries, formatted for BLAST.",
                        "release": "2024_01",
                        "approx_bytes": 254803968
                      },
                      {
                        "id": "uniref50-blast",
                        "name": "UniRef50 (BLAST)",
                        "description": "Clustered at 50% identity. Broader, slower.",
                        "release": "2024_01",
                        "approx_bytes": 8589934592
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/hmmer/databases": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listHmmerDatabases",
        "summary": "Sequence databases each hmmer tool may search.",
        "description": "Same response shape as the blast catalog \u2014 one endpoint pattern per family\nrather than two, so a client can handle both with one code path.\n\nProvisioned members only, for the same reason the blast catalog says so: a\nselectable option that fails inside the container minutes later is the\nfailure this endpoint exists to prevent. The databases here are plain\nFASTA, which is what jackhmmer and hmmsearch read.\n",
        "responses": {
          "200": {
            "description": "Selectable databases, keyed by tool id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseCatalog"
                },
                "example": {
                  "tools": {
                    "hmmer-homologs": [
                      {
                        "id": "swissprot-fasta",
                        "name": "Swiss-Prot (FASTA)",
                        "description": "Reviewed UniProtKB entries as plain FASTA.",
                        "release": "2026_01",
                        "approx_bytes": 274826836
                      },
                      {
                        "id": "uniref90-fasta",
                        "name": "UniRef90 (FASTA)",
                        "description": "Clustered at 90% identity. Broader, slower.",
                        "release": "2026_02",
                        "approx_bytes": 60945600428
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers/databases": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBioparsersDatabases",
        "summary": "Source releases each bioparsers tool may read.",
        "description": "Same response shape as the blast catalog \u2014 one endpoint pattern per family\nrather than two, so a client can handle both with one code path.\n",
        "responses": {
          "200": {
            "description": "Selectable databases, keyed by tool id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseCatalog"
                },
                "example": {
                  "tools": {
                    "bioparsers-uniprot": [
                      {
                        "id": "swissprot-release",
                        "name": "Swiss-Prot flat file",
                        "description": "uniprot_sprot.dat.gz, as UniProt ships it.",
                        "release": "2024_01",
                        "approx_bytes": 629145600
                      }
                    ],
                    "bioparsers-build-swissprot-fields": [
                      {
                        "id": "swissprot-parsed",
                        "name": "Swiss-Prot (parsed JSONL)",
                        "description": "The JSONL a Swiss-Prot parse produced.",
                        "release": "2024_01",
                        "approx_bytes": 1073741824
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers/builders": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBioparsersBuilders",
        "summary": "What each build tool produces, and which fields it can select.",
        "description": "The checkbox list a client renders and the list the dispatcher validates\nagainst must be the same list, so it is fetched rather than shipped as a\ncopy that drifts the first time a field is added.\n",
        "responses": {
          "200": {
            "description": "Builders, keyed by tool id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuilderCatalog"
                },
                "example": {
                  "tools": {
                    "bioparsers-build-swissprot-fields": {
                      "id": "swissprot_fields_v1",
                      "name": "Swiss-Prot curated fields",
                      "description": "Build a dataset choosing which annotations to include.",
                      "output": "jsonl.gz",
                      "filters": [
                        "pfam_ids"
                      ],
                      "required_filters": [],
                      "field_set": {
                        "id": "swissprot_v1",
                        "name": "Swiss-Prot fields",
                        "description": "Annotations extractable from a parsed Swiss-Prot record.",
                        "groups": [
                          "core",
                          "annotation"
                        ],
                        "fields": [
                          {
                            "id": "sequence",
                            "label": "Sequence",
                            "group": "core",
                            "kind": "string",
                            "description": "The amino-acid sequence.",
                            "default": true
                          },
                          {
                            "id": "ec_number",
                            "label": "EC number",
                            "group": "annotation",
                            "kind": "string",
                            "description": "Enzyme Commission number, where assigned.",
                            "default": false
                          }
                        ]
                      }
                    },
                    "bioparsers-build-swissprot-legacy": {
                      "id": "swissprot_legacy_v1",
                      "name": "Swiss-Prot (legacy reproduction)",
                      "description": "Reproduces the legacy BioM3 dataset. Fields are fixed.",
                      "output": "jsonl.gz",
                      "filters": [],
                      "required_filters": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3/weights": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBiom3Weights",
        "summary": "Weights a biom3 job may run against.",
        "description": "The bundles `weights_bundle` accepts, and the registered weights a\nper-stage override may name. Provisioned members only.\n",
        "responses": {
          "200": {
            "description": "Selectable bundles, and registered weights keyed by stage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeightsCatalog"
                },
                "example": {
                  "bundles": [
                    {
                      "tag": "run1_base",
                      "name": "Run 1 base",
                      "description": "Base weights for all three stages.",
                      "default": true
                    }
                  ],
                  "stages": {
                    "proteoscribe": [
                      {
                        "id": "proteoscribe-sh3-ft-v1",
                        "name": "ProteoScribe \u2014 SH3 finetune v1",
                        "description": "Finetuned on the SH3 domain corpus.",
                        "base_bundle": "run1_base",
                        "size_bytes": 3221225472
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3/datasets": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBiom3Datasets",
        "summary": "Training corpora and embedding bundles this deployment publishes.",
        "description": "What a caller may name instead of supplying their own file: the corpora\nbehind the published decoders, and the BioM3 latents already computed over\nthem. A projection names an embedding bundle here rather than re-embedding\na corpus it does not own.\n",
        "responses": {
          "200": {
            "description": "Registered corpora, and the embedding bundles derived from them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BioM3DatasetCatalog"
                },
                "example": {
                  "datasets": [
                    {
                      "id": "structdrop142k",
                      "name": "SH3 structdrop142k corpus",
                      "description": "SH3/PF00018, boundary-trimmed.",
                      "format": "csv",
                      "n_records": 179679,
                      "size_bytes": 72050762,
                      "channels": {
                        "sequence": {
                          "at": "protein_sequence"
                        },
                        "caption": {
                          "at": "[final]text_caption"
                        }
                      }
                    }
                  ],
                  "embeddings": [
                    {
                      "id": "structdrop142k-run1-base",
                      "name": "SH3 structdrop142k embeddings (run1_base)",
                      "description": "Encoded with dynamic padding at batch size 32.",
                      "format": "pt",
                      "n_records": 179679,
                      "size_bytes": 1187809754,
                      "base_bundle": "run1_base",
                      "derived_from": "structdrop142k",
                      "channels": {
                        "zp": {
                          "at": "z_p",
                          "shape": [
                            179679,
                            512
                          ],
                          "dtype": "float32"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3/prompts": {
      "get": {
        "tags": [
          "catalogs"
        ],
        "operationId": "listBiom3Prompts",
        "summary": "Curated text prompts this deployment offers, per decoder.",
        "description": "Captions we have run and would run again, so the generation step has a\nworked example beside its three builders rather than only a blank box.\n\nServed whole and filtered by the client: `weights` says which decoders an\nentry is offered for, an empty list means all of them, and the decoder is\na dropdown the reader changes while looking at the list.\n",
        "responses": {
          "200": {
            "description": "Every curated prompt, in curation order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BioM3PromptCatalog"
                },
                "example": {
                  "prompts": [
                    {
                      "id": "sh3-canonical",
                      "name": "Canonical SH3",
                      "weights": [
                        "sh3-production"
                      ],
                      "text": "PROTEIN NAME: SH3 domain-containing protein. SIMILARITY: Belongs to the SH3 family.",
                      "sequence": null,
                      "note": "Our baseline SH3 prompt. Folds cleanly at default replicates."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/compute/enroll": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "enrollComputeHost",
        "summary": "Exchange an enrollment code for a host token.",
        "description": "Called once by a machine being registered. The code is the credential \u2014\nthis is the only route here that takes no bearer token \u2014 and it buys\nexactly one exchange: it is single-use, expires within a day, and is\nstored only as a hash.\n\nA refused code says nothing about why. \"Already redeemed\" and \"never\nexisted\" answer identically, because distinguishing them would confirm to\nwhoever holds a code that it was once real.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollRequest"
              },
              "example": {
                "code": "NMC-7KQ4H-8XR2V-B19TC"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The host is now active. Store the token; it is shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrollResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The code is not usable. Deliberately does not say why."
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/download": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "computeHostDownloadUrl",
        "summary": "A URL to read one file inside a job this host is running.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobFileRequest"
              },
              "example": {
                "path": "input.csv",
                "owner": "210b85f0-e0b1-7086-879f-0f3e428c567e"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A presigned URL. Bytes move directly to and from S3.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The host, the job, or the path is not one this token reaches. One\nanswer for all three \u2014 a caller that guessed wrong should not learn\nwhich part of the guess was right.\n"
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/upload": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "computeHostUploadUrl",
        "summary": "A URL to write one file into a job this host is running.",
        "description": "The declared `size_bytes` is signed into the returned policy as a\n`content-length-range`, so S3 itself rejects a body outside it. Without\nthat, a URL for a small status file would be a URL for an arbitrarily\nlarge one.\n\nRefused for a job that has already finished: results someone may have read\nare not rewritten by a late or duplicated delivery.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobUploadRequest"
              },
              "example": {
                "path": "results/result.tsv",
                "size_bytes": 51234,
                "owner": "210b85f0-e0b1-7086-879f-0f3e428c567e"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Post the file to this URL with these fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not reachable by this token, or the job has finished."
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/list": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "computeHostListJobFiles",
        "summary": "The files under a prefix inside a job this host is running.",
        "description": "For reading a tree \u2014 a prior job's output consumed as this job's input.\nBounded by the job, which is why it is offered at all: a reference\ndatabase is not, and a host that needs one is not sent the job.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobListRequest"
              },
              "example": {
                "prefix": "results/",
                "owner": "210b85f0-e0b1-7086-879f-0f3e428c567e"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per file, each with its own URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListResponse"
                }
              }
            }
          },
          "403": {
            "description": "Not reachable by this token."
          },
          "413": {
            "description": "Too many files under that prefix. This is a convenience for a job's\nown tree, not a general manifest service.\n"
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/stage": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "computeHostStageAsset",
        "summary": "URLs for one shared asset this job stages.",
        "description": "A job's own files are addressed by a path inside the job. Staged assets\nare not in the job at all \u2014 they are shared reference data, owned by no\nsingle job \u2014 so the host names the *asset*, by the handle its own spec\nalready uses, and the portal resolves that against the lease it issued.\nThe host chooses which name; it never chooses what the name points at,\nand cannot reach an asset this job was not dispatched with.\n\nReturns every object under the asset \u2014 path, size and a URL each. A\nsingle-object asset returns one file.\n\nThe size is what makes this serve a cache rather than a download: these\nassets are immutable within a version, so a host that already holds a\npath at that size skips it. On a host paying its own egress that is the\ndifference between a warm job and an expensive one.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where to fetch each object in the asset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StageResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a valid host token, not this host's job, or an asset name the job\ndoes not stage.\n"
          },
          "409": {
            "description": "No live lease for this job."
          }
        }
      }
    },
    "/api/compute/jobs/next": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "leaseNextJob",
        "summary": "Lease the next job queued for this host.",
        "description": "Long-polls for up to 20 seconds and returns one job, or `job: null` when\nthe queue is idle \u2014 which is a 200, not a 404: an empty queue is the\nnormal answer for a daemon polling in a loop.\n\nThe `lease_id` is opaque. The underlying queue receipt stays server-side,\nso a host can neither extend nor delete its own message directly; it\nreleases work by calling `complete`. If it never does \u2014 because it\ncrashed \u2014 the lease lapses on its own and the job is offered again.\n",
        "responses": {
          "200": {
            "description": "A job and its lease, or nothing to do.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeaseResponse"
                }
              }
            }
          },
          "403": {
            "description": "Not a valid host token."
          },
          "409": {
            "description": "This host has no work queue \u2014 it was registered before one could be\ncreated. Re-register the machine.\n"
          }
        }
      }
    },
    "/api/compute/registry-token": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "computeHostRegistryToken",
        "summary": "A short-lived docker credential for pulling tool images.",
        "description": "A registered host holds no cloud credential, so it cannot authenticate to\nthe image registry itself. This returns one that expires \u2014 the same trade\nthe job-data routes make with presigned URLs: the capability crosses the\nwire, never the key that minted it.\n\nThe credential is pull-only and reaches only the tool repositories. It is\nnot scoped to a single image: a host runs jobs for many tools over its\nlife, and every repository it can reach is one it is already entitled to\nrun.\n\nCall it before each pull rather than caching it. It is valid for hours,\nnot days, and asking again is cheaper than diagnosing a login that expired\nmid-queue.\n\nName the image and the answer may be `credentials: null` \u2014 some tool\nimages are public. That decision belongs here rather than on the host: a\nmachine that recognises registries by hostname is a machine that knows\nwhich backend it is talking to, and one that guesses wrong skips\nauthentication silently.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegistryTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A credential usable with `docker login` until it expires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Not a valid host token."
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/status": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "reportJobStatus",
        "summary": "Report a job's progress.",
        "description": "How a job leaves `queued`. The machine holds no AWS credential and cannot\nwrite the job's record itself, so it says what changed and the portal\nwrites it.\n\nOnly the fields a supervisor owns are accepted \u2014 `status`, `step`,\n`error`, `started_at`, `finished_at`, `progress`. Anything else is\ndropped: the rest of the record belongs to the dispatcher, and two of\nthose fields are what decides whether this host may touch the job at all.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobStatusRequest"
              },
              "example": {
                "owner": "210b85f0-e0b1-7086-879f-0f3e428c567e",
                "status_patch": {
                  "status": "running",
                  "step": "1/3"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied. Lists which fields were taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                }
              }
            }
          },
          "400": {
            "description": "The patch named no writable field."
          },
          "403": {
            "description": "Not a job this token reaches."
          },
          "409": {
            "description": "The job has already finished. Said rather than ignored, so a daemon\nretrying a stale delivery learns why instead of appearing to succeed\nagainst results someone has read.\n"
          }
        }
      }
    },
    "/api/compute/jobs/{job_id}/complete": {
      "post": {
        "tags": [
          "compute"
        ],
        "operationId": "releaseJobLease",
        "summary": "Release a lease once the job is finished with.",
        "description": "Deletes the underlying queued message, which is what stops the job being\noffered again. Idempotent: releasing a lease that is already gone answers\n200 with `released: false`, so a daemon retrying is not told it failed.\n",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "A job belonging to the caller. Another caller's id resolves as not found \u2014\nownership is structural, since the caller's subject is part of the storage\nkey rather than something the handler checks.\n",
            "schema": {
              "type": "string",
              "pattern": "^[^/\\\\]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobCompleteRequest"
              },
              "example": {
                "lease_id": "8Xr2vB19tCQ4h7Kq"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether this call was the one that released it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCompleteResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a valid host token."
          }
        }
      }
    },
    "/api/me/organizations": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "listMyOrganizations",
        "summary": "The organizations the caller belongs to, and which one is active.",
        "description": "A user can belong to several organizations. Jobs are written beneath the\nactive one, so which organization is active is not a display preference \u2014\nit decides where the caller's data lands and who else can be granted\naccess to it.\n\n`active_org_id` is what is **in effect**, which is not always what the\ncaller last chose. An unset preference resolves to their first membership,\nand a stored organization they have since left is skipped over. Clients\nshould render `active_org_id`; `stored_org_id` is exposed only so a\nsettings page can say \"your saved choice is no longer available\" instead\nof silently showing something else as active.\n\nA caller who belongs to no organization gets an empty list and a null\n`active_org_id` rather than an error \u2014 this endpoint backs a settings page\nthat a not-yet-provisioned user still needs to be able to load.\n",
        "responses": {
          "200": {
            "description": "The caller's organizations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationList"
                },
                "example": {
                  "organizations": [
                    {
                      "org_id": "natural-machine",
                      "display_name": "Natural Machine",
                      "is_active": true
                    },
                    {
                      "org_id": "example-lab",
                      "display_name": "Example Lab",
                      "is_active": false
                    }
                  ],
                  "active_org_id": "natural-machine",
                  "stored_org_id": "natural-machine"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/me/active-organization": {
      "put": {
        "tags": [
          "account"
        ],
        "operationId": "setMyActiveOrganization",
        "summary": "Choose which organization the caller is acting under.",
        "description": "Sets the organization subsequent jobs are submitted under. Takes effect on\nthe caller's next request; work already submitted is unaffected and stays\nwhere it was written.\n\nThe stored value is a preference, never a grant. It is re-checked against\nlive memberships on every request, so it can only ever narrow to something\nthe caller is genuinely a member of \u2014 losing a membership silently stops\nthe preference applying rather than leaving stale access behind.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetActiveOrganizationRequest"
              },
              "example": {
                "org_id": "example-lab"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The organization now in effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveOrganization"
                },
                "example": {
                  "active_org_id": "example-lab"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The caller is not a member of that organization. Deliberately not a\n404: the organization may well exist, and a non-member should not\nlearn that from this endpoint.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotAMemberError"
                },
                "example": {
                  "error": "Not a member of organization 'other-org'.",
                  "organizations": [
                    "natural-machine"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/organizations": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "adminListOrganizations",
        "summary": "Every organization, with its grants and member count.",
        "description": "System administrators only. `/api/me/organizations` answers \"mine\"; this\nanswers \"every\", which had no home outside the database.\n\n`member_count` is the field that earns its place. An org holding compute\ngrants and no members reads, from every other view, as a machine somebody\ncan reach \u2014 and the only way to tell was to read two tables by hand.\n",
        "responses": {
          "200": {
            "description": "All organizations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminOrganizationList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator. Deliberately the same shape as the\norg-admin refusal: whether the platform role exists is not a caller's\nbusiness.\n"
          }
        }
      },
      "post": {
        "tags": [
          "account"
        ],
        "operationId": "adminCreateOrganization",
        "summary": "Create an organization.",
        "description": "System administrators only. Conditional on the slug being free, so two\nadmins creating the same org cannot silently replace one another's grants.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminCreateOrganization"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminOrganization"
                }
              }
            }
          },
          "400": {
            "description": "Invalid slug, or an organization by that name exists."
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator."
          }
        }
      }
    },
    "/api/admin/organizations/{org_id}/members": {
      "parameters": [
        {
          "name": "org_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "adminListOrgMembers",
        "summary": "Everyone in one organization.",
        "description": "System administrators only. Membership had no read surface outside the\ndatabase, so \"who is in this org, and which of them are admins\" was a\nDynamoDB query.\n",
        "responses": {
          "200": {
            "description": "The organization's members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminOrgMemberList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator."
          }
        }
      },
      "post": {
        "tags": [
          "account"
        ],
        "operationId": "adminAddOrgMember",
        "summary": "Add somebody to an organization.",
        "description": "System administrators only. Creates the Cognito account when the address\nis new \u2014 Cognito emails a temporary password \u2014 and then writes the\nmembership row. That order matters: if account creation fails no row is\nwritten, whereas the reverse leaves a member who cannot sign in.\n\nAn address that already has an account is added without a second\ninvitation, which is the ordinary path for putting someone in a second\norganization.\n\nRefuses an existing membership rather than overwriting it, so re-adding\nsomebody cannot quietly reset an admin back to member.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminAddOrgMember"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminOrgMemberEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Unknown organization, malformed email or role, or already a member."
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator."
          },
          "502": {
            "description": "The account could not be created. No membership row was written, so\nthe organization is unchanged and the request can be retried.\n"
          }
        }
      }
    },
    "/api/admin/organizations/{org_id}/members/{email}": {
      "parameters": [
        {
          "name": "org_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "email",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "email"
          }
        }
      ],
      "put": {
        "tags": [
          "account"
        ],
        "operationId": "adminSetOrgMemberRole",
        "summary": "Change what somebody may do inside an organization.",
        "description": "System administrators only. Conditional on the membership existing, so a\nmistyped address is an error rather than a new membership with a role and\nno invitation behind it.\n\nThis is the only writer of the field. `scripts/identity/invite_user.py`\nhardcodes `member`, so before this route every org admin had to be made by\nediting DynamoDB directly \u2014 and prod ran with none at all, which made\nregistering a machine answer 403 for everybody.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminSetOrgMemberRole"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminOrgMemberEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "Not a member of this organization, or an invalid role."
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator."
          }
        }
      }
    },
    "/api/admin/compute-hosts": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "adminListComputeHosts",
        "summary": "Every registered machine and the organizations it serves.",
        "description": "System administrators only, and paired with the org list on one page on\npurpose. An org's `default_compute_targets` says which machines it may\nuse; a host's `org_ids` says which orgs it serves. The two have to agree \u2014\nwhen they do not, a job dispatches and then fails at its first data call,\nwhich is hard to recognise from a log and obvious side by side.\n",
        "responses": {
          "200": {
            "description": "All registered hosts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminComputeHostList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a system administrator."
          }
        }
      }
    },
    "/api/admin/compute-hosts/{host_id}/orgs": {
      "put": {
        "tags": [
          "account"
        ],
        "operationId": "adminSetComputeHostOrgs",
        "summary": "Replace the set of organizations a machine serves.",
        "description": "Two callers, different reach. A system administrator may name any\norganization \u2014 that is how a machine we provision is shared with an org\nthey are not a member of. An organization administrator may name only\norgs they administer, and only for a machine that already serves one of\ntheirs, which lets someone who brought a machine share it across their\nown organizations without handing it to a stranger's.\n\nReplaces rather than adds. The caller sends the set they want, so the\nresult does not depend on what was there and two admins editing at once\ncannot interleave into a set neither asked for.\n\nAn empty set is refused. A machine serving nobody still polls and still\nleases, but every data call it makes is refused \u2014 from the daemon that\nreads as an outage. Revoking is how a machine is taken out of service.\n",
        "parameters": [
          {
            "name": "host_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "org_ids"
                ],
                "properties": {
                  "org_ids": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "org_ids": [
                  "nm",
                  "nm-exec"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The machine's new organization set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "host_id",
                    "org_ids"
                  ],
                  "properties": {
                    "host_id": {
                      "type": "string"
                    },
                    "org_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No organizations named."
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An organization was named that the caller does not administer.\n"
          },
          "404": {
            "description": "No such machine \u2014 also returned when the caller administers no\norganization this machine serves, since whether a host exists\nelsewhere is not theirs to learn.\n"
          }
        }
      }
    },
    "/api/me/compute-hosts": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "listComputeHosts",
        "summary": "The machines registered to the caller's organization.",
        "description": "Administrators only. Never returns a secret: an enrollment code and a host\ntoken are stored hashed and shown once, so there is nothing here to leak.\n",
        "responses": {
          "200": {
            "description": "The organization's registered machines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComputeHostList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an administrator of the active organization."
          }
        }
      },
      "post": {
        "tags": [
          "account"
        ],
        "operationId": "registerComputeHost",
        "summary": "Register a machine and get a one-time enrollment code.",
        "description": "Administrators only: the code this returns lets a machine obtain a\ncredential that reads and writes the organization's job data for jobs\ndispatched to it.\n\nThe code is shown **once**. Only its hash is stored, so it cannot be\nretrieved again \u2014 if it is lost, register the machine afresh. That is what\nlets the table be read without reading a credential out of it.\n\nThe machine is `pending` until it redeems the code, and is dispatched\nnothing until it is `active`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterComputeHostRequest"
              },
              "example": {
                "display_name": "Lab workstation",
                "classes": [
                  "gpu-daemon"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered. Hand the code to whoever runs the machine.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterComputeHostResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an administrator of the active organization."
          }
        }
      }
    },
    "/api/me/compute-hosts/{host_id}": {
      "delete": {
        "tags": [
          "account"
        ],
        "operationId": "revokeComputeHost",
        "summary": "Revoke a registered machine.",
        "description": "Administrators only. The machine's token stops working at once; no\ncredential TTL has to expire first.\n\nThe record is kept rather than deleted \u2014 it is the history of a machine\nthat once had access, and removing it would erase that. A revoked host can\nnever be reactivated; register again, which issues a fresh code.\n",
        "parameters": [
          {
            "name": "host_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "byoc-1f4c2a9b8d3e5607"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokedComputeHost"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an administrator of the active organization."
          },
          "404": {
            "description": "No such host in this organization. Deliberately not distinguished from\na host that exists elsewhere \u2014 whose ids exist is not this\norganization's to learn.\n"
          }
        }
      }
    },
    "/api/feedback": {
      "post": {
        "tags": [
          "support"
        ],
        "operationId": "submitFeedback",
        "summary": "File a problem report, question, or suggestion.",
        "description": "Opens an issue on the portal's own repository on the reporter's behalf.\nThe reporter needs no GitHub account and never sees the repository \u2014\nwhich is what lets the reports stay private alongside the code rather\nthan being collected in a public tracker.\n\nAuthentication is the only gate. Organization membership deliberately is\nnot: a user who has signed in but has not been provisioned into an\norganization cannot use the portal at all, and that is exactly the\ncondition they most need to be able to report.\n\nThe filed issue carries the reporter's account and organization, taken\nfrom the verified token. A client cannot set, spoof, or suppress them,\nand there is no parameter here that names another user.\n\nReports are one-way. There is no route to read one back, because the\nrepository is private and a reporter has no standing to read anyone\nelse's report \u2014 `report_id` is a reference to quote, not a resource.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackReport"
              },
              "example": {
                "kind": "bug",
                "title": "BioM3 generation stays queued forever on spark-nm",
                "body": "I submitted a generation job at about 10:15 and it has been\nqueued since. My Jobs shows no error.\n",
                "context": {
                  "route": "#/tools/biom3/generate-from-prompt",
                  "job_id": "0f3c2a91-77d2-4c1e-9a55-2b1d6e4f8a03"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Filed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackReceipt"
                },
                "example": {
                  "report_id": 412
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Reporting is not configured in this environment, or the issue\ntracker refused the request. Distinguished from a 400 on purpose:\nnothing about the report was wrong and resubmitting the same text\nlater is the right response.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers/accessions/{accession}": {
      "parameters": [
        {
          "name": "accession",
          "in": "path",
          "required": true,
          "description": "A UniProtKB accession, Swiss-Prot or TrEMBL, optionally with an isoform\nsuffix (P12345-2). Case-insensitive. Anything that is not accession-shaped\nis rejected here rather than forwarded upstream.\n",
          "schema": {
            "type": "string",
            "pattern": "^(?:[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9](?:[A-Z][A-Z0-9]{2}[0-9]){1,2})(?:-[0-9]+)?$"
          },
          "example": "P12345"
        }
      ],
      "get": {
        "tags": [
          "lookup"
        ],
        "operationId": "getUniProtEntry",
        "summary": "One UniProtKB entry, in the dataset field vocabulary.",
        "description": "Answered in the request rather than as a job. Every other bioparsers tool\nruns a container over a staged database; the parsed Swiss-Prot mirror is a\n661 MB gzipped JSONL with no accession index and TrEMBL is not provisioned\nat all, so a single lookup over the mirror would take minutes to answer a\nquestion a person is waiting on.\n\nRead from UniProt directly, which is what makes TrEMBL accessions and\nrecently revised entries resolve. The trade is that values are current\nUniProt rather than the pinned release a dataset was built from \u2014 right\nfor composing a prompt about a protein, wrong for reproducing a training\ncorpus, which is what the parse tools remain for.\n",
        "responses": {
          "200": {
            "description": "The entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UniProtEntry"
                },
                "example": {
                  "accession": "P12345",
                  "entry_name": "AATM_RABIT",
                  "reviewed": true,
                  "sequence": "MALLHSARVLSGVASAFHPGLAAAASARASSWWAHVEMGPPDPILGVTEAYKRDTNSKK",
                  "fields": {
                    "protein_name": "Aspartate aminotransferase, mitochondrial",
                    "lineage": [
                      "Eukaryota",
                      "Metazoa",
                      "Chordata"
                    ],
                    "function": [
                      "Catalyzes the irreversible transamination of the L-tryptophan metabolite L-kynurenine to form kynurenic acid."
                    ],
                    "catalytic_activity": [
                      "L-aspartate + 2-oxoglutarate = oxaloacetate + L-glutamate"
                    ],
                    "subcellular_location": [
                      "Mitochondrion matrix",
                      "Cell membrane"
                    ],
                    "family_names": [
                      "Aminotran_1_2"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such entry. Also the answer for an accession that was deleted, or\nmerged into another entry.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "UniProt answered, but not with an entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "UniProt could not be reached in time. Distinct from a 502: the lookup\nis worth retrying.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/align-batch": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitAlignBatch",
        "summary": "Structure alignment \u2014 batch against a reference",
        "description": "Structurally superpose a set of protein structures onto one reference and score how similar each is. Takes a directory of structures \u2014 normally a completed ESMFold job's results \u2014 plus a reference, and returns a table of TM-scores, RMSDs and aligned lengths, best match first. The alignment is sequence-independent, so it compares folds rather than sequences and works on proteins with no detectable sequence similarity. TM-score runs 0 to 1: above ~0.5 the two structures share a fold, below ~0.3 the similarity is no better than random. Scores are normalized by the reference's length so they are comparable across queries of different sizes. For viewing a single pair superposed and interactively, use the alignment view rather than this job. Uses US-align (Zhang, Freddolino & Zhang, Nat Protoc 2026; Zhang, Shine, Pyle & Zhang, Nat Methods 2022; Zhang & Pyle, iScience 2022).",
        "x-nm-tool-id": "align-batch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlignBatchSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/align-pairs": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitAlignPairs",
        "summary": "Structure alignment \u2014 each onto its own reference",
        "description": "Structurally superpose each of a set of protein structures onto its OWN reference and score how similar the two are. Takes a directory of structures \u2014 normally a completed ESMFold job's results \u2014 plus a map saying which structure is measured against which, and returns one row per pair: TM-score, RMSD, aligned length, and the reference it was scored against. Use this when every structure has a different thing to be compared to, such as generated sequences each measured against their own nearest BLAST hit; use align-batch when one reference serves the whole set. The alignment is sequence-independent, so it compares folds rather than sequences. TM-score runs 0 to 1: above ~0.5 the two structures share a fold, below ~0.3 the similarity is no better than random. Scores are normalized by the reference's length. Uses US-align (Zhang, Freddolino & Zhang, Nat Protoc 2026; Zhang, Shine, Pyle & Zhang, Nat Methods 2022; Zhang & Pyle, iScience 2022).",
        "x-nm-tool-id": "align-pairs",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlignPairsSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-embed-joint": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3EmbedJoint",
        "summary": "BioM3 \u2014 Joint (paired) \u2192 z_t + z_p + z_c",
        "description": "The complete embedding: from paired text + protein sequence, run the full Stage 1 PenCL -> Stage 2 Facilitator pipeline and return all three latents (z_t, z_p, z_c). Use for contrastive / homology analysis over paired data, with the facilitated latent available too. Input is a paired CSV (a protein_sequence column and a text_caption column). For a single side only, use one of the narrow embed tools. Single-purpose split of the former biom3-embedding tool.",
        "x-nm-tool-id": "biom3-embed-joint",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3EmbedJointSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-embed-seq-zp": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3EmbedSeqZp",
        "summary": "BioM3 \u2014 Sequence \u2192 z_p (protein embedding)",
        "description": "Embed protein sequence(s) into the sequence latent z_p (Stage 1 PenCL only). z_p is the sequence's representation in the shared latent space \u2014 use it for sequence-side similarity/clustering/homology. Input is protein sequences only (FASTA, or one sequence per line); the dispatcher assembles the joint CSV BioM3 Stage 1 requires (text_caption left empty). Single-purpose split of the former biom3-embedding tool.",
        "x-nm-tool-id": "biom3-embed-seq-zp",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3EmbedSeqZpSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-embed-text-zc": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3EmbedTextZc",
        "summary": "BioM3 \u2014 Text \u2192 z_c (facilitated embedding)",
        "description": "Embed text caption(s) into the facilitated latent z_c: Stage 1 PenCL encodes the caption to z_t, then Stage 2 Facilitator maps it into the protein-embedding distribution as z_c. z_c is the latent BioM3 Generation conditions on, so this is the embedding tool to run before biom3-generate-from-embedding. Input is text only; the dispatcher assembles the joint CSV BioM3 Stage 1 requires (protein_sequence left empty). Single-purpose split of the former biom3-embedding tool.",
        "x-nm-tool-id": "biom3-embed-text-zc",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3EmbedTextZcSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-embed-text-zt": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3EmbedTextZt",
        "summary": "BioM3 \u2014 Text \u2192 z_t (text embedding)",
        "description": "Embed text caption(s) into the text latent z_t (Stage 1 PenCL only). z_t is the caption's representation in the shared latent space \u2014 use it for text-side similarity/clustering. For the latent Generation conditions on, use biom3-embed-text-zc instead. Input is text only; the dispatcher assembles the joint CSV BioM3 Stage 1 requires (protein_sequence left empty). Single-purpose split of the former biom3-embedding tool.",
        "x-nm-tool-id": "biom3-embed-text-zt",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3EmbedTextZtSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-embedding": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3Embedding",
        "summary": "BioM3 \u2014 Embedding (Stages 1+2)",
        "description": "Encode text + sequence into a shared latent space (Stage 1 PenCL); optionally facilitate text embeddings into the protein-embedding distribution (Stage 2 Facilitator). The dispatcher (lambdas/biom3_api/handler.py) assembles a joint CSV (primary_Accession,protein_sequence,[final]text_caption) from the user-selected input mode before publishing to the host queue; the runtime input shape is the joint CSV regardless of how the user provided the data. Per-stage `model_arg` + `model_path_prefix` let the user pick a specific .bin variant from S3 \u2014 handler.py's _build_steps substitutes the choice into --model_path, and _build_weights_includes narrows the container's `aws s3 sync` to just the selected file(s) + LLMs/*.",
        "x-nm-tool-id": "biom3-embedding",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3EmbeddingSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-finetune-generalized": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3FinetuneGeneralized",
        "summary": "BioM3 \u2014 ProteoScribe finetuning (generalized)",
        "description": "Finetune ProteoScribe (Stage 3) on a curated sequence/annotation dataset. Unlike the legacy path, which trains on z_c embeddings frozen at compile time, this composes a text caption from each record's annotation fields on every epoch and embeds it to z_c on-device through a frozen PenCL text branch and Facilitator. Recomposing the caption each epoch is the point: with per-field dropout the model sees a different subset of the annotation each time and learns to generate from partial descriptions rather than one fixed phrasing. Takes the output of a Swiss-Prot annotation-fields dataset build as its input. Training runs for hours to days \u2014 submit it and come back; the output is a browsable run directory of checkpoints, metrics history and the exact config used, not a single file.",
        "x-nm-tool-id": "biom3-finetune-generalized",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3FinetuneGeneralizedSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-generate-from-embedding": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3GenerateFromEmbedding",
        "summary": "BioM3 \u2014 Generate from embedding (Stage 3)",
        "description": "Generate novel protein sequences with the Stage 3 ProteoScribe diffusion model, conditioned on a prior embedding job's facilitated latent z_c. Chain off a completed biom3-embed-text-zc job (text only) or biom3-embed-joint job (paired sequence + caption), referenced by name \u2014 both run the same Stage 1 PenCL -> Stage 2 Facilitator pipeline and emit the same z_c. Use this to reuse one embedding across many generation runs; to go straight from a text prompt, use biom3-generate-from-prompt. Single-purpose split of the former biom3-generation tool.",
        "x-nm-tool-id": "biom3-generate-from-embedding",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3GenerateFromEmbeddingSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-generate-from-prompt": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3GenerateFromPrompt",
        "summary": "BioM3 \u2014 Generate from prompt (Stages 1\u21922\u21923)",
        "description": "Generate novel protein sequences directly from text prompt(s). Runs the full pipeline in one job: Stage 1 PenCL and Stage 2 Facilitator embed each prompt to z_c, then Stage 3 ProteoScribe generates sequences conditioned on it. Paste prompts one per line. To reuse an existing embedding instead, use biom3-generate-from-embedding. Single-purpose split of the former biom3-generation tool.",
        "x-nm-tool-id": "biom3-generate-from-prompt",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3GenerateFromPromptSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/biom3-generation": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBiom3Generation",
        "summary": "BioM3 \u2014 Generation (Stage 3)",
        "description": "Generate novel protein sequences from text-conditioned embeddings via a conditional diffusion transformer (ProteoScribe).",
        "x-nm-tool-id": "biom3-generation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Biom3GenerationSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-build-swissprot-fields": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersBuildSwissprotFields",
        "summary": "Build \u2014 Swiss-Prot annotation fields",
        "description": "Build a curated sequence/annotation dataset from parsed UniProtKB, choosing which annotation fields to keep and which entries to include. Each record carries the selected fields twice: in `fields` in their source shape (a list per comment block, a string, a number) and in `caption_fields` as cleaned single strings ready to drop into a caption. Nothing decides the caption's order, separators or field subset at build time, so a trainer can compose captions on the fly \u2014 annotation dropout, field randomization \u2014 rather than consuming one phrasing frozen into the data. There is nothing to upload: the parsed release is curated global data, so pick it, choose your fields and filters, and run.",
        "x-nm-tool-id": "bioparsers-build-swissprot-fields",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersBuildSwissprotFieldsSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-build-swissprot-legacy": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersBuildSwissprotLegacy",
        "summary": "Build \u2014 Swiss-Prot legacy captions",
        "description": "Reproduce the Swiss-Prot section of the legacy BioM3 finetuning dataset from parsed UniProtKB: each kept entry's sequence plus an assembled `[final]text_caption` in the legacy field order and phrasing, and the annotation fields it was built from. The field set is fixed by what this reproduces and is deliberately not selectable \u2014 use the annotation-fields build to choose your own. Note that this is an approximate reproduction: it is built against a current UniProt and Pfam release, so the entry set has drifted from the published dataset.",
        "x-nm-tool-id": "bioparsers-build-swissprot-legacy",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersBuildSwissprotLegacySubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-csv": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersCsv",
        "summary": "bioparsers \u2014 Delimited table (CSV / TSV)",
        "description": "Convert a delimited table to JSONL \u2014 one object per row, keyed by the header, values kept verbatim as strings. This is the parser for sources that already ship as a structured table (supplemental datasets, curated spreadsheets) rather than as a database release, so unlike the other bioparsers tools this one takes your file as input. The delimiter defaults to a comma; set it explicitly for a tab-separated table.",
        "x-nm-tool-id": "bioparsers-csv",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersCsvSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-pfam": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersPfam",
        "summary": "bioparsers \u2014 Pfam-A families (full alignments)",
        "description": "Parse Pfam-A full alignments into JSONL \u2014 one record per family: accession, name, description, type, clan, references, GA/TC/NC thresholds, cross-references, and member count, with the member list and each member's ungapped sequence available on request. Families must be named explicitly: scanning stops once they are all found, which is what keeps a job over a ~16 GB release fast and its output a usable size. The database is staged from curated global data, so no input file is submitted. Runs on the cloud host only: Pfam-A.full is 23.8 GB, which lands on that host's persistent volume and is re-used by later jobs. On a Spark it would stage to per-job scratch and be re-fetched every run \u2014 and those hosts do not have the free space for it.",
        "x-nm-tool-id": "bioparsers-pfam",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersPfamSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-pfam-fasta": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersPfamFasta",
        "summary": "bioparsers \u2014 Pfam-A member sequences",
        "description": "Parse the Pfam-A member FASTA (the redundancy-reduced member set) into JSONL \u2014 one record per member sequence: member accession and name, aligned region, its Pfam family, and the ungapped residues. Lighter than the full alignments when the member sequences are all that is wanted. Families must be named explicitly. The database is staged from curated global data, so no input file is submitted.",
        "x-nm-tool-id": "bioparsers-pfam-fasta",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersPfamFastaSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/bioparsers-uniprot": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBioparsersUniprot",
        "summary": "bioparsers \u2014 UniProtKB (Swiss-Prot / TrEMBL)",
        "description": "Parse a UniProtKB flat-file release into JSONL \u2014 one typed record per entry, with accessions, reviewed status, names, gene names, organism, lineage and taxon, references, comments, features, cross-references, keywords, and the amino-acid sequence (validated against the ID/SQ length and CRC64). The database is staged from curated global data, so no input file is submitted: pick a release and run. Output is gzipped JSONL, the input format the dataset builders consume.",
        "x-nm-tool-id": "bioparsers-uniprot",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BioparsersUniprotSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/blast-identify": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitBlastIdentify",
        "summary": "BLAST \u2014 Identify (characterize a sequence)",
        "description": "Quickly characterize a protein sequence: run a BLAST search against a small, well-annotated database (SwissProt/UniRef50) to find its nearest natural proteins, similarity, and taxonomy. Returns the raw BLAST hit table (tab-separated). For a thorough homolog collection against a large database, use blast-search (later). Runs on a compute-target host's daemon; the container syncs the selected DB from S3 before searching.",
        "x-nm-tool-id": "blast-identify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlastIdentifySubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/esmfold-predict": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitEsmfoldPredict",
        "summary": "ESMFold \u2014 Predict structure",
        "description": "Predict 3D structure for one or more protein sequences with ESMFold. Single-sequence: no multiple-sequence alignment and no template search, so a batch of designs folds in seconds to minutes rather than hours. Submit a FASTA (or paste a bare sequence) and get one PDB per sequence plus a summary table of mean pLDDT and pTM confidence. Runs on a GPU compute-target host; the container stages the ESMFold weights from curated global data before folding.",
        "x-nm-tool-id": "esmfold-predict",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EsmfoldPredictSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/hmmer-homologs": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitHmmerHomologs",
        "summary": "HMMER \u2014 Homolog search (jackhmmer \u2192 hmmsearch)",
        "description": "Build a sequence dataset from one query: collect homologs, extend the domain hits, filter them by taxonomy, function and length, and align the result into an MSA. Runs nm-data-assembly's pipeline end to end \u2014 jackhmmer against Swiss-Prot, hmmsearch against a large database (UniRef90), extend_domains, filter_data, then build_MSA (pyfamsa). The MSA is the product; the intermediates are returned beside it. Slower and far more sensitive than blast-identify: use BLAST to ask what a sequence is, this to build a dataset around it. Requires a host with a persistent cache, because the large database is tens of gigabytes.",
        "x-nm-tool-id": "hmmer-homologs",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HmmerHomologsSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/mysca-core": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitMyscaCore",
        "summary": "Mysca Core \u2014 eigendecomp + ICA + sector assignment",
        "description": "Runs sca-core on the output of a prior Preprocess job (covariance matrix, eigendecomp + bootstrap, ICA, sector assignment).",
        "x-nm-tool-id": "mysca-core",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MyscaCoreSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/mysca-pipeline": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitMyscaPipeline",
        "summary": "Mysca Pipeline \u2014 preprocess + core SCA",
        "description": "Runs sca-preprocess \u2192 sca-core on an aligned MSA. Skips prealign; use the standalone Prealign tool if your input is unaligned.",
        "x-nm-tool-id": "mysca-pipeline",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MyscaPipelineSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/mysca-prealign": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitMyscaPrealign",
        "summary": "Mysca Prealign \u2014 align raw FASTA",
        "description": "Runs sca-prealign on a raw (unaligned) FASTA and emits an aligned MSA ready for Preprocess.",
        "x-nm-tool-id": "mysca-prealign",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MyscaPrealignSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/mysca-preprocess": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitMyscaPreprocess",
        "summary": "Mysca Preprocess \u2014 filter aligned MSA",
        "description": "Runs sca-preprocess on an aligned MSA: gap/similarity filtering + sequence weighting. Output feeds into Core SCA.",
        "x-nm-tool-id": "mysca-preprocess",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MyscaPreprocessSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/project-pca": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitProjectPca",
        "summary": "Projection \u2014 PCA",
        "description": "Project points onto their principal components: the orthogonal directions of greatest variance. Takes a numeric matrix or a completed BioM3 embedding job and returns the projected coordinates plus how much variance each component explains. Linear, deterministic, and the one projection here whose axes mean something on their own \u2014 a component is a direction in the input space, so distances and directions in the output are faithful to the input. Start here before reaching for t-SNE or UMAP: if two groups separate under PCA they are genuinely far apart, and the explained-variance figures say how much of the data the picture accounts for. Also the standard preprocessing step before a neighbour-based projection of very high-dimensional data.",
        "x-nm-tool-id": "project-pca",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectPcaSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/project-tsne": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitProjectTsne",
        "summary": "Projection \u2014 t-SNE",
        "description": "Project points with t-SNE (t-distributed stochastic neighbour embedding): lay them out in two or three dimensions so that near neighbours in the input stay near in the picture. Takes a numeric matrix or a completed BioM3 embedding job. Better than PCA at revealing cluster structure that a linear projection flattens. Read the result carefully \u2014 the axes mean nothing, the distance BETWEEN clusters means nothing, and cluster sizes are not comparable; only which points sit together is informative. It is also stochastic, so vary the seed before believing a cluster. Slower than PCA and superlinear in the number of rows; run project-pca first when the input has thousands of features. For a layout that preserves more of the global arrangement, use project-umap.",
        "x-nm-tool-id": "project-tsne",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectTsneSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/project-umap": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitProjectUmap",
        "summary": "Projection \u2014 UMAP",
        "description": "Project points with UMAP (uniform manifold approximation and projection): build a neighbour graph in the input space and lay it out in a few dimensions. Takes a numeric matrix or a completed BioM3 embedding job. Like t-SNE it reveals cluster structure, and unlike t-SNE it keeps more of the arrangement BETWEEN clusters, so the broad layout carries some meaning \u2014 though the axes still do not, and distances remain qualitative. Faster than t-SNE on large inputs and it scales past a few output dimensions, so it also works as a preprocessing step rather than only as a picture. Stochastic: vary the seed before believing a cluster.",
        "x-nm-tool-id": "project-umap",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUmapSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/sbm": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitSbm",
        "summary": "SBM \u2014 Stochastic Boltzmann Machine",
        "description": "Infer fields and pairwise couplings from an MSA using a Stochastic Boltzmann Machine (MCMC + L-BFGS gradient descent). Defaults are tuned for quick first-light runs; production runs use much larger N_iter/N_chains/k_MCMC.",
        "x-nm-tool-id": "sbm",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SbmSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/sbm-sample": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitSbmSample",
        "summary": "SBM \u2014 Sample (generate sequences)",
        "description": "Sample novel protein sequences from a trained SBM/Potts model at a chosen temperature, via the model's MCMC (Metropolis) sampler. Chains off a completed sbm-train job (referenced by name); output is a FASTA of generated sequences. Note: the underlying C sampler is not fully seedable (it seeds from wall-clock), so the seed here only fixes the initial-state RNG \u2014 runs are not bit-for-bit reproducible.",
        "x-nm-tool-id": "sbm-sample",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SbmSampleSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/sbm-train": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitSbmTrain",
        "summary": "SBM \u2014 Train (infer fields + couplings)",
        "description": "Train a Potts model: infer fields h and pairwise couplings J from an MSA using a Stochastic Boltzmann Machine (MCMC + L-BFGS gradient descent) or plain Boltzmann Machine. Output is a single pickled .npy model (h, J, options) that biom3/sbm-sample can sample from. Defaults are tuned for quick first-light runs; production runs use much larger N_iter/N_chains/k_MCMC. Single-purpose split of the legacy sbm tool (adds a Seed for reproducibility).",
        "x-nm-tool-id": "sbm-train",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SbmTrainSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tools/tensor-score": {
      "post": {
        "tags": [
          "tools"
        ],
        "operationId": "submitTensorScore",
        "summary": "Tensor \u2014 Cosine and dot product against reference vectors",
        "description": "Score the rows of an array against one or more vectors: cosine similarity, dot product, or both. Optionally score a second array the same way and report where each of its rows falls in the first array's distribution, as a percentile \u2014 which is what turns a raw similarity into a number that means something. Reads .npy, and .pt/.npz holding named arrays. Runs no model and needs no GPU; a 179,679 x 512 array scores in about four seconds.",
        "x-nm-tool-id": "tensor-score",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TensorScoreSubmitRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll `status_url` until a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "job_id": "7b3e-2f19",
                  "status": "queued",
                  "status_url": "/api/tools/blast/jobs/7b3e-2f19",
                  "target_id": "spark-nm"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or an argument was invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, expired, or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated, but not permitted \u2014 no org membership, or the compute target\nor tool is not granted to this caller.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource for this caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The input exceeds the ceiling for this argument.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The compute target refused the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cognitoJwt": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "A Cognito ID token (browser) or access token (MCP clients). The caller's\n`sub` claim scopes every request \u2014 no endpoint takes a user identifier as\na parameter, so one caller can never address another's data.\n"
      }
    },
    "schemas": {
      "ActiveOrganization": {
        "type": "object",
        "required": [
          "active_org_id"
        ],
        "properties": {
          "active_org_id": {
            "type": "string",
            "description": "The organization now in effect for subsequent requests.",
            "example": "example-lab"
          }
        }
      },
      "AdminAddOrgMember": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "string",
            "enum": [
              "member",
              "admin"
            ],
            "default": "member"
          }
        }
      },
      "AdminComputeHostList": {
        "type": "object",
        "required": [
          "hosts"
        ],
        "properties": {
          "hosts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "host_id",
                "display_name",
                "status",
                "org_ids"
              ],
              "properties": {
                "host_id": {
                  "type": "string"
                },
                "display_name": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "classes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "org_ids": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Every org this machine serves. One entry for a machine registered\nby an org; several for one we provision and share.\n"
                },
                "has_queue": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "AdminCreateOrganization": {
        "type": "object",
        "required": [
          "org_id"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "pattern": "^[a-z0-9-]{2,32}$",
            "example": "acme-research"
          },
          "display_name": {
            "type": "string",
            "description": "Defaults to the slug."
          },
          "default_compute_targets": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AdminOrgMember": {
        "type": "object",
        "required": [
          "email",
          "role",
          "signed_in"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "string",
            "enum": [
              "member",
              "admin"
            ],
            "description": "What this person may do *inside* this organization. An admin may\nregister a machine for it. Orthogonal to the platform-wide system-admin\nstatus, which is per-user and lives elsewhere.\n"
          },
          "signed_in": {
            "type": "boolean",
            "description": "Whether they have ever signed in. Derived from the membership row\ncarrying a `cognito_sub`, which the first-sign-in backfill writes \u2014 so\n`false` means \"invited and never arrived\", which is the answer an admin\nneeds when someone reports they cannot see an organization.\n"
          }
        }
      },
      "AdminOrgMemberEnvelope": {
        "type": "object",
        "required": [
          "member"
        ],
        "properties": {
          "member": {
            "$ref": "#/components/schemas/AdminOrgMember"
          }
        }
      },
      "AdminOrgMemberList": {
        "type": "object",
        "required": [
          "members"
        ],
        "properties": {
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AdminOrgMember"
            }
          }
        }
      },
      "AdminOrganization": {
        "type": "object",
        "required": [
          "org_id",
          "display_name",
          "default_compute_targets",
          "member_count"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "example": "nm-exec"
          },
          "display_name": {
            "type": "string"
          },
          "default_compute_targets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Targets every member of this org may dispatch to."
          },
          "member_count": {
            "type": "integer",
            "description": "How many people are in it. Zero means its grants reach nobody.\n"
          }
        }
      },
      "AdminOrganizationList": {
        "type": "object",
        "required": [
          "organizations"
        ],
        "properties": {
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AdminOrganization"
            }
          }
        }
      },
      "AdminSetOrgMemberRole": {
        "type": "object",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "member",
              "admin"
            ]
          }
        }
      },
      "AlignBatchParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "structures"
        ],
        "oneOf": [
          {
            "title": "Supply a reference structure",
            "required": [
              "reference"
            ]
          },
          {
            "title": "Name one from the submitted set",
            "required": [
              "reference_name"
            ]
          }
        ],
        "properties": {
          "structures": {
            "$ref": "#/components/schemas/ProteinStructures",
            "title": "Structures to align",
            "description": "A directory of structures to align against the reference: a completed job (prior-job, by name or id \u2014 an ESMFold run is the usual source), or a folder in My Data (user-data, by path). PDB and mmCIF files are read; anything else is ignored."
          },
          "reference": {
            "$ref": "#/components/schemas/ProteinStructure",
            "title": "Reference structure",
            "description": "The structure everything is aligned onto, as a PDB or mmCIF file. Leave empty and set 'Reference from the set' instead when the reference is one of the structures above."
          },
          "reference_name": {
            "type": "string",
            "title": "Reference from the set",
            "maxLength": 255,
            "description": "Filename of a structure inside the submitted set to use as the reference, e.g. design_07.pdb. Use this instead of uploading a reference when comparing a group against one of its own members. Exactly one of this and 'Reference structure' is required."
          },
          "include_matrices": {
            "type": "boolean",
            "title": "Include transformation matrices",
            "default": false,
            "description": "Also emit matrices.json, the 4x4 rotation+translation that superposes each structure onto the reference. Useful for rendering superpositions elsewhere. Costs one extra alignment pass per structure, so it roughly doubles the run time."
          }
        }
      },
      "AlignBatchSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/AlignBatchParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "align-batch",
          "family": "align",
          "name": "Structure alignment \u2014 batch against a reference",
          "description": "Structurally superpose a set of protein structures onto one reference and score how similar each is. Takes a directory of structures \u2014 normally a completed ESMFold job's results \u2014 plus a reference, and returns a table of TM-scores, RMSDs and aligned lengths, best match first. The alignment is sequence-independent, so it compares folds rather than sequences and works on proteins with no detectable sequence similarity. TM-score runs 0 to 1: above ~0.5 the two structures share a fold, below ~0.3 the similarity is no better than random. Scores are normalized by the reference's length so they are comparable across queries of different sizes. For viewing a single pair superposed and interactively, use the alignment view rather than this job. Uses US-align (Zhang, Freddolino & Zhang, Nat Protoc 2026; Zhang, Shine, Pyle & Zhang, Nat Methods 2022; Zhang & Pyle, iScience 2022).",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_structures": 500,
            "max_residues": 1500
          },
          "produces": "../kinds.yaml#/StructureAlignmentScores"
        }
      },
      "AlignPairsParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "structures",
          "pairs"
        ],
        "properties": {
          "structures": {
            "$ref": "#/components/schemas/ProteinStructures",
            "title": "Structures to align",
            "description": "The set both halves of every pair are taken from \u2014 normally a completed ESMFold job. Both the structure and the reference it is measured against must be in here; nothing is staged separately."
          },
          "pairs": {
            "type": "object",
            "title": "Structure to reference",
            "description": "What to align onto what: a map of structure filename to the filename of the reference it is measured against, both inside the submitted set. Comparing generated designs against their own nearest BLAST hit is the case this exists for \u2014 each design has a different nearest relative, so a single reference cannot express it, and one align-batch job per design would be dozens of jobs for comparisons that take milliseconds each. A map rather than a list of pairs because a structure has exactly one reference here, which the shape should enforce rather than the validator."
          },
          "include_matrices": {
            "type": "boolean",
            "title": "Include transformation matrices",
            "default": false,
            "description": "Also emit matrices.json, the 4x4 that superposes each structure onto its own reference. Free here, unlike in align-batch: this tool already runs one US-align process per pair, and that is where the matrix comes from."
          }
        }
      },
      "AlignPairsSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/AlignPairsParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "align-pairs",
          "family": "align",
          "name": "Structure alignment \u2014 each onto its own reference",
          "description": "Structurally superpose each of a set of protein structures onto its OWN reference and score how similar the two are. Takes a directory of structures \u2014 normally a completed ESMFold job's results \u2014 plus a map saying which structure is measured against which, and returns one row per pair: TM-score, RMSD, aligned length, and the reference it was scored against. Use this when every structure has a different thing to be compared to, such as generated sequences each measured against their own nearest BLAST hit; use align-batch when one reference serves the whole set. The alignment is sequence-independent, so it compares folds rather than sequences. TM-score runs 0 to 1: above ~0.5 the two structures share a fold, below ~0.3 the similarity is no better than random. Scores are normalized by the reference's length. Uses US-align (Zhang, Freddolino & Zhang, Nat Protoc 2026; Zhang, Shine, Pyle & Zhang, Nat Methods 2022; Zhang & Pyle, iScience 2022).",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_pairs": 500,
            "max_residues": 1500
          },
          "produces": "../kinds.yaml#/StructureAlignmentScores"
        }
      },
      "AlignedSequences": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "aligned-sequences",
        "description": "A multiple sequence alignment.\n\nA separate kind from ProteinSequences rather than a variant of it: an\nalignment is not \"more sequences\", it is sequences plus the alignment, and\nthe two are not interchangeable. Handing unaligned sequences to a tool that\nneeds an MSA produces nonsense rather than an error, which is exactly the\nsubstitution this distinction exists to prevent.\n"
      },
      "BioM3CaptionedSequenceDataset": {
        "type": "object",
        "description": "One training corpus the deployment publishes: caption/sequence records in a\nsingle delimited or line-JSON file.\n",
        "required": [
          "id",
          "name",
          "format",
          "channels"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send when naming this corpus."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "type": "string",
            "description": "How the record file is encoded \u2014 `csv`, `jsonl`, `jsonl.gz`."
          },
          "n_records": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "channels": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/BioM3DataChannel"
            }
          },
          "annotations": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BioM3DatasetAnnotations"
              },
              {
                "type": "null"
              }
            ],
            "description": "The corpus's annotation sidecar, or `null` when it has none. Null rather\nthan absent: \"this corpus has no annotations\" is a fact a client\nrenders, and a missing key reads as \"the server did not say\".\n"
          }
        }
      },
      "BioM3CaptionedSequenceEmbeddings": {
        "type": "object",
        "description": "One embedding bundle: BioM3 latents for a corpus, in a single loadable file\ncarrying the row labels beside them. Named by a projection's `embeddings`\nparameter with `source: registry`.\n",
        "required": [
          "id",
          "name",
          "format",
          "base_bundle",
          "channels"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send as the `embeddings` reference's `id`."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the numbers came from, including anything about how they were\nencoded that cannot be recovered from the file \u2014 batching and ordering\nchange a latent under dynamic padding.\n"
          },
          "format": {
            "type": "string"
          },
          "n_records": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "base_bundle": {
            "type": "string",
            "description": "The weights bundle whose PenCL and Facilitator produced these latents. A\nz_c encoded under a different pair is not comparable and does not mean\nthe same thing, and nothing downstream can detect that.\n"
          },
          "derived_from": {
            "type": [
              "string",
              "null"
            ],
            "description": "The `BioM3CaptionedSequenceDataset.id` these latents were encoded from, when the\ncorpus is published too. Absent for a bundle that arrived from outside.\n"
          },
          "channels": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/BioM3DataChannel"
            }
          }
        }
      },
      "BioM3DataChannel": {
        "type": "object",
        "description": "Where one named role lives inside a registered file, and what shape it is.\n\n`at` is a column name in a CSV, a JSON key in JSONL, and a dict key in a\ntorch pickle \u2014 the entry's `format` says which. Roles are `caption`,\n`sequence`, `zp`, `zt`, `zc`, and the record-level `accession` and `pfam_id`.\n\nEvery role is optional and presence is the semantics: a corpus with no\ncaption channel is one whose captions are composed at training time rather\nthan frozen into the file.\n",
        "required": [
          "at"
        ],
        "properties": {
          "at": {
            "type": "string"
          },
          "shape": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Declared rather than measured, so a caller can check a latent's\ndimension against a checkpoint without transferring the file.\n"
          },
          "dtype": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "BioM3DatasetAnnotations": {
        "type": "object",
        "description": "A row-aligned sidecar of per-record annotations \u2014 taxonomy, species,\ntrain/validation split, anything else worth colouring a projection by.\n\nOne row per corpus record, in the corpus's own order. That positional\ncorrespondence is the whole contract, which is why `n_records` is required:\na sidecar one row short does not fail, it shifts, and every annotation after\nthe gap then describes the wrong sequence.\n\n**The columns are not listed here, deliberately.** They are whatever the\nfile's header says, so adding one is editing a spreadsheet rather than\nediting a manifest \u2014 and a declared list would be a second place for the\ntruth to live. A client learns the columns from a projection's own\n`annotations.tsv`, which is the file that actually has them.\n\nTheir **types** are a different matter and are declared, in `types`. A\ncolumn's name is in the header; whether it is a measurement or a set of\ncategories is not, and it is not reliably derivable either \u2014 a sequence\nlength is numbers and a replica index is numbers, and only one of them is a\nmeasurement. Inferring it from the values was tried and is exactly the kind\nof heuristic that is wrong without saying so.\n",
        "required": [
          "format",
          "n_records"
        ],
        "properties": {
          "format": {
            "type": "string",
            "description": "How the sidecar is encoded \u2014 `csv` or `tsv`."
          },
          "n_records": {
            "type": "integer",
            "minimum": 1,
            "description": "Rows in the sidecar, which must equal the corpus's own. A projection\nnaming a bundle whose record count disagrees is refused at submit.\n"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "types": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "number",
                "category"
              ]
            },
            "description": "Declared type per column, keyed by column name. `number` is a\nmeasurement and is drawn as a continuous ramp; `category` is a set of\ndiscrete values and is drawn as distinct hues.\n\n**Sparse, and empty by default.** A column absent from this map is a\ncategory \u2014 the reading that always renders. The types come from a\nschema file sitting beside the sidecar itself\n(`<sidecar>.schema.json`), so declaring them is uploading a file rather\nthan editing a manifest and redeploying; a corpus without one is\npublished exactly as before and simply declares nothing.\n\nA type this enum does not list is dropped rather than passed through:\nan unknown type means a schema written against a newer client, and\nfalling back to a category renders correctly instead of failing.\n"
          }
        }
      },
      "BioM3DatasetCatalog": {
        "type": "object",
        "description": "The training corpora this deployment publishes, the embedding bundles\nderived from them, and which weights they relate to. One response because they are two halves of one question:\na bundle is only interpretable against the corpus it came from, and a corpus\nis only projectable through a bundle.\n",
        "required": [
          "datasets",
          "embeddings",
          "associations"
        ],
        "properties": {
          "datasets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BioM3CaptionedSequenceDataset"
            }
          },
          "embeddings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BioM3CaptionedSequenceEmbeddings"
            }
          },
          "associations": {
            "type": "array",
            "description": "Served here rather than from a route of its own because a client asking\n\"what corpus goes with this decoder\" needs the corpus metadata in the\nsame breath, and a second round trip to learn only a pair of ids would\nbe answered by a third.\n",
            "items": {
              "$ref": "#/components/schemas/BioM3WeightsDatasetAssociation"
            }
          }
        }
      },
      "BioM3Embeddings": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "biom3-embeddings",
        "description": "Latents produced by a BioM3 embedding run \u2014 z_t, z_p and/or z_c depending on\nwhich stages ran. What a generation job conditions on.\n"
      },
      "BioM3Prompt": {
        "type": "object",
        "description": "One curated BioM3 text prompt: a caption we have run and would run again,\noffered beside the prompt builders so a caption does not have to be written\nfrom a blank box.\n\n`weights` is a **list, and an empty one means every decoder**. A prompt is\nvalidated against a protein family rather than against one checkpoint, and\nan epoch sweep publishes several decoders over one corpus \u2014 so a scalar\nfield would force the same curated text to be listed once per checkpoint.\nSome captions are about the caption format rather than about a family, and\nthose name nothing and are offered whatever is selected.\n\nNo id here is guaranteed to resolve. A prompt naming a decoder this\ndeployment has not registered simply never matches, which is the same\noutcome as not listing it.\n",
        "required": [
          "id",
          "text",
          "weights"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Handle-shaped, and unique within the deployment."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "A short label for a list. Absent for most entries \u2014 the caption is\nusually its own best label.\n"
          },
          "weights": {
            "type": "array",
            "description": "`RegisteredWeights.id`s this prompt is offered for. Empty means all of\nthem.\n",
            "items": {
              "type": "string"
            }
          },
          "text": {
            "type": "string",
            "description": "The caption itself, in the LABEL/value register."
          },
          "sequence": {
            "type": [
              "string",
              "null"
            ],
            "description": "A paired amino-acid sequence, where the prompt was curated with one \u2014\nthe reference half of a sequence-blended generation. Residues only, with\nany FASTA line-wrapping already removed.\n"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why this prompt is here and what it produced. Shown on hover, so it is\none or two sentences rather than a paragraph.\n"
          }
        }
      },
      "BioM3PromptCatalog": {
        "type": "object",
        "description": "Every curated prompt this deployment publishes, unfiltered.\n\nUnfiltered on purpose: the decoder is a dropdown the reader changes while\nlooking at the list, so the client filters an in-memory list by `weights`\nrather than making a round trip per change. The list is a dozen short\nstrings; the round trip is the expensive half.\n",
        "required": [
          "prompts"
        ],
        "properties": {
          "prompts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BioM3Prompt"
            }
          }
        }
      },
      "BioM3RegistryInput": {
        "type": "object",
        "description": "An object this deployment publishes in one of the BioM3 registries, named by\nits id. Ids come from `GET /api/tools/biom3/datasets` and\n`GET /api/tools/biom3/weights`; S3 keys are never exposed.\n",
        "required": [
          "source",
          "catalog",
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "registry"
          },
          "catalog": {
            "type": "string",
            "enum": [
              "weights",
              "embeddings",
              "dataset"
            ],
            "description": "Which registry the id belongs to: a registered decoder, an embedding bundle, or a corpus. This selects a catalog, not a channel \u2014 which array to read inside an embedding bundle is still `matrix_key`."
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The registry id, not a path."
          }
        }
      },
      "BioM3WeightsDatasetAssociation": {
        "type": "object",
        "description": "One pairing of registered weights with a registered corpus, and what the\npairing means.\n\nA relation rather than a field on either side, because it is many-to-many:\none corpus trains several decoders \u2014 an epoch sweep over the same data is\nordinary \u2014 and one decoder can be paired with several corpora, the set it\nwas finetuned on and the set it is evaluated against being different\nquestions.\n\n`role` is open text. `finetuning` is the one that matters today: it says\nthis corpus is what the weights learned from, which is what makes it a\nmeaningful baseline to project generated sequences against. Match the roles\nyou understand and ignore the rest.\n\nNeither id is guaranteed to resolve. An association naming a corpus this\ndeployment has not registered yields no baseline, which is the same outcome\nas no association at all.\n",
        "required": [
          "weights",
          "dataset",
          "role"
        ],
        "properties": {
          "weights": {
            "type": "string",
            "description": "A `RegisteredWeights.id`."
          },
          "dataset": {
            "type": "string",
            "description": "A `BioM3CaptionedSequenceDataset.id`. Never an embedding bundle: the\nbundle is reached through the dataset, by `derived_from` and a matching\n`base_bundle`, so a decoder is not pinned to one encoding of its own\ntraining data.\n"
          },
          "role": {
            "type": "string"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Biom3EmbedJointParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "records"
        ],
        "properties": {
          "records": {
            "$ref": "#/components/schemas/TextSequencePairs",
            "title": "Paired records (CSV)",
            "description": "Header-bearing CSV with one column of protein sequences and one of text captions. An optional primary_Accession / id / accession column is reused as the accession; otherwise rows are numbered ROW_NNN."
          },
          "sequence_column": {
            "type": "string",
            "title": "Sequence column",
            "default": "protein_sequence",
            "description": "Which column holds the protein sequence."
          },
          "text_column": {
            "type": "string",
            "title": "Caption column",
            "default": "text_caption",
            "description": "Which column holds the text caption."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          },
          "facilitator_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "Facilitator (Stage 2) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run Facilitator (Stage 2) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3EmbedJointSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3EmbedJointParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-embed-joint",
          "family": "biom3",
          "name": "BioM3 \u2014 Joint (paired) \u2192 z_t + z_p + z_c",
          "description": "The complete embedding: from paired text + protein sequence, run the full Stage 1 PenCL -> Stage 2 Facilitator pipeline and return all three latents (z_t, z_p, z_c). Use for contrastive / homology analysis over paired data, with the facilitated latent available too. Input is a paired CSV (a protein_sequence column and a text_caption column). For a single side only, use one of the narrow embed tools. Single-purpose split of the former biom3-embedding tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL); both stages always run for this tool.",
            "enabled": true,
            "blacklist": [
              "--input_data_path",
              "-i",
              "--output_path",
              "-o",
              "--output_data_path",
              "--config_path",
              "-c",
              "--model_path",
              "-m"
            ]
          },
          "produces": "../kinds.yaml#/BioM3Embeddings"
        }
      },
      "Biom3EmbedSeqZpParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sequences"
        ],
        "properties": {
          "sequences": {
            "$ref": "#/components/schemas/ProteinSequences",
            "title": "Protein sequences",
            "description": "Protein sequences to embed: FASTA when the content starts with a greater-than sign, otherwise one sequence per line. The dispatcher fills protein_sequence (uppercased, whitespace stripped) and leaves the caption empty."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3EmbedSeqZpSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3EmbedSeqZpParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-embed-seq-zp",
          "family": "biom3",
          "name": "BioM3 \u2014 Sequence \u2192 z_p (protein embedding)",
          "description": "Embed protein sequence(s) into the sequence latent z_p (Stage 1 PenCL only). z_p is the sequence's representation in the shared latent space \u2014 use it for sequence-side similarity/clustering/homology. Input is protein sequences only (FASTA, or one sequence per line); the dispatcher assembles the joint CSV BioM3 Stage 1 requires (text_caption left empty). Single-purpose split of the former biom3-embedding tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL), the only stage.",
            "enabled": true,
            "blacklist": [
              "--input_data_path",
              "-i",
              "--output_path",
              "-o",
              "--config_path",
              "-c",
              "--model_path",
              "-m"
            ]
          },
          "input_shape": {
            "description": "Protein sequences: FASTA (if the content starts with '>'), else one sequence per line. The dispatcher assembles the joint CSV (primary_Accession,protein_sequence,[final]text_caption), filling protein_sequence (uppercased, whitespace-stripped) and leaving text_caption empty. Handled by _build_biom3_lines_csv (input_builder=biom3_lines, input_kind=sequence)."
          },
          "produces": "../kinds.yaml#/BioM3Embeddings"
        }
      },
      "Biom3EmbedTextZcParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "captions"
        ],
        "properties": {
          "captions": {
            "$ref": "#/components/schemas/TextCaptions",
            "title": "Text captions",
            "description": "Text captions to embed, one per non-empty line. The dispatcher fills the caption column and leaves protein_sequence empty."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          },
          "facilitator_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "Facilitator (Stage 2) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run Facilitator (Stage 2) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3EmbedTextZcSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3EmbedTextZcParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-embed-text-zc",
          "family": "biom3",
          "name": "BioM3 \u2014 Text \u2192 z_c (facilitated embedding)",
          "description": "Embed text caption(s) into the facilitated latent z_c: Stage 1 PenCL encodes the caption to z_t, then Stage 2 Facilitator maps it into the protein-embedding distribution as z_c. z_c is the latent BioM3 Generation conditions on, so this is the embedding tool to run before biom3-generate-from-embedding. Input is text only; the dispatcher assembles the joint CSV BioM3 Stage 1 requires (protein_sequence left empty). Single-purpose split of the former biom3-embedding tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL); both stages always run for this tool.",
            "enabled": true,
            "blacklist": [
              "--input_data_path",
              "-i",
              "--output_path",
              "-o",
              "--output_data_path",
              "--config_path",
              "-c",
              "--model_path",
              "-m"
            ]
          },
          "input_shape": {
            "description": "Text captions, one per line. The dispatcher assembles the joint CSV (primary_Accession,protein_sequence,[final]text_caption) BioM3 Stage 1 requires, filling the caption column and leaving protein_sequence empty. Handled by _build_biom3_lines_csv (routed via input_builder=biom3_lines, input_kind=text)."
          },
          "produces": "../kinds.yaml#/BioM3Embeddings"
        }
      },
      "Biom3EmbedTextZtParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "captions"
        ],
        "properties": {
          "captions": {
            "$ref": "#/components/schemas/TextCaptions",
            "title": "Text captions",
            "description": "Text captions to embed, one per non-empty line. The dispatcher fills the caption column and leaves protein_sequence empty."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3EmbedTextZtSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3EmbedTextZtParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-embed-text-zt",
          "family": "biom3",
          "name": "BioM3 \u2014 Text \u2192 z_t (text embedding)",
          "description": "Embed text caption(s) into the text latent z_t (Stage 1 PenCL only). z_t is the caption's representation in the shared latent space \u2014 use it for text-side similarity/clustering. For the latent Generation conditions on, use biom3-embed-text-zc instead. Input is text only; the dispatcher assembles the joint CSV BioM3 Stage 1 requires (protein_sequence left empty). Single-purpose split of the former biom3-embedding tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL), the only stage.",
            "enabled": true,
            "blacklist": [
              "--input_data_path",
              "-i",
              "--output_path",
              "-o",
              "--config_path",
              "-c",
              "--model_path",
              "-m"
            ]
          },
          "input_shape": {
            "description": "Text captions, one per line. The dispatcher assembles the joint CSV (primary_Accession,protein_sequence,[final]text_caption), filling the caption column and leaving protein_sequence empty. Handled by _build_biom3_lines_csv (input_builder=biom3_lines, input_kind=text)."
          },
          "produces": "../kinds.yaml#/BioM3Embeddings"
        }
      },
      "Biom3EmbeddingParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "records"
        ],
        "properties": {
          "records": {
            "$ref": "#/components/schemas/DataRef",
            "title": "Input records",
            "description": "The sequences and/or captions to embed, in whichever of the four shapes `mode` names. The dispatcher assembles the joint CSV BioM3 Stage 1 requires and exposes it as an additional download beside the result."
          },
          "mode": {
            "type": "string",
            "title": "Input shape",
            "enum": [
              "fasta",
              "sequence_csv",
              "text_csv",
              "joint_csv"
            ],
            "default": "fasta",
            "description": "How `records` is read. fasta: multiple records fine, accession is the first token after \">\", caption left empty. sequence_csv: one row per sequence, `has_header` plus `column`/`column_index` select it, caption left empty. text_csv: the same but the column is the caption and the sequence is left empty. joint_csv: header-bearing CSV, with `sequence_column` and `text_column` selecting the two; an optional primary_Accession/id/accession column is reused as the accession, else ROW_NNN."
          },
          "has_header": {
            "type": "boolean",
            "title": "First row is a header",
            "default": true,
            "description": "Whether the CSV modes should treat row one as column names. Decides which of `column` and `column_index` is read."
          },
          "column": {
            "type": "string",
            "title": "Column name",
            "description": "Which column the sequence_csv / text_csv modes read, by name. Used when `has_header` is true. Defaults to protein_sequence or text_caption for the respective mode."
          },
          "column_index": {
            "type": "integer",
            "title": "Column index",
            "minimum": 0,
            "default": 0,
            "description": "Which column the sequence_csv / text_csv modes read, as a 0-based index. Used when `has_header` is false."
          },
          "sequence_column": {
            "type": "string",
            "title": "Sequence column",
            "default": "protein_sequence",
            "description": "Which column holds the protein sequence, for joint_csv."
          },
          "text_column": {
            "type": "string",
            "title": "Caption column",
            "default": "text_caption",
            "description": "Which column holds the text caption, for joint_csv."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "stage2_enabled": {
            "type": "boolean",
            "title": "Run Stage 2 (Facilitator)",
            "default": true
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          },
          "facilitator_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "Facilitator (Stage 2) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run Facilitator (Stage 2) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3EmbeddingSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3EmbeddingParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-embedding",
          "family": "biom3",
          "name": "BioM3 \u2014 Embedding (Stages 1+2)",
          "description": "Encode text + sequence into a shared latent space (Stage 1 PenCL); optionally facilitate text embeddings into the protein-embedding distribution (Stage 2 Facilitator). The dispatcher (lambdas/biom3_api/handler.py) assembles a joint CSV (primary_Accession,protein_sequence,[final]text_caption) from the user-selected input mode before publishing to the host queue; the runtime input shape is the joint CSV regardless of how the user provided the data. Per-stage `model_arg` + `model_path_prefix` let the user pick a specific .bin variant from S3 \u2014 handler.py's _build_steps substitutes the choice into --model_path, and _build_weights_includes narrows the container's `aws s3 sync` to just the selected file(s) + LLMs/*.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL), which always runs \u2014 Stage 2 is conditional, so attaching there would silently drop extras when stage2_enabled is false. A Facilitator-only flag passed here fails loudly at the PenCL CLI rather than vanishing.",
            "enabled": true,
            "blacklist": [
              "--input_data_path",
              "-i",
              "--output_path",
              "-o",
              "--output_data_path",
              "--config_path",
              "-c",
              "--model_path",
              "-m"
            ]
          },
          "input_shape": {
            "description": "Multi-mode input. Handled specially by _build_biom3_embedding_csv() in handler.py \u2014 not consumed by the generic schema-driven input resolver. The dispatcher writes the assembled joint CSV to jobs/<sub>/<id>/input.csv and exposes it as an additional download alongside result.pt."
          },
          "produces": "../kinds.yaml#/BioM3Embeddings"
        }
      },
      "Biom3FinetuneGeneralizedParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "dataset"
        ],
        "properties": {
          "dataset": {
            "$ref": "#/components/schemas/ProteoScribeFinetuningDataset",
            "title": "Training dataset",
            "description": "A completed Swiss-Prot annotation-fields dataset build. That builder is the designed pairing: it emits both `fields` (raw per-field lists) and `caption_fields` (the same values cleaned to one string each), which is what the record schema presets read. The legacy-caption builder is deliberately not offered \u2014 its caption is assembled at build time, so recomposing one per epoch would be training against a phrasing that is already frozen."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to start from. One choice supplies all three weights this job needs: the ProteoScribe checkpoint being finetuned, plus the frozen PenCL text branch and Facilitator that embed the captions."
          },
          "num_nodes": {
            "type": "integer",
            "title": "Number of nodes",
            "default": 1,
            "minimum": 1,
            "description": "How many machines to train across. Currently 1 on every target: a job is dispatched as one container on one host, so there is no path that joins two machines yet."
          },
          "devices_per_node": {
            "type": "integer",
            "title": "Devices per node",
            "default": 1,
            "minimum": 1,
            "description": "How many accelerators to use on each node. The DGX Spark hosts have one GB10 each, so 1 is the only accepted value there today; the argument is validated against the target's real capacity at submit rather than failing inside the container."
          },
          "record_schema_preset": {
            "type": "string",
            "title": "Caption composition",
            "x-nm-catalog": "record-schema",
            "default": "annotation_dropout_v1",
            "description": "How each record's annotation fields become the text caption the model is conditioned on, recomposed every epoch. This is a training hyperparameter, not a data format: annotation dropout is the augmentation that makes the model robust to partial descriptions. Use the raw `record_schema` box under advanced options to supply your own."
          },
          "epochs": {
            "type": "integer",
            "title": "Epochs",
            "default": 100,
            "minimum": 1,
            "description": "Passes over the training set."
          },
          "batch_size": {
            "type": "integer",
            "title": "Batch size",
            "default": 32,
            "minimum": 1,
            "description": "Examples per optimizer step, per device."
          },
          "lr": {
            "type": "number",
            "title": "Learning rate",
            "default": 0.0001,
            "minimum": 0,
            "description": "Base learning rate. 1e-4 is the finetuning default; pretraining used 3e-4."
          },
          "finetune_last_n_blocks": {
            "type": "integer",
            "title": "Trainable transformer blocks",
            "default": 1,
            "minimum": -1,
            "description": "How many of the last transformer blocks to unfreeze. -1 unfreezes all of them, 0 none. Ignored when LoRA is enabled, which freezes the base entirely and trains adapters instead."
          },
          "finetune_last_n_layers": {
            "type": "integer",
            "title": "Trainable layers per block",
            "default": -1,
            "minimum": -1,
            "description": "How many of the last layers within each unfrozen block to train. -1 trains all of them."
          },
          "finetune_output_layers": {
            "type": "boolean",
            "title": "Train the output layers",
            "default": true,
            "description": "Also train the final norm and output projection."
          },
          "valid_size": {
            "type": "number",
            "title": "Validation fraction",
            "default": 0.2,
            "minimum": 0,
            "maximum": 1,
            "description": "Fraction of the dataset held out for validation."
          },
          "seed": {
            "type": "integer",
            "title": "Random seed",
            "default": 0,
            "minimum": 0,
            "description": "Seeds initialisation, the train/validation split, and the per-epoch caption dropout."
          },
          "train_alpha": {
            "type": "string",
            "title": "Training conditioning",
            "default": "zc",
            "description": "What the model is conditioned on while training, as the blend y = alpha*z_p + (1-alpha)*z_c. 'zc' (default) conditions on the text caption only. 'zp' conditions on the sequence only. 'blend' mixes per example. Anything putting weight on z_p triggers a one-off pass precomputing z_p for every unique sequence, which costs time up front."
          },
          "eval_alpha": {
            "type": "string",
            "title": "Validation conditioning",
            "default": "spread",
            "description": "The blend used for validation. 'spread' (default) gives each validation example its own alpha covering the whole range, so best-checkpoint selection reflects the full operating range rather than one point. A constant ('zc', 'zp', or a number) evaluates at a single blend."
          },
          "time_limit": {
            "type": "string",
            "title": "Wall-time budget",
            "description": "Optional hh:mm:ss budget. Training stops gracefully when exceeded and still writes every end-of-run artifact, with the run summary recording that the limit was the reason. Worth setting on a shared host."
          },
          "description": {
            "type": "string",
            "title": "Run note",
            "maxLength": 500,
            "description": "Free-text note recorded in the run's args.json \u2014 what this run is for, and what you changed from the last one."
          },
          "dry_run": {
            "type": "boolean",
            "title": "Dry run (preview only)",
            "default": false,
            "description": "Print the effective config with provenance, the resolved output paths, the distributed and batch arithmetic, and an a-priori memory estimate \u2014 then exit without training. The cheapest way to find out a run is misconfigured before it costs GPU hours."
          },
          "dry_run_output": {
            "type": "string",
            "title": "Dry-run report destination",
            "description": "Where the dry-run report is written. Defaults to 'True' whenever a dry run is requested, which writes dry_run_report.json into the run's artifacts directory so it is downloadable. 'False' prints to the container's stdout instead, which through the portal means the host-worker journal \u2014 readable only on the host."
          },
          "proteoscribe_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "ProteoScribe checkpoint override",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "prior-job",
              "global-data",
              "user-data"
            ],
            "description": "Optional. Finetune from a specific checkpoint instead of the job's bundle. The frozen PenCL and Facilitator weights always come from the bundle and cannot be overridden \u2014 they define the z_c space the checkpoint was trained in, and mismatching them silently changes what the captions mean."
          },
          "record_schema": {
            "type": "object",
            "title": "Custom record schema (JSON)",
            "description": "Advanced. A record_schema object, overriding the preset above. Compose functions are referenced by name and resolved inside the container, so a name that does not exist is only discovered once the job starts \u2014 prefer a preset unless you need something they do not cover."
          },
          "caption_key": {
            "type": "string",
            "title": "Caption output key",
            "description": "Which key of the record schema's output feeds the text encoder. Only change this alongside a custom schema."
          },
          "sequence_output_key": {
            "type": "string",
            "title": "Sequence output key",
            "description": "Which key of the record schema's output feeds the protein-sequence encoder. Only change this alongside a custom schema."
          },
          "length_field": {
            "type": "string",
            "title": "Length field",
            "description": "Record key holding the precomputed sequence length used for length filtering. Computed from the sequence when absent."
          },
          "lazy_records": {
            "type": "boolean",
            "title": "Stream records from disk",
            "description": "Read records lazily instead of loading the dataset into memory. Slower per step; necessary for a dataset that does not fit."
          },
          "use_lora": {
            "type": "boolean",
            "title": "Use LoRA adapters",
            "description": "Train low-rank adapters on the attention projections instead of unfreezing whole blocks. Freezes the base entirely, so the block and layer counts above stop applying. Far fewer trainable parameters, and the usual choice for a small dataset."
          },
          "lora_r": {
            "type": "integer",
            "title": "LoRA rank",
            "minimum": 1,
            "description": "Rank of the adapters. Default 16."
          },
          "lora_alpha": {
            "type": "integer",
            "title": "LoRA alpha",
            "minimum": 1,
            "description": "Scaling factor, conventionally about twice the rank. Default 32."
          },
          "lora_dropout": {
            "type": "number",
            "title": "LoRA dropout",
            "minimum": 0,
            "maximum": 1,
            "description": "Dropout applied to the adapter input. Default 0.05."
          },
          "lora_target_patterns": {
            "type": "string",
            "title": "LoRA target modules",
            "description": "Comma-separated module-name substrings to wrap with adapters. Default '.fn.to_q,.fn.to_v' (the attention query and value projections)."
          },
          "lora_unfreeze_y_mlp": {
            "type": "boolean",
            "title": "Also train the conditioning MLP",
            "description": "Train the y_mlp that injects z_c alongside the adapters. Default true."
          },
          "zp_batch_size": {
            "type": "integer",
            "title": "z_p precompute batch size",
            "minimum": 1,
            "description": "Batch size for the one-off z_p precompute pass. Only used when the training conditioning puts weight on z_p."
          },
          "choose_optim": {
            "type": "string",
            "title": "Optimizer",
            "description": "Default AdamW."
          },
          "weight_decay": {
            "type": "number",
            "title": "Weight decay",
            "minimum": 0,
            "description": "Default 1e-6."
          },
          "scheduler_gamma": {
            "type": "string",
            "title": "Learning-rate schedule",
            "description": "Schedule name (e.g. 'coswarmup') or a numeric decay factor."
          },
          "warmup_steps": {
            "type": "integer",
            "title": "Warmup steps",
            "minimum": 0,
            "description": "Learning-rate warmup length. Default 500."
          },
          "scale_learning_rate": {
            "type": "string",
            "title": "Scale LR by device count",
            "enum": [
              "true",
              "linear",
              "sqrt",
              "false"
            ],
            "description": "Scale the learning rate by the total number of devices. Immaterial while every target is single-device."
          },
          "acc_grad_batches": {
            "type": "integer",
            "title": "Gradient accumulation",
            "minimum": 1,
            "description": "Batches accumulated per optimizer step \u2014 the way to raise the effective batch size past what the device's memory holds."
          },
          "precision": {
            "type": "string",
            "title": "Precision",
            "enum": [
              "no",
              "fp16",
              "bf16",
              "32"
            ],
            "description": "Training precision. bf16 is the default for this path."
          },
          "float32_matmul_precision": {
            "type": "string",
            "title": "fp32 matmul precision",
            "enum": [
              "highest",
              "high",
              "medium"
            ],
            "description": "Tradeoff for fp32 matmuls: 'medium' uses the bf16 path, 'high' uses TF32 tensor cores, 'highest' keeps full fp32."
          },
          "distributed_strategy": {
            "type": "string",
            "title": "Distributed strategy",
            "enum": [
              "deepspeed_zero2",
              "ddp"
            ],
            "description": "Lightning trainer strategy. DeepSpeed ZeRO stage 2 with CPU offload by default. Distinct from how the data is mixed."
          },
          "max_steps": {
            "type": "integer",
            "title": "Maximum steps",
            "minimum": 1,
            "description": "Hard cap on optimizer steps, whichever comes first with the epoch count."
          },
          "val_check_interval": {
            "type": "integer",
            "title": "Validation interval (steps)",
            "minimum": 1,
            "description": "Steps between validation runs."
          },
          "check_val_every_n_epoch": {
            "type": "integer",
            "title": "Validation interval (epochs)",
            "minimum": 1,
            "description": "Epochs between validation runs. Checkpoints are monitored, so this also paces checkpoint saving."
          },
          "limit_val_batches": {
            "type": "number",
            "title": "Validation batch cap",
            "minimum": 0,
            "description": "Values above 1 are an absolute batch count (predictable wall time); values in (0,1] are a fraction of the validation set (scales with the dataset)."
          },
          "limit_train_batches": {
            "type": "number",
            "title": "Training batch cap",
            "minimum": 0,
            "description": "Same convention as the validation cap. Unset uses the whole training set."
          },
          "log_every_n_steps": {
            "type": "integer",
            "title": "Metric logging interval",
            "minimum": 1,
            "description": "How often metrics are flushed. Defaults to once per epoch."
          },
          "num_workers": {
            "type": "integer",
            "title": "Dataloader workers",
            "minimum": 0,
            "description": "Worker processes for data loading."
          },
          "early_stopping_metric": {
            "type": "string",
            "title": "Early-stopping metric",
            "description": "Metric to monitor, e.g. val_loss. Unset disables early stopping."
          },
          "early_stopping_patience": {
            "type": "integer",
            "title": "Early-stopping patience",
            "minimum": 1,
            "description": "Checks without improvement before stopping. Default 10."
          },
          "early_stopping_min_delta": {
            "type": "number",
            "title": "Early-stopping minimum delta",
            "minimum": 0,
            "description": "Smallest change counted as an improvement."
          },
          "early_stopping_mode": {
            "type": "string",
            "title": "Early-stopping direction",
            "enum": [
              "min",
              "max"
            ],
            "description": "Whether the monitored metric should be minimised or maximised."
          },
          "checkpoint_every_n_steps": {
            "type": "integer",
            "title": "Periodic checkpoint (steps)",
            "minimum": 1,
            "description": "Snapshot every N steps, in addition to best-metric saves. Written separately and never pruned by the best-metric cap."
          },
          "checkpoint_every_n_epochs": {
            "type": "integer",
            "title": "Periodic checkpoint (epochs)",
            "minimum": 1,
            "description": "Snapshot every N epochs, in addition to best-metric saves."
          },
          "checkpoint_periodic_max_keep": {
            "type": "integer",
            "title": "Periodic snapshots to keep",
            "enum": [
              -1,
              0,
              1
            ],
            "description": "-1 keeps all periodic snapshots, 0 disables them, 1 keeps only the most recent. Lightning forbids other values for unmonitored checkpoints."
          },
          "artifact_sync_on_best": {
            "type": "boolean",
            "title": "Re-emit artifacts on each best",
            "description": "Rebuild the ready-to-use state dict each time a new best checkpoint lands, so a run killed by its time limit still leaves a usable artifact. Costs a DeepSpeed-to-fp32 conversion each time."
          },
          "artifact_sync_every_n_val": {
            "type": "integer",
            "title": "Throttle artifact sync",
            "minimum": 1,
            "description": "Sync at most every Nth validation epoch."
          },
          "diffusion_steps": {
            "type": "integer",
            "title": "Diffusion steps",
            "minimum": 1,
            "description": "Diffusion steps in the training objective. Must match the architecture the checkpoint was pretrained with \u2014 change it only with a matching config."
          },
          "save_metrics_history": {
            "type": "boolean",
            "title": "Save metrics history",
            "description": "Write per-step training and validation metrics to the run directory. On by default, and the main thing worth reading afterwards."
          },
          "metrics_history_every_n_steps": {
            "type": "integer",
            "title": "Metrics history interval",
            "minimum": 1,
            "description": "Record training metrics every N steps. Buffered in memory and flushed in batches."
          },
          "save_benchmark": {
            "type": "boolean",
            "title": "Save per-epoch benchmark",
            "description": "Record per-epoch timing and peak memory. Useful when sizing a longer run from a short one."
          }
        }
      },
      "Biom3FinetuneGeneralizedSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3FinetuneGeneralizedParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-finetune-generalized",
          "family": "biom3",
          "name": "BioM3 \u2014 ProteoScribe finetuning (generalized)",
          "description": "Finetune ProteoScribe (Stage 3) on a curated sequence/annotation dataset. Unlike the legacy path, which trains on z_c embeddings frozen at compile time, this composes a text caption from each record's annotation fields on every epoch and embeds it to z_c on-device through a frozen PenCL text branch and Facilitator. Recomposing the caption each epoch is the point: with per-field dropout the model sees a different subset of the annotation each time and learns to generate from partial descriptions rather than one fixed phrasing. Takes the output of a Swiss-Prot annotation-fields dataset build as its input. Training runs for hours to days \u2014 submit it and come back; the output is a browsable run directory of checkpoints, metrics history and the exact config used, not a single file.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "enabled": true,
            "blacklist": [
              "--config_path",
              "-c",
              "--finetune_data_path",
              "--output_root",
              "--run_id",
              "--stage1_config_path",
              "--stage2_config_path",
              "--pretrained_weights",
              "--pencl_weights",
              "--facilitator_weights",
              "--record_schema",
              "--compose_plugins",
              "--wandb",
              "--wandb_name",
              "--wandb_entity",
              "--wandb_project",
              "--wandb_tags",
              "--finetune",
              "--device",
              "--checkpoints_folder",
              "--runs_folder",
              "--output_path",
              "--primary_data_path",
              "--secondary_data_paths",
              "--dry_run_output"
            ]
          }
        }
      },
      "Biom3GenerateFromEmbeddingParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "embeddings"
        ],
        "properties": {
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "embeddings": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "Embeddings (from a prior Text \u2192 z_c or Joint embedding job)"
          },
          "num_sequences": {
            "type": "number",
            "title": "Number of sequences per prompt",
            "default": 5
          },
          "seed": {
            "type": "number",
            "title": "Random seed (0 = random)",
            "default": 0
          },
          "token_strategy": {
            "type": "string",
            "title": "Sampling strategy (sample = stochastic, argmax = deterministic)"
          },
          "unmasking_order": {
            "type": "string",
            "title": "Unmasking order (random / confidence / confidence_no_pad)"
          },
          "max_length": {
            "type": "number",
            "title": "Maximum sequence length (residues)",
            "minimum": 1,
            "maximum": 1022,
            "description": "Optional cap on how long a generated sequence may be; leave unset for the model's full context. Stage 3 freezes the tail of the diffusion state as <PAD> and generates only the leading positions, so sequences come out at or under the cap. It is a ceiling, not a target \u2014 the model still ends a sequence early. Capping also makes generation proportionally faster, since it runs that many diffusion steps rather than the full 1024."
          },
          "proteoscribe_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "ProteoScribe (Stage 3) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "prior-job",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run ProteoScribe (Stage 3) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3GenerateFromEmbeddingSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3GenerateFromEmbeddingParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-generate-from-embedding",
          "family": "biom3",
          "name": "BioM3 \u2014 Generate from embedding (Stage 3)",
          "description": "Generate novel protein sequences with the Stage 3 ProteoScribe diffusion model, conditioned on a prior embedding job's facilitated latent z_c. Chain off a completed biom3-embed-text-zc job (text only) or biom3-embed-joint job (paired sequence + caption), referenced by name \u2014 both run the same Stage 1 PenCL -> Stage 2 Facilitator pipeline and emit the same z_c. Use this to reuse one embedding across many generation runs; to go straight from a text prompt, use biom3-generate-from-prompt. Single-purpose split of the former biom3-generation tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "enabled": true,
            "blacklist": [
              "--input_path",
              "-i",
              "--output_path",
              "-o",
              "--config_path",
              "-c",
              "--model_path",
              "-m",
              "--fasta",
              "--fasta_dir",
              "--fasta_merge",
              "--seed",
              "--token_strategy",
              "--unmasking_order"
            ]
          },
          "produces": "../kinds.yaml#/ProteinSequences"
        }
      },
      "Biom3GenerateFromPromptParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "prompts"
        ],
        "properties": {
          "prompts": {
            "$ref": "#/components/schemas/TextCaptions",
            "title": "Text prompts",
            "description": "Text prompts to generate from, one per non-empty line. The dispatcher fills the caption column and leaves protein_sequence empty."
          },
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "num_sequences": {
            "type": "number",
            "title": "Number of sequences per prompt",
            "default": 5
          },
          "seed": {
            "type": "number",
            "title": "Random seed (0 = random)",
            "default": 0
          },
          "token_strategy": {
            "type": "string",
            "title": "Sampling strategy (sample = stochastic, argmax = deterministic)"
          },
          "unmasking_order": {
            "type": "string",
            "title": "Unmasking order (random / confidence / confidence_no_pad)"
          },
          "max_length": {
            "type": "number",
            "title": "Maximum sequence length (residues)",
            "minimum": 1,
            "maximum": 1022,
            "description": "Optional cap on how long a generated sequence may be; leave unset for the model's full context. Stage 3 freezes the tail of the diffusion state as <PAD> and generates only the leading positions, so sequences come out at or under the cap. It is a ceiling, not a target \u2014 the model still ends a sequence early. Capping also makes generation proportionally faster, since it runs that many diffusion steps rather than the full 1024."
          },
          "pencl_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "PenCL (Stage 1) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run PenCL (Stage 1) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          },
          "facilitator_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "Facilitator (Stage 2) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run Facilitator (Stage 2) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          },
          "proteoscribe_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "ProteoScribe (Stage 3) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "prior-job",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run ProteoScribe (Stage 3) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3GenerateFromPromptSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3GenerateFromPromptParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-generate-from-prompt",
          "family": "biom3",
          "name": "BioM3 \u2014 Generate from prompt (Stages 1\u21922\u21923)",
          "description": "Generate novel protein sequences directly from text prompt(s). Runs the full pipeline in one job: Stage 1 PenCL and Stage 2 Facilitator embed each prompt to z_c, then Stage 3 ProteoScribe generates sequences conditioned on it. Paste prompts one per line. To reuse an existing embedding instead, use biom3-generate-from-embedding. Single-purpose split of the former biom3-generation tool.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "comment": "extras target Stage 1 (PenCL), which always runs.",
            "enabled": true,
            "blacklist": [
              "--input_path",
              "-i",
              "--input_data_path",
              "--output_path",
              "-o",
              "--output_data_path",
              "--config_path",
              "-c",
              "--model_path",
              "-m",
              "--fasta",
              "--fasta_dir",
              "--fasta_merge",
              "--seed",
              "--token_strategy",
              "--unmasking_order"
            ]
          },
          "input_shape": {
            "description": "Text prompts, one per line. The dispatcher assembles the joint CSV (primary_Accession,protein_sequence,[final]text_caption) BioM3 Stage 1 requires, filling the caption column and leaving protein_sequence empty. Handled by _build_biom3_lines_csv (input_builder=biom3_lines, input_kind=text)."
          },
          "produces": "../kinds.yaml#/ProteinSequences"
        }
      },
      "Biom3GenerationParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "embeddings"
        ],
        "properties": {
          "weights_bundle": {
            "type": "string",
            "title": "Weights bundle",
            "default": "run1_base",
            "description": "Which published weights bundle to run against (ghcr.io/natural-machine/biom3-weights). A bundle is a matched set of weights for every stage, so this one choice selects them all."
          },
          "embeddings": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "Embeddings (facilitator_embeddings.pt from a prior Embedding job)"
          },
          "seed": {
            "type": "number",
            "title": "Random seed (0 = random)",
            "default": 0
          },
          "max_length": {
            "type": "number",
            "title": "Maximum sequence length (residues)",
            "minimum": 1,
            "maximum": 1022,
            "description": "Optional cap on how long a generated sequence may be; leave unset for the model's full context. Stage 3 freezes the tail of the diffusion state as <PAD> and generates only the leading positions, so sequences come out at or under the cap. It is a ceiling, not a target \u2014 the model still ends a sequence early. Capping also makes generation proportionally faster, since it runs that many diffusion steps rather than the full 1024."
          },
          "proteoscribe_model": {
            "$ref": "#/components/schemas/Biom3WeightsRef",
            "type": "object",
            "title": "ProteoScribe (Stage 3) weights override (optional)",
            "x-nm-catalog": "weights",
            "x-nm-sources": [
              "bundle",
              "registry",
              "prior-job",
              "global-data",
              "user-data"
            ],
            "x-nm-explicit-required": true,
            "description": "Optional. Run ProteoScribe (Stage 3) against a specific weight instead of the job's bundle. Pairing an off-bundle weight with a compatible config is your responsibility \u2014 use config_override if the architecture differs."
          }
        }
      },
      "Biom3GenerationSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/Biom3GenerationParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "biom3-generation",
          "family": "biom3",
          "name": "BioM3 \u2014 Generation (Stage 3)",
          "description": "Generate novel protein sequences from text-conditioned embeddings via a conditional diffusion transformer (ProteoScribe).",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "extra_args": {
            "enabled": true,
            "blacklist": [
              "--input_path",
              "-i",
              "--output_path",
              "-o",
              "--config_path",
              "-c",
              "--model_path",
              "-m",
              "--fasta",
              "--fasta_dir",
              "--fasta_merge",
              "--seed"
            ]
          },
          "produces": "../kinds.yaml#/ProteinSequences"
        }
      },
      "Biom3WeightsRef": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/Biom3WeightsRefBundle"
          },
          {
            "$ref": "#/components/schemas/Biom3WeightsRefRegistry"
          },
          {
            "$ref": "#/components/schemas/Biom3WeightsRefPriorJob"
          },
          {
            "$ref": "#/components/schemas/Biom3WeightsRefGlobalData"
          },
          {
            "$ref": "#/components/schemas/Biom3WeightsRefUserData"
          }
        ],
        "description": "Which weight one stage loads, instead of the one the job's `weights_bundle` supplies. Each source identifies its weight differently, so the value is an object."
      },
      "Biom3WeightsRefBundle": {
        "type": "object",
        "additionalProperties": false,
        "title": "Another published bundle",
        "required": [
          "source"
        ],
        "properties": {
          "source": {
            "const": "bundle"
          },
          "tag": {
            "type": "string",
            "description": "A `WeightsBundle.tag`. Omit to name the job's own bundle."
          }
        }
      },
      "Biom3WeightsRefGlobalData": {
        "type": "object",
        "additionalProperties": false,
        "title": "A curated dataset used as a weight",
        "required": [
          "source",
          "id"
        ],
        "properties": {
          "source": {
            "const": "global-data"
          },
          "id": {
            "type": "string",
            "description": "A global-data handle. Carries no stage or bundle, so nothing checks that it belongs where it is used. Prefer `registry`."
          }
        }
      },
      "Biom3WeightsRefPriorJob": {
        "type": "object",
        "additionalProperties": false,
        "title": "A checkpoint one of your own finetuning runs produced",
        "description": "The ProteoScribe checkpoint produced by a completed `biom3-finetune-generalized` job.\n",
        "required": [
          "source"
        ],
        "properties": {
          "source": {
            "const": "prior-job"
          },
          "job_id": {
            "type": "string"
          },
          "job_name": {
            "type": "string"
          }
        },
        "anyOf": [
          {
            "required": [
              "job_id"
            ]
          },
          {
            "required": [
              "job_name"
            ]
          }
        ]
      },
      "Biom3WeightsRefRegistry": {
        "type": "object",
        "additionalProperties": false,
        "title": "A weight registered with this deployment",
        "required": [
          "source",
          "id"
        ],
        "properties": {
          "source": {
            "const": "registry"
          },
          "id": {
            "type": "string",
            "description": "A `RegisteredWeights.id` for this stage. Its `base_bundle` must equal the job's `weights_bundle`."
          }
        }
      },
      "Biom3WeightsRefUserData": {
        "type": "object",
        "additionalProperties": false,
        "title": "The caller's own upload",
        "required": [
          "source",
          "file_id"
        ],
        "properties": {
          "source": {
            "const": "user-data"
          },
          "file_id": {
            "type": "string",
            "description": "A `UserDataItem.file_id` belonging to the caller."
          }
        }
      },
      "BioparsersBuildSwissprotFieldsParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "pfam_ids"
        ],
        "properties": {
          "database": {
            "type": "string",
            "title": "Parsed dataset",
            "x-nm-catalog": "reference-database",
            "description": "Which parsed UniProtKB dataset to build from. Swiss-Prot is the reviewed, manually annotated section. TrEMBL is far larger and is not provisioned yet."
          },
          "pfam_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "PF[0-9]{5}"
            },
            "title": "Pfam families",
            "maxItems": 50,
            "description": "Keep only entries carrying at least one of these Pfam domains, e.g. PF00018 for SH3. Required: without a domain filter the build emits every entry in the release."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "title": "Annotation fields",
            "x-nm-catalog": "field-set",
            "description": "Which annotation fields each record carries. Defaults to the field set behind the legacy BioM3 caption, so an unconfigured build is comparable to the legacy one."
          },
          "reviewed_only": {
            "type": "boolean",
            "title": "Reviewed entries only",
            "default": true,
            "description": "Keep only entries UniProt marks Reviewed. Always true in effect when building from Swiss-Prot, which is entirely reviewed; it matters when building from TrEMBL or a mixed source."
          },
          "min_length": {
            "type": "number",
            "title": "Minimum sequence length",
            "default": 0,
            "description": "Drop entries shorter than this many residues. 0 keeps everything."
          },
          "include_caption_fields": {
            "type": "boolean",
            "title": "Include caption-ready text",
            "default": true,
            "description": "Emit the `caption_fields` dict alongside `fields`. Turn off when only the structured values are wanted."
          },
          "description": {
            "type": "string",
            "title": "Build note",
            "maxLength": 500,
            "description": "Free-text note recorded in the output's build manifest \u2014 what this dataset is for, and why these fields."
          }
        }
      },
      "BioparsersBuildSwissprotFieldsSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersBuildSwissprotFieldsParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-build-swissprot-fields",
          "family": "bioparsers",
          "name": "Build \u2014 Swiss-Prot annotation fields",
          "description": "Build a curated sequence/annotation dataset from parsed UniProtKB, choosing which annotation fields to keep and which entries to include. Each record carries the selected fields twice: in `fields` in their source shape (a list per comment block, a string, a number) and in `caption_fields` as cleaned single strings ready to drop into a caption. Nothing decides the caption's order, separators or field subset at build time, so a trainer can compose captions on the fly \u2014 annotation dropout, field randomization \u2014 rather than consuming one phrasing frozen into the data. There is nothing to upload: the parsed release is curated global data, so pick it, choose your fields and filters, and run.",
          "allowed_target_classes": [
            "persistent-cache"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/ProteoScribeFinetuningDataset"
        }
      },
      "BioparsersBuildSwissprotLegacyParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "pfam_ids"
        ],
        "properties": {
          "database": {
            "type": "string",
            "title": "Parsed dataset",
            "x-nm-catalog": "reference-database",
            "description": "Which parsed UniProtKB dataset to build from. The legacy dataset's Swiss-Prot section came from Swiss-Prot."
          },
          "pfam_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "PF[0-9]{5}"
            },
            "title": "Pfam families",
            "maxItems": 50,
            "description": "Keep only entries carrying at least one of these Pfam domains. The legacy SH3 dataset used PF00018. Required: without a domain filter the build emits every entry in the release."
          },
          "min_length": {
            "type": "number",
            "title": "Minimum sequence length",
            "default": 0,
            "description": "Drop entries shorter than this many residues. 0 keeps everything, which is what the legacy dataset did."
          },
          "description": {
            "type": "string",
            "title": "Build note",
            "maxLength": 500,
            "description": "Free-text note recorded in the output's build manifest."
          }
        }
      },
      "BioparsersBuildSwissprotLegacySubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersBuildSwissprotLegacyParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-build-swissprot-legacy",
          "family": "bioparsers",
          "name": "Build \u2014 Swiss-Prot legacy captions",
          "description": "Reproduce the Swiss-Prot section of the legacy BioM3 finetuning dataset from parsed UniProtKB: each kept entry's sequence plus an assembled `[final]text_caption` in the legacy field order and phrasing, and the annotation fields it was built from. The field set is fixed by what this reproduces and is deliberately not selectable \u2014 use the annotation-fields build to choose your own. Note that this is an approximate reproduction: it is built against a current UniProt and Pfam release, so the entry set has drifted from the published dataset.",
          "allowed_target_classes": [
            "persistent-cache"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          }
        }
      },
      "BioparsersCsvParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "table"
        ],
        "properties": {
          "table": {
            "$ref": "#/components/schemas/TabularRecords",
            "title": "Table (CSV / TSV)",
            "description": "The delimited table to convert. Paste it, upload a file, or pick a curated dataset."
          },
          "delimiter": {
            "type": "string",
            "title": "Delimiter",
            "default": ",",
            "maxLength": 4,
            "description": "Field delimiter. Defaults to a comma. Pass a literal tab, or the two characters \\t, for a tab-separated table \u2014 the submitted file is staged under a fixed name, so the extension cannot be used to infer this."
          }
        }
      },
      "BioparsersCsvSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersCsvParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-csv",
          "family": "bioparsers",
          "name": "bioparsers \u2014 Delimited table (CSV / TSV)",
          "description": "Convert a delimited table to JSONL \u2014 one object per row, keyed by the header, values kept verbatim as strings. This is the parser for sources that already ship as a structured table (supplemental datasets, curated spreadsheets) rather than as a database release, so unlike the other bioparsers tools this one takes your file as input. The delimiter defaults to a comma; set it explicitly for a tab-separated table.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          }
        }
      },
      "BioparsersPfamFastaParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "pfam_ids"
        ],
        "properties": {
          "database": {
            "type": "string",
            "title": "Database release",
            "x-nm-catalog": "reference-database",
            "description": "Which Pfam-A member-FASTA release to parse."
          },
          "pfam_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^PF[0-9]{5}$"
            },
            "title": "Pfam families",
            "maxItems": 50,
            "description": "Pfam accessions whose member sequences to extract, e.g. PF00018 (SH3_1)."
          }
        }
      },
      "BioparsersPfamFastaSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersPfamFastaParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-pfam-fasta",
          "family": "bioparsers",
          "name": "bioparsers \u2014 Pfam-A member sequences",
          "description": "Parse the Pfam-A member FASTA (the redundancy-reduced member set) into JSONL \u2014 one record per member sequence: member accession and name, aligned region, its Pfam family, and the ungapped residues. Lighter than the full alignments when the member sequences are all that is wanted. Families must be named explicitly. The database is staged from curated global data, so no input file is submitted.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          }
        }
      },
      "BioparsersPfamParams": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "database": {
            "type": "string",
            "title": "Database release",
            "x-nm-catalog": "reference-database",
            "description": "Which Pfam-A full-alignment release to parse."
          },
          "pfam_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^PF[0-9]{5}$"
            },
            "title": "Pfam families",
            "maxItems": 50,
            "description": "Keep only these Pfam families, e.g. PF00018 for SH3. LEAVE EMPTY to parse every family in the release \u2014 that is how the catalogued family-metadata artifact is produced, and it is a provisioning run rather than an everyday one: it streams the whole 23.8 GB release. For a normal parse, name the families you want."
          },
          "with_member_accessions": {
            "type": "boolean",
            "title": "Include member list",
            "default": false,
            "description": "Attach each family's per-member list (accession, name, aligned region). Omitted by default because the member count alone is usually enough."
          },
          "with_member_sequences": {
            "type": "boolean",
            "title": "Include member sequences",
            "default": false,
            "description": "Attach each member's ungapped sequence, derived from the alignment and validated against its region span. Implies the member list, and makes the output substantially larger."
          }
        }
      },
      "BioparsersPfamSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersPfamParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-pfam",
          "family": "bioparsers",
          "name": "bioparsers \u2014 Pfam-A families (full alignments)",
          "description": "Parse Pfam-A full alignments into JSONL \u2014 one record per family: accession, name, description, type, clan, references, GA/TC/NC thresholds, cross-references, and member count, with the member list and each member's ungapped sequence available on request. Families must be named explicitly: scanning stops once they are all found, which is what keeps a job over a ~16 GB release fast and its output a usable size. The database is staged from curated global data, so no input file is submitted. Runs on the cloud host only: Pfam-A.full is 23.8 GB, which lands on that host's persistent volume and is re-used by later jobs. On a Spark it would stage to per-job scratch and be re-fetched every run \u2014 and those hosts do not have the free space for it.",
          "allowed_target_classes": [
            "persistent-cache"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          }
        }
      },
      "BioparsersUniprotParams": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "database": {
            "type": "string",
            "title": "Database release",
            "x-nm-catalog": "reference-database",
            "description": "Which UniProtKB section to parse. Swiss-Prot is the reviewed, manually annotated section (~570k entries). TrEMBL is the unreviewed section and is not provisioned yet."
          }
        }
      },
      "BioparsersUniprotSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BioparsersUniprotParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "bioparsers-uniprot",
          "family": "bioparsers",
          "name": "bioparsers \u2014 UniProtKB (Swiss-Prot / TrEMBL)",
          "description": "Parse a UniProtKB flat-file release into JSONL \u2014 one typed record per entry, with accessions, reviewed status, names, gene names, organism, lineage and taxon, references, comments, features, cross-references, keywords, and the amino-acid sequence (validated against the ID/SQ length and CRC64). The database is staged from curated global data, so no input file is submitted: pick a release and run. Output is gzipped JSONL, the input format the dataset builders consume.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          }
        }
      },
      "BlastIdentifyParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "$ref": "#/components/schemas/ProteinSequences",
            "title": "Query sequence(s) (FASTA)",
            "description": "Protein query sequence(s) in FASTA format."
          },
          "database": {
            "type": "string",
            "title": "Database",
            "x-nm-catalog": "reference-database",
            "default": "swissprot-tax-blast",
            "description": "Which database to search. Provisioned options come from the reference-database catalog; GET /api/tools/blast/databases lists them with descriptions."
          },
          "program": {
            "type": "string",
            "title": "BLAST program",
            "default": "blastp",
            "maxLength": 10,
            "description": "BLAST program. blastp for protein queries."
          },
          "evalue": {
            "type": "number",
            "title": "E-value threshold",
            "default": 0.001,
            "description": "Maximum E-value for reported hits. Lower = stricter. Default 1e-3."
          },
          "max_target_seqs": {
            "type": "number",
            "title": "Max hits",
            "default": 50,
            "description": "Maximum number of aligned sequences to report. Default 50."
          }
        }
      },
      "BlastIdentifySubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/BlastIdentifyParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "blast-identify",
          "family": "blast",
          "name": "BLAST \u2014 Identify (characterize a sequence)",
          "description": "Quickly characterize a protein sequence: run a BLAST search against a small, well-annotated database (SwissProt/UniRef50) to find its nearest natural proteins, similarity, and taxonomy. Returns the raw BLAST hit table (tab-separated). For a thorough homolog collection against a large database, use blast-search (later). Runs on a compute-target host's daemon; the container syncs the selected DB from S3 before searching.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "spark-nm",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/TabularRecords"
        }
      },
      "Builder": {
        "type": "object",
        "description": "What a build tool produces and what it can be asked for.\n\n`field_set` is **absent** \u2014 not empty \u2014 for a fixed-field builder. \"You\ncannot choose\" and \"you may choose nothing\" are different statements, and a\nUI that conflated them would render an empty checkbox list where it should\nrender an explanation.\n",
        "required": [
          "id",
          "output"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "output": {
            "type": "string",
            "description": "What the build emits."
          },
          "filters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "required_filters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "field_set": {
            "$ref": "#/components/schemas/FieldSet"
          }
        }
      },
      "BuilderCatalog": {
        "type": "object",
        "required": [
          "tools"
        ],
        "properties": {
          "tools": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Builder"
            }
          }
        }
      },
      "ComputeHost": {
        "type": "object",
        "required": [
          "host_id",
          "display_name",
          "status",
          "classes"
        ],
        "properties": {
          "host_id": {
            "type": "string",
            "example": "byoc-1f4c2a9b8d3e5607"
          },
          "display_name": {
            "type": "string",
            "example": "Lab workstation"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "revoked"
            ],
            "description": "`pending` until the machine redeems its enrollment code; `active` once\nit has. `revoked` is terminal \u2014 a revoked machine is never reactivated.\n"
          },
          "classes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the machine offers, in the same vocabulary tools use to say what\nthey need \u2014 `gpu-daemon`, `cpu-daemon`. A tool reaches this host when\nthe two intersect.\n",
            "example": [
              "gpu-daemon"
            ]
          },
          "created_at": {
            "type": "integer"
          },
          "enrolled_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null while the machine has not yet redeemed its code."
          }
        }
      },
      "ComputeHostList": {
        "type": "object",
        "required": [
          "hosts"
        ],
        "properties": {
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ComputeHost"
            }
          }
        }
      },
      "CreateUploadRequest": {
        "type": "object",
        "required": [
          "size_bytes"
        ],
        "additionalProperties": false,
        "properties": {
          "size_bytes": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "CreateUploadResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PresignedUpload"
          },
          {
            "type": "object",
            "required": [
              "upload_id",
              "max_bytes"
            ],
            "properties": {
              "upload_id": {
                "type": "string"
              },
              "max_bytes": {
                "type": "integer"
              }
            }
          }
        ]
      },
      "CreateUserDataRequest": {
        "type": "object",
        "required": [
          "display_name",
          "size_bytes"
        ],
        "additionalProperties": false,
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0,
            "description": "Pinned into the upload policy by S3."
          },
          "category": {
            "type": "string",
            "maxLength": 50,
            "default": "other"
          },
          "description": {
            "type": "string",
            "maxLength": 1000
          }
        }
      },
      "CreateUserDataResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PresignedUpload"
          },
          {
            "type": "object",
            "required": [
              "file_id"
            ],
            "properties": {
              "file_id": {
                "type": "string"
              }
            }
          }
        ]
      },
      "DataRef": {
        "$ref": "#/components/schemas/JobInput",
        "description": "A pointer to data the server can fetch on the caller's behalf.\n\nWhich places a given parameter may be satisfied from is execution policy,\ndeclared per tool in `lambdas/<family>_api/runbooks/`, not here. A tool does\nnot become unable to read a PDB because of where it was stored.\n"
      },
      "DatabaseCatalog": {
        "type": "object",
        "description": "Selectable databases, keyed by tool id. A tool with no database argument is\nabsent from the map rather than present-and-empty.\n",
        "required": [
          "tools"
        ],
        "properties": {
          "tools": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/ReferenceDatabase"
              }
            }
          }
        }
      },
      "DeleteUserDataResponse": {
        "type": "object",
        "required": [
          "deleted"
        ],
        "properties": {
          "deleted": {
            "type": "string",
            "description": "The file_id that was removed."
          }
        }
      },
      "DownloadResponse": {
        "type": "object",
        "required": [
          "url",
          "expires_in_seconds"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "expires_in_seconds": {
            "type": "integer"
          }
        }
      },
      "EnrollRequest": {
        "type": "object",
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The enrollment code, as issued. Case, spacing and dashes are ignored, so\na code someone retyped by hand is accepted as long as the characters\nmatch.\n",
            "example": "NMC-7KQ4H-8XR2V-B19TC"
          }
        }
      },
      "EnrollResponse": {
        "type": "object",
        "required": [
          "host_id",
          "token"
        ],
        "properties": {
          "host_id": {
            "type": "string",
            "example": "byoc-1f4c2a9b8d3e5607"
          },
          "token": {
            "type": "string",
            "description": "The host's long-lived credential, returned once and never retrievable\nagain \u2014 only its hash is stored. Losing it means registering the machine\nafresh, which is the property that makes the table safe to read.\n"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable. Wording is not part of the contract; do not match on it."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode",
            "description": "**No handler emits this yet.** Every error body today is `{\"error\": ...}`\nalone, so a client that branches on `code` branches on `undefined`.\n\nIt is declared \u2014 and optional rather than required \u2014 so the vocabulary is\nagreed before handlers adopt it, and so adding it later is additive\nrather than breaking. Until then, branch on the HTTP status.\n"
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional, shape determined by `code`. Present where a client can act on\nspecifics \u2014 e.g. `quota_exceeded` carries the quota.\n"
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "A stable, machine-readable identifier for the failure. Clients branch on this;\n`error` is for humans and its wording is not part of the contract.\n\nClosed enum \u2014 a code not listed here is a contract violation, which is what\nstops the vocabulary growing by accident.\n",
        "enum": [
          "invalid_request",
          "unknown_tool",
          "unknown_argument",
          "invalid_argument",
          "missing_argument",
          "input_too_large",
          "input_not_found",
          "source_not_allowed",
          "unauthorized",
          "forbidden",
          "not_found",
          "quota_exceeded",
          "job_not_finished",
          "target_not_allowed",
          "not_provisioned",
          "dispatch_failed",
          "internal"
        ]
      },
      "EsmfoldPredictParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sequences"
        ],
        "properties": {
          "sequences": {
            "$ref": "#/components/schemas/ProteinSequences",
            "title": "Sequence(s) (FASTA)",
            "description": "Protein sequence(s) to fold, as FASTA or a single bare amino-acid sequence. Each record's id names its output PDB. The 20 standard amino acids plus X are accepted."
          },
          "num_recycles": {
            "type": "number",
            "title": "Recycles",
            "description": "How many times the folding trunk refines its own prediction. More recycles cost proportionally more time for usually small accuracy gains. Omit to use the model's own default."
          },
          "chunk_size": {
            "type": "number",
            "title": "Trunk chunk size",
            "description": "Splits the trunk's attention into chunks, trading speed for peak GPU memory. Omit unless a long sequence runs out of memory; then try 128, then 64."
          }
        }
      },
      "EsmfoldPredictSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/EsmfoldPredictParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "esmfold-predict",
          "family": "esmfold",
          "name": "ESMFold \u2014 Predict structure",
          "description": "Predict 3D structure for one or more protein sequences with ESMFold. Single-sequence: no multiple-sequence alignment and no template search, so a batch of designs folds in seconds to minutes rather than hours. Submit a FASTA (or paste a bare sequence) and get one PDB per sequence plus a summary table of mean pLDDT and pTM confidence. Runs on a GPU compute-target host; the container stages the ESMFold weights from curated global data before folding.",
          "allowed_target_classes": [
            "gpu-daemon"
          ],
          "default_target_id": "spark-nm",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_sequences": 128,
            "max_residues": 1024
          },
          "produces": "../kinds.yaml#/ProteinStructures"
        }
      },
      "FeatureMatrix": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "feature-matrix",
        "description": "A numeric matrix \u2014 one row per item, one column per feature. Delimited text\nwith a header row, or a NumPy `.npy` array.\n\nDistinct from TabularRecords, which is a table of records where a column may\nbe anything. Here every column that is not named as a label is a number, and\nthe rows are points in one space. A tool that projects or clusters needs\nthat promise; a tool that parses a table does not.\n"
      },
      "FeedbackContext": {
        "type": "object",
        "additionalProperties": false,
        "description": "Where the reporter was when they filed. Every field is client-supplied and\ntherefore unverified \u2014 it is reproduction detail, not identity, and the\nhandler records it as such under a heading that says so. The reporter's\naccount and organization are read from the token instead.\n",
        "properties": {
          "route": {
            "type": "string",
            "maxLength": 200,
            "description": "The page the reporter was on, as a route hash.",
            "example": "#/tools/biom3/generate-from-prompt"
          },
          "job_id": {
            "type": "string",
            "maxLength": 200,
            "description": "A job the report is about, when the reporter was looking at one. This\nis the field that most often turns a report into something reproducible,\nso the form offers it whenever a job is on screen.\n"
          },
          "conversation_id": {
            "type": "string",
            "maxLength": 200,
            "description": "The assistant conversation open at the time, if any."
          },
          "user_agent": {
            "type": "string",
            "maxLength": 500,
            "description": "Browser and platform string, for rendering and upload bugs."
          }
        }
      },
      "FeedbackReceipt": {
        "type": "object",
        "required": [
          "report_id"
        ],
        "additionalProperties": false,
        "properties": {
          "report_id": {
            "type": "integer",
            "description": "The filed report's number, for the reporter to quote when following up.\nDeliberately not a URL: reports land in a private repository the\nreporter cannot open, and handing them a link that 404s reads as the\nreport having failed.\n",
            "example": 412
          }
        }
      },
      "FeedbackReport": {
        "type": "object",
        "required": [
          "kind",
          "title",
          "body"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "bug",
              "idea",
              "question"
            ],
            "description": "What sort of report this is. Chooses the label on the filed issue and\nnothing else \u2014 a client cannot set labels directly, so this is the whole\nof its say in how the report is triaged.\n",
            "example": "bug"
          },
          "title": {
            "type": "string",
            "minLength": 3,
            "maxLength": 140,
            "description": "One-line summary, used as the issue title.",
            "example": "BioM3 generation stays queued forever on spark-nm"
          },
          "body": {
            "type": "string",
            "minLength": 10,
            "maxLength": 8000,
            "description": "What happened, in the reporter's words. Rendered as Markdown on the\nissue. Capped so a report cannot be used as free storage; the cap is\nwell above any description someone types by hand.\n",
            "example": "I submitted a generation job at about 10:15 and it has been queued\nsince. My Jobs shows no error.\n"
          },
          "context": {
            "$ref": "#/components/schemas/FeedbackContext"
          }
        }
      },
      "Field": {
        "type": "object",
        "description": "One selectable output field of a dataset builder.",
        "required": [
          "id",
          "label"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send in the tool's `fields` argument."
          },
          "label": {
            "type": "string"
          },
          "group": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display grouping; one of the parent field set's `groups`."
          },
          "kind": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "default": {
            "type": "boolean",
            "description": "Whether this field is selected when the caller sends none."
          }
        }
      },
      "FieldSet": {
        "type": "object",
        "required": [
          "id",
          "fields"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            }
          }
        }
      },
      "GlobalDataInput": {
        "type": "object",
        "description": "An admin-curated dataset, addressed by its handle. Handles come from\n`GET /api/global_data`; S3 keys are never exposed.\n",
        "required": [
          "source",
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "global-data"
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The dataset handle, not a path."
          }
        }
      },
      "GlobalDataListing": {
        "type": "object",
        "required": [
          "version",
          "datasets"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "Catalog shape version. Bumped when the dataset entry shape changes in a\nway a client must notice.\n"
          },
          "datasets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GlobalDataset"
            }
          }
        }
      },
      "GlobalDataset": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The stable handle. Never an S3 key \u2014 keys are not exposed to clients."
          },
          "name": {
            "type": "string"
          },
          "folder": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display grouping. Clients split on \"/\" to build a tree."
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Short-lived presigned GET."
          }
        }
      },
      "HmmerHomologsParams": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "query": {
            "$ref": "#/components/schemas/ProteinSequences",
            "title": "Query sequence(s) (FASTA)",
            "description": "Protein query sequence(s) in FASTA format. Starts a jackhmmer search for homologs. Give this OR pfam_id, not both."
          },
          "pfam_id": {
            "type": "string",
            "title": "Pfam family",
            "pattern": "^PF[0-9]{5}$",
            "example": "PF00080",
            "description": "A Pfam accession (e.g. PF00080). Pulls every protein in the family from InterPro instead of searching. Give this OR query, not both."
          },
          "reviewed": {
            "type": "boolean",
            "title": "Reviewed entries only",
            "default": false,
            "description": "Pfam only \u2014 restrict to Swiss-Prot (reviewed) entries."
          },
          "database": {
            "type": "string",
            "title": "Database to search",
            "x-nm-catalog": "reference-database",
            "default": "uniref90-fasta",
            "description": "The large database the profile is searched against in stage 2. Stage 1 always runs against Swiss-Prot. Provisioned options come from the reference-database catalog; GET /api/tools/hmmer/databases lists them with descriptions."
          },
          "iterations": {
            "type": "number",
            "title": "jackhmmer iterations",
            "default": 5,
            "minimum": 1,
            "maximum": 20,
            "description": "How many iterations stage 1 runs against Swiss-Prot. More iterations reach further from the query and risk drifting into unrelated families. Default 5, the reference implementation's."
          },
          "extend": {
            "type": "boolean",
            "title": "Extend domains",
            "default": false,
            "description": "Pad each domain hit outward with its flanking residues, for when the sequences need to reach a target length. Off by default \u2014 the domains as found are the dataset unless you want them longer."
          },
          "pad": {
            "type": "number",
            "title": "Domain padding (residues)",
            "default": 10,
            "minimum": 0,
            "description": "Residues added on each side of each domain hit. Only applies when extend is on."
          },
          "n_pad": {
            "type": "number",
            "title": "N-terminal padding",
            "minimum": 0,
            "description": "Overrides the padding on the N-terminal side only."
          },
          "c_pad": {
            "type": "number",
            "title": "C-terminal padding",
            "minimum": 0,
            "description": "Overrides the padding on the C-terminal side only."
          },
          "build_msa": {
            "type": "boolean",
            "title": "Build the MSA",
            "default": true,
            "description": "Align the collected sequences with pyfamsa. On by default, because the MSA is what the downstream analyses consume; turn it off to get the sequence sets alone."
          }
        }
      },
      "HmmerHomologsSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/HmmerHomologsParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "hmmer-homologs",
          "family": "hmmer",
          "name": "HMMER \u2014 Homolog search (jackhmmer \u2192 hmmsearch)",
          "description": "Build a sequence dataset from one query: collect homologs, extend the domain hits, filter them by taxonomy, function and length, and align the result into an MSA. Runs nm-data-assembly's pipeline end to end \u2014 jackhmmer against Swiss-Prot, hmmsearch against a large database (UniRef90), extend_domains, filter_data, then build_MSA (pyfamsa). The MSA is the product; the intermediates are returned beside it. Slower and far more sensitive than blast-identify: use BLAST to ask what a sequence is, this to build a dataset around it. Requires a host with a persistent cache, because the large database is tens of gigabytes.",
          "allowed_target_classes": [
            "persistent-cache"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/AlignedSequences"
        }
      },
      "JobAccepted": {
        "type": "object",
        "description": "The 202 body. Poll `status_url` from here.",
        "required": [
          "job_id",
          "status",
          "status_url",
          "target_id"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/JobState"
          },
          "status_url": {
            "type": "string",
            "description": "Path to poll, relative to the API root."
          },
          "target_id": {
            "type": "string",
            "description": "The compute target the job was dispatched to."
          }
        }
      },
      "JobCompleteRequest": {
        "type": "object",
        "required": [
          "lease_id"
        ],
        "properties": {
          "lease_id": {
            "type": "string"
          }
        }
      },
      "JobCompleteResponse": {
        "type": "object",
        "required": [
          "released"
        ],
        "properties": {
          "released": {
            "type": "boolean",
            "description": "False when the lease was already gone \u2014 not an error, just a statement\nthat this call was not the one that released it.\n"
          }
        }
      },
      "JobFile": {
        "type": "object",
        "required": [
          "path",
          "size_bytes"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Relative to the job folder root, so `results/\u2026` is included."
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "JobFileContent": {
        "type": "object",
        "required": [
          "path",
          "size_bytes"
        ],
        "description": "Either `content` or `download_url` is present, never both. Small UTF-8 text\ncomes back inline; anything larger than the inline cap, and anything that is\nnot valid UTF-8, comes back as a presigned URL with `note` saying which of\nthe two reasons applied.\n",
        "properties": {
          "path": {
            "type": "string",
            "description": "Echoes the requested path, relative to the job folder root."
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0
          },
          "content_type": {
            "type": "string",
            "description": "The stored Content-Type, when S3 recorded one."
          },
          "content": {
            "type": "string",
            "description": "The file decoded as UTF-8. Present only for small text files."
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Short-lived presigned GET. Fetch it directly and **without** an\nAuthorization header \u2014 the signature is in the URL, and an extra auth\nheader makes S3 reject it.\n"
          },
          "note": {
            "type": "string",
            "description": "Why the content was not inlined. Present whenever it was not."
          }
        }
      },
      "JobFileListing": {
        "type": "object",
        "required": [
          "job_id",
          "files"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobFile"
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the job holds more files than one page can list."
          }
        }
      },
      "JobFileRequest": {
        "type": "object",
        "required": [
          "path",
          "owner"
        ],
        "properties": {
          "path": {
            "$ref": "#/components/schemas/JobRelativePath"
          },
          "owner": {
            "$ref": "#/components/schemas/JobOwner"
          }
        }
      },
      "JobInput": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextInput"
          },
          {
            "$ref": "#/components/schemas/GlobalDataInput"
          },
          {
            "$ref": "#/components/schemas/UserDataInput"
          },
          {
            "$ref": "#/components/schemas/UploadInput"
          },
          {
            "$ref": "#/components/schemas/PriorJobInput"
          }
        ],
        "discriminator": {
          "propertyName": "source",
          "mapping": {
            "text": "#/components/schemas/TextInput",
            "global-data": "#/components/schemas/GlobalDataInput",
            "user-data": "#/components/schemas/UserDataInput",
            "upload": "#/components/schemas/UploadInput",
            "prior-job": "#/components/schemas/PriorJobInput"
          }
        }
      },
      "JobListRequest": {
        "type": "object",
        "required": [
          "prefix",
          "owner"
        ],
        "properties": {
          "prefix": {
            "type": "string",
            "description": "A job-relative prefix. Subject to the same scoping as a path.",
            "example": "results/"
          },
          "owner": {
            "$ref": "#/components/schemas/JobOwner"
          }
        }
      },
      "JobListing": {
        "type": "object",
        "required": [
          "jobs"
        ],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Total jobs available, for paging. May exceed the page returned."
          }
        }
      },
      "JobOwner": {
        "type": "string",
        "description": "The `user_sub` of the job's submitter, as carried in the dispatch envelope.\n\nAddressing input, not authorization: it says which record to read, and every\nfact that decides access is then read out of that record. Naming another\nuser reaches a job whose target is not this host, which is refused like\nanything else. It is sent rather than looked up so the server addresses the\njob directly instead of scanning an organization's whole history, which\ngrows without bound now that job data is retained.\n"
      },
      "JobRelativePath": {
        "type": "string",
        "description": "A path inside the job, relative to its own folder. Never absolute, and never\nescaping the job \u2014 `..` that would leave it is refused rather than clamped.\n",
        "example": "results/result.tsv"
      },
      "JobState": {
        "type": "string",
        "description": "`queued` is written by the dispatcher at submit. `running` and the terminal\nstates are written by the supervisor \u2014 the on-prem daemon or the container\nLambda shim \u2014 never by the job itself, because a container that is killed\ncannot report its own death.\n\n`cancelling` is the one state the *API* writes after submit, and it is a\nrequest rather than an outcome: `cancelJob` records that the caller wants\nthe job stopped, and the supervisor is what acts on it. The portal cannot\nstop a job itself \u2014 a queued job is an SQS message it holds no receipt\nhandle for, and a running job is a container on a host it cannot reach.\n\n`cancelled` is terminal and is written by the supervisor once it has\nactually abandoned or killed the job.\n\nA client must therefore treat `cancelling` as non-terminal and must not\nassume it leads to `cancelled`: a job that finishes before the supervisor\nnotices the request ends `done`, which is the honest outcome.\n",
        "enum": [
          "queued",
          "running",
          "cancelling",
          "cancelled",
          "done",
          "failed"
        ],
        "x-nm-terminal": [
          "done",
          "failed",
          "cancelled"
        ]
      },
      "JobStatus": {
        "description": "One job's full status.\n\n`additionalProperties: true` is deliberate: this endpoint returns the stored\nstatus document, whose shape is whatever the dispatcher wrote and the\nsupervisor merged. The fields named here are the ones clients may rely on.\nNarrowing it would break the `extras` chaining path the frontend reads.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/JobSummary"
          },
          {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "error": {
                "type": "string",
                "description": "Present on `failed`. Human-readable."
              },
              "download_url": {
                "type": "string",
                "format": "uri",
                "description": "Short-lived presigned GET for the results archive. Finished jobs only."
              },
              "download_filename": {
                "type": "string",
                "description": "Suggested filename for the archive."
              },
              "results_prefix": {
                "type": "string",
                "description": "Browsable results tree, relative to the outputs bucket."
              },
              "extras": {
                "type": "object",
                "additionalProperties": true,
                "description": "Tool-specific bag, nested so it can never collide with standard fields.\nJob chaining records its source job here as `prior_job_id`.\n"
              }
            }
          }
        ]
      },
      "JobStatusRequest": {
        "type": "object",
        "required": [
          "owner",
          "status_patch"
        ],
        "properties": {
          "owner": {
            "$ref": "#/components/schemas/JobOwner"
          },
          "status_patch": {
            "type": "object",
            "additionalProperties": true,
            "description": "Fields to merge into the job's record. Only `status`, `step`, `error`,\n`started_at`, `finished_at` and `progress` are taken; anything else is\nignored rather than rejected, so a daemon sending extra context does not\nfail over it.\n"
          }
        }
      },
      "JobStatusResponse": {
        "type": "object",
        "required": [
          "job_id",
          "applied"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "applied": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which fields were actually written."
          }
        }
      },
      "JobSummary": {
        "type": "object",
        "description": "One row of a job listing.",
        "required": [
          "job_id",
          "status"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "job_name": {
            "type": "string"
          },
          "tool_id": {
            "type": "string"
          },
          "target_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/JobState"
          },
          "step": {
            "type": "string",
            "description": "Free-text progress label from the supervisor, e.g. a pipeline stage name.\nAbsent for jobs whose supervisor does not report one.\n"
          },
          "submitted_at": {
            "type": "number",
            "description": "Epoch seconds, fractional."
          },
          "finished_at": {
            "type": [
              "number",
              "null"
            ],
            "description": "Epoch seconds. Null while the job has not reached a terminal state."
          },
          "submitted_by_email": {
            "type": "string"
          },
          "result_size_bytes": {
            "type": "integer",
            "description": "Size of the downloadable archive. Present on finished jobs only."
          },
          "output_extension": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "JobUploadRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/JobFileRequest"
          },
          {
            "type": "object",
            "required": [
              "size_bytes"
            ],
            "properties": {
              "size_bytes": {
                "type": "integer",
                "minimum": 0,
                "description": "The exact size of the body to be uploaded. Signed into the returned\npolicy, so S3 rejects anything outside it.\n",
                "example": 51234
              }
            }
          }
        ]
      },
      "LeaseResponse": {
        "type": "object",
        "required": [
          "job"
        ],
        "properties": {
          "job": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "The dispatch envelope \u2014 the same one every daemon-backed host receives,\nwhich is what lets one daemon serve both kinds of machine. Null when\nthere is nothing queued.\n"
          },
          "lease_id": {
            "type": "string",
            "description": "Opaque. Present only when a job is. Pass it to `complete` when done; the\nqueue receipt it stands for never leaves the server.\n"
          }
        }
      },
      "ListResponse": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "path",
                "size_bytes",
                "url"
              ],
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Relative to the requested prefix."
                },
                "size_bytes": {
                  "type": "integer"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          }
        }
      },
      "MyscaCoreParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input_dir"
        ],
        "properties": {
          "input_dir": {
            "$ref": "#/components/schemas/SCAPreprocessingResults",
            "title": "Prior Preprocess job",
            "description": "A directory of Preprocess output: a completed Preprocess job (prior-job, by name or id), a saved My Data dataset (user-data, by name/file_id), or an arbitrary My Data path (user-data, path)."
          }
        }
      },
      "MyscaCoreSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/MyscaCoreParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "mysca-core",
          "family": "mysca",
          "name": "Mysca Core \u2014 eigendecomp + ICA + sector assignment",
          "description": "Runs sca-core on the output of a prior Preprocess job (covariance matrix, eigendecomp + bootstrap, ICA, sector assignment).",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": true,
            "applies_to_step": "core",
            "blacklist": [
              "--use_jax",
              "--load_data",
              "--background",
              "-i",
              "-o",
              "--indir",
              "--outdir"
            ]
          }
        }
      },
      "MyscaPipelineParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "fasta"
        ],
        "properties": {
          "fasta": {
            "$ref": "#/components/schemas/AlignedSequences",
            "title": "Aligned MSA (FASTA)",
            "description": "Aligned multiple sequence alignment in FASTA format."
          },
          "reference": {
            "type": "string",
            "title": "Reference sequence ID",
            "maxLength": 200,
            "description": "ID of the anchor sequence in the MSA. Optional. Forwarded to sca-preprocess as --reference."
          }
        }
      },
      "MyscaPipelineSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/MyscaPipelineParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "mysca-pipeline",
          "family": "mysca",
          "name": "Mysca Pipeline \u2014 preprocess + core SCA",
          "description": "Runs sca-preprocess \u2192 sca-core on an aligned MSA. Skips prealign; use the standalone Prealign tool if your input is unaligned.",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": true,
            "applies_to_step": "core",
            "blacklist": [
              "--use_jax",
              "--weight_method",
              "--load_data",
              "--background",
              "-i",
              "-o",
              "--input_fpath",
              "--outdir",
              "--indir",
              "--msa_fpath"
            ]
          }
        }
      },
      "MyscaPrealignParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "fasta"
        ],
        "properties": {
          "fasta": {
            "$ref": "#/components/schemas/ProteinSequences",
            "title": "Raw FASTA (unaligned)",
            "description": "Unaligned protein sequences in FASTA format."
          }
        }
      },
      "MyscaPrealignSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/MyscaPrealignParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "mysca-prealign",
          "family": "mysca",
          "name": "Mysca Prealign \u2014 align raw FASTA",
          "description": "Runs sca-prealign on a raw (unaligned) FASTA and emits an aligned MSA ready for Preprocess.",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": true,
            "applies_to_step": "prealign",
            "blacklist": [
              "--align_bin",
              "--cluster_bin",
              "--align_extra",
              "--align_args",
              "-i",
              "-o",
              "--input_fpath",
              "--outdir"
            ]
          },
          "produces": "../kinds.yaml#/AlignedSequences"
        }
      },
      "MyscaPreprocessParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "fasta"
        ],
        "properties": {
          "fasta": {
            "$ref": "#/components/schemas/AlignedSequences",
            "title": "Aligned MSA (FASTA)",
            "description": "Aligned multiple sequence alignment in FASTA format."
          },
          "reference": {
            "type": "string",
            "title": "Reference sequence ID",
            "maxLength": 200,
            "description": "ID of the anchor sequence in the MSA. Optional. Forwarded as --reference."
          }
        }
      },
      "MyscaPreprocessSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/MyscaPreprocessParams"
              },
              "extra_args": {
                "type": "string",
                "description": "Extra CLI flags appended verbatim to the container command, shlex-split. Flags on this tool's denylist, and any that shadow a parameter above, are refused. Prefer a named parameter: every legitimate use of this field is one that is missing."
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "mysca-preprocess",
          "family": "mysca",
          "name": "Mysca Preprocess \u2014 filter aligned MSA",
          "description": "Runs sca-preprocess on an aligned MSA: gap/similarity filtering + sequence weighting. Output feeds into Core SCA.",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": true,
            "applies_to_step": "preprocess",
            "blacklist": [
              "--use_jax",
              "--weight_method",
              "--load_data",
              "--background",
              "-i",
              "-o",
              "--input_fpath",
              "--outdir",
              "--msa_fpath"
            ]
          },
          "produces": "../kinds.yaml#/SCAPreprocessingResults"
        }
      },
      "NotAMemberError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "organizations"
            ],
            "properties": {
              "organizations": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The organization ids the caller *is* a member of, so a client can\ncorrect the request without a second round trip.\n",
                "example": [
                  "natural-machine"
                ]
              }
            }
          }
        ]
      },
      "Organization": {
        "type": "object",
        "required": [
          "org_id",
          "display_name",
          "is_active"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "description": "Stable slug, `^[a-z0-9-]{2,32}$`. This is the value that appears in\nstorage paths, so it is the organization's identity and never changes.\n",
            "example": "natural-machine"
          },
          "display_name": {
            "type": "string",
            "description": "Human-readable name for display. Falls back to `org_id` where an\norganization record has no name set, so this is always populated.\n",
            "example": "Natural Machine"
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether this is the organization the caller is currently acting under.\nExactly one entry is true whenever the list is non-empty.\n"
          },
          "role": {
            "type": "string",
            "enum": [
              "member",
              "admin"
            ],
            "description": "The caller's role *in this organization*. Someone may administer one and\nbe an ordinary member of another, so it belongs on the entry rather than\non the caller. A membership row with no role recorded reads as `member`:\nabsence must not confer administration by omission.\n"
          }
        }
      },
      "OrganizationList": {
        "type": "object",
        "required": [
          "organizations",
          "active_org_id"
        ],
        "properties": {
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organization"
            },
            "description": "Every organization the caller belongs to. Empty for a user who has not\nbeen provisioned yet \u2014 an empty list, not an error.\n"
          },
          "active_org_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The organization in effect. Null only when `organizations` is empty.\nThis is the one to render.\n",
            "example": "natural-machine"
          },
          "stored_org_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The caller's last explicit choice, before it is reconciled against their\ncurrent memberships. Differs from `active_org_id` when the preference is\nunset (null) or names an organization they have since left. Exposed so a\nsettings page can explain the discrepancy rather than silently showing a\ndifferent organization as active.\n"
          },
          "is_admin_of_active": {
            "type": "boolean",
            "description": "Whether the caller administers the organization they are *acting under*\n\u2014 which is the question an admin surface actually asks, rather than\nwhether they administer something somewhere. Convenience: it is derivable\nfrom `organizations` and `active_org_id`, and is returned so a client\nneed not re-derive it.\n"
          }
        }
      },
      "PresignedUpload": {
        "type": "object",
        "description": "A presigned S3 POST. The client submits a multipart form to `upload_url`\ncarrying every key/value in `upload_fields` alongside the file part.\n\nThe declared size is pinned into the policy, so a signed URL for a small file\ncannot be reused to store a large one.\n",
        "required": [
          "upload_url",
          "upload_fields",
          "upload_method",
          "expires_in_seconds"
        ],
        "properties": {
          "upload_url": {
            "type": "string",
            "format": "uri"
          },
          "upload_fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Form fields S3 requires. Opaque to the client; send them verbatim."
          },
          "upload_method": {
            "type": "string",
            "const": "POST"
          },
          "expires_in_seconds": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "PriorJobInput": {
        "type": "object",
        "description": "The output of one of the caller's earlier jobs. Addressed by id, or by name \u2014\nin which case the newest completed match wins. Which upstream tools are\nacceptable is stated per-argument by the tool schema.\n\nNaming the job rather than restating what it produced is the point: the file\nis copied server-side, so chaining never depends on anything reproducing the\nbytes. That matters most for sequences, where a single altered residue is a\ndifferent protein and nothing about it looks wrong.\n",
        "required": [
          "source"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "prior-job"
          },
          "job_id": {
            "type": "string"
          },
          "job_name": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "Which file inside that job's results, relative to its results root, for\nan argument that takes a single file. Optional: each tool defaults to\nwhere the runs it chains from leave their output, so the common case\nnames only the job. Ignored by directory-shaped arguments, which take\nthe whole tree. Path traversal is rejected.\n"
          }
        },
        "anyOf": [
          {
            "required": [
              "job_id"
            ]
          },
          {
            "required": [
              "job_name"
            ]
          }
        ]
      },
      "ProjectPcaParams": {
        "type": "object",
        "additionalProperties": false,
        "oneOf": [
          {
            "title": "Project a submitted matrix",
            "required": [
              "matrix"
            ],
            "not": {
              "required": [
                "overlay"
              ]
            }
          },
          {
            "title": "Project a prior BioM3 job's latents",
            "required": [
              "embeddings"
            ]
          },
          {
            "title": "Project several sets in one fit",
            "required": [
              "sets"
            ]
          }
        ],
        "properties": {
          "matrix": {
            "$ref": "#/components/schemas/FeatureMatrix",
            "title": "Feature matrix",
            "description": "The points to project, as delimited text with a header row or a NumPy .npy array. Every column except the label column is read as a number; a row with a non-numeric value in a feature column is rejected rather than silently coerced."
          },
          "label_column": {
            "type": "string",
            "title": "Label column",
            "maxLength": 255,
            "description": "Name of the column holding each row's label \u2014 an identifier, not a feature. Excluded from the numbers and carried through to the output so a plotted point can be named. Delimited input only. Defaults to the first column when it is non-numeric."
          },
          "embeddings": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "BioM3 embeddings to project",
            "description": "BioM3 latents to project \u2014 either a completed embedding job of your own, named rather than given by id, or a registered embedding bundle this deployment publishes. Row labels come from the accessions or prompts stored beside the latents, so points stay identifiable without a label column."
          },
          "overlay": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "Sequences to overlay on the baseline",
            "description": "A second set of BioM3 latents, projected in the same fit as `embeddings` so the two are comparable. Usually a freshly embedded set of generated sequences, laid over the corpus its decoder was finetuned on. Every point here is projected \u2014 this set is never deduplicated and never subsampled, because it is the set being asked about and dropping any of it is the one thing that would make the picture misleading. Must carry the same `latent` as the baseline, encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Requires `embeddings`; there is nothing to overlay a submitted matrix on."
          },
          "sets": {
            "type": "array",
            "title": "Sets to project together",
            "minItems": 1,
            "maxItems": 8,
            "description": "Several sets of BioM3 latents, all projected in one fit so that every point in the picture is comparable to every other. The general form of `embeddings` + `overlay`, and the one to write against: a corpus, a second corpus, and the sequences a decoder generated are three sets rather than a baseline and an exception to it. Each set carries its own settings, because they are genuinely different questions \u2014 a 180,000-row corpus wants deduplicating and may want subsampling, while a set of 40 generated sequences wants neither and would be erased by the second. Every set must carry the same `latent` and be encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Mutually exclusive with `embeddings` and with `matrix`.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "embeddings"
              ],
              "properties": {
                "embeddings": {
                  "$ref": "#/components/schemas/BioM3Embeddings",
                  "title": "This set's latents",
                  "description": "Where this set's latents come from \u2014 a registered bundle this deployment publishes, or a completed embedding job of your own."
                },
                "label": {
                  "type": "string",
                  "title": "What to call this set",
                  "maxLength": 64,
                  "description": "The name this set is given in `annotations.tsv` and in anything drawn from it. Defaults to the bundle id or the job id, which is what a reader can act on; supply one when several sets would otherwise be told apart only by an id."
                },
                "dedup": {
                  "type": "boolean",
                  "default": true,
                  "title": "Merge identical points in this set",
                  "description": "Project each distinct point of this set once. Per set rather than per run: a corpus carries the same sequence many times over and their latents are byte-identical, so projecting every row lays coincident copies and inflates the local density UMAP and t-SNE both read. A generated set has no such duplication to merge, and merging across it would collapse two prompts that happened to write the same sequence \u2014 which is a result worth seeing, not a duplicate."
                },
                "max_points": {
                  "type": "integer",
                  "minimum": 2,
                  "title": "Maximum points from this set",
                  "description": "Project a random subset of this many of the set's points, drawn after its duplicates are merged. Left unset, all of them. Per set for the reason the whole parameter is: one ceiling across a union would spend most of itself on the largest corpus and leave the small set it is being compared against under-represented or gone."
                }
              }
            }
          },
          "latent": {
            "type": "string",
            "title": "Which latent",
            "enum": [
              "z_c",
              "z_p",
              "z_t"
            ],
            "default": "z_c",
            "description": "Which latent to project, for a prior BioM3 job. z_c is the facilitated embedding generation conditions on and is the usual choice; z_p is the sequence latent and z_t the text latent. Ignored when a matrix is submitted instead. A job that did not run the stage producing the named latent is rejected, with the latents it does carry \u2014 and when an overlay is given, both sets are checked, because a merge is only meaningful in one space. z_p is the usual choice for an overlay of generated sequences: it is the only latent a sequence-only embedding run produces, and a generated set's z_c would be the latent it was CONDITIONED on rather than its own."
          },
          "n_components": {
            "type": "integer",
            "title": "Number of components",
            "default": 2,
            "minimum": 1,
            "description": "How many dimensions to project down to.",
            "maximum": 200
          },
          "standardize": {
            "type": "boolean",
            "title": "Standardize features",
            "default": false,
            "description": "Center each feature and scale it to unit variance before projecting. Use it when features are on different scales; leave it off for embeddings, whose dimensions are already comparable."
          },
          "dedup": {
            "type": "boolean",
            "title": "Merge identical points",
            "default": true,
            "description": "Project each distinct point once. A corpus usually carries the same sequence many times over \u2014 the SH3 corpus has 59,893 distinct sequences across 179,679 rows \u2014 and their latents are byte-identical, so projecting every row lays several coincident copies of the same point. That is not only wasted work: UMAP and t-SNE read local density, so duplicates inflate it and change the picture. Rows are matched exactly, not approximately. Every original row is still reported, through `row_map.tsv`, so nothing about the input's indexing is lost. Turn it off only to reproduce an older run."
          },
          "max_points": {
            "type": "integer",
            "title": "Maximum points to project",
            "minimum": 2,
            "description": "Project a random subset of this many points, drawn after duplicates are merged so the number counts distinct points rather than rows. Seeded by `seed`, so a run is reproducible. Left unset, every distinct point is projected. Use it to get a fast look at a large corpus, or to bring one under a tool's row cap. Rows whose point was not drawn are still listed in `row_map.tsv`, without coordinates. Applies to the baseline only: an overlay is projected whole, so the number of points fitted is this many plus the size of the overlay."
          },
          "whiten": {
            "type": "boolean",
            "title": "Whiten components",
            "default": false,
            "description": "Scale each component to unit variance. Makes the components comparable in magnitude, which some downstream models want, and destroys the relative scale a plot reads as spread."
          }
        }
      },
      "ProjectPcaSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/ProjectPcaParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "project-pca",
          "family": "project",
          "name": "Projection \u2014 PCA",
          "description": "Project points onto their principal components: the orthogonal directions of greatest variance. Takes a numeric matrix or a completed BioM3 embedding job and returns the projected coordinates plus how much variance each component explains. Linear, deterministic, and the one projection here whose axes mean something on their own \u2014 a component is a direction in the input space, so distances and directions in the output are faithful to the input. Start here before reaching for t-SNE or UMAP: if two groups separate under PCA they are genuinely far apart, and the explained-variance figures say how much of the data the picture accounts for. Also the standard preprocessing step before a neighbour-based projection of very high-dimensional data.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_rows": 200000,
            "max_features": 10000
          },
          "produces": "../kinds.yaml#/FeatureMatrix"
        }
      },
      "ProjectTsneParams": {
        "type": "object",
        "additionalProperties": false,
        "oneOf": [
          {
            "title": "Project a submitted matrix",
            "required": [
              "matrix"
            ],
            "not": {
              "required": [
                "overlay"
              ]
            }
          },
          {
            "title": "Project a prior BioM3 job's latents",
            "required": [
              "embeddings"
            ]
          },
          {
            "title": "Project several sets in one fit",
            "required": [
              "sets"
            ]
          }
        ],
        "properties": {
          "matrix": {
            "$ref": "#/components/schemas/FeatureMatrix",
            "title": "Feature matrix",
            "description": "The points to project, as delimited text with a header row or a NumPy .npy array. Every column except the label column is read as a number; a row with a non-numeric value in a feature column is rejected rather than silently coerced."
          },
          "label_column": {
            "type": "string",
            "title": "Label column",
            "maxLength": 255,
            "description": "Name of the column holding each row's label \u2014 an identifier, not a feature. Excluded from the numbers and carried through to the output so a plotted point can be named. Delimited input only. Defaults to the first column when it is non-numeric."
          },
          "embeddings": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "BioM3 embeddings to project",
            "description": "BioM3 latents to project \u2014 either a completed embedding job of your own, named rather than given by id, or a registered embedding bundle this deployment publishes. Row labels come from the accessions or prompts stored beside the latents, so points stay identifiable without a label column."
          },
          "overlay": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "Sequences to overlay on the baseline",
            "description": "A second set of BioM3 latents, projected in the same fit as `embeddings` so the two are comparable. Usually a freshly embedded set of generated sequences, laid over the corpus its decoder was finetuned on. Every point here is projected \u2014 this set is never deduplicated and never subsampled, because it is the set being asked about and dropping any of it is the one thing that would make the picture misleading. Must carry the same `latent` as the baseline, encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Requires `embeddings`; there is nothing to overlay a submitted matrix on."
          },
          "sets": {
            "type": "array",
            "title": "Sets to project together",
            "minItems": 1,
            "maxItems": 8,
            "description": "Several sets of BioM3 latents, all projected in one fit so that every point in the picture is comparable to every other. The general form of `embeddings` + `overlay`, and the one to write against: a corpus, a second corpus, and the sequences a decoder generated are three sets rather than a baseline and an exception to it. Each set carries its own settings, because they are genuinely different questions \u2014 a 180,000-row corpus wants deduplicating and may want subsampling, while a set of 40 generated sequences wants neither and would be erased by the second. Every set must carry the same `latent` and be encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Mutually exclusive with `embeddings` and with `matrix`.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "embeddings"
              ],
              "properties": {
                "embeddings": {
                  "$ref": "#/components/schemas/BioM3Embeddings",
                  "title": "This set's latents",
                  "description": "Where this set's latents come from \u2014 a registered bundle this deployment publishes, or a completed embedding job of your own."
                },
                "label": {
                  "type": "string",
                  "title": "What to call this set",
                  "maxLength": 64,
                  "description": "The name this set is given in `annotations.tsv` and in anything drawn from it. Defaults to the bundle id or the job id, which is what a reader can act on; supply one when several sets would otherwise be told apart only by an id."
                },
                "dedup": {
                  "type": "boolean",
                  "default": true,
                  "title": "Merge identical points in this set",
                  "description": "Project each distinct point of this set once. Per set rather than per run: a corpus carries the same sequence many times over and their latents are byte-identical, so projecting every row lays coincident copies and inflates the local density UMAP and t-SNE both read. A generated set has no such duplication to merge, and merging across it would collapse two prompts that happened to write the same sequence \u2014 which is a result worth seeing, not a duplicate."
                },
                "max_points": {
                  "type": "integer",
                  "minimum": 2,
                  "title": "Maximum points from this set",
                  "description": "Project a random subset of this many of the set's points, drawn after its duplicates are merged. Left unset, all of them. Per set for the reason the whole parameter is: one ceiling across a union would spend most of itself on the largest corpus and leave the small set it is being compared against under-represented or gone."
                }
              }
            }
          },
          "latent": {
            "type": "string",
            "title": "Which latent",
            "enum": [
              "z_c",
              "z_p",
              "z_t"
            ],
            "default": "z_c",
            "description": "Which latent to project, for a prior BioM3 job. z_c is the facilitated embedding generation conditions on and is the usual choice; z_p is the sequence latent and z_t the text latent. Ignored when a matrix is submitted instead. A job that did not run the stage producing the named latent is rejected, with the latents it does carry \u2014 and when an overlay is given, both sets are checked, because a merge is only meaningful in one space. z_p is the usual choice for an overlay of generated sequences: it is the only latent a sequence-only embedding run produces, and a generated set's z_c would be the latent it was CONDITIONED on rather than its own."
          },
          "n_components": {
            "type": "integer",
            "title": "Number of components",
            "default": 2,
            "minimum": 1,
            "description": "How many dimensions to project down to.",
            "maximum": 3
          },
          "standardize": {
            "type": "boolean",
            "title": "Standardize features",
            "default": false,
            "description": "Center each feature and scale it to unit variance before projecting. Use it when features are on different scales; leave it off for embeddings, whose dimensions are already comparable."
          },
          "dedup": {
            "type": "boolean",
            "title": "Merge identical points",
            "default": true,
            "description": "Project each distinct point once. A corpus usually carries the same sequence many times over \u2014 the SH3 corpus has 59,893 distinct sequences across 179,679 rows \u2014 and their latents are byte-identical, so projecting every row lays several coincident copies of the same point. That is not only wasted work: UMAP and t-SNE read local density, so duplicates inflate it and change the picture. Rows are matched exactly, not approximately. Every original row is still reported, through `row_map.tsv`, so nothing about the input's indexing is lost. Turn it off only to reproduce an older run."
          },
          "max_points": {
            "type": "integer",
            "title": "Maximum points to project",
            "minimum": 2,
            "description": "Project a random subset of this many points, drawn after duplicates are merged so the number counts distinct points rather than rows. Seeded by `seed`, so a run is reproducible. Left unset, every distinct point is projected. Use it to get a fast look at a large corpus, or to bring one under a tool's row cap. Rows whose point was not drawn are still listed in `row_map.tsv`, without coordinates. Applies to the baseline only: an overlay is projected whole, so the number of points fitted is this many plus the size of the overlay."
          },
          "perplexity": {
            "type": "number",
            "title": "Perplexity",
            "default": 30,
            "minimum": 1,
            "maximum": 500,
            "description": "Roughly how many neighbours each point is fitted against \u2014 the knob that trades local detail for global shape. Low values (5-10) fragment the data into many small clusters; high values (50+) blur them together. Must be smaller than the number of rows; a run whose perplexity is too large for its input is rejected before it starts rather than failing inside the container."
          },
          "learning_rate": {
            "type": "number",
            "title": "Learning rate",
            "minimum": 1,
            "maximum": 10000,
            "description": "Step size for the optimizer. Left unset it is chosen from the number of rows, which is the right default for almost every run. Set far too low it leaves the points in one dense ball."
          },
          "seed": {
            "type": "integer",
            "title": "Random seed",
            "default": 0,
            "description": "This projection is stochastic, and two runs of the same data under different seeds give different pictures. Fixed at 0 by default so a result is reproducible; change it to check that a cluster you are reading is not an artifact of one initialization."
          }
        }
      },
      "ProjectTsneSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/ProjectTsneParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "project-tsne",
          "family": "project",
          "name": "Projection \u2014 t-SNE",
          "description": "Project points with t-SNE (t-distributed stochastic neighbour embedding): lay them out in two or three dimensions so that near neighbours in the input stay near in the picture. Takes a numeric matrix or a completed BioM3 embedding job. Better than PCA at revealing cluster structure that a linear projection flattens. Read the result carefully \u2014 the axes mean nothing, the distance BETWEEN clusters means nothing, and cluster sizes are not comparable; only which points sit together is informative. It is also stochastic, so vary the seed before believing a cluster. Slower than PCA and superlinear in the number of rows; run project-pca first when the input has thousands of features. For a layout that preserves more of the global arrangement, use project-umap.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_rows": 75000,
            "max_features": 10000
          },
          "produces": "../kinds.yaml#/FeatureMatrix"
        }
      },
      "ProjectUmapParams": {
        "type": "object",
        "additionalProperties": false,
        "oneOf": [
          {
            "title": "Project a submitted matrix",
            "required": [
              "matrix"
            ],
            "not": {
              "required": [
                "overlay"
              ]
            }
          },
          {
            "title": "Project a prior BioM3 job's latents",
            "required": [
              "embeddings"
            ]
          },
          {
            "title": "Project several sets in one fit",
            "required": [
              "sets"
            ]
          }
        ],
        "properties": {
          "matrix": {
            "$ref": "#/components/schemas/FeatureMatrix",
            "title": "Feature matrix",
            "description": "The points to project, as delimited text with a header row or a NumPy .npy array. Every column except the label column is read as a number; a row with a non-numeric value in a feature column is rejected rather than silently coerced."
          },
          "label_column": {
            "type": "string",
            "title": "Label column",
            "maxLength": 255,
            "description": "Name of the column holding each row's label \u2014 an identifier, not a feature. Excluded from the numbers and carried through to the output so a plotted point can be named. Delimited input only. Defaults to the first column when it is non-numeric."
          },
          "embeddings": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "BioM3 embeddings to project",
            "description": "BioM3 latents to project \u2014 either a completed embedding job of your own, named rather than given by id, or a registered embedding bundle this deployment publishes. Row labels come from the accessions or prompts stored beside the latents, so points stay identifiable without a label column."
          },
          "overlay": {
            "$ref": "#/components/schemas/BioM3Embeddings",
            "title": "Sequences to overlay on the baseline",
            "description": "A second set of BioM3 latents, projected in the same fit as `embeddings` so the two are comparable. Usually a freshly embedded set of generated sequences, laid over the corpus its decoder was finetuned on. Every point here is projected \u2014 this set is never deduplicated and never subsampled, because it is the set being asked about and dropping any of it is the one thing that would make the picture misleading. Must carry the same `latent` as the baseline, encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Requires `embeddings`; there is nothing to overlay a submitted matrix on."
          },
          "sets": {
            "type": "array",
            "title": "Sets to project together",
            "minItems": 1,
            "maxItems": 8,
            "description": "Several sets of BioM3 latents, all projected in one fit so that every point in the picture is comparable to every other. The general form of `embeddings` + `overlay`, and the one to write against: a corpus, a second corpus, and the sequences a decoder generated are three sets rather than a baseline and an exception to it. Each set carries its own settings, because they are genuinely different questions \u2014 a 180,000-row corpus wants deduplicating and may want subsampling, while a set of 40 generated sequences wants neither and would be erased by the second. Every set must carry the same `latent` and be encoded under the same weights bundle; latents from a different bundle are not comparable and a submission naming one is refused. Mutually exclusive with `embeddings` and with `matrix`.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "embeddings"
              ],
              "properties": {
                "embeddings": {
                  "$ref": "#/components/schemas/BioM3Embeddings",
                  "title": "This set's latents",
                  "description": "Where this set's latents come from \u2014 a registered bundle this deployment publishes, or a completed embedding job of your own."
                },
                "label": {
                  "type": "string",
                  "title": "What to call this set",
                  "maxLength": 64,
                  "description": "The name this set is given in `annotations.tsv` and in anything drawn from it. Defaults to the bundle id or the job id, which is what a reader can act on; supply one when several sets would otherwise be told apart only by an id."
                },
                "dedup": {
                  "type": "boolean",
                  "default": true,
                  "title": "Merge identical points in this set",
                  "description": "Project each distinct point of this set once. Per set rather than per run: a corpus carries the same sequence many times over and their latents are byte-identical, so projecting every row lays coincident copies and inflates the local density UMAP and t-SNE both read. A generated set has no such duplication to merge, and merging across it would collapse two prompts that happened to write the same sequence \u2014 which is a result worth seeing, not a duplicate."
                },
                "max_points": {
                  "type": "integer",
                  "minimum": 2,
                  "title": "Maximum points from this set",
                  "description": "Project a random subset of this many of the set's points, drawn after its duplicates are merged. Left unset, all of them. Per set for the reason the whole parameter is: one ceiling across a union would spend most of itself on the largest corpus and leave the small set it is being compared against under-represented or gone."
                }
              }
            }
          },
          "latent": {
            "type": "string",
            "title": "Which latent",
            "enum": [
              "z_c",
              "z_p",
              "z_t"
            ],
            "default": "z_c",
            "description": "Which latent to project, for a prior BioM3 job. z_c is the facilitated embedding generation conditions on and is the usual choice; z_p is the sequence latent and z_t the text latent. Ignored when a matrix is submitted instead. A job that did not run the stage producing the named latent is rejected, with the latents it does carry \u2014 and when an overlay is given, both sets are checked, because a merge is only meaningful in one space. z_p is the usual choice for an overlay of generated sequences: it is the only latent a sequence-only embedding run produces, and a generated set's z_c would be the latent it was CONDITIONED on rather than its own."
          },
          "n_components": {
            "type": "integer",
            "title": "Number of components",
            "default": 2,
            "minimum": 1,
            "description": "How many dimensions to project down to.",
            "maximum": 100
          },
          "standardize": {
            "type": "boolean",
            "title": "Standardize features",
            "default": false,
            "description": "Center each feature and scale it to unit variance before projecting. Use it when features are on different scales; leave it off for embeddings, whose dimensions are already comparable."
          },
          "dedup": {
            "type": "boolean",
            "title": "Merge identical points",
            "default": true,
            "description": "Project each distinct point once. A corpus usually carries the same sequence many times over \u2014 the SH3 corpus has 59,893 distinct sequences across 179,679 rows \u2014 and their latents are byte-identical, so projecting every row lays several coincident copies of the same point. That is not only wasted work: UMAP and t-SNE read local density, so duplicates inflate it and change the picture. Rows are matched exactly, not approximately. Every original row is still reported, through `row_map.tsv`, so nothing about the input's indexing is lost. Turn it off only to reproduce an older run."
          },
          "max_points": {
            "type": "integer",
            "title": "Maximum points to project",
            "minimum": 2,
            "description": "Project a random subset of this many points, drawn after duplicates are merged so the number counts distinct points rather than rows. Seeded by `seed`, so a run is reproducible. Left unset, every distinct point is projected. Use it to get a fast look at a large corpus, or to bring one under a tool's row cap. Rows whose point was not drawn are still listed in `row_map.tsv`, without coordinates. Applies to the baseline only: an overlay is projected whole, so the number of points fitted is this many plus the size of the overlay."
          },
          "n_neighbors": {
            "type": "integer",
            "title": "Neighbours",
            "default": 15,
            "minimum": 2,
            "maximum": 500,
            "description": "How many neighbours define each point's local neighbourhood \u2014 the local-versus-global knob. Small values (2-5) preserve fine local detail and fragment the layout; large values (50+) preserve the broad shape and merge small clusters. Must be smaller than the number of rows."
          },
          "min_dist": {
            "type": "number",
            "title": "Minimum distance",
            "default": 0.1,
            "minimum": 0,
            "maximum": 1,
            "description": "How tightly points may be packed together in the output. Near 0 gives dense clumps that show fine structure; larger values spread points out and preserve broader topology. Affects the picture only, never which points are neighbours."
          },
          "metric": {
            "type": "string",
            "title": "Distance metric",
            "enum": [
              "euclidean",
              "cosine",
              "correlation",
              "manhattan"
            ],
            "default": "euclidean",
            "description": "How distance between two rows is measured. Cosine ignores magnitude and compares direction only, which is usually what you want for embeddings; euclidean is the right default for features in real units."
          },
          "seed": {
            "type": "integer",
            "title": "Random seed",
            "default": 0,
            "description": "This projection is stochastic, and two runs of the same data under different seeds give different pictures. Fixed at 0 by default so a result is reproducible; change it to check that a cluster you are reading is not an artifact of one initialization."
          }
        }
      },
      "ProjectUmapSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/ProjectUmapParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "project-umap",
          "family": "project",
          "name": "Projection \u2014 UMAP",
          "description": "Project points with UMAP (uniform manifold approximation and projection): build a neighbour graph in the input space and lay it out in a few dimensions. Takes a numeric matrix or a completed BioM3 embedding job. Like t-SNE it reveals cluster structure, and unlike t-SNE it keeps more of the arrangement BETWEEN clusters, so the broad layout carries some meaning \u2014 though the axes still do not, and distances remain qualitative. Faster than t-SNE on large inputs and it scales past a few output dimensions, so it also works as a preprocessing step rather than only as a picture. Stochastic: vary the seed before believing a cluster.",
          "allowed_target_classes": [
            "cpu-daemon"
          ],
          "default_target_id": "cloud-cpu",
          "extra_args": {
            "enabled": false
          },
          "limits": {
            "max_rows": 200000,
            "max_features": 10000
          },
          "produces": "../kinds.yaml#/FeatureMatrix"
        }
      },
      "ProteinSequences": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "protein-sequences",
        "description": "Protein sequences, unaligned. Usually FASTA; some tools also accept one\nsequence per line. Format is stated per parameter where it matters \u2014 it is\nnot part of the kind, because the same sequences in a different encoding are\nstill the same sequences.\n"
      },
      "ProteinStructure": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "protein-structure",
        "description": "One protein structure, as PDB or mmCIF."
      },
      "ProteinStructures": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "protein-structures",
        "description": "A set of protein structures, as PDB or mmCIF. Whether that arrives as a\nfolder, an archive or a multi-model file is not stated: it is the server's\nproblem, and a caller should not have to know.\n"
      },
      "ProteoScribeFinetuningDataset": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "proteoscribe-finetuning-dataset",
        "description": "A prepared dataset for finetuning ProteoScribe: records carrying the fields\nthe configured record schema reads.\n"
      },
      "Quota": {
        "type": "object",
        "required": [
          "used_bytes",
          "max_bytes"
        ],
        "properties": {
          "used_bytes": {
            "type": "integer",
            "minimum": 0
          },
          "max_bytes": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "QuotaExceededError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "quota"
            ],
            "properties": {
              "quota": {
                "$ref": "#/components/schemas/Quota"
              }
            }
          }
        ]
      },
      "ReferenceDatabase": {
        "type": "object",
        "description": "One searchable or parseable reference artifact.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send as the tool's `database` argument."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "release": {
            "type": [
              "string",
              "null"
            ],
            "description": "Publisher's release identifier, e.g. a UniProt release."
          },
          "approx_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Rough staged size. Useful for warning that a search will be slow on a\ncold host; not exact, and not a quota.\n"
          }
        }
      },
      "RegisterComputeHostRequest": {
        "type": "object",
        "required": [
          "display_name"
        ],
        "properties": {
          "display_name": {
            "type": "string",
            "description": "How the machine is shown in the portal. For people, not routing.",
            "example": "Lab workstation"
          },
          "classes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the machine offers. Omit it and the host is registered offering\nnothing, which is the safe default \u2014 it receives no work until someone\nsays what it can do.\n",
            "example": [
              "gpu-daemon"
            ]
          }
        }
      },
      "RegisterComputeHostResponse": {
        "type": "object",
        "required": [
          "host_id",
          "enrollment_code"
        ],
        "properties": {
          "host_id": {
            "type": "string"
          },
          "enrollment_code": {
            "type": "string",
            "description": "Single-use, expires within a day, and shown once \u2014 only its hash is\nstored. Losing it means registering the machine again.\n",
            "example": "NMC-7KQ4H-8XR2V-B19TC"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "RegisteredWeights": {
        "type": "object",
        "description": "One curated weight the deployment offers for a single pipeline stage, named\nby a weights override with `source: registry`.\n",
        "required": [
          "id",
          "name",
          "base_bundle"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send as a registry override's `id`."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "base_bundle": {
            "type": "string",
            "description": "The `WeightsBundle.tag` this weight substitutes into. A submission whose\n`weights_bundle` differs is refused \u2014 the stages that were not overridden\nwould run against weights this one was never trained beside.\n"
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          }
        }
      },
      "RegistryTokenRequest": {
        "type": "object",
        "properties": {
          "image": {
            "type": "string",
            "description": "The image about to be pulled. The portal answers whether it needs a\ncredential at all \u2014 a public image needs none \u2014 so the host does not\nhave to recognise registries itself. Omitted, credentials are returned\nunconditionally, which is what older hosts expect.\n",
            "example": "123456789012.dkr.ecr.us-east-2.amazonaws.com/sbm:latest"
          }
        }
      },
      "RegistryTokenResponse": {
        "type": "object",
        "required": [
          "registry",
          "username",
          "password",
          "expires_at"
        ],
        "properties": {
          "registry": {
            "type": "string",
            "description": "Registry endpoint to authenticate against.",
            "example": "https://123456789012.dkr.ecr.us-east-2.amazonaws.com"
          },
          "username": {
            "type": "string",
            "description": "Always the registry's fixed principal name rather than anything\nidentifying this host. The password is the credential.\n",
            "example": "AWS"
          },
          "password": {
            "type": "string",
            "description": "Secret. Pass on stdin to `docker login --password-stdin`; never as an\nargument, where it lands in the process list and the shell history.\n"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this credential stops working."
          },
          "credentials": {
            "type": "null",
            "description": "Present and null when the named image needs no credential. The other\nfields are then absent: there is nothing to log in with, and nothing\nshould be.\n"
          }
        }
      },
      "ResultFile": {
        "type": "object",
        "required": [
          "path",
          "size_bytes"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Relative to the job's results/ tree."
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ResultListing": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultFile"
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the job produced more files than one page can list."
          }
        }
      },
      "RevokedComputeHost": {
        "type": "object",
        "required": [
          "host_id",
          "status"
        ],
        "properties": {
          "host_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "revoked"
            ]
          }
        }
      },
      "SBMModel": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "sbm-model",
        "description": "A trained selective-blocking model, ready to sample from."
      },
      "SCAPreprocessingResults": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "sca-preprocessing-results",
        "description": "The output of SCA preprocessing: the filtered alignment, sequence weights\nand derived matrices that eigendecomposition reads.\n"
      },
      "SaveJobToUserDataRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Display name. Defaults to the job's own name."
          }
        }
      },
      "SaveJobToUserDataResponse": {
        "type": "object",
        "required": [
          "file_id",
          "size_bytes",
          "object_count"
        ],
        "properties": {
          "file_id": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0
          },
          "object_count": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "SbmParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "fasta"
        ],
        "properties": {
          "fasta": {
            "$ref": "#/components/schemas/AlignedSequences",
            "title": "Aligned MSA (FASTA)",
            "description": "Aligned multiple sequence alignment in FASTA format."
          },
          "Model": {
            "type": "string",
            "title": "Model",
            "default": "SBM",
            "maxLength": 10,
            "description": "SBM (Stochastic Boltzmann Machine) or BM (Boltzmann Machine)."
          },
          "N_iter": {
            "type": "number",
            "title": "Gradient descent iterations",
            "default": 5,
            "description": "Number of gradient-descent iterations. Production runs use ~400."
          },
          "N_chains": {
            "type": "number",
            "title": "MCMC chains",
            "default": 20,
            "description": "Number of parallel MCMC chains. Production runs use ~150."
          },
          "k_MCMC": {
            "type": "number",
            "title": "MCMC steps per iteration",
            "default": 1000,
            "description": "MCMC steps per gradient iteration. Production runs use 100000."
          },
          "m": {
            "type": "number",
            "title": "Hessian rank",
            "default": 1,
            "description": "Hessian matrix rank for SBM. Ignored when Model=BM."
          },
          "theta": {
            "type": "number",
            "title": "Sequence-similarity threshold",
            "default": 0.3,
            "description": "Similarity threshold used in the effective-sequence-count weighting."
          },
          "lambda_h": {
            "type": "number",
            "title": "Fields regularization",
            "default": 0.0,
            "description": "L2 regularization on the fields h."
          },
          "lambda_J": {
            "type": "number",
            "title": "Couplings regularization",
            "default": 0.0,
            "description": "L2 regularization on the couplings J."
          },
          "Param_init": {
            "type": "string",
            "title": "Parameter initialization",
            "default": "Zero",
            "maxLength": 20,
            "description": "Initial values for fields and couplings: Zero, Profile, or Custom."
          }
        }
      },
      "SbmSampleParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "model"
        ],
        "properties": {
          "model": {
            "$ref": "#/components/schemas/SBMModel",
            "title": "Trained model (from a prior sbm-train job)",
            "description": "A completed sbm-train job whose .npy model to sample from."
          },
          "num_sequences": {
            "type": "number",
            "title": "Number of sequences",
            "default": 100,
            "description": "How many sequences to generate (one independent MCMC chain each)."
          },
          "temperature": {
            "type": "number",
            "title": "Sampling temperature",
            "default": 1.0,
            "description": "T>1 flattens the distribution (more diverse), T<1 sharpens it (more like-the-model). Default 1.0."
          },
          "delta_t": {
            "type": "number",
            "title": "MCMC steps per chain",
            "description": "MCMC sweeps per chain (burn-in). Blank = use the model's stored k_MCMC. Higher = better mixing."
          },
          "seed": {
            "type": "number",
            "title": "Initial-state seed (blank = random)",
            "description": "Seeds the NumPy initial-state RNG only. The C Metropolis kernel still seeds from wall-clock, so runs are not fully deterministic."
          }
        }
      },
      "SbmSampleSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/SbmSampleParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "sbm-sample",
          "family": "sbm",
          "name": "SBM \u2014 Sample (generate sequences)",
          "description": "Sample novel protein sequences from a trained SBM/Potts model at a chosen temperature, via the model's MCMC (Metropolis) sampler. Chains off a completed sbm-train job (referenced by name); output is a FASTA of generated sequences. Note: the underlying C sampler is not fully seedable (it seeds from wall-clock), so the seed here only fixes the initial-state RNG \u2014 runs are not bit-for-bit reproducible.",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/AlignedSequences"
        }
      },
      "SbmSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/SbmParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "sbm",
          "family": "sbm",
          "name": "SBM \u2014 Stochastic Boltzmann Machine",
          "description": "Infer fields and pairwise couplings from an MSA using a Stochastic Boltzmann Machine (MCMC + L-BFGS gradient descent). Defaults are tuned for quick first-light runs; production runs use much larger N_iter/N_chains/k_MCMC.",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/SBMModel"
        }
      },
      "SbmTrainParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "fasta"
        ],
        "properties": {
          "fasta": {
            "$ref": "#/components/schemas/AlignedSequences",
            "title": "Aligned MSA (FASTA)",
            "description": "Aligned multiple sequence alignment in FASTA format."
          },
          "Model": {
            "type": "string",
            "title": "Model",
            "default": "SBM",
            "maxLength": 10,
            "description": "SBM (Stochastic Boltzmann Machine) or BM (Boltzmann Machine)."
          },
          "N_iter": {
            "type": "number",
            "title": "Gradient descent iterations",
            "default": 5,
            "description": "Number of gradient-descent iterations. Production runs use ~400."
          },
          "N_chains": {
            "type": "number",
            "title": "MCMC chains",
            "default": 20,
            "description": "Number of parallel MCMC chains. Production runs use ~150."
          },
          "k_MCMC": {
            "type": "number",
            "title": "MCMC steps per iteration",
            "default": 1000,
            "description": "MCMC steps per gradient iteration. Production runs use 100000."
          },
          "theta": {
            "type": "number",
            "title": "Sequence-similarity threshold",
            "default": 0.3,
            "description": "Similarity threshold used in the effective-sequence-count weighting."
          },
          "Seed": {
            "type": "number",
            "title": "Random seed (blank = random)",
            "description": "RNG seed for reproducible training. Leave blank for a fresh random seed each run."
          },
          "m": {
            "type": "number",
            "title": "Hessian rank (SBM only)",
            "default": 1,
            "description": "Hessian memory rank for the SBM L-BFGS optimizer. Ignored when Model=BM."
          },
          "lambda_h": {
            "type": "number",
            "title": "Fields regularization",
            "default": 0.0,
            "description": "L2 regularization on the fields h."
          },
          "lambda_J": {
            "type": "number",
            "title": "Couplings regularization",
            "default": 0.0,
            "description": "L2 regularization on the couplings J."
          },
          "Param_init": {
            "type": "string",
            "title": "Parameter initialization",
            "default": "Zero",
            "maxLength": 20,
            "description": "Initial values for fields and couplings: Zero, Profile, Random, or Custom."
          }
        }
      },
      "SbmTrainSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/SbmTrainParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "sbm-train",
          "family": "sbm",
          "name": "SBM \u2014 Train (infer fields + couplings)",
          "description": "Train a Potts model: infer fields h and pairwise couplings J from an MSA using a Stochastic Boltzmann Machine (MCMC + L-BFGS gradient descent) or plain Boltzmann Machine. Output is a single pickled .npy model (h, J, options) that biom3/sbm-sample can sample from. Defaults are tuned for quick first-light runs; production runs use much larger N_iter/N_chains/k_MCMC. Single-purpose split of the legacy sbm tool (adds a Seed for reproducibility).",
          "allowed_target_classes": [
            "gpu-daemon",
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "enabled": false
          },
          "produces": "../kinds.yaml#/SBMModel"
        }
      },
      "SetActiveOrganizationRequest": {
        "type": "object",
        "required": [
          "org_id"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "description": "An organization the caller is a member of. Anything else is rejected \u2014\nthis endpoint sets a preference and cannot grant membership.\n",
            "example": "example-lab"
          }
        }
      },
      "StageRequest": {
        "type": "object",
        "required": [
          "name",
          "owner"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The asset's handle, as it appears in this job's spec \u2014 `configs`,\n`weights-stage3`. Not a path and not a URI.\n",
            "example": "weights-stage3"
          },
          "owner": {
            "$ref": "#/components/schemas/JobOwner"
          }
        }
      },
      "StageResponse": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "description": "Every object in the asset. Empty is a valid answer \u2014 an asset can\nlegitimately be empty, and that is not an error.\n",
            "items": {
              "type": "object",
              "required": [
                "path",
                "size",
                "url"
              ],
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Where to write this object, relative to the asset's destination.\n",
                  "example": "inference/stage1_PenCL.json"
                },
                "size": {
                  "type": "integer",
                  "description": "Bytes. A caller that already holds this path at this size holds\nthis object \u2014 these assets are immutable within a version \u2014 so\nthis is what lets a warm host skip the transfer instead of paying\nfor it again.\n"
                },
                "url": {
                  "type": "string",
                  "description": "Presigned, read-only, and long-lived enough for the largest object in the asset."
                }
              }
            }
          }
        }
      },
      "SubmitRequestBase": {
        "type": "object",
        "properties": {
          "job_name": {
            "type": "string",
            "description": "Display name. Sanitized server-side; a default is generated from the tool\nid and submission time when omitted. Names are not unique.\n"
          },
          "target_id": {
            "type": "string",
            "description": "Which compute target runs the job. Must be one of the tool's\n`allowed_targets` and granted to the caller. Defaults to the tool's\n`default_target_id`.\n\nThis is the one execution concept that is deliberately client-facing:\nthe user picks it. Everything else about how a job runs \u2014 the image, the\nCLI wiring, the staging plan \u2014 is backend-only and is not described here.\n"
          }
        }
      },
      "TabularRecords": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "tabular-records",
        "description": "A table of records, one row each. CSV or TSV."
      },
      "TensorArray": {
        "x-nm-kind": "tensor-array",
        "description": "Numeric arrays on disk: a NumPy `.npy`, or a `.pt`/`.npz` holding several\narrays under names \u2014 in which case the caller says which name to read.\n\nRows are vectors and nothing here interprets what they mean. A tool taking\nthis does arithmetic on numbers; whether those numbers are protein latents,\ndocument embeddings or anything else is the caller's business.\n",
        "oneOf": [
          {
            "$ref": "#/components/schemas/PriorJobInput"
          },
          {
            "$ref": "#/components/schemas/GlobalDataInput"
          },
          {
            "$ref": "#/components/schemas/UserDataInput"
          },
          {
            "$ref": "#/components/schemas/BioM3RegistryInput"
          }
        ]
      },
      "TensorScoreParams": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "matrix",
          "vectors"
        ],
        "oneOf": [
          {
            "title": "All pairs \u2014 every row of `matrix` against every row of `vectors`",
            "properties": {
              "mode": {
                "const": "all-pairs"
              }
            }
          },
          {
            "title": "Paired \u2014 row i against row i, one score per row",
            "required": [
              "mode"
            ],
            "properties": {
              "mode": {
                "const": "paired"
              }
            },
            "not": {
              "required": [
                "query"
              ]
            }
          }
        ],
        "properties": {
          "matrix": {
            "$ref": "#/components/schemas/TensorArray",
            "title": "Rows to score",
            "description": "The array whose rows are scored, one score per row. Also the reference distribution: when `query` is given, each of its rows is reported as a percentile among these scores, which is usually the number that means something \u2014 a similarity of 0.93 says nothing until you know what the rest of the set scores."
          },
          "vectors": {
            "$ref": "#/components/schemas/TensorArray",
            "title": "Vectors to score against",
            "description": "What the rows of `matrix` are scored against. In `all-pairs` mode, one or more vectors as rows: every row of `matrix` is scored against every one of them, each producing its own pair of output columns, and loading `matrix` is the expensive part of the job so several vectors belong in one submission rather than several. In `paired` mode, either one row per row of `matrix` \u2014 row i against row i \u2014 or a single row, which is scored against all of them. Must have the same width as `matrix` either way; a dot product between differently-shaped rows is refused rather than broadcast."
          },
          "query": {
            "$ref": "#/components/schemas/TensorArray",
            "title": "A second set of rows to place (optional)",
            "description": "Rows scored against the same vectors and reported with their percentile in `matrix`'s distribution. Written to a table of its own. Omit to score `matrix` alone. `all-pairs` only: a percentile answers \"where does this sit among the others scored against the same vector\", and in `paired` mode there is no shared vector to be among."
          },
          "mode": {
            "type": "string",
            "title": "How rows are paired",
            "default": "all-pairs",
            "enum": [
              "all-pairs",
              "paired"
            ],
            "description": "`all-pairs` scores every row of `matrix` against every row of `vectors`, writing one pair of columns per vector \u2014 the shape for placing a few designs against a corpus. `paired` scores row i against row i and writes one score per row, which is the only way to ask a question about N pre-matched couples: a latent against the caption it was conditioned on, across a whole corpus. The two differ in output shape, not just in arithmetic, which is why this is a mode rather than a flag."
          },
          "matrix_key": {
            "type": "string",
            "title": "Array name inside `matrix`",
            "maxLength": 255,
            "description": "Which array to read when the file holds several under names, as a `.pt` or `.npz` does. Ignored for a bare `.npy`. Required rather than guessed when the file has more than one array \u2014 picking the first would be a silent choice about what is being measured."
          },
          "vectors_key": {
            "type": "string",
            "title": "Array name inside `vectors`",
            "maxLength": 255,
            "description": "As `matrix_key`, for the `vectors` file."
          },
          "query_key": {
            "type": "string",
            "title": "Array name inside `query`",
            "maxLength": 255,
            "description": "As `matrix_key`, for the `query` file. Defaults to `matrix_key`, since the two sets have to be the same kind of thing to be comparable at all."
          },
          "label_key": {
            "type": "string",
            "title": "Array of row labels",
            "maxLength": 255,
            "description": "Optional array of one string per row, carried through to the output so a scored row can be identified. Absent, rows are named by their index."
          },
          "metric": {
            "type": "string",
            "title": "What to write",
            "default": "both",
            "enum": [
              "both",
              "cosine",
              "dot"
            ],
            "description": "`cosine` divides out each row's magnitude; `dot` does not. They are the same ranking only when the rows are unit-norm, and when they are not the difference can be total \u2014 on one real 179,679-row set the top hundred by each measure overlapped by under three percent. `both` is the default so that the choice can be made from the data rather than before seeing it."
          }
        }
      },
      "TensorScoreSubmitRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SubmitRequestBase"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "$ref": "#/components/schemas/TensorScoreParams"
              }
            }
          }
        ],
        "x-nm-tool": {
          "id": "tensor-score",
          "family": "tensor",
          "name": "Tensor \u2014 Cosine and dot product against reference vectors",
          "description": "Score the rows of an array against one or more vectors: cosine similarity, dot product, or both. Optionally score a second array the same way and report where each of its rows falls in the first array's distribution, as a percentile \u2014 which is what turns a raw similarity into a number that means something. Reads .npy, and .pt/.npz holding named arrays. Runs no model and needs no GPU; a 179,679 x 512 array scores in about four seconds.",
          "allowed_target_classes": [
            "lambda"
          ],
          "default_target_id": "lambda",
          "extra_args": {
            "comment": "no CLI to pass through; this runs numpy, not a program.",
            "enabled": false
          },
          "output_shape": {
            "description": "results/scores.tsv \u2014 one row per row of `matrix`, in its own order, carrying `row` and `label` when a label array was named. In `all-pairs` mode it then carries `cos_<n>`/`dot_<n>` per scored vector (whichever the `metric` selects), and with `query` a second table results/query_scores.tsv adds `pct_<n>`, the percentile of that row's score among the scores of `matrix`. In `paired` mode there is one score per row rather than one per vector, so the columns are `cos`/`dot` unsuffixed and there is no second table."
          },
          "produces": "../kinds.yaml#/TabularRecords"
        }
      },
      "TextCaptions": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "text-captions",
        "description": "Free-text captions or prompts, one per line."
      },
      "TextInput": {
        "type": "object",
        "description": "Content pasted or typed directly into the request.",
        "required": [
          "source",
          "content"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "text"
          },
          "content": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "TextSequencePairs": {
        "$ref": "#/components/schemas/DataRef",
        "x-nm-kind": "text-sequence-pairs",
        "description": "Protein sequences paired with text captions \u2014 the two columns BioM3's\nStage 1 conditions on together.\n"
      },
      "ToolCatalog": {
        "type": "object",
        "required": [
          "tools"
        ],
        "properties": {
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ToolDefinition"
            }
          }
        }
      },
      "ToolDefinition": {
        "type": "object",
        "description": "One tool, as a client needs it: what it is called, where it may run, and the\nschema of its parameters.\n\nWhat is deliberately absent is how it runs. The per-dispatcher tool JSON this\nis composed alongside carries `operation`, `stages`, `result_kind` and\n`input_builder` from the execution runbook; none of that reaches here, and a\nclient that finds itself wanting one of them has found a missing parameter\nrather than a missing field.\n",
        "required": [
          "id",
          "name",
          "description",
          "allowed_target_classes",
          "params"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The tool id, and the last segment of its submission path."
          },
          "family": {
            "type": "string",
            "description": "Which dispatcher serves it. Client-facing only because the job-listing\nand target routes still take a family segment; nothing else needs it.\n"
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "description": {
            "type": "string",
            "description": "What the tool does and when to reach for it. Written for a scientist\nchoosing between tools, and used verbatim as the agent tool description.\n"
          },
          "allowed_target_classes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this tool needs from a machine, rather than which machines happen\nto provide it \u2014 `gpu-daemon`, `cpu-daemon`, `persistent-cache`,\n`lambda`. A target qualifies when it offers any one of them.\n\nNamed this way so a machine added after this catalog was published is\nreachable without re-authoring every tool. Resolve it to concrete\ntargets with `GET /api/tools/{family}/targets`, which also filters by\nthe caller's grants \u2014 this field is the tool's half of the decision and\ndoes not consider who is asking.\n"
          },
          "default_target_id": {
            "type": "string",
            "description": "Used when a submission names no target. Absent if there is none."
          },
          "limits": {
            "type": "object",
            "additionalProperties": true,
            "description": "Ceilings the runner enforces, e.g. `max_structures`. Advisory to a\nclient \u2014 stated so a form can warn before a submission that will fail.\n"
          },
          "extra_args_enabled": {
            "type": "boolean",
            "description": "Whether this tool accepts freeform CLI passthrough. The blacklist that\nguards it is not published: it is a list of container flags, and\ndocumenting it would document the internals this catalog keeps out.\n"
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "A JSON Schema for the flat submission form \u2014 the same object\n`POST /api/tools/{tool_id}` accepts under `params`. Data references carry\n`x-nm-sources` (which variants of `JobInput` that parameter accepts) and,\nwhere there is one, `x-nm-max-size-bytes`.\n"
          }
        }
      },
      "UniProtEntry": {
        "type": "object",
        "description": "One UniProtKB entry, expressed in the same annotation-field vocabulary a\nbioparsers dataset build emits. The shape deliberately mirrors a build's\noutput record \u2014 accession, sequence, fields \u2014 so anything that consumes a\ndataset row can consume one of these.\n\nValues reflect UniProt as it is now, not the pinned release behind\n`swissprot-parsed`. An entry revised since that release will differ from\nthe same entry in a dataset built off the mirror.\n",
        "required": [
          "accession",
          "reviewed",
          "fields"
        ],
        "properties": {
          "accession": {
            "type": "string",
            "description": "The entry's primary accession. May differ from the one requested: a\nsecondary accession resolves to the entry that absorbed it.\n"
          },
          "entry_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "UniProt's mnemonic identifier, e.g. AATM_RABIT."
          },
          "reviewed": {
            "type": "boolean",
            "description": "True for a Swiss-Prot entry (manually curated), false for TrEMBL\n(automatically annotated). Worth carrying into anything built from the\nentry: it is the difference between curated and predicted annotation.\n"
          },
          "sequence": {
            "type": [
              "string",
              "null"
            ],
            "description": "The amino-acid sequence, one letter per residue."
          },
          "fields": {
            "$ref": "#/components/schemas/UniProtFields"
          }
        }
      },
      "UniProtFields": {
        "type": "object",
        "description": "Annotation fields, keyed by the field ids `GET /api/tools/bioparsers/builders`\npublishes for the `uniprot` field set \u2014 the same ids a build's `fields`\nargument selects from. Keys appear in catalog order.\n\nA field the entry has nothing to say for is **absent**, never null or\nempty: a caption builder iterating this never emits a label with nothing\nafter it, and a consumer tests presence rather than truthiness.\n\nEach value's shape follows the field's declared `kind` in that catalog \u2014\n`text` a string, `text-list` an array of strings, `number` an integer.\nThe keys are not enumerated here because the catalog owns that list;\nrestating it would be a second copy that drifts the first time a field is\nadded.\n",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            {
              "type": "integer"
            }
          ]
        }
      },
      "UploadInput": {
        "type": "object",
        "description": "A scratch upload from `POST /api/uploads`. Not quota-counted, not listed, and\nexpires on its own \u2014 it exists to be submitted once.\n",
        "required": [
          "source",
          "upload_id"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "upload"
          },
          "upload_id": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "UploadResponse": {
        "type": "object",
        "required": [
          "url",
          "fields",
          "method",
          "size_bytes",
          "expires_in_seconds"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Form fields to send with the file. Order is preserved."
          },
          "method": {
            "type": "string",
            "enum": [
              "POST"
            ]
          },
          "size_bytes": {
            "type": "integer"
          },
          "expires_in_seconds": {
            "type": "integer"
          }
        }
      },
      "UserDataInput": {
        "type": "object",
        "description": "A file or dataset from the caller's own My Data. `file_id` addresses a single\nitem; `path` addresses a subtree of a saved dataset and is only meaningful for\ndirectory-shaped inputs.\n",
        "required": [
          "source"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "const": "user-data"
          },
          "file_id": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "Relative to the caller's own My Data root. Path traversal is rejected;\none caller can never address another's data.\n"
          }
        },
        "anyOf": [
          {
            "required": [
              "file_id"
            ]
          },
          {
            "required": [
              "path"
            ]
          }
        ]
      },
      "UserDataItem": {
        "type": "object",
        "description": "One My Data entry. Two variants share this shape: a single uploaded file, and\na dataset saved from a finished job. The dataset-only fields are absent on\nplain uploads.\n",
        "required": [
          "file_id",
          "display_name",
          "size_bytes",
          "uploaded_at"
        ],
        "properties": {
          "file_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "maxLength": 200
          },
          "category": {
            "type": "string",
            "maxLength": 50
          },
          "description": {
            "type": "string",
            "maxLength": 1000
          },
          "uploaded_at": {
            "type": "number",
            "description": "Epoch seconds, fractional."
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0
          },
          "shared_with_teams": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "download_url": {
            "type": "string",
            "format": "uri"
          },
          "kind": {
            "type": "string",
            "const": "dataset",
            "description": "Present only on job-saved datasets; absent on plain uploads.\n\nAbsence-means-file is a wart inherited from the existing API. It is\nrecorded here rather than fixed because changing it is a breaking change\nfor the frontend; see contracts/README.md.\n"
          },
          "source_job_id": {
            "type": "string",
            "description": "Dataset only."
          },
          "source_tool_id": {
            "type": "string",
            "description": "Dataset only."
          },
          "output_subpath": {
            "type": "string",
            "description": "Dataset only. Relative path of the source job's primary output directory,\nso the dataset can be chained into a downstream job without re-deriving\nthe layout.\n"
          },
          "object_count": {
            "type": "integer",
            "description": "Dataset only."
          }
        }
      },
      "UserDataListing": {
        "type": "object",
        "required": [
          "items",
          "quota"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UserDataItem"
            },
            "description": "Newest first."
          },
          "quota": {
            "$ref": "#/components/schemas/Quota"
          }
        }
      },
      "WeightsBundle": {
        "type": "object",
        "description": "A matched set of weights covering every stage of a pipeline. Choosing one is\na job's baseline; per-stage overrides layer on top.\n",
        "required": [
          "tag",
          "name"
        ],
        "properties": {
          "tag": {
            "type": "string",
            "description": "The value to send as `weights_bundle`."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "default": {
            "type": "boolean",
            "description": "Set on the one bundle a client should pre-select."
          }
        }
      },
      "WeightsCatalog": {
        "type": "object",
        "description": "What a job may choose for its weights: the bundles `weights_bundle` accepts,\nand the registered per-stage weights an override may name. One response\nbecause they are one decision \u2014 which registered weights are legal depends on\nthe bundle selected.\n",
        "required": [
          "bundles",
          "stages"
        ],
        "properties": {
          "bundles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WeightsBundle"
            }
          },
          "stages": {
            "type": "object",
            "description": "Registered weights keyed by pipeline stage (`pencl`, `facilitator`,\n`proteoscribe`), so one stage's weights are selectable from every tool\nthat runs it. A stage with nothing registered is absent.\n",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/RegisteredWeights"
              }
            }
          }
        }
      }
    }
  }
}
