openapi: 3.0.3
info:
  title: Server4Agent REST API
  version: 0.1.0
  description: >
    REST API for Server4Agent — provision servers, run agent tasks and builds,
    manage projects and files, and subscribe to webhooks.


    This is the code *around* your agent (your backend, a CI job, a webhook
    receiver). If your agent itself does tool-calling, point it at the MCP
    server instead.


    Authenticate every request with an `sk_live_` API key as a bearer token:
    `Authorization: Bearer sk_live_...`. Keys are server-side secrets — never
    ship them in a browser bundle.


    Scoped keys can access only their explicitly allowed servers and projects.
    Account-level operations such as creating servers, managing keys, and
    managing webhooks require an account-wide key.
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://api.server4agent.com
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Servers
    description: Sized compute boxes. Multiple projects share a server's resources.
  - name: Tasks
    description: Give the on-server agent a goal; it plans, writes code, runs it, and can deploy.
  - name: Builds
    description: Structured build pipeline (plan → scaffold → implement → install → build → deploy).
  - name: Files
    description: Read and write files in a server's workspace.
  - name: Deployments
    description: Publish a server's workspace to a live public URL.
  - name: Projects
    description: A workspace + app inside a server, with its own slug and public URL.
  - name: Templates
    description: Starter templates for new projects.
  - name: API Keys
    description: Manage the API keys on your account.
  - name: Webhooks
    description: Subscribe to events. Deliveries are HMAC-signed.

paths:
  /servers:
    get:
      operationId: listServers
      tags: [Servers]
      summary: List servers
      responses:
        "200":
          description: The caller's servers.
          content:
            application/json:
              schema:
                type: object
                required: [servers]
                properties:
                  servers:
                    type: array
                    items: { $ref: "#/components/schemas/Server" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createServer
      tags: [Servers]
      summary: Provision a server
      description: >
        No plan field — the allowed tiers and limits come from the account
        behind the key.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServerCreate" }
      responses:
        "201":
          description: The provisioned server.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedServer" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/ForbiddenOrPlanLimit" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /servers/{serverId}:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: getServer
      tags: [Servers]
      summary: Get a server
      responses:
        "200":
          description: The server.
          content:
            application/json:
              schema:
                type: object
                required: [server]
                properties:
                  server: { $ref: "#/components/schemas/Server" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateServer
      tags: [Servers]
      summary: Rename or relocate a server
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServerUpdate" }
      responses:
        "200":
          description: The updated server.
          content:
            application/json:
              schema:
                type: object
                required: [server]
                properties:
                  server: { $ref: "#/components/schemas/Server" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteServer
      tags: [Servers]
      summary: Delete a server
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/actions:
    parameters:
      - $ref: "#/components/parameters/serverId"
    post:
      operationId: controlServer
      tags: [Servers]
      summary: Start, stop, or restart a server
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServerActionRequest" }
      responses:
        "200":
          description: The server's new state.
          content:
            application/json:
              schema:
                type: object
                required: [id, status]
                properties:
                  id: { type: string }
                  status: { $ref: "#/components/schemas/ServerStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/ComputeBackendError" }

  /servers/{serverId}/exec:
    parameters:
      - $ref: "#/components/parameters/serverId"
    post:
      operationId: execCommand
      tags: [Servers]
      summary: Run a shell command on the server
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExecRequest" }
      responses:
        "200":
          description: Command output.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExecResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/deployments:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: listDeployments
      tags: [Deployments]
      summary: List a server's deployments
      responses:
        "200":
          description: Deployments.
          content:
            application/json:
              schema:
                type: object
                required: [deployments]
                properties:
                  deployments:
                    type: array
                    items: { $ref: "#/components/schemas/Deployment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: deployServer
      tags: [Deployments]
      summary: Publish the workspace to a live URL
      responses:
        "201":
          description: The live deployment.
          content:
            application/json:
              schema:
                type: object
                required: [url, status]
                properties:
                  url: { type: string, format: uri }
                  status: { type: string, enum: [live] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/files:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: readOrListFiles
      tags: [Files]
      summary: List files, or read one with ?path=
      parameters:
        - name: path
          in: query
          required: false
          schema: { type: string }
          description: When set, returns that file's content instead of the file list.
      responses:
        "200":
          description: A file listing, or a single file's content.
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [files]
                    properties:
                      files:
                        type: array
                        items: { type: string }
                  - type: object
                    required: [path, content]
                    properties:
                      path: { type: string }
                      content: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      operationId: writeFile
      tags: [Files]
      summary: Write a file
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FileWrite" }
      responses:
        "201":
          description: Written.
          content:
            application/json:
              schema:
                type: object
                required: [path, written]
                properties:
                  path: { type: string }
                  written: { type: boolean, enum: [true] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteFile
      tags: [Files]
      summary: Delete a file
      parameters:
        - name: path
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                required: [path, deleted]
                properties:
                  path: { type: string }
                  deleted: { type: boolean, enum: [true] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/tasks:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: listTasks
      tags: [Tasks]
      summary: List a server's tasks
      responses:
        "200":
          description: Tasks.
          content:
            application/json:
              schema:
                type: object
                required: [tasks]
                properties:
                  tasks:
                    type: array
                    items: { $ref: "#/components/schemas/Task" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: createTask
      tags: [Tasks]
      summary: Give the agent a goal
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TaskCreate" }
      responses:
        "201":
          description: The created task.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedTask" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/BudgetExceeded" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/tasks/{taskId}:
    parameters:
      - $ref: "#/components/parameters/serverId"
      - name: taskId
        in: path
        required: true
        schema: { type: string }
    get:
      operationId: getTask
      tags: [Tasks]
      summary: Get a task
      responses:
        "200":
          description: The task.
          content:
            application/json:
              schema:
                type: object
                required: [task]
                properties:
                  task: { $ref: "#/components/schemas/Task" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/builds:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: listBuilds
      tags: [Builds]
      summary: List a server's builds
      responses:
        "200":
          description: Builds.
          content:
            application/json:
              schema:
                type: object
                required: [builds]
                properties:
                  builds:
                    type: array
                    items: { $ref: "#/components/schemas/Build" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: startBuild
      tags: [Builds]
      summary: Start a build pipeline
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuildCreate" }
      responses:
        "201":
          description: The started build.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedBuild" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/builds/{buildId}:
    parameters:
      - $ref: "#/components/parameters/serverId"
      - name: buildId
        in: path
        required: true
        schema: { type: string }
    get:
      operationId: getBuild
      tags: [Builds]
      summary: Get a build
      responses:
        "200":
          description: The build.
          content:
            application/json:
              schema:
                type: object
                required: [build]
                properties:
                  build: { $ref: "#/components/schemas/Build" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /servers/{serverId}/projects:
    parameters:
      - $ref: "#/components/parameters/serverId"
    get:
      operationId: listServerProjects
      tags: [Projects]
      summary: List a server's projects
      responses:
        "200":
          description: Projects on the server.
          content:
            application/json:
              schema:
                type: object
                required: [projects]
                properties:
                  projects:
                    type: array
                    items: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: createProject
      tags: [Projects]
      summary: Create a project on a server
      description: Defaults to private; pass visibility=public to expose its URL.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectCreate" }
      responses:
        "201":
          description: The created project.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/PlanLimit" }
        "404": { $ref: "#/components/responses/NotFound" }

  /projects:
    get:
      operationId: listProjects
      tags: [Projects]
      summary: List every project across the caller's servers
      responses:
        "200":
          description: Projects.
          content:
            application/json:
              schema:
                type: object
                required: [projects]
                properties:
                  projects:
                    type: array
                    items: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /projects/{projectId}:
    parameters:
      - name: projectId
        in: path
        required: true
        schema: { type: string }
    get:
      operationId: getProject
      tags: [Projects]
      summary: Get a project
      responses:
        "200":
          description: The project.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProject
      tags: [Projects]
      summary: Rename, redescribe, or toggle visibility/lifecycle
      description: The slug and URL stay the same across visibility toggles.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectUpdate" }
      responses:
        "200":
          description: The updated project.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteProject
      tags: [Projects]
      summary: Delete a project
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /projects/{projectId}/cleanup:
    parameters:
      - name: projectId
        in: path
        required: true
        schema: { type: string }
    post:
      operationId: cleanupProject
      tags: [Projects]
      summary: Tear down an ephemeral project's runtime
      description: Ephemeral projects only. Use delete for persistent ones.
      responses:
        "200":
          description: Cleaned up.
          content:
            application/json:
              schema:
                type: object
                required: [id, cleaned]
                properties:
                  id: { type: string }
                  cleaned: { type: boolean, enum: [true] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /templates:
    get:
      operationId: listTemplates
      tags: [Templates]
      summary: List project templates
      responses:
        "200":
          description: Templates.
          content:
            application/json:
              schema:
                type: object
                required: [templates]
                properties:
                  templates:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectTemplate" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /keys:
    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: List API keys
      responses:
        "200":
          description: API keys (masked).
          content:
            application/json:
              schema:
                type: object
                required: [keys]
                properties:
                  keys:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Mint a new API key
      description: The plaintext `key` is returned in this creation response. The authenticated dashboard can also reveal the encrypted-at-rest value.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyCreate" }
      responses:
        "201":
          description: The new key, including its plaintext value.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedApiKey" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /keys/{keyId}:
    parameters:
      - name: keyId
        in: path
        required: true
        schema: { type: string }
    patch:
      operationId: updateApiKeyScope
      tags: [API Keys]
      summary: Replace a key's access scope
      description: Empty/omitted arrays mean the key can reach everything.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyScopeUpdate" }
      responses:
        "200":
          description: The updated scope.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  allowed_server_ids:
                    type: array
                    items: { type: string }
                  allowed_project_ids:
                    type: array
                    items: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: revokeApiKey
      tags: [API Keys]
      summary: Revoke a key
      responses:
        "200":
          description: Revoked.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks:
    get:
      operationId: listWebhooks
      tags: [Webhooks]
      summary: List webhook subscriptions
      responses:
        "200":
          description: Webhooks.
          content:
            application/json:
              schema:
                type: object
                required: [webhooks]
                properties:
                  webhooks:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      operationId: createWebhook
      tags: [Webhooks]
      summary: Subscribe to events
      description: The signing `secret` is returned once, here. Store it for signature verification.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookCreate" }
      responses:
        "201":
          description: The subscription, including its one-time signing secret.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedWebhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /webhooks/{webhookId}:
    parameters:
      - name: webhookId
        in: path
        required: true
        schema: { type: string }
    delete:
      operationId: deleteWebhook
      tags: [Webhooks]
      summary: Remove a webhook subscription
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "An `sk_live_` API key sent as `Authorization: Bearer sk_live_...`."

  parameters:
    serverId:
      name: serverId
      in: path
      required: true
      schema: { type: string }
      description: The server id.

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: missing_or_invalid_api_key, message: Provide a valid Bearer API key. }
    NotFound:
      description: No such resource.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: not_found, message: server not found }
    BadRequest:
      description: The request was malformed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: bad_request, message: goal is required }
    PlanLimit:
      description: The account hit a plan limit (e.g. a tier's project cap).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: plan_limit, message: This tier allows at most 3 projects. }
    Forbidden:
      description: The API key is valid but its resource scope does not allow this operation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: forbidden, message: Scoped API keys cannot manage API keys. }
    ForbiddenOrPlanLimit:
      description: The key is scoped, or the account's plan does not allow the requested server.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BudgetExceeded:
      description: Authorized, but the account is out of budget (credits/cap reached).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: budget_exceeded, message: AI credits exhausted for this period. }
    ComputeBackendError:
      description: The compute backend returned an error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: compute_backend_error, message: backend unavailable }

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable explanation.

    ServerTier:
      type: string
      enum: [small, medium, large]
    ServerStatus:
      type: string
      enum: [running, sleeping, building]
    ProjectVisibility:
      type: string
      enum: [public, private]
    ProjectLifecycle:
      type: string
      enum: [persistent, ephemeral]
    ProjectRuntimeStatus:
      type: string
      enum: [unbuilt, running, paused]
    TaskStatus:
      type: string
      enum: [running, completed, failed, awaiting_input, cancelled]
    BuildStatus:
      type: string
      enum: [queued, running, completed]
    WebhookEvent:
      type: string
      enum:
        - task.created
        - task.completed
        - task.failed
        - deployment.live
        - deployment.removed
        - server.created
        - server.stopped
        - server.deleted
        - quota.threshold
        - quota.cap_reached

    Server:
      type: object
      additionalProperties: false
      required: [id, task, tier, status, region, creditsUsed, storageUsedGb]
      properties:
        id: { type: string }
        task: { type: string, description: A short label for the server. }
        tier: { $ref: "#/components/schemas/ServerTier" }
        status: { $ref: "#/components/schemas/ServerStatus" }
        region: { type: string, enum: [us-east, us-west, eu-west] }
        creditsUsed: { type: number }
        storageUsedGb: { type: number }
        publicUrl: { type: string, nullable: true }
        createdAt: { type: string, format: date-time, nullable: true }

    CreatedServer:
      type: object
      required: [id, status, tier]
      properties:
        id: { type: string }
        url: { type: string, nullable: true }
        status: { $ref: "#/components/schemas/ServerStatus" }
        tier: { $ref: "#/components/schemas/ServerTier" }

    ServerCreate:
      type: object
      additionalProperties: false
      properties:
        task: { type: string }
        tier: { $ref: "#/components/schemas/ServerTier" }
        region: { type: string, enum: [us-east, us-west, eu-west] }

    ServerUpdate:
      type: object
      additionalProperties: false
      properties:
        task: { type: string }
        region: { type: string, enum: [us-east, us-west, eu-west] }

    ServerActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [start, stop, restart]

    ExecRequest:
      type: object
      required: [command]
      properties:
        command: { type: string }
    ExecResult:
      type: object
      required: [stdout]
      properties:
        stdout: { type: string }

    FileWrite:
      type: object
      required: [path, content]
      properties:
        path: { type: string }
        content: { type: string }

    Deployment:
      type: object
      required: [url, live]
      properties:
        url: { type: string, format: uri }
        live: { type: boolean }

    Project:
      type: object
      required:
        [id, server_id, name, slug, visibility, lifecycle, runtime_status, created_at, updated_at]
      properties:
        id: { type: string }
        server_id: { type: string }
        name: { type: string }
        slug: { type: string }
        description: { type: string, nullable: true }
        visibility: { $ref: "#/components/schemas/ProjectVisibility" }
        lifecycle: { $ref: "#/components/schemas/ProjectLifecycle" }
        runtime_status: { $ref: "#/components/schemas/ProjectRuntimeStatus" }
        template: { type: string, nullable: true }
        url:
          type: string
          nullable: true
          description: Live URL, or null while the project is private.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectCreate:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        visibility: { $ref: "#/components/schemas/ProjectVisibility" }
        lifecycle: { $ref: "#/components/schemas/ProjectLifecycle" }
        template: { type: string, description: A template id from listTemplates. }

    ProjectUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        visibility: { $ref: "#/components/schemas/ProjectVisibility" }
        lifecycle: { $ref: "#/components/schemas/ProjectLifecycle" }

    ProjectTemplateDefaults:
      type: object
      required: [name, description, visibility, lifecycle]
      properties:
        name: { type: string }
        description: { type: string }
        visibility: { $ref: "#/components/schemas/ProjectVisibility" }
        lifecycle: { $ref: "#/components/schemas/ProjectLifecycle" }
    ProjectTemplate:
      type: object
      required: [id, label, description, defaults]
      properties:
        id: { type: string }
        label: { type: string }
        description: { type: string }
        defaults: { $ref: "#/components/schemas/ProjectTemplateDefaults" }

    Task:
      type: object
      additionalProperties: false
      required: [id, serverId, goal, status, createdAt]
      properties:
        id: { type: string }
        serverId: { type: string }
        goal: { type: string }
        status: { $ref: "#/components/schemas/TaskStatus" }
        createdAt: { type: string, format: date-time }
        result: { type: string }
        question:
          type: string
          description: Set when status is awaiting_input — what the agent needs from you.
    CreatedTask:
      type: object
      required: [task_id, status]
      properties:
        task_id: { type: string }
        status: { $ref: "#/components/schemas/TaskStatus" }
    TaskCreate:
      type: object
      required: [goal]
      properties:
        goal: { type: string }

    BuildStep:
      type: object
      required: [key, label, status]
      properties:
        key: { type: string }
        label: { type: string }
        status: { type: string, enum: [pending, running, done] }
    Build:
      type: object
      additionalProperties: false
      required: [id, serverId, goal, status, steps, artifacts, createdAt]
      properties:
        id: { type: string }
        serverId: { type: string }
        goal: { type: string }
        status: { $ref: "#/components/schemas/BuildStatus" }
        steps:
          type: array
          items: { $ref: "#/components/schemas/BuildStep" }
        artifacts:
          type: array
          items: { type: string }
        url: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
    CreatedBuild:
      type: object
      required: [build_id, status, steps]
      properties:
        build_id: { type: string }
        status: { $ref: "#/components/schemas/BuildStatus" }
        steps:
          type: array
          items: { $ref: "#/components/schemas/BuildStep" }
    BuildCreate:
      type: object
      required: [goal]
      properties:
        goal: { type: string }

    ApiKey:
      type: object
      required: [id, label, masked, allowed_server_ids, allowed_project_ids, created_at]
      properties:
        id: { type: string }
        label: { type: string }
        masked:
          type: string
          description: Masked display form (e.g. "sk_live_abc···wxyz"). Never the full secret.
        allowed_server_ids:
          type: array
          items: { type: string }
        allowed_project_ids:
          type: array
          items: { type: string }
        created_at: { type: string, format: date-time }
        last_used_at: { type: string, format: date-time, nullable: true }
    CreatedApiKey:
      allOf:
        - $ref: "#/components/schemas/ApiKey"
        - type: object
          required: [key]
          properties:
            key:
              type: string
              description: The plaintext sk_live_ key returned when the key is created.
    ApiKeyCreate:
      type: object
      required: [label]
      properties:
        label: { type: string, maxLength: 40 }
    ApiKeyScopeUpdate:
      type: object
      additionalProperties: false
      properties:
        allowed_server_ids:
          type: array
          items: { type: string }
        allowed_project_ids:
          type: array
          items: { type: string }

    Webhook:
      type: object
      required: [id, url, events, created_at]
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: "#/components/schemas/WebhookEvent" }
        created_at: { type: string, format: date-time }
    CreatedWebhook:
      allOf:
        - $ref: "#/components/schemas/Webhook"
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: The HMAC signing secret (whsec_...). Returned once; store it now.
    WebhookCreate:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: Public HTTPS endpoint. Private, loopback, link-local, and redirect destinations are rejected.
        events:
          type: array
          items: { $ref: "#/components/schemas/WebhookEvent" }

    DeleteResult:
      type: object
      required: [id, deleted]
      properties:
        id: { type: string }
        deleted: { type: boolean, enum: [true] }
