{
  "openapi": "3.1.0",
  "info": {
    "title": "Nuved platform API",
    "version": "v1",
    "description": "## What this is\n\nThe API behind **console.nuved.io**. Everything the console does is here, and the `nuved` CLI and coding agents use the same endpoints with a token.\n\n## The four addresses\n\n| Address | What you do there |\n|---|---|\n| `console.nuved.io` | Sign in, name your workspace, top up, create projects, mint tokens. |\n| `api.nuved.io` | This API: the same things, for the CLI and for anything you write. |\n| `kube.nuved.io` | The Kubernetes API for your projects, filtered to what your credential owns. Point kubectl at it with the kubeconfig from `GET /v1/projects/{name}/kubeconfig`, and use the upstream Kubernetes API reference for its endpoints — they are not described here. |\n| `\u003cservice\u003e-\u003cproject\u003e.nuved.io` | Where your own services answer, once you deploy something a route points at. |\n\n## A typical session\n\n1. **Sign in** — in a browser at `/login`, or with `nuved login`, which runs the device flow and posts the result to `POST /auth/cli`.\n2. **Top up** — `POST /topup` from the console; the balance is what everything spends.\n3. **Create a project with caps** — `POST /v1/projects` with cpu and mem.\n4. **Mint an agent token for it** — `POST /v1/tokens`; it is shown once and reaches that project only.\n5. **Download the kubeconfig and deploy** — `GET /v1/projects/{name}/kubeconfig`, then kubectl as usual.\n\n## Workspace\n\nA **workspace** is yours or your team's: its people, projects and balance. The paths say org (`/v1/orgs`) and the fields say `tenant` or `org`; they all mean the workspace.\n\n## Credentials\n\nA **session** (`nvd_sess_…`) is a person signed in: it speaks for the whole workspace, including money, caps and credentials. An **agent token** (`nvd_agent_…`) is minted for one project: it can deploy, read and fetch its own kubeconfig, and nothing else. Both travel as `Authorization: Bearer \u003ctoken\u003e`; the console sends its session in a cookie instead. Every refusal is `{\"error\": \"a sentence\"}`."
  },
  "servers": [
    {
      "url": "https://api.nuved.io",
      "description": "the platform API"
    }
  ],
  "tags": [
    {
      "name": "Sign-in",
      "description": "getting a session, and naming a workspace the first time"
    },
    {
      "name": "Account",
      "description": "who you are and what is left on the balance"
    },
    {
      "name": "Projects",
      "description": "isolated projects, their caps, what runs in them, and kubeconfigs"
    },
    {
      "name": "Tokens",
      "description": "credentials for agents and CI, scoped to one project"
    },
    {
      "name": "People",
      "description": "who is in a workspace, and the invitations that put them there"
    },
    {
      "name": "Activity",
      "description": "who did what to a workspace: people, agents, and the platform itself"
    },
    {
      "name": "Billing",
      "description": "what each project cost and when, top-ups with their receipts, and spend alerts"
    },
    {
      "name": "Targets",
      "description": "the machines services are deployed to, and what runs where"
    },
    {
      "name": "Browser",
      "description": "the pages and form posts the console itself uses — you will not call these by hand"
    },
    {
      "name": "Billing webhook",
      "description": "what Stripe calls when a payment completes — you will not call this by hand"
    },
    {
      "name": "Health",
      "description": "whether the console answers, for monitors outside it"
    }
  ],
  "paths": {
    "/": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The front door: to your console with a session, to the sign-in page without one.",
        "operationId": "root",
        "responses": {
          "303": {
            "description": "/console or /login",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "308": {
            "description": "/console, from /console/, the Overview's address as the console app writes its link",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "any other path under the root handler; /console/status, which the old console's Overview polled, says where its figures are now"
          }
        },
        "security": []
      }
    },
    "/.well-known/nuved-edge-keys": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The public keys that check the edge's identity header, as a JWKS.",
        "description": "An address behind the console's sign-in hands its service `X-Nuved-Identity` on every request the edge lets through as somebody: a JWT (ES256) saying who, minted for that one address and good for five minutes (docs/edge-identity.md). The keys that check it are here, `kid` naming each. A new key replaces the current one every 42 days and is published two hours before it signs; a retired one stays published for 7 days. No credential, and kept for an hour. 404 when the edge is not switched on.",
        "operationId": "edgeKeys",
        "responses": {
          "200": {
            "description": "the published keys, newest first: `{\"keys\": [{\"kty\": \"EC\", \"crv\": \"P-256\", \"alg\": \"ES256\", \"kid\": …, \"x\": …, \"y\": …}]}`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "the edge is not switched on here"
          },
          "503": {
            "description": "the keys could not be read just now"
          }
        },
        "security": []
      }
    },
    "/app/": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Where the React console was previewed: every path under /app/ answers a permanent redirect to the same path under /console/.",
        "description": "Kept so an address bookmarked during the preview still opens its page. The query string goes along; the session is asked for where the redirect lands.",
        "operationId": "consoleAppMoved",
        "responses": {
          "308": {
            "description": "the same path under /console/",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/artifacts/go": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Carry this host's session to the artifacts host: the first half of the handoff.",
        "description": "A browser that opens an artifact's address (`https://artifacts.nuved.app/\u003cid\u003e/`) without a session there is sent here, where the console's session cookie lives. Signed in, it leaves with a one-time code (sixty seconds, one use) for /_session on the artifacts host, which sets the same session as that host's cookie and sends the browser on to `to`, a path on that host and nothing else. Signed out, it goes to the sign-in first and comes back. 404 when artifacts are not switched on.",
        "operationId": "artifactsGo",
        "parameters": [
          {
            "name": "to",
            "in": "query",
            "description": "the path on the artifacts host to return to, `/\u003cid\u003e/` as a rule",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "https://artifacts.nuved.app/_session?code=…; or /login?next=… when signed out",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "artifacts are not switched on here"
          },
          "503": {
            "description": "a code could not be made just now"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/auth/claim": {
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Name the workspace a freshly verified address owns, and sign in to it.",
        "description": "Takes the one-time proof from the page the callback rendered. Refuses a name that belongs to another billing model or already has an account, and an address that already signs in somewhere.",
        "operationId": "claimOrganization",
        "requestBody": {
          "description": "the form the claim page posts",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "the address the provider verified"
                  },
                  "next": {
                    "type": "string",
                    "description": "where to land afterwards; a path on this site"
                  },
                  "org": {
                    "type": "string",
                    "example": "testcorp"
                  },
                  "proof": {
                    "type": "string",
                    "description": "the one-time proof from the claim page"
                  }
                },
                "required": [
                  "org",
                  "email",
                  "proof"
                ]
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "/console, signed in to the new workspace",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "no name typed",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "the proof is spent or unknown",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "that name is taken, or that address already signs in elsewhere",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "too many attempts",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/cli": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Sign the nuved CLI in with an id_token from the identity provider's device flow.",
        "description": "The CLI runs the device flow against the provider itself — it never sees a password, and this platform never sees the device code — and posts the id_token here. It is verified exactly as the browser callback verifies one: signature, issuer, expiry, and an audience that is one of this platform's own clients. What comes back is a session token, the same shape the console's cookie carries. Ask GET /v1/auth for the issuer and client id to run the flow against.",
        "operationId": "cliSignIn",
        "requestBody": {
          "description": "the id_token the device flow produced",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id_token": {
                    "type": "string",
                    "description": "a JWT from the provider"
                  }
                },
                "required": [
                  "id_token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the session token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenant": {
                      "type": "string",
                      "example": "novid"
                    },
                    "token": {
                      "type": "string",
                      "example": "nvd_sess_…"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "no id_token in the body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "the token is not valid, or was issued for another application",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "that address has no workspace yet, the console creates one; or it is only invited to one, which it joins on the welcome page first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "a sentence"
                    },
                    "invited_to": {
                      "type": "string",
                      "description": "only invited: the workspace",
                      "example": "acme"
                    },
                    "welcome_url": {
                      "type": "string",
                      "description": "only invited: where to join it, before signing in again",
                      "example": "https://console.nuved.io/welcome?org=acme"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "the identity provider did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/device": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Start an app's sign-in that a person allows on the console's own page.",
        "description": "RFC 8628's device authorization request, served by the console (NPB-294). The app shows user_code, opens verification_uri_complete, and polls POST /auth/device/token. A person already signed in to the console allows it with one button; without a console session the console's own sign-in comes first. GET /v1/auth names this address as device_sign_in.",
        "operationId": "deviceSignInStart",
        "requestBody": {
          "description": "optional: what the app calls itself, shown on the Allow page, and whether it is the desktop app",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client": {
                    "type": "string",
                    "description": "desktop: the nuved app for Mac says it is; the session minted for this sign-in is then the app's own, the only kind that may make a VPN device (POST /v1/vpn/peers). Left out, or any other word, the session is an ordinary one. The word is the app's say-so: whoever sends it gets what the app gets.",
                    "example": "desktop",
                    "enum": [
                      "desktop"
                    ]
                  },
                  "client_name": {
                    "type": "string",
                    "example": "nuved for Mac"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "device_code": {
                      "type": "string",
                      "description": "what the app polls with; never shown"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 600
                    },
                    "interval": {
                      "type": "integer",
                      "example": 2
                    },
                    "user_code": {
                      "type": "string",
                      "example": "BCDF-GHJK"
                    },
                    "verification_uri": {
                      "type": "string",
                      "example": "https://console.nuved.io/device"
                    },
                    "verification_uri_complete": {
                      "type": "string",
                      "example": "https://console.nuved.io/device?code=BCDF-GHJK"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "too many sign-ins are waiting, or no code could be made",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/device/token": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "summary": "The app's poll: the session token once the person allowed the sign-in.",
        "description": "RFC 8628's token request. Until the person answers it is 400 authorization_pending (slow_down when polled faster than the interval); a refusal is access_denied and an unknown or lapsed code expired_token. On Allow it is the same session token POST /auth/cli gives, for the organization the person allowed it from, once.",
        "operationId": "deviceSignInToken",
        "requestBody": {
          "description": "the device_code from POST /auth/device",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_code": {
                    "type": "string"
                  }
                },
                "required": [
                  "device_code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the session token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenant": {
                      "type": "string",
                      "example": "novid"
                    },
                    "token": {
                      "type": "string",
                      "example": "nvd_sess_…"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "authorization_pending, slow_down, access_denied, expired_token or invalid_request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "authorization_pending"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "the session could not be stored just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/oidc/callback": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Where the identity provider sends the browser back; exchanges the code and signs the customer in.",
        "description": "An address that already owns a workspace gets a session and the console. An address that owns none gets the page that names one.",
        "operationId": "oidcCallback",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "description": "the authorization code",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "must match the cookie this browser was given",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "in": "query",
            "description": "set when the provider refused",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the \"name your workspace\" page, for an address that owns none",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "/console, or wherever the sign-in started",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "the state did not match, or the provider refused",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no identity provider is configured"
          },
          "503": {
            "description": "the provider did not answer",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/oidc/start": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Begin signing in with the identity provider (OpenID Connect, authorization code + PKCE).",
        "description": "Writes one short-lived signed cookie holding the state, nonce and PKCE verifier, and redirects to the provider. 404 when no provider is configured. With `with=github` the authorization URL names the source, and the provider's flow sends the browser to GitHub instead of asking again.",
        "operationId": "oidcStart",
        "parameters": [
          {
            "name": "next",
            "in": "query",
            "description": "where to land after signing in; only a path on this site is honoured",
            "schema": {
              "type": "string",
              "example": "/console"
            }
          },
          {
            "name": "with",
            "in": "query",
            "description": "the identity source to go straight to, skipping the provider's own sign-in page. Only `github` today; anything else is ignored.",
            "schema": {
              "type": "string",
              "example": "github"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "the provider's authorization endpoint",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no identity provider is configured"
          },
          "503": {
            "description": "the provider did not answer; the page says so",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/password": {
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Sign in with an email address or username and a password.",
        "description": "The password is checked against the identity provider's own authentication flow, driven from here, so the customer stays on this site. An account that asks for a second factor is told to use the provider's page instead, because a form with one password box cannot ask for one. Limited per address and per caller.",
        "operationId": "signInWithPassword",
        "responses": {
          "200": {
            "description": "the sign-in page again, with what was wrong",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "/console, or wherever they were headed",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "too many attempts",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auto-topup/card": {
      "post": {
        "tags": [
          "Browser"
        ],
        "summary": "Save a card at Stripe and turn automatic top-ups on with the form's settings.",
        "description": "The Billing form's way to Stripe Checkout in setup mode: nothing is charged, the card is saved to the workspace, and Stripe's page shows the mandate. Automatic top-ups turn on when Stripe confirms the card, and every member gets a letter. Also how another card replaces the saved one. A person's decision: an agent token is refused, and the refusal is on the activity list.",
        "operationId": "autoTopUpCard",
        "requestBody": {
          "description": "the settings to turn on with",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "add": {
                    "type": "string",
                    "example": "euros",
                    "enum": [
                      "euros",
                      "days"
                    ]
                  },
                  "add_days": {
                    "type": "string",
                    "description": "1 to 30, read when add is days",
                    "example": "14"
                  },
                  "add_euros": {
                    "type": "string",
                    "description": "10 to 500, read when add is euros",
                    "example": "25"
                  },
                  "limit": {
                    "type": "string",
                    "description": "whole euros a calendar month, 10 to 2000",
                    "example": "100"
                  },
                  "when": {
                    "type": "string",
                    "example": "days",
                    "enum": [
                      "days",
                      "euros"
                    ]
                  },
                  "when_days": {
                    "type": "string",
                    "description": "1 to 30, read when when is days",
                    "example": "3"
                  },
                  "when_euros": {
                    "type": "string",
                    "description": "2 to 500, read when when is euros",
                    "example": "10"
                  }
                },
                "required": [
                  "when",
                  "add",
                  "limit"
                ]
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "Stripe Checkout, in setup mode",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "a value out of range"
          },
          "401": {
            "description": "sign in first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused: agent tokens can't change automatic top-ups"
          },
          "409": {
            "description": "there is no balance yet: top up once first"
          },
          "502": {
            "description": "Stripe did not answer"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/cancel": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Where Stripe returns a customer who changed their mind.",
        "operationId": "checkoutCancel",
        "responses": {
          "200": {
            "description": "a line saying the payment was cancelled",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/confirm": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Confirm an address and switch the account on.",
        "description": "The link from the confirmation letter. The account becomes usable, the invitation is spent, and the page offers the sign-in with the address filled in.",
        "operationId": "confirmAccount",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "the token from the confirmation letter",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the sign-in page, confirmed",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/confirm-address": {
      "get": {
        "tags": [
          "Account",
          "Browser"
        ],
        "summary": "The link from an address's letter: which account asked for which address, and a Confirm button.",
        "description": "The letter POST /v1/me/addresses sends to the address itself links here. Opening the link changes nothing, because mail scanners open every link in a letter; the page names the account and the address, and its button posts the token back. A link that expired (24 hours) or was used shows the sign-in page saying so.",
        "operationId": "confirmAddressPage",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "the token from the address's letter",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the page asking to confirm; or the sign-in page, the link spent or expired",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "the request could not be read just now",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "Account",
          "Browser"
        ],
        "summary": "Confirm an address added for commits: it becomes one of the account's verified addresses.",
        "description": "The page's Confirm button. The token works once; anybody else waiting for the same address loses their request. The answer is the sign-in page, saying the address is confirmed, or why it was not.",
        "operationId": "confirmAddress",
        "requestBody": {
          "description": "the form the page posts",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "the token from the address's letter"
                  }
                },
                "required": [
                  "token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the sign-in page: the address confirmed, or the link spent, expired or somebody else's",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "the address could not be confirmed just now",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/console": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's front page: the workspace's projects grouped by what they need, getting set up, and recent activity.",
        "operationId": "console",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/activity": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Activity page: what people, agent tokens and the platform did, refusals marked.",
        "operationId": "consoleActivity",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/addresses": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Addresses page: every public address with its state in words, and reserve, release and take back.",
        "operationId": "consoleAddresses",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/assets/{name}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's scripts and stylesheet, from the binary, each named by its content hash.",
        "description": "Only files the build put in its assets folder answer; anything else is 404. Cached for a year: a name never changes meaning.",
        "operationId": "consoleAsset",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the file",
            "schema": {
              "type": "string",
              "example": "index-B3kqV1xz.js"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "text/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no such file"
          }
        },
        "security": []
      }
    },
    "/console/billing": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Billing page: the balance and top-ups, spend alerts, spending by day and by project, and the history with receipts.",
        "operationId": "consoleBilling",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/icons/{name}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The installed console's icons, drawn from the nuved mark.",
        "operationId": "consoleIcon",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the icon",
            "schema": {
              "type": "string",
              "example": "icon-192.png"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the icon",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no such icon"
          }
        },
        "security": []
      }
    },
    "/console/live": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Live page: every task an agent holds, one line each with its track from Planned to Made and the agents standing on it, and a timeline of which session ran when.",
        "operationId": "consoleLive",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/live/{name}/{item}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "One task's page on Live: its track from Planned to Made with the agents standing on it, and the trail of one agent under it.",
        "operationId": "consoleLiveTask",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "item",
            "in": "path",
            "required": true,
            "description": "the task's id on the board, as the Tasks page lists it",
            "schema": {
              "type": "string",
              "example": "NPB-457"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/machines": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Build machines page, the operator's: the machines made for waiting builds, what each build cost apart from their idle time, the day's spend against its limit, and the provider calls this hour against the budget. Not in the sidebar.",
        "operationId": "consoleMachines",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/manifest.webmanifest": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's web app manifest: its name, colors and icons once it is installed on a phone or a desktop.",
        "operationId": "consoleManifest",
        "responses": {
          "200": {
            "description": "the manifest",
            "content": {
              "application/manifest+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "a binary built without the console"
          }
        },
        "security": []
      }
    },
    "/console/people": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's People page: the workspace, and invitations to nuved with their progress.",
        "operationId": "consolePeople",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/plan": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Plan page: Free or Pro, what each includes, the next renewal and what it costs, and switching between them.",
        "operationId": "consolePlan",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The Projects page's old address: the console app sends it to the front page, where the projects are.",
        "operationId": "consoleProjects",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Services tab: what runs in it, where it answers, expose and unexpose.",
        "operationId": "consoleProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/builds": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Builds tab: each production build, newest first, with what it came to, how long it took, and the line a failed one stopped on.",
        "operationId": "consoleProjectBuilds",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/builds/{id}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "One build: its commit, its times, what a running one is doing, what a failed one printed, and Build again.",
        "operationId": "consoleProjectBuild",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the build's number, as the Builds tab and the API's deploy list have it",
            "schema": {
              "type": "integer",
              "example": 175
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/code": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Code tab: the commits pushed to it, newest first, with who pushed each and what it changed, and its copy on GitHub.",
        "operationId": "consoleProjectCode",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/code/github/connect": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Copying a project to GitHub: the repository's name, the deploy key to add there, and what is copied.",
        "operationId": "consoleProjectGitHubConnect",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/code/github/import": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Bringing a repository from GitHub into a project with no commits: the connect page in its import form.",
        "operationId": "consoleProjectGitHubImport",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/code/{sha}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "One commit: its message, whether it is signed, and its diff, with the words that changed marked inside each changed line.",
        "operationId": "consoleProjectCommit",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "sha",
            "in": "path",
            "required": true,
            "description": "the commit, short or whole, as git prints it",
            "schema": {
              "type": "string",
              "example": "cc33666"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/logs": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Logs tab: what its services write, filtered, followed and downloaded.",
        "operationId": "consoleProjectLogs",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/settings": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Settings tab: its caps, its kubeconfig, who reaches it, and deleting it.",
        "operationId": "consoleProjectSettings",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/projects/{name}/tasks": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "A project's Tasks tab: its tasks by work state, who holds which right now, the open ones by rank, and one task's details.",
        "operationId": "consoleProjectTasks",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as the sidebar lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/runs": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Runs page: each run with where it runs, its cost so far, its last checkpoint and its three checks.",
        "operationId": "consoleRuns",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/runs/{id}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "One run's page: everything the Runs page says about it, and a form that sends it a message.",
        "operationId": "consoleRun",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the run's number, as the Runs page and the API list it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/security": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Security page: your password, two-step login, and where you are signed in in every workspace, with ending every other session.",
        "operationId": "consoleSecurity",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/services": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Services page: the services you reach in production, their state, and whether each project's newest change went live.",
        "operationId": "consoleServices",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/sessions": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Sessions page: where you are signed in, browsers and the CLI, and ending one or every other.",
        "operationId": "consoleSessions",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/settings": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Settings page: what the workspace is called, which its owners change, and its name in addresses.",
        "operationId": "consoleSettings",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/sw.js": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's service worker, which a browser installs with the console as an app.",
        "description": "It keeps this build's scripts, stylesheet and icons, and answers a console page that the network does not with the offline page. It never answers /v1 or a sign-in path: those go to the network. Service-Worker-Allowed lets it look after /console as well as the pages under it. Not kept: a browser asks again, and finds a new deploy.",
        "operationId": "consoleServiceWorker",
        "responses": {
          "200": {
            "description": "the worker",
            "content": {
              "text/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "a binary built without the console"
          }
        },
        "security": []
      }
    },
    "/console/tasks": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Tasks page: what is in progress in every project you reach, what waits for a person, and each project's open, in progress and in review counts.",
        "operationId": "consoleTasks",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/console/tokens": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The console's Tokens page: agent tokens, kubeconfigs, and letting a coding agent in.",
        "operationId": "consoleTokens",
        "responses": {
          "200": {
            "description": "the console app's page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "/login, when there is no session",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not open the console",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "a page saying this binary was compiled without the console",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/device": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "The page a person allows an app's sign-in on.",
        "description": "With a console session it names the app, the address and the organization, with Allow and Deny; without one it sends the browser to the console's sign-in and back. Without a code it asks for the one the app shows.",
        "operationId": "devicePage",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "description": "the user code the app shows",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the Allow page, or why the code no longer works",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "the console's sign-in, which comes back here",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Allow or deny an app's sign-in.",
        "description": "The form on GET /device: code and answer (allow or deny). It needs the console session and a request from this page.",
        "operationId": "deviceAnswer",
        "responses": {
          "200": {
            "description": "what was answered",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "the console's sign-in",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "the answer did not come from this console's own page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/docs": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "This document, as a page to read.",
        "operationId": "docsPage",
        "responses": {
          "200": {
            "description": "the API reference",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/edge/go": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Sign a browser in to an exposed service's address: the first half of the handoff.",
        "description": "A browser that opens a compose project's exposed address (`https://\u003cservice\u003e-\u003cproject\u003e.nuved.app/…`) without a sign-in there is sent here by the platform's edge. Signed in, and in the project's workspace (or on the project), it leaves with a one-time code (sixty seconds, one use, that host only) for `/_nuved/session` on that host, which sets that host's own sign-in as its `__Host-nuved_session` cookie and sends the browser on to the path. Its value is never the console's session: the host's requests go to the customer's service. Signed out, it goes to the sign-in first and comes back. `rd` is read as the whole rest of the query, as the edge writes it, unencoded. 404 when the edge is not switched on.",
        "operationId": "edgeGo",
        "parameters": [
          {
            "name": "rd",
            "in": "query",
            "required": true,
            "description": "the address to return to: https, on an exposed host, no port",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "`https://\u003chost\u003e/_nuved/session?code=…\u0026rd=/path`; or /login?next=… when signed out",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "rd is not an https address on a host the edge serves"
          },
          "403": {
            "description": "signed in, and not in the project's workspace nor on the project"
          },
          "404": {
            "description": "the edge is not switched on here"
          },
          "503": {
            "description": "a code could not be made just now"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/edge/guest": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The guests' sign-in page for one exposed address: the e-mail field.",
        "description": "A person outside the workspace whom its owner invited to one service (`POST /v1/projects/{name}/services/{service}/guests`) opens that service's addresses with a code from their inbox, and nothing else. This page asks for the address they were invited with, and links members to the console's own sign-in. Server-rendered, no JavaScript. `rd` is read as /edge/go reads it. 404 when guests are not switched on.",
        "operationId": "edgeGuest",
        "parameters": [
          {
            "name": "rd",
            "in": "query",
            "required": true,
            "description": "the address to return to: https, on an exposed host, no port",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "rd is not an https address on a host the edge serves",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "guests are not switched on here"
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "Browser"
        ],
        "summary": "Ask for a code: the same answer for every address.",
        "description": "Answers the code page, \"if that address is invited here, a code is on its way\", whoever asks: an invited address, an unknown one and a revoked one get the same page, the address echoed. A letter (`Your code for \u003chost\u003e`) goes only to a live guest of rd's host, one a minute at most. The code is 6 digits, works once, for 10 minutes, on that host only; a new one voids the old; the fifth wrong try voids it.",
        "operationId": "edgeGuestSend",
        "requestBody": {
          "description": "the address, and where to return",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "the address the workspace's owner invited",
                    "example": "ada@example.com"
                  },
                  "rd": {
                    "type": "string",
                    "description": "the address to return to, as the page's own field carries it",
                    "example": "https://shop-acme.nuved.app/"
                  }
                },
                "required": [
                  "email",
                  "rd"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the code page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "not an e-mail address, or rd is not an address the edge serves",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "guests are not switched on here"
          }
        },
        "security": []
      }
    },
    "/edge/guest/code": {
      "post": {
        "tags": [
          "Browser"
        ],
        "summary": "Spend a guest's code: on to the address with a one-time handoff.",
        "description": "A right code, for the address it was asked on, sends the browser to `https://\u003chost\u003e/_nuved/guest?code=…\u0026rd=/path` with a one-time handoff (sixty seconds, that host only), where the guest's own sign-in on that host is set: 24 hours at most, never past the invitation. A wrong or ended code, or an address that is not a guest's, is the code page again with one sentence saying so.",
        "operationId": "edgeGuestCode",
        "requestBody": {
          "description": "the code from the letter",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "the 6 digits in the letter; spaces are ignored",
                    "example": "042917"
                  },
                  "email": {
                    "type": "string",
                    "description": "the address the code was asked for",
                    "example": "ada@example.com"
                  },
                  "rd": {
                    "type": "string",
                    "description": "the address to return to",
                    "example": "https://shop-acme.nuved.app/"
                  }
                },
                "required": [
                  "email",
                  "rd",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "`https://\u003chost\u003e/_nuved/guest?code=…\u0026rd=/path`",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "the code page again: the code is wrong, has ended, or is not 6 digits",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "guests are not switched on here"
          },
          "503": {
            "description": "the code could not be checked just now"
          }
        },
        "security": []
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Whether the console answers: 200 when its database answers within two seconds, 503 otherwise.",
        "description": "For monitors outside the box. No credential, and the body is one word, `ok` or `unavailable`: what failed is in the console's log, never in the answer. Both answers carry `X-Nuved-Revision`, the commit the answering binary was built from, so a rollout can tell the old console from the new one; the header is left out when the binary has no clean revision.",
        "operationId": "healthz",
        "responses": {
          "200": {
            "description": "the console and its database answer",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "ok"
                }
              }
            },
            "headers": {
              "X-Nuved-Revision": {
                "description": "the commit the answering binary was built from (a full 40-digit git hash); absent when it has no clean revision",
                "schema": {
                  "type": "string",
                  "example": "6297314d81eb3fbc64ed3a34e9868412e84b715a"
                }
              }
            }
          },
          "503": {
            "description": "the database did not answer in time",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "unavailable"
                }
              }
            },
            "headers": {
              "X-Nuved-Revision": {
                "description": "the commit the answering binary was built from (a full 40-digit git hash); absent when it has no clean revision",
                "schema": {
                  "type": "string",
                  "example": "6297314d81eb3fbc64ed3a34e9868412e84b715a"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/login": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "The sign-in page: two tabs, sign in and create an account, with the prices under them.",
        "operationId": "loginPage",
        "parameters": [
          {
            "name": "next",
            "in": "query",
            "description": "where to land after signing in; only a path on this site is honoured",
            "schema": {
              "type": "string",
              "example": "/console"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the sign-in page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/logout": {
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Sign out: revoke this session, and end the one the identity provider keeps.",
        "description": "The session is revoked everywhere, not only in this browser, and the browser is sent to the provider's end-session endpoint so the next Sign in asks who you are. Needs no credential: signing out when there is nothing to sign out of still has the provider's session to end.",
        "operationId": "logout",
        "responses": {
          "303": {
            "description": "the provider's end-session endpoint, or /login?signed_out=1",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "This document.",
        "operationId": "openapiDocument",
        "responses": {
          "200": {
            "description": "the OpenAPI 3.1 description of this platform",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/provisioned": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Has the webhook built this workspace's first project yet? Polled by the page after a payment.",
        "operationId": "provisioned",
        "parameters": [
          {
            "name": "org",
            "in": "query",
            "required": true,
            "description": "the workspace to ask about",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "whether it is ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "org": {
                      "type": "string"
                    },
                    "ready": {
                      "type": "boolean"
                    },
                    "workspaces": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "the cluster could not be read"
          }
        },
        "security": []
      }
    },
    "/recover": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Ask for a link to set a new password.",
        "operationId": "recoverPage",
        "responses": {
          "200": {
            "description": "the form",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Send the password link.",
        "description": "The answer is the same whether or not there is an account for that address. The link is the identity provider's own recovery page, since it holds the password; the letter around it is ours.",
        "operationId": "recover",
        "responses": {
          "200": {
            "description": "the same page, saying a letter is on its way",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/signup": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "The create-account page.",
        "description": "With `invite=\u003ctoken\u003e` from an invitation letter it is the form: username, name, the invited address (fixed) and a password. Without one it says accounts are by invitation, which is how sign-up stays closed.",
        "operationId": "signup",
        "parameters": [
          {
            "name": "next",
            "in": "query",
            "description": "where to land after signing in; only a path on this site is honoured",
            "schema": {
              "type": "string",
              "example": "/console"
            }
          },
          {
            "name": "invite",
            "in": "query",
            "description": "the token from an invitation letter",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the create-account page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Create the account for an invitation, and send the confirmation letter.",
        "description": "The address is the invitation's, never the form's. The account is created at the identity provider switched off, its password is set, and it stays off until the confirmation link is opened. Field errors come back on the same page.",
        "operationId": "signupSubmit",
        "responses": {
          "200": {
            "description": "the page again: field errors, or \"check your inbox\"",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/signup/start": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Begin creating an account at the identity provider's enrolment flow.",
        "description": "Carries `next=https://\u003chost\u003e/auth/oidc/start`, so a finished enrolment lands on the page that names the new workspace. With sign-up closed it goes back to /signup rather than to a flow that is not open.",
        "operationId": "signupStart",
        "responses": {
          "303": {
            "description": "the enrolment flow, or back to /signup while sign-up is closed",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/start": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The walkthrough: from an invitation to a service of your own answering on the internet.",
        "description": "Six steps with the commands in them. Signed in, the page carries the reader's own workspace and project, so the commands can be copied as they stand.",
        "operationId": "startHere",
        "responses": {
          "200": {
            "description": "the walkthrough",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/static/{name}": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "The fonts and the icon the pages share, from the binary.",
        "description": "Only files in the static folder answer; anything else is 404. Every file is cached for a year: none changes under its name.",
        "operationId": "staticAsset",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the file",
            "schema": {
              "type": "string",
              "example": "geist-variable.woff2"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "font/woff2": {
                "schema": {
                  "type": "string"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no such file"
          }
        },
        "security": []
      }
    },
    "/success": {
      "get": {
        "tags": [
          "Browser"
        ],
        "summary": "Where Stripe returns a customer after paying; waits with them until the project exists.",
        "description": "The workspace comes from the Checkout session, never from the query string. With a session for that workspace the page opens the console by itself.",
        "operationId": "checkoutSuccess",
        "parameters": [
          {
            "name": "session_id",
            "in": "query",
            "description": "the Stripe Checkout session",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "accepted and ignored: payments started during the React console's preview come back with app. The page opens /console either way. Leaves this document in the next release",
            "schema": {
              "type": "string",
              "enum": [
                "app"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the waiting page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/topup": {
      "post": {
        "tags": [
          "Browser"
        ],
        "summary": "Start a Stripe Checkout for a top-up of 10 to 500 euros.",
        "description": "An owner's alone (decision 2026-10-21-roles-who-organises-people). The workspace is the session's; the form carries only the amount, a whole number of euros from 10 to 500, which the server checks before Stripe sees it. Checkout is locked to the address that signs in, and returns the customer to the host they started on.",
        "operationId": "topUp",
        "requestBody": {
          "description": "how much",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "string",
                    "description": "whole euros, 10 to 500, digits only",
                    "example": "25"
                  },
                  "to": {
                    "type": "string",
                    "description": "accepted and ignored: a console page loaded during the React console's preview sends app. Stripe returns every customer to /success, which opens /console. Leaves this document in the next release",
                    "enum": [
                      "app"
                    ]
                  }
                },
                "required": [
                  "amount"
                ]
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "Stripe Checkout",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "not a whole number of euros between 10 and 500"
          },
          "401": {
            "description": "sign in first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace: `Only an owner of \u003corg\u003e tops up.`"
          },
          "409": {
            "description": "the workspace belongs to another billing model"
          },
          "502": {
            "description": "whether the caller is an owner could not be read just now"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/activity": {
      "get": {
        "tags": [
          "Activity"
        ],
        "summary": "Who did what to this workspace, newest first: people, agents and the platform itself.",
        "description": "Every change made through this API or through kubectl at kube.nuved.io, every request the kube proxy refused and why, top-ups, invite credits, and services paused or resumed by billing. Reads that went through are not listed.",
        "operationId": "listActivity",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "how many events, newest first: 1 to 200, default 50; more than 200 is 200",
            "schema": {
              "type": "integer",
              "example": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Event"
                  }
                }
              }
            }
          },
          "400": {
            "description": "limit is not a whole number of 1 or more",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the workspace's activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/activity/tasks": {
      "post": {
        "tags": [
          "Activity"
        ],
        "summary": "The task board's events, for the Activity list: only the board posts here, on the internal network, signed.",
        "description": "The task board (nuved-tasks) posts every event of a workspace, in order, at least once. X-Nuved-Timestamp is the unix second it was sent and X-Nuved-Signature is `v1=` and the hex of HMAC-SHA256 over the timestamp, a full stop and the body, with the secret both services read from TASKS_WEBHOOK_SECRET. A signature that does not match, a timestamp more than 300 s from this console's clock, and anything that came through the edge are refused; the board keeps the delivery and makes it again. Claims, hand-ins, verdicts, closes and drains (drained, undrained) become Activity lines, once per event id, by the agent or person who did them; reports, edits and the rest are taken and dropped. No credential: the signature is the check.",
        "operationId": "receiveTaskEvents",
        "parameters": [
          {
            "name": "X-Nuved-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 1793000000
            }
          },
          {
            "name": "X-Nuved-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "v1=c241da273ba0a2c37c410c2875cbf8e783bbf835399a532d410376b13147ce2a"
            }
          }
        ],
        "requestBody": {
          "description": "a delivery",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "after": {
                    "type": "integer",
                    "description": "the board's cursor before this delivery",
                    "example": 1042
                  },
                  "delivery_id": {
                    "type": "string",
                    "example": "dlv_3f9c2a71b0e4d8c6a5f1e2d3"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "the board's event envelope (nuved-tasks api/openapi.yaml, Event)",
                      "properties": {
                        "action": {
                          "type": "string",
                          "example": "handed_in"
                        },
                        "actor": {
                          "type": "object",
                          "description": "kind human, agent or system; id; name; where"
                        },
                        "at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "data": {
                          "type": "object"
                        },
                        "id": {
                          "type": "string",
                          "example": "ws_qvalvswj3y2snw6efietdufj6o:1043"
                        },
                        "item": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "example": "NPB-120"
                        },
                        "project": {
                          "type": "string",
                          "example": "novid-plans-backend"
                        },
                        "seq": {
                          "type": "integer",
                          "example": 1043
                        }
                      },
                      "required": [
                        "id",
                        "seq",
                        "project",
                        "action",
                        "actor",
                        "at"
                      ]
                    }
                  },
                  "summary": {
                    "type": "object",
                    "description": "instead of events, when the console was more than 10,000 events behind",
                    "properties": {
                      "actions": {
                        "type": "object"
                      },
                      "events": {
                        "type": "integer"
                      }
                    }
                  },
                  "through": {
                    "type": "integer",
                    "description": "the last event this delivery stands for",
                    "example": 1043
                  },
                  "workspace": {
                    "type": "string",
                    "description": "the workspace's fixed id",
                    "example": "ws_qvalvswj3y2snw6efietdufj6o"
                  }
                },
                "required": [
                  "delivery_id",
                  "workspace",
                  "after",
                  "through",
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "taken: how many lines it wrote",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lines": {
                      "type": "integer",
                      "example": 1
                    }
                  },
                  "required": [
                    "lines"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not a task board delivery",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unsigned, a signature that does not match, or a timestamp out of the window",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "it came through the edge",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "more than a delivery carries",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the deliveries are not set up here, or the lines could not be filed just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/addresses": {
      "get": {
        "tags": [
          "Addresses"
        ],
        "summary": "Every public address the workspace holds, with its state in words.",
        "description": "An address is `name-organization.nuved.dev`: a lease while a tunnel from your machine holds it, or a reservation you keep until you release it. A lease whose tunnel disconnects stays yours for 24 hours; a released name stays yours alone for 7 days after that. `state_text` says which, and until when, in the words the console shows. How many reservations the workspace keeps is its plan's (GET /v1/entitlements, `reserved_names`); a reservation costs nothing beyond the plan. `held_until` is set on a reservation held since Pro ended. Times are UTC. Names back in the pool are not listed. Names under `app.fromnovid.com`, the domain before nuved.dev, are listed until they lapse.",
        "operationId": "listAddresses",
        "responses": {
          "200": {
            "description": "the addresses, by host",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one public address and the state it is in",
                    "properties": {
                      "bound_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "when you took it",
                        "example": "2026-09-24T12:00:00Z"
                      },
                      "expires_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when the current state runs out; null while live",
                        "example": "2026-09-25T14:04:00Z"
                      },
                      "held_until": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when a reservation held since the workspace's Pro ended is released; null when it is not held",
                        "example": "2026-10-20T14:30:00Z"
                      },
                      "host": {
                        "type": "string",
                        "example": "shop-novid.nuved.dev"
                      },
                      "kind": {
                        "type": "string",
                        "enum": [
                          "lease",
                          "reservation"
                        ]
                      },
                      "last_seen_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last time its tunnel was heard from; null for a reserved name never connected",
                        "example": "2026-09-24T14:02:00Z"
                      },
                      "project": {
                        "type": "string",
                        "description": "the project it belongs to; empty when none",
                        "example": "novid-shop"
                      },
                      "released_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it was released; null unless quarantined",
                        "example": "2026-09-24T15:00:00Z"
                      },
                      "state": {
                        "type": "string",
                        "description": "live: answering, or reserved; idle: its tunnel is disconnected; quarantined: released, and only you may take it back",
                        "enum": [
                          "live",
                          "idle",
                          "quarantined"
                        ]
                      },
                      "state_text": {
                        "type": "string",
                        "description": "the state as a sentence a person reads",
                        "example": "Connected. This address answers from your machine."
                      }
                    },
                    "required": [
                      "host",
                      "kind",
                      "project",
                      "state",
                      "state_text",
                      "bound_at",
                      "last_seen_at",
                      "expires_at",
                      "released_at",
                      "held_until"
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not list the workspace's addresses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the address registry could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/addresses/{host}": {
      "delete": {
        "tags": [
          "Addresses"
        ],
        "summary": "Give an address up: for 7 days only you can take it back, then it goes back to the pool.",
        "description": "A lease or a reservation. Releasing one already in its quarantine answers with it unchanged, so a retried release does not restart the seven days.",
        "operationId": "releaseAddress",
        "parameters": [
          {
            "name": "host",
            "in": "path",
            "required": true,
            "description": "the whole address, as GET /v1/addresses lists it",
            "schema": {
              "type": "string",
              "example": "shop-novid.nuved.dev"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the address, quarantined, with released_at and expires_at set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one public address and the state it is in",
                  "properties": {
                    "bound_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when you took it",
                      "example": "2026-09-24T12:00:00Z"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the current state runs out; null while live",
                      "example": "2026-09-25T14:04:00Z"
                    },
                    "held_until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when a reservation held since the workspace's Pro ended is released; null when it is not held",
                      "example": "2026-10-20T14:30:00Z"
                    },
                    "host": {
                      "type": "string",
                      "example": "shop-novid.nuved.dev"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "lease",
                        "reservation"
                      ]
                    },
                    "last_seen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last time its tunnel was heard from; null for a reserved name never connected",
                      "example": "2026-09-24T14:02:00Z"
                    },
                    "project": {
                      "type": "string",
                      "description": "the project it belongs to; empty when none",
                      "example": "novid-shop"
                    },
                    "released_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was released; null unless quarantined",
                      "example": "2026-09-24T15:00:00Z"
                    },
                    "state": {
                      "type": "string",
                      "description": "live: answering, or reserved; idle: its tunnel is disconnected; quarantined: released, and only you may take it back",
                      "enum": [
                        "live",
                        "idle",
                        "quarantined"
                      ]
                    },
                    "state_text": {
                      "type": "string",
                      "description": "the state as a sentence a person reads",
                      "example": "Connected. This address answers from your machine."
                    }
                  },
                  "required": [
                    "host",
                    "kind",
                    "project",
                    "state",
                    "state_text",
                    "bound_at",
                    "last_seen_at",
                    "expires_at",
                    "released_at",
                    "held_until"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, which is always refused (a person's sign-in only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "the workspace does not hold that address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the address registry could not be written just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/addresses/{host}/reserve": {
      "post": {
        "tags": [
          "Addresses"
        ],
        "summary": "Keep an address until you release it, whether or not its tunnel is connected.",
        "description": "Reserves a name the workspace holds (a lease, or one in its quarantine, which takes it back), a bare name (`shop.nuved.dev`) nobody holds, or a name the relay gave one of its shares (`novid-k3j5h2m4pq2x.nuved.dev`, or under `fromnovid.com` where the relay named it before; whole, as the relay wrote it; the console never makes a name up), when the relay has linked that tenant to the workspace. A name nobody holds under `app.fromnovid.com`, the domain before nuved.dev, is not reserved; one the workspace holds there still is. The plan decides how many: Free keeps none, Pro keeps three, bare names counted with the rest, for nothing beyond the plan. When Pro ends, the reserved names are held, not answering and not released, for 14 days; Pro again within them gives them back. Reserving a name already reserved answers with it unchanged.",
        "operationId": "reserveAddress",
        "parameters": [
          {
            "name": "host",
            "in": "path",
            "required": true,
            "description": "the whole address, as GET /v1/addresses lists it",
            "schema": {
              "type": "string",
              "example": "shop-novid.nuved.dev"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the address, reserved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one public address and the state it is in",
                  "properties": {
                    "bound_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when you took it",
                      "example": "2026-09-24T12:00:00Z"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the current state runs out; null while live",
                      "example": "2026-09-25T14:04:00Z"
                    },
                    "held_until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when a reservation held since the workspace's Pro ended is released; null when it is not held",
                      "example": "2026-10-20T14:30:00Z"
                    },
                    "host": {
                      "type": "string",
                      "example": "shop-novid.nuved.dev"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "lease",
                        "reservation"
                      ]
                    },
                    "last_seen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last time its tunnel was heard from; null for a reserved name never connected",
                      "example": "2026-09-24T14:02:00Z"
                    },
                    "project": {
                      "type": "string",
                      "description": "the project it belongs to; empty when none",
                      "example": "novid-shop"
                    },
                    "released_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was released; null unless quarantined",
                      "example": "2026-09-24T15:00:00Z"
                    },
                    "state": {
                      "type": "string",
                      "description": "live: answering, or reserved; idle: its tunnel is disconnected; quarantined: released, and only you may take it back",
                      "enum": [
                        "live",
                        "idle",
                        "quarantined"
                      ]
                    },
                    "state_text": {
                      "type": "string",
                      "description": "the state as a sentence a person reads",
                      "example": "Connected. This address answers from your machine."
                    }
                  },
                  "required": [
                    "host",
                    "kind",
                    "project",
                    "state",
                    "state_text",
                    "bound_at",
                    "last_seen_at",
                    "expires_at",
                    "released_at",
                    "held_until"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not an address this platform hands out, or a new name under the domain before nuved.dev",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Free keeps no reserved names: Pro lifts the limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a relay name, and the relay has linked no tenant to this workspace; or an agent token, which is always refused (a person's sign-in only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not available to this workspace: not one it holds (share it first), somebody else's, or not its relay tenant's; which workspace holds a name is never said",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Pro's reserved names are all in use: release one first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the address registry could not be written just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/admin/addresses": {
      "get": {
        "tags": [
          "Back office"
        ],
        "summary": "Every address published on the platform, across every workspace: the operator's only.",
        "description": "The routes the console serves itself (a cluster project's service exposed on nuved.app, a self-hosted project's address) from its own records, and every name the tunnel relay holds, asked workspace by workspace. A relay tenant linked to no workspace is in no answer and so not listed. `relay` says how the relay's half went; a workspace it did not answer for is in `relay_unread`, and the rest of the list is still there. Only a session of the workspace that runs the platform; everybody else is refused.",
        "operationId": "listAllAddresses",
        "responses": {
          "200": {
            "description": "every address, by workspace, then kind, then hostname",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "addresses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "agent": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "a share: the relay agent that owns it; null for the console's own routes",
                            "properties": {
                              "connected": {
                                "type": "boolean",
                                "example": true
                              },
                              "kind": {
                                "type": "string",
                                "example": "machine",
                                "enum": [
                                  "cluster",
                                  "machine"
                                ]
                              },
                              "tunnel_ip": {
                                "type": "string",
                                "example": "10.77.0.7"
                              }
                            }
                          },
                          "auth": {
                            "type": "string",
                            "description": "none; login: the edge asks Authentik first (a hosted address); invite: the share's invited addresses; authentik: the relay asks Authentik; empty for a held share",
                            "example": "authentik"
                          },
                          "held_until": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "a share: when a plan's hold on it ends",
                            "example": "2026-10-09T18:59:30Z"
                          },
                          "hostname": {
                            "type": "string",
                            "description": "empty for a route whose first expose is not carried out yet",
                            "example": "novid-k3j2h1g0f9e8.nuved.dev"
                          },
                          "kept_until": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "a share: when it lapses unless its agent reports it again",
                            "example": "2026-10-09T18:59:30Z"
                          },
                          "kind": {
                            "type": "string",
                            "description": "share: a name the tunnel relay holds; route: a cluster project's service exposed on nuved.app; hosted: a self-hosted project's address on nuved.app",
                            "example": "share",
                            "enum": [
                              "share",
                              "route",
                              "hosted"
                            ]
                          },
                          "label": {
                            "type": "string",
                            "description": "a share: what its agent calls it",
                            "example": "Jaeger"
                          },
                          "last_seen": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "a share: when its agent last reported it; null for a route or a hosted address, which the console records no such moment for",
                            "example": "2026-10-09T18:59:30Z"
                          },
                          "name": {
                            "type": "string",
                            "description": "the service behind a route or hosted address; the share's own name",
                            "example": "jaeger"
                          },
                          "org": {
                            "type": "string",
                            "example": "novid"
                          },
                          "paused": {
                            "type": "boolean",
                            "description": "a share its agent paused, or a route of a workspace the meter paused at zero balance",
                            "example": false
                          },
                          "project": {
                            "type": "string",
                            "description": "the project of a route or a hosted address; empty for a share",
                            "example": ""
                          },
                          "reserved": {
                            "type": "boolean",
                            "description": "a share the workspace keeps",
                            "example": false
                          },
                          "state": {
                            "type": "string",
                            "description": "a share: live, offline (its agent is away) or held (kept, not served); a route: live, pending, failed or removing; a hosted address: registered",
                            "example": "live"
                          },
                          "tenant": {
                            "type": "string",
                            "description": "a share: the relay tenant that holds it",
                            "example": "novid"
                          },
                          "url": {
                            "type": "string",
                            "example": "https://novid-k3j2h1g0f9e8.nuved.dev"
                          }
                        },
                        "required": [
                          "hostname",
                          "url",
                          "org",
                          "project",
                          "kind",
                          "name",
                          "label",
                          "tenant",
                          "auth",
                          "state",
                          "reserved",
                          "paused",
                          "last_seen",
                          "kept_until",
                          "held_until",
                          "agent"
                        ]
                      }
                    },
                    "relay": {
                      "type": "string",
                      "example": "read",
                      "enum": [
                        "read",
                        "off",
                        "unreachable",
                        "partial"
                      ]
                    },
                    "relay_unread": {
                      "type": "array",
                      "description": "workspaces the relay did not answer for",
                      "items": {
                        "type": "string"
                      },
                      "example": []
                    }
                  },
                  "required": [
                    "addresses",
                    "relay",
                    "relay_unread"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "not the workspace that runs the platform, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "whether the caller runs the platform could not be checked just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the console's records could not be read, or the back office is not switched on",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/admin/build-machines": {
      "get": {
        "tags": [
          "Back office"
        ],
        "summary": "The build machines, what each build cost apart from their idle time, the day's spend and the provider calls this hour: the operator's only.",
        "description": "The machines the platform made for waiting builds and their states, each with the provider's price by the hour and what it cost over the time it lived, split into the time it was kept for builds and the rest (idle, priced apart), the started hours the provider bills, each build's share, the day's spend against its cap, the builds still waiting, and the provider calls let through in the last minute and hour against the budget (this process's own count, from `calls.since`). An off or closed controller answers `enabled: false` with `why`. Only a session of the workspace that runs the platform; everybody else is refused.",
        "operationId": "getBuildMachines",
        "responses": {
          "200": {
            "description": "the build machines now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when this was read",
                      "example": "2026-11-06T12:00:00Z"
                    },
                    "builds": {
                      "type": "array",
                      "description": "the builds those machines were given, newest first",
                      "items": {
                        "type": "object",
                        "properties": {
                          "cost": {
                            "type": "string",
                            "description": "The machine's price over that time, prorated. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "customer": {
                            "type": "string",
                            "example": "acme"
                          },
                          "id": {
                            "type": "integer",
                            "description": "the build's deploy id",
                            "example": 7
                          },
                          "machine": {
                            "type": "string",
                            "example": "build-7-1"
                          },
                          "project": {
                            "type": "string",
                            "example": "site"
                          },
                          "running": {
                            "type": "boolean",
                            "description": "still going",
                            "example": false
                          },
                          "started": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the machine was kept for it",
                            "example": "2026-11-06T12:00:00Z"
                          },
                          "status": {
                            "type": "string",
                            "description": "the deploy's: queued, running, live, failed or cancelled",
                            "example": "live"
                          },
                          "took_seconds": {
                            "type": "integer",
                            "description": "until it ended, or until now while it runs",
                            "example": 1200
                          }
                        },
                        "required": [
                          "cost",
                          "customer",
                          "id",
                          "machine",
                          "project",
                          "running",
                          "started",
                          "status",
                          "took_seconds"
                        ]
                      }
                    },
                    "calls": {
                      "type": "object",
                      "properties": {
                        "by_op": {
                          "type": "object",
                          "description": "the last hour's calls by operation (create, list, delete, server_types)",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "held_until": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "when the provider's 429 stops holding calls; null when nothing holds them",
                          "example": "2026-11-06T12:00:00Z"
                        },
                        "in_hour": {
                          "type": "integer",
                          "description": "calls let through in the last hour",
                          "example": 7
                        },
                        "in_minute": {
                          "type": "integer",
                          "description": "calls let through in the last minute",
                          "example": 1
                        },
                        "per_hour": {
                          "type": "integer",
                          "description": "the budget for an hour",
                          "example": 300
                        },
                        "per_minute": {
                          "type": "integer",
                          "description": "the budget for a minute",
                          "example": 30
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time",
                          "description": "when this process began counting; calls before it are not in the figures",
                          "example": "2026-11-06T12:00:00Z"
                        }
                      },
                      "required": [
                        "by_op",
                        "held_until",
                        "in_hour",
                        "in_minute",
                        "per_hour",
                        "per_minute",
                        "since"
                      ]
                    },
                    "caps": {
                      "type": "object",
                      "properties": {
                        "build_org": {
                          "type": "string",
                          "description": "the platform organisation that holds every build machine",
                          "example": "platform"
                        },
                        "fleet": {
                          "type": "string",
                          "description": "the provider label the fleet wears",
                          "example": "build-fleet"
                        },
                        "idle_delete_seconds": {
                          "type": "integer",
                          "description": "an idle machine is deleted after this",
                          "example": 60
                        },
                        "max_hours_per_machine_seconds": {
                          "type": "integer",
                          "description": "a machine older than this takes no more work",
                          "example": 7200
                        },
                        "max_machines": {
                          "type": "integer",
                          "description": "machines alive at once",
                          "example": 3
                        },
                        "max_spend_per_day": {
                          "type": "string",
                          "description": "The most the machines made in a UTC day may cost, a started hour counted for each (5 euro unless set otherwise). In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                          "example": "0.0063",
                          "pattern": "^\\d+\\.\\d{4}$"
                        },
                        "per_org_max": {
                          "type": "integer",
                          "description": "of them, one customer's",
                          "example": 2
                        },
                        "shutdown_grace_seconds": {
                          "type": "integer",
                          "description": "what a machine holding a build has between the stop request and the delete",
                          "example": 120
                        }
                      },
                      "required": [
                        "build_org",
                        "fleet",
                        "idle_delete_seconds",
                        "max_hours_per_machine_seconds",
                        "max_machines",
                        "max_spend_per_day",
                        "per_org_max",
                        "shutdown_grace_seconds"
                      ]
                    },
                    "enabled": {
                      "type": "boolean",
                      "example": true
                    },
                    "machines": {
                      "type": "array",
                      "description": "live ones and those that ended in the last day, newest first",
                      "items": {
                        "type": "object",
                        "properties": {
                          "alive_seconds": {
                            "type": "integer",
                            "description": "from the provider making it to its end, or now",
                            "example": 1800
                          },
                          "billed": {
                            "type": "string",
                            "description": "What the provider bills for those started hours: what to expect on the invoice. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "billed_hours": {
                            "type": "integer",
                            "description": "the started hours the provider bills",
                            "example": 1
                          },
                          "build_cost": {
                            "type": "string",
                            "description": "The builds' share of the time lived, prorated. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "builds": {
                            "type": "integer",
                            "description": "how many builds it was given",
                            "example": 1
                          },
                          "busy_seconds": {
                            "type": "integer",
                            "description": "of that, kept for builds",
                            "example": 1200
                          },
                          "created": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the provider made it",
                            "example": "2026-11-06T12:00:00Z"
                          },
                          "customer": {
                            "type": "string",
                            "description": "the organisation the machine serves, for life",
                            "example": "acme"
                          },
                          "end_why": {
                            "type": "string",
                            "description": "why we deleted it, in plain words (idle for 1 minute); empty for a machine we did not delete or have not decided to",
                            "example": "idle for 1 minute"
                          },
                          "ended": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "when it was deleted or failed; null while it lives",
                            "example": "2026-11-06T12:00:00Z"
                          },
                          "hourly": {
                            "type": "string",
                            "description": "The price by the hour. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "idle_cost": {
                            "type": "string",
                            "description": "The idle share of the time lived, prorated, priced apart from the builds. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "idle_seconds": {
                            "type": "integer",
                            "description": "the rest: making, joining, waiting for the next build, being deleted",
                            "example": 600
                          },
                          "location": {
                            "type": "string",
                            "example": "nbg1"
                          },
                          "name": {
                            "type": "string",
                            "example": "build-7-1"
                          },
                          "purpose": {
                            "type": "string",
                            "description": "what the machine is for, read from its record: build, or service",
                            "example": "build",
                            "enum": [
                              "build",
                              "service"
                            ]
                          },
                          "reason": {
                            "type": "string",
                            "description": "why it failed, was removed at the provider, or is late being deleted; empty otherwise",
                            "example": ""
                          },
                          "state": {
                            "type": "string",
                            "description": "requested, created, joined, busy, idle, deleting, gone or failed",
                            "example": "busy",
                            "enum": [
                              "requested",
                              "created",
                              "joined",
                              "busy",
                              "idle",
                              "deleting",
                              "gone",
                              "failed"
                            ]
                          },
                          "type": {
                            "type": "string",
                            "description": "the provider's server type",
                            "example": "cpx22"
                          }
                        },
                        "required": [
                          "alive_seconds",
                          "billed",
                          "billed_hours",
                          "build_cost",
                          "builds",
                          "busy_seconds",
                          "created",
                          "customer",
                          "end_why",
                          "ended",
                          "hourly",
                          "idle_cost",
                          "idle_seconds",
                          "location",
                          "name",
                          "purpose",
                          "reason",
                          "state",
                          "type"
                        ]
                      }
                    },
                    "oldest_wait": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the oldest began to wait; null when none waits",
                      "example": "2026-11-06T12:00:00Z"
                    },
                    "running": {
                      "type": "integer",
                      "description": "machines that have not ended, the count the machine cap is held to",
                      "example": 2
                    },
                    "spend": {
                      "type": "object",
                      "properties": {
                        "builds": {
                          "type": "string",
                          "description": "Today's machines, prorated: the builds' share. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                          "example": "0.0063",
                          "pattern": "^\\d+\\.\\d{4}$"
                        },
                        "cap": {
                          "type": "string",
                          "description": "The cap. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                          "example": "0.0063",
                          "pattern": "^\\d+\\.\\d{4}$"
                        },
                        "counted": {
                          "type": "string",
                          "description": "What the day's cap counts: a started hour of each machine made today. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                          "example": "0.0063",
                          "pattern": "^\\d+\\.\\d{4}$"
                        },
                        "idle": {
                          "type": "string",
                          "description": "Today's machines, prorated: the idle share. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                          "example": "0.0063",
                          "pattern": "^\\d+\\.\\d{4}$"
                        }
                      },
                      "required": [
                        "builds",
                        "cap",
                        "counted",
                        "idle"
                      ]
                    },
                    "strays": {
                      "type": "array",
                      "description": "servers at the provider that wear the fleet label and that no record of the controller's stands behind: it leaves them alone and a person decides",
                      "items": {
                        "type": "object",
                        "properties": {
                          "created": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the provider made it",
                            "example": "2026-11-06T12:00:00Z"
                          },
                          "name": {
                            "type": "string",
                            "example": "build-12-1"
                          },
                          "purpose": {
                            "type": "string",
                            "description": "the purpose label the server wears",
                            "example": "build"
                          },
                          "server_id": {
                            "type": "integer",
                            "description": "the provider's id of the server",
                            "example": 900000097
                          }
                        },
                        "required": [
                          "created",
                          "name",
                          "purpose",
                          "server_id"
                        ]
                      }
                    },
                    "strays_error": {
                      "type": "string",
                      "description": "when the provider's list of the fleet could not be read: the sentence that says so; empty otherwise",
                      "example": ""
                    },
                    "waiting": {
                      "type": "integer",
                      "description": "builds with no machine yet",
                      "example": 1
                    },
                    "why": {
                      "type": "string",
                      "description": "when not enabled: the sentence that says why",
                      "example": ""
                    }
                  },
                  "required": [
                    "at",
                    "enabled",
                    "why",
                    "caps",
                    "machines",
                    "builds",
                    "waiting",
                    "running",
                    "oldest_wait",
                    "spend",
                    "calls",
                    "strays",
                    "strays_error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "not the workspace that runs the platform, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "whether the caller runs the platform could not be checked just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the build machines could not be read, or are not wired here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/admin/build-machines/events": {
      "get": {
        "tags": [
          "Back office"
        ],
        "summary": "The history of the build machines, newest first: every machine made or deleted and the reason, the operator's only.",
        "description": "Every step in a build machine's life, and every create or delete that was refused or failed, each with the plain sentence the service log holds for it: why a machine was made, when it joined, which build it took, why it was deleted, how long it lived and what it cost. Kept for at least 30 days. A page begins with the newest; when `next_before` is not null, ask again with `before` set to it for the older events. Only a session of the workspace that runs the platform; everybody else is refused.",
        "operationId": "getBuildMachineEvents",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "how many events, 1 or more; at most 200 (more is 200); 50 when not given",
            "schema": {
              "type": "integer",
              "example": 50
            }
          },
          {
            "name": "before",
            "in": "query",
            "description": "begin with the event before the one with this id (the `next_before` of the page before); the newest when not given",
            "schema": {
              "type": "integer",
              "example": 41
            }
          }
        ],
        "responses": {
          "200": {
            "description": "a page of the history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "description": "newest first",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the step happened",
                            "example": "2026-11-09T10:31:00Z"
                          },
                          "billed_hours": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "the started hours the provider bills for it; null unless the machine was deleted",
                            "example": 1
                          },
                          "build": {
                            "type": "integer",
                            "description": "the build the step is about; 0 where none",
                            "example": 101
                          },
                          "cap": {
                            "type": "string",
                            "description": "for create_refused: which cap held the build (machines, organisation, day or calls); empty otherwise",
                            "example": ""
                          },
                          "cost": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "What those started hours cost at its hourly price; null unless the machine was deleted. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "customer": {
                            "type": "string",
                            "description": "the organisation the machine serves, for life",
                            "example": "acme"
                          },
                          "hourly": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The machine's price by the hour; null where no machine is named. In euro, net of VAT, as an exact decimal of four places; the provider's price, never a selling price.",
                            "example": "0.0063",
                            "pattern": "^\\d+\\.\\d{4}$"
                          },
                          "id": {
                            "type": "integer",
                            "description": "the number the history gave the event, rising with each: the cursor `before` takes",
                            "example": 41
                          },
                          "kind": {
                            "type": "string",
                            "description": "made, joined, took_build, idle, deleted (the steps of a machine's life, once each per machine and build); create_refused (a build waits because a cap holds it), create_failed (a machine could not be made), delete_refused (our own checks refused the delete), delete_failed (the provider refused it)",
                            "example": "deleted",
                            "enum": [
                              "made",
                              "joined",
                              "took_build",
                              "idle",
                              "deleted",
                              "create_refused",
                              "create_failed",
                              "delete_refused",
                              "delete_failed"
                            ]
                          },
                          "lived_seconds": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "how long the machine lived, to the nearest second; null unless the machine was deleted",
                            "example": 1561
                          },
                          "location": {
                            "type": "string",
                            "example": "nbg1"
                          },
                          "machine": {
                            "type": "string",
                            "description": "the machine's name; empty for a create that never named one",
                            "example": "build-101-1"
                          },
                          "project": {
                            "type": "string",
                            "description": "the build's project; empty where the controller does not know it",
                            "example": "shop"
                          },
                          "sentence": {
                            "type": "string",
                            "description": "the one plain line the service log holds for the step",
                            "example": "machine build-101-1 deleted: idle for 1 minute; it lived 26 minutes 1 second and cost 0.0063 euro"
                          },
                          "server_id": {
                            "type": "integer",
                            "description": "the provider's id of the server; 0 where there is none",
                            "example": 900000001
                          },
                          "type": {
                            "type": "string",
                            "description": "the provider's server type it was made as; empty where there is no machine",
                            "example": "cpx22"
                          },
                          "why": {
                            "type": "string",
                            "description": "the reason in words: the rung's for a made machine, what ended a deleted one, the refusal's or the failure's sentence",
                            "example": "idle for 1 minute"
                          }
                        },
                        "required": [
                          "at",
                          "billed_hours",
                          "build",
                          "cap",
                          "cost",
                          "customer",
                          "hourly",
                          "id",
                          "kind",
                          "lived_seconds",
                          "location",
                          "machine",
                          "project",
                          "sentence",
                          "server_id",
                          "type",
                          "why"
                        ]
                      }
                    },
                    "next_before": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "the id to pass as `before` for the older events; null when this page is the last",
                      "example": 41
                    }
                  },
                  "required": [
                    "events",
                    "next_before"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "limit or before is not a whole number of 1 or more",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not the workspace that runs the platform, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "whether the caller runs the platform could not be checked just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the history could not be read, or the machines are not wired here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/alerts": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "The spend alerts: a letter when few days of balance are left, and one under a floor.",
        "description": "Both are on until somebody changes them: 3 days, and €5. null is that alert off. Each letter goes to the owner and every member, at most once a day.",
        "operationId": "getAlerts",
        "responses": {
          "200": {
            "description": "the alerts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alerts"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the alerts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Billing"
        ],
        "summary": "Set both spend alerts at once; null turns one off.",
        "description": "An owner's alone (decision 2026-10-21-roles-who-organises-people). Both keys are required: a key left out is refused rather than read as off, so a client that knows only one alert cannot switch the other off by accident.",
        "operationId": "putAlerts",
        "requestBody": {
          "description": "both alerts",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "days_left": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "1 to 30, or null for off",
                    "example": 3
                  },
                  "floor_micro": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "1000000 (€1) to 500000000 (€500), or null for off",
                    "example": 5000000
                  }
                },
                "required": [
                  "days_left",
                  "floor_micro"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the alerts as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Alerts"
                }
              }
            }
          },
          "400": {
            "description": "a key missing, or a value out of range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner: `Only an owner of \u003corg\u003e changes the billing.`; or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/artifacts": {
      "get": {
        "tags": [
          "Artifacts"
        ],
        "summary": "The workspace's artifacts whose files are still there, newest first.",
        "description": "Up to 200. A removed or expired artifact leaves the list; its record stays readable by id. With `task`, only those published for that board item, superseded versions among them.",
        "operationId": "listArtifacts",
        "parameters": [
          {
            "name": "task",
            "in": "query",
            "description": "a board item's full id: only its artifacts",
            "schema": {
              "type": "string",
              "example": "NPB-161"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the artifacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "a file or folder made to be looked at, and its address",
                    "properties": {
                      "bytes": {
                        "type": "integer"
                      },
                      "commit": {
                        "type": "string",
                        "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                        "example": "13e4c4b2"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "created_by": {
                        "type": "string",
                        "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                      },
                      "decided_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "decided_by": {
                        "type": "string"
                      },
                      "decision": {
                        "type": "string",
                        "description": "the owner's word, empty until given",
                        "enum": [
                          "",
                          "confirmed",
                          "rejected"
                        ]
                      },
                      "deleted_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "set once the file is gone; the record stays"
                      },
                      "deleted_why": {
                        "type": "string",
                        "description": "expired, or removed by `\u003caddress\u003e`"
                      },
                      "entry": {
                        "type": "string",
                        "description": "the file that answers at the address: index.html of a folder, or the one file",
                        "example": "index.html"
                      },
                      "expires_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                      },
                      "files": {
                        "type": "integer"
                      },
                      "id": {
                        "type": "string",
                        "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                        "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                      },
                      "pinned": {
                        "type": "boolean"
                      },
                      "project": {
                        "type": "string",
                        "description": "the board project of the task it was published for; empty on a loose one",
                        "example": "novid-plans-backend"
                      },
                      "purpose": {
                        "type": "string",
                        "description": "what it is to the task; empty on a loose one",
                        "enum": [
                          "",
                          "directions",
                          "mock",
                          "screenshot",
                          "report",
                          "evidence"
                        ]
                      },
                      "sha256": {
                        "type": "string",
                        "description": "of the bytes as they were received: the one file, or the tar"
                      },
                      "superseded_by": {
                        "type": "string",
                        "description": "the newer version of the same purpose on the task that replaced it"
                      },
                      "task": {
                        "type": "string",
                        "description": "the board item it was published for, its full id; empty on a loose one",
                        "example": "NPB-161"
                      },
                      "task_closed_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                      },
                      "title": {
                        "type": "string",
                        "description": "as published, or the file's name"
                      },
                      "url": {
                        "type": "string",
                        "description": "where the workspace's people open it",
                        "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                      }
                    },
                    "required": [
                      "id",
                      "url",
                      "title",
                      "entry",
                      "files",
                      "bytes",
                      "sha256",
                      "created_by",
                      "created_at",
                      "expires_at",
                      "pinned",
                      "decision",
                      "project",
                      "task",
                      "purpose"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "task is not a board item's full id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a person with project roles only: artifacts are the workspace's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such artifact in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Publish a file or folder to be looked at, and get its address.",
        "description": "An HTML mock, a report, a screenshot: one file, or a folder sent as a tar with index.html at its top, up to 15 MiB and 500 files. The answer carries the address, `https://artifacts.nuved.app/\u003cid\u003e/`, which opens for the workspace's people after the console's sign-in and for nobody else: the id is 128 random bits made here, there is no index, and a wrong id answers the same 404 as another workspace's. The artifact expires after `days` (14 unless said, at most 90); the owner's confirm or reject brings that to seven days after the word; a pin holds it. Send multipart/form-data with the text fields first and the file last. A person's session or an agent token may publish; a workspace holds at most 200 artifacts and 500 MiB; evidence kept for a done task counts toward the 500 MiB, not the 200. (~/claude/pm/decisions/2026-10-11-artifacts.md)\n\nOn a task: with `task` and `purpose` (and `project` for a person; an agent token's is its own) the artifact is linked to that board item before it is kept, and stays while the item is open, at most 90 days. Evidence stays with the task once it is done, until the task is deleted on the board or the file is removed; other files, and evidence on a cancelled task, go seven days after the close. The owner's word does not shorten that. A task the board does not take (another project's, another workspace's, none) is 404 and nothing is kept; a board that does not answer is 503 and nothing is kept. `replaces` marks an earlier artifact of the same purpose on the item superseded. A publish without `task` never asks the board. (plan 2026-10-25-task-artifacts)",
        "operationId": "publishArtifact",
        "requestBody": {
          "description": "the fields, then the file",
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "integer",
                    "description": "how long it stays, 1 to 90; 14 when empty",
                    "example": 14
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "the file, or the folder as a tar; last"
                  },
                  "kind": {
                    "type": "string",
                    "description": "tar for a folder; taken from a .tar name when empty",
                    "enum": [
                      "file",
                      "tar"
                    ]
                  },
                  "project": {
                    "type": "string",
                    "description": "the board project of the task, one the credential reaches; an agent token's own when empty",
                    "example": "novid-plans-backend"
                  },
                  "purpose": {
                    "type": "string",
                    "description": "what it is to the task; with task",
                    "enum": [
                      "directions",
                      "mock",
                      "screenshot",
                      "report",
                      "evidence"
                    ]
                  },
                  "replaces": {
                    "type": "string",
                    "description": "the id of an earlier artifact of the same purpose on the task, which this one supersedes"
                  },
                  "task": {
                    "type": "string",
                    "description": "the board item it was made for, its full id; with purpose",
                    "example": "NPB-161"
                  },
                  "title": {
                    "type": "string",
                    "description": "what to call it, up to 120 characters; the file's name when empty"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the artifact, with its address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a file or folder made to be looked at, and its address",
                  "properties": {
                    "bytes": {
                      "type": "integer"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                      "example": "13e4c4b2"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "decided_by": {
                      "type": "string"
                    },
                    "decision": {
                      "type": "string",
                      "description": "the owner's word, empty until given",
                      "enum": [
                        "",
                        "confirmed",
                        "rejected"
                      ]
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "set once the file is gone; the record stays"
                    },
                    "deleted_why": {
                      "type": "string",
                      "description": "expired, or removed by `\u003caddress\u003e`"
                    },
                    "entry": {
                      "type": "string",
                      "description": "the file that answers at the address: index.html of a folder, or the one file",
                      "example": "index.html"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                    },
                    "files": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                      "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                    },
                    "pinned": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string",
                      "description": "the board project of the task it was published for; empty on a loose one",
                      "example": "novid-plans-backend"
                    },
                    "purpose": {
                      "type": "string",
                      "description": "what it is to the task; empty on a loose one",
                      "enum": [
                        "",
                        "directions",
                        "mock",
                        "screenshot",
                        "report",
                        "evidence"
                      ]
                    },
                    "sha256": {
                      "type": "string",
                      "description": "of the bytes as they were received: the one file, or the tar"
                    },
                    "superseded_by": {
                      "type": "string",
                      "description": "the newer version of the same purpose on the task that replaced it"
                    },
                    "task": {
                      "type": "string",
                      "description": "the board item it was published for, its full id; empty on a loose one",
                      "example": "NPB-161"
                    },
                    "task_closed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                    },
                    "title": {
                      "type": "string",
                      "description": "as published, or the file's name"
                    },
                    "url": {
                      "type": "string",
                      "description": "where the workspace's people open it",
                      "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "title",
                    "entry",
                    "files",
                    "bytes",
                    "sha256",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "pinned",
                    "decision",
                    "project",
                    "task",
                    "purpose"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not multipart, no file, two files, a bad name or path, a folder without index.html, days out of range; a task without purpose, a bad purpose, days with a task, replaces that is not an earlier one of the task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a person with project roles only: artifacts are the workspace's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a task the board does not take, or a project the credential does not reach: nothing was kept",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the workspace holds the most artifacts, or the most bytes, it may; evidence kept for a done task counts toward the bytes, not the artifacts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "more than 15 MiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, the database did not answer, or, with a task, the board did not take the link: nothing was kept",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/artifacts/{id}": {
      "delete": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Remove an artifact's file now.",
        "description": "The address stops answering at once; the record stays, saying who removed it. A person's decision, not an agent's.",
        "operationId": "deleteArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the artifact's id, as POST and GET /v1/artifacts answer it",
            "schema": {
              "type": "string",
              "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the record, with deleted_at",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a file or folder made to be looked at, and its address",
                  "properties": {
                    "bytes": {
                      "type": "integer"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                      "example": "13e4c4b2"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "decided_by": {
                      "type": "string"
                    },
                    "decision": {
                      "type": "string",
                      "description": "the owner's word, empty until given",
                      "enum": [
                        "",
                        "confirmed",
                        "rejected"
                      ]
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "set once the file is gone; the record stays"
                    },
                    "deleted_why": {
                      "type": "string",
                      "description": "expired, or removed by `\u003caddress\u003e`"
                    },
                    "entry": {
                      "type": "string",
                      "description": "the file that answers at the address: index.html of a folder, or the one file",
                      "example": "index.html"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                    },
                    "files": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                      "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                    },
                    "pinned": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string",
                      "description": "the board project of the task it was published for; empty on a loose one",
                      "example": "novid-plans-backend"
                    },
                    "purpose": {
                      "type": "string",
                      "description": "what it is to the task; empty on a loose one",
                      "enum": [
                        "",
                        "directions",
                        "mock",
                        "screenshot",
                        "report",
                        "evidence"
                      ]
                    },
                    "sha256": {
                      "type": "string",
                      "description": "of the bytes as they were received: the one file, or the tar"
                    },
                    "superseded_by": {
                      "type": "string",
                      "description": "the newer version of the same purpose on the task that replaced it"
                    },
                    "task": {
                      "type": "string",
                      "description": "the board item it was published for, its full id; empty on a loose one",
                      "example": "NPB-161"
                    },
                    "task_closed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                    },
                    "title": {
                      "type": "string",
                      "description": "as published, or the file's name"
                    },
                    "url": {
                      "type": "string",
                      "description": "where the workspace's people open it",
                      "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "title",
                    "entry",
                    "files",
                    "bytes",
                    "sha256",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "pinned",
                    "decision",
                    "project",
                    "task",
                    "purpose"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person with project roles only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such artifact in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "get": {
        "tags": [
          "Artifacts"
        ],
        "summary": "One artifact's record, which outlives its file.",
        "description": "After the file expired or was removed, the record still says what it was, who published it, what the owner decided and when, and why the file went.",
        "operationId": "getArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the artifact's id, as POST and GET /v1/artifacts answer it",
            "schema": {
              "type": "string",
              "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the artifact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a file or folder made to be looked at, and its address",
                  "properties": {
                    "bytes": {
                      "type": "integer"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                      "example": "13e4c4b2"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "decided_by": {
                      "type": "string"
                    },
                    "decision": {
                      "type": "string",
                      "description": "the owner's word, empty until given",
                      "enum": [
                        "",
                        "confirmed",
                        "rejected"
                      ]
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "set once the file is gone; the record stays"
                    },
                    "deleted_why": {
                      "type": "string",
                      "description": "expired, or removed by `\u003caddress\u003e`"
                    },
                    "entry": {
                      "type": "string",
                      "description": "the file that answers at the address: index.html of a folder, or the one file",
                      "example": "index.html"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                    },
                    "files": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                      "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                    },
                    "pinned": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string",
                      "description": "the board project of the task it was published for; empty on a loose one",
                      "example": "novid-plans-backend"
                    },
                    "purpose": {
                      "type": "string",
                      "description": "what it is to the task; empty on a loose one",
                      "enum": [
                        "",
                        "directions",
                        "mock",
                        "screenshot",
                        "report",
                        "evidence"
                      ]
                    },
                    "sha256": {
                      "type": "string",
                      "description": "of the bytes as they were received: the one file, or the tar"
                    },
                    "superseded_by": {
                      "type": "string",
                      "description": "the newer version of the same purpose on the task that replaced it"
                    },
                    "task": {
                      "type": "string",
                      "description": "the board item it was published for, its full id; empty on a loose one",
                      "example": "NPB-161"
                    },
                    "task_closed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                    },
                    "title": {
                      "type": "string",
                      "description": "as published, or the file's name"
                    },
                    "url": {
                      "type": "string",
                      "description": "where the workspace's people open it",
                      "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "title",
                    "entry",
                    "files",
                    "bytes",
                    "sha256",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "pinned",
                    "decision",
                    "project",
                    "task",
                    "purpose"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "a person with project roles only: artifacts are the workspace's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such artifact in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/artifacts/{id}/decision": {
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Confirm or reject an artifact: the owner's word on a mock.",
        "description": "Records the decision and brings the expiry to seven days from now (unless pinned, or sooner already): the work is built against it and compared, then the file goes by itself. A second word replaces the first.",
        "operationId": "decideArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the artifact's id, as POST and GET /v1/artifacts answer it",
            "schema": {
              "type": "string",
              "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
            }
          }
        ],
        "requestBody": {
          "description": "the decision",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "confirmed",
                      "rejected"
                    ]
                  }
                },
                "required": [
                  "decision"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the artifact, with its decision and new expiry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a file or folder made to be looked at, and its address",
                  "properties": {
                    "bytes": {
                      "type": "integer"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                      "example": "13e4c4b2"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "decided_by": {
                      "type": "string"
                    },
                    "decision": {
                      "type": "string",
                      "description": "the owner's word, empty until given",
                      "enum": [
                        "",
                        "confirmed",
                        "rejected"
                      ]
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "set once the file is gone; the record stays"
                    },
                    "deleted_why": {
                      "type": "string",
                      "description": "expired, or removed by `\u003caddress\u003e`"
                    },
                    "entry": {
                      "type": "string",
                      "description": "the file that answers at the address: index.html of a folder, or the one file",
                      "example": "index.html"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                    },
                    "files": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                      "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                    },
                    "pinned": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string",
                      "description": "the board project of the task it was published for; empty on a loose one",
                      "example": "novid-plans-backend"
                    },
                    "purpose": {
                      "type": "string",
                      "description": "what it is to the task; empty on a loose one",
                      "enum": [
                        "",
                        "directions",
                        "mock",
                        "screenshot",
                        "report",
                        "evidence"
                      ]
                    },
                    "sha256": {
                      "type": "string",
                      "description": "of the bytes as they were received: the one file, or the tar"
                    },
                    "superseded_by": {
                      "type": "string",
                      "description": "the newer version of the same purpose on the task that replaced it"
                    },
                    "task": {
                      "type": "string",
                      "description": "the board item it was published for, its full id; empty on a loose one",
                      "example": "NPB-161"
                    },
                    "task_closed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                    },
                    "title": {
                      "type": "string",
                      "description": "as published, or the file's name"
                    },
                    "url": {
                      "type": "string",
                      "description": "where the workspace's people open it",
                      "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "title",
                    "entry",
                    "files",
                    "bytes",
                    "sha256",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "pinned",
                    "decision",
                    "project",
                    "task",
                    "purpose"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or a decision that is neither confirmed nor rejected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person with project roles only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such artifact in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/artifacts/{id}/pin": {
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Keep an artifact past its expiry, or let it go again.",
        "description": "Pinned, expires_at is null and the purge leaves it. Unpinned, it gets a fresh life from now: seven days when the owner has decided, fourteen otherwise. Evidence kept for its done task stays kept either way: the keep is the task's, the pin a person's.",
        "operationId": "pinArtifact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the artifact's id, as POST and GET /v1/artifacts answer it",
            "schema": {
              "type": "string",
              "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
            }
          }
        ],
        "requestBody": {
          "description": "pinned or not",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pinned": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "pinned"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the artifact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a file or folder made to be looked at, and its address",
                  "properties": {
                    "bytes": {
                      "type": "integer"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the commit the publisher's repository stood at when it was made, 7 to 40 hex; absent when not said",
                      "example": "13e4c4b2"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`"
                    },
                    "decided_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "decided_by": {
                      "type": "string"
                    },
                    "decision": {
                      "type": "string",
                      "description": "the owner's word, empty until given",
                      "enum": [
                        "",
                        "confirmed",
                        "rejected"
                      ]
                    },
                    "deleted_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "set once the file is gone; the record stays"
                    },
                    "deleted_why": {
                      "type": "string",
                      "description": "expired, or removed by `\u003caddress\u003e`"
                    },
                    "entry": {
                      "type": "string",
                      "description": "the file that answers at the address: index.html of a folder, or the one file",
                      "example": "index.html"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the file goes by itself; null while pinned, and for evidence on a task that is done, which stays with the task"
                    },
                    "files": {
                      "type": "integer"
                    },
                    "id": {
                      "type": "string",
                      "description": "26 base32 characters of 128 random bits, made here; the whole secret of the address",
                      "example": "k7m2qp8xa4w3j6n9r5t2y8c4e6"
                    },
                    "pinned": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string",
                      "description": "the board project of the task it was published for; empty on a loose one",
                      "example": "novid-plans-backend"
                    },
                    "purpose": {
                      "type": "string",
                      "description": "what it is to the task; empty on a loose one",
                      "enum": [
                        "",
                        "directions",
                        "mock",
                        "screenshot",
                        "report",
                        "evidence"
                      ]
                    },
                    "sha256": {
                      "type": "string",
                      "description": "of the bytes as they were received: the one file, or the tar"
                    },
                    "superseded_by": {
                      "type": "string",
                      "description": "the newer version of the same purpose on the task that replaced it"
                    },
                    "task": {
                      "type": "string",
                      "description": "the board item it was published for, its full id; empty on a loose one",
                      "example": "NPB-161"
                    },
                    "task_closed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the board said the task closed: evidence stays with the task once it is done; other files go seven days after the close, at the latest 90 days after publishing"
                    },
                    "title": {
                      "type": "string",
                      "description": "as published, or the file's name"
                    },
                    "url": {
                      "type": "string",
                      "description": "where the workspace's people open it",
                      "example": "https://artifacts.nuved.app/k7m2qp8xa4w3j6n9r5t2y8c4e6/"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "title",
                    "entry",
                    "files",
                    "bytes",
                    "sha256",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "pinned",
                    "decision",
                    "project",
                    "task",
                    "purpose"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person with project roles only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such artifact in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "artifacts are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/auth": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Where to sign in: the identity provider, and which client to be.",
        "description": "The nuved CLI asks this before running the device flow, so the issuer and the client id live in one place — here — rather than in every program that signs somebody in. No credential: a caller that has one does not need this.",
        "operationId": "signInInfo",
        "responses": {
          "200": {
            "description": "where to sign in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignIn"
                }
              }
            }
          },
          "503": {
            "description": "no identity provider is configured, or it did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/auto-topup": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Automatic top-ups: when the balance tops itself up, by how much, and at most how much a month.",
        "description": "Off until somebody turns them on, at the defaults: below 3 days of spend, add €25, at most €100 a month. `threshold_now` and `amount_now` are what days of spend come to at today's rate. `state` is off, on, declined, card (a decline that needs another card), needs_you (the bank wants the card holder) or stopped (3 failures in a row), and `banner` says it in a sentence. An agent token is refused, and the refusal is on the activity list.",
        "operationId": "getAutoTopUp",
        "responses": {
          "200": {
            "description": "the automatic top-ups",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoTopUp"
                }
              }
            }
          },
          "403": {
            "description": "Refused: agent tokens can't see automatic top-ups",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Billing"
        ],
        "summary": "Change automatic top-ups, or turn them off.",
        "description": "Every key is required. A higher monthly limit takes effect 24 hours after it is set; a lower one, or turning them off, at once. Every member gets a letter about every change. Turning them on is not done here: Billing in the console asks Stripe for the card first.",
        "operationId": "putAutoTopUp",
        "requestBody": {
          "description": "the settings",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_kind": {
                    "type": "string",
                    "description": "a fixed amount, or enough for days of spend",
                    "example": "euros",
                    "enum": [
                      "euros",
                      "days"
                    ]
                  },
                  "amount_value": {
                    "type": "integer",
                    "description": "€10 to €500, or 1 to 30 days (the amount is then held to €10 to €500)",
                    "example": 25
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "false turns them off; true keeps them on",
                    "example": true
                  },
                  "monthly_limit_micro": {
                    "type": "integer",
                    "description": "whole euros from €10 to €2,000, in micro-euros",
                    "example": 100000000
                  },
                  "threshold_kind": {
                    "type": "string",
                    "description": "a fixed line, or days of spend at today's rate",
                    "example": "days",
                    "enum": [
                      "euros",
                      "days"
                    ]
                  },
                  "threshold_value": {
                    "type": "integer",
                    "description": "€2 to €500, or 1 to 30 days",
                    "example": 3
                  }
                },
                "required": [
                  "enabled",
                  "threshold_kind",
                  "threshold_value",
                  "amount_kind",
                  "amount_value",
                  "monthly_limit_micro"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the automatic top-ups as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoTopUp"
                }
              }
            }
          },
          "400": {
            "description": "a key missing, or a value out of range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused: agent tokens can't change automatic top-ups",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "turning them on needs a card saved through Stripe, or there is no balance yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/billing/history": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Money in and out, newest first: one row per day of usage, and every top-up with its receipt.",
        "description": "A day's usage is one row however many ten-minute charges it took. A top-up carries `receipt_url`, Stripe's own receipt for the payment; every other row has it empty.",
        "operationId": "getBillingHistory",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "how many rows: 1 to 365, default 60",
            "schema": {
              "type": "integer",
              "example": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/HistoryRow"
                  }
                }
              }
            }
          },
          "400": {
            "description": "limit is not a whole number from 1 to 365",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the workspace's spending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/board/{rest}": {
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Delete an item, or a dependency, on the task board.",
        "description": "The console's own pages reach the task board here: the request goes on to the board's `/v1/tasks/{rest}` with the session as the bearer, and the board's answer comes back as it is. The board decides who may do what (its contract is nuved-tasks api/openapi.yaml); a session is all the console asks for. A write carrying the session cookie must come from the console's own pages, as every write does. CLI and agent tokens call the board at /v1/tasks/ directly.",
        "operationId": "boardDelete",
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "description": "the board's path after /v1/tasks/, slashes and all: `novid/projects/novid-shop/items`, `novid/projects/novid-shop/items/GG-142/rank`. Each segment is a plain name; anything else is 404 here",
            "schema": {
              "type": "string",
              "example": "novid/projects/novid-shop/items"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the board's answer, as it gave it; any of its statuses, with its sentence, comes back the same way"
          },
          "401": {
            "description": "no session, or one that has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a write from another site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a path the board cannot have, or the board's own not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the task board did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Read the task board as the person signed in: items, claims, links, events.",
        "description": "The console's own pages reach the task board here: the request goes on to the board's `/v1/tasks/{rest}` with the session as the bearer, and the board's answer comes back as it is. The board decides who may do what (its contract is nuved-tasks api/openapi.yaml); a session is all the console asks for. A write carrying the session cookie must come from the console's own pages, as every write does. CLI and agent tokens call the board at /v1/tasks/ directly.",
        "operationId": "boardGet",
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "description": "the board's path after /v1/tasks/, slashes and all: `novid/projects/novid-shop/items`, `novid/projects/novid-shop/items/GG-142/rank`. Each segment is a plain name; anything else is 404 here",
            "schema": {
              "type": "string",
              "example": "novid/projects/novid-shop/items"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the board's answer, as it gave it; any of its statuses, with its sentence, comes back the same way"
          },
          "401": {
            "description": "no session, or one that has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a write from another site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a path the board cannot have, or the board's own not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the task board did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Change an item's fields, or a project's board settings, on the task board.",
        "description": "The console's own pages reach the task board here: the request goes on to the board's `/v1/tasks/{rest}` with the session as the bearer, and the board's answer comes back as it is. The board decides who may do what (its contract is nuved-tasks api/openapi.yaml); a session is all the console asks for. A write carrying the session cookie must come from the console's own pages, as every write does. CLI and agent tokens call the board at /v1/tasks/ directly.",
        "operationId": "boardPatch",
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "description": "the board's path after /v1/tasks/, slashes and all: `novid/projects/novid-shop/items`, `novid/projects/novid-shop/items/GG-142/rank`. Each segment is a plain name; anything else is 404 here",
            "schema": {
              "type": "string",
              "example": "novid/projects/novid-shop/items"
            }
          }
        ],
        "requestBody": {
          "description": "the body the board's route takes",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the board's answer, as it gave it; any of its statuses, with its sentence, comes back the same way"
          },
          "401": {
            "description": "no session, or one that has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a write from another site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a path the board cannot have, or the board's own not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the task board did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "A change on the task board as the person signed in: make an item, rank, a status verb, a claim.",
        "description": "The console's own pages reach the task board here: the request goes on to the board's `/v1/tasks/{rest}` with the session as the bearer, and the board's answer comes back as it is. The board decides who may do what (its contract is nuved-tasks api/openapi.yaml); a session is all the console asks for. A write carrying the session cookie must come from the console's own pages, as every write does. CLI and agent tokens call the board at /v1/tasks/ directly.",
        "operationId": "boardPost",
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "description": "the board's path after /v1/tasks/, slashes and all: `novid/projects/novid-shop/items`, `novid/projects/novid-shop/items/GG-142/rank`. Each segment is a plain name; anything else is 404 here",
            "schema": {
              "type": "string",
              "example": "novid/projects/novid-shop/items"
            }
          }
        ],
        "requestBody": {
          "description": "the body the board's route takes",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the board's answer, as it gave it; any of its statuses, with its sentence, comes back the same way"
          },
          "401": {
            "description": "no session, or one that has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a write from another site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a path the board cannot have, or the board's own not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the task board did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Projects"
        ],
        "summary": "Change a role of the workspace on the task board.",
        "description": "The console's own pages reach the task board here: the request goes on to the board's `/v1/tasks/{rest}` with the session as the bearer, and the board's answer comes back as it is. The board decides who may do what (its contract is nuved-tasks api/openapi.yaml); a session is all the console asks for. A write carrying the session cookie must come from the console's own pages, as every write does. CLI and agent tokens call the board at /v1/tasks/ directly.",
        "operationId": "boardPut",
        "parameters": [
          {
            "name": "rest",
            "in": "path",
            "required": true,
            "description": "the board's path after /v1/tasks/, slashes and all: `novid/projects/novid-shop/items`, `novid/projects/novid-shop/items/GG-142/rank`. Each segment is a plain name; anything else is 404 here",
            "schema": {
              "type": "string",
              "example": "novid/projects/novid-shop/items"
            }
          }
        ],
        "requestBody": {
          "description": "the body the board's route takes",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the board's answer, as it gave it; any of its statuses, with its sentence, comes back the same way"
          },
          "401": {
            "description": "no session, or one that has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a write from another site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "a path the board cannot have, or the board's own not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the task board did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/claims": {
      "get": {
        "tags": [
          "Claims"
        ],
        "summary": "The workspace's open claims, newest heartbeat first, then the last day's done ones.",
        "description": "project narrows to one; an agent token sees its own project whatever it asks for; a person with project roles sees the projects they reach.",
        "operationId": "listClaims",
        "parameters": [
          {
            "name": "project",
            "in": "query",
            "description": "one project's claims",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the claims",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "agent": {
                        "type": "string",
                        "description": "what the claimant calls itself",
                        "example": "Claude Code (Opus 5.5)"
                      },
                      "by": {
                        "type": "string",
                        "description": "a person's address, or agent: followed by the project and the token's name",
                        "example": "agent:shop/claude"
                      },
                      "commit": {
                        "type": "string",
                        "description": "what landed, from done",
                        "example": "a1b2c3d"
                      },
                      "done_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "feature": {
                        "type": "string",
                        "example": "checkout"
                      },
                      "heartbeat_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "id": {
                        "type": "integer",
                        "example": 12
                      },
                      "note": {
                        "type": "string",
                        "description": "the test command, the shared files, anything the next reader needs"
                      },
                      "paths": {
                        "type": "array",
                        "description": "the paths the claimant owns",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "web/src/checkout/",
                          "api/coupons.go"
                        ]
                      },
                      "project": {
                        "type": "string",
                        "example": "shop"
                      },
                      "started_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "state": {
                        "type": "string",
                        "description": "stale: no heartbeat for thirty minutes",
                        "enum": [
                          "live",
                          "stale",
                          "done"
                        ]
                      },
                      "task": {
                        "type": "string",
                        "description": "what is being worked on",
                        "example": "checkout: the coupon field"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such claim in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "claims are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/claims/{id}/done": {
      "post": {
        "tags": [
          "Claims"
        ],
        "summary": "Finished: closes the claim with the commit that landed.",
        "description": "Only the claimant's own credential may. The claim stays in the listing for a day, then in the record.",
        "operationId": "doneClaim",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "description": "what landed",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "commit": {
                    "type": "string",
                    "description": "what landed, from done",
                    "example": "a1b2c3d"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the claim, done",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "type": "string",
                      "description": "what the claimant calls itself",
                      "example": "Claude Code (Opus 5.5)"
                    },
                    "by": {
                      "type": "string",
                      "description": "a person's address, or agent: followed by the project and the token's name",
                      "example": "agent:shop/claude"
                    },
                    "commit": {
                      "type": "string",
                      "description": "what landed, from done",
                      "example": "a1b2c3d"
                    },
                    "done_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "feature": {
                      "type": "string",
                      "example": "checkout"
                    },
                    "heartbeat_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "note": {
                      "type": "string",
                      "description": "the test command, the shared files, anything the next reader needs"
                    },
                    "paths": {
                      "type": "array",
                      "description": "the paths the claimant owns",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "web/src/checkout/",
                        "api/coupons.go"
                      ]
                    },
                    "project": {
                      "type": "string",
                      "example": "shop"
                    },
                    "started_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "state": {
                      "type": "string",
                      "description": "stale: no heartbeat for thirty minutes",
                      "enum": [
                        "live",
                        "stale",
                        "done"
                      ]
                    },
                    "task": {
                      "type": "string",
                      "description": "what is being worked on",
                      "example": "checkout: the coupon field"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "commit is not one word of at most 64 characters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the claim is another's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such claim in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "claims are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/claims/{id}/heartbeat": {
      "post": {
        "tags": [
          "Claims"
        ],
        "summary": "Still working: moves the claim's heartbeat to now.",
        "description": "Only the claimant's own credential may; a done claim is returned unchanged.",
        "operationId": "heartbeatClaim",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the claim",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "type": "string",
                      "description": "what the claimant calls itself",
                      "example": "Claude Code (Opus 5.5)"
                    },
                    "by": {
                      "type": "string",
                      "description": "a person's address, or agent: followed by the project and the token's name",
                      "example": "agent:shop/claude"
                    },
                    "commit": {
                      "type": "string",
                      "description": "what landed, from done",
                      "example": "a1b2c3d"
                    },
                    "done_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "feature": {
                      "type": "string",
                      "example": "checkout"
                    },
                    "heartbeat_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "note": {
                      "type": "string",
                      "description": "the test command, the shared files, anything the next reader needs"
                    },
                    "paths": {
                      "type": "array",
                      "description": "the paths the claimant owns",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "web/src/checkout/",
                        "api/coupons.go"
                      ]
                    },
                    "project": {
                      "type": "string",
                      "example": "shop"
                    },
                    "started_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "state": {
                      "type": "string",
                      "description": "stale: no heartbeat for thirty minutes",
                      "enum": [
                        "live",
                        "stale",
                        "done"
                      ]
                    },
                    "task": {
                      "type": "string",
                      "description": "what is being worked on",
                      "example": "checkout: the coupon field"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "the claim is another's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such claim in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "claims are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/console-releases": {
      "get": {
        "tags": [
          "Back office"
        ],
        "summary": "The console's releases, newest first, with the reports about each, and which is desired and which previous.",
        "description": "At most 50 releases, each with its reports (oldest first), the machine marked as the console host, and who approved the desired one and when. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "listConsoleReleases",
        "responses": {
          "200": {
            "description": "the releases",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "approved_by": {
                      "type": "string"
                    },
                    "breaking_acknowledged": {
                      "type": "boolean"
                    },
                    "desired": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "host": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "previous": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "releases": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "builder": {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "run": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "kind",
                              "name",
                              "run"
                            ]
                          },
                          "commit": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{40}$"
                          },
                          "events": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "at": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "id": {
                                  "type": "integer"
                                },
                                "proofs": {
                                  "type": "object",
                                  "description": "what the host proved, free keys"
                                },
                                "reason": {
                                  "type": "string"
                                },
                                "revision": {
                                  "type": "string",
                                  "description": "the commit of the console that took the report; empty when it has no clean revision"
                                },
                                "state": {
                                  "type": "string",
                                  "enum": [
                                    "started",
                                    "proved",
                                    "live",
                                    "failed",
                                    "rolled_back"
                                  ]
                                },
                                "step": {
                                  "type": "string"
                                },
                                "target": {
                                  "type": "string",
                                  "description": "the machine that reported"
                                }
                              },
                              "required": [
                                "at",
                                "id",
                                "proofs",
                                "reason",
                                "revision",
                                "state",
                                "step",
                                "target"
                              ]
                            }
                          },
                          "files": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "bytes": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string",
                                  "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                                },
                                "sha256": {
                                  "type": "string",
                                  "pattern": "^[0-9a-f]{64}$"
                                },
                                "url": {
                                  "type": "string",
                                  "description": "where the host downloads the file; only in what a target is handed"
                                }
                              },
                              "required": [
                                "name",
                                "sha256",
                                "bytes"
                              ]
                            }
                          },
                          "id": {
                            "type": "string",
                            "description": "the sha256 of manifest.json"
                          },
                          "pruned": {
                            "type": "boolean",
                            "description": "the files were taken away; the row stays"
                          },
                          "registered_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "registered_by": {
                            "type": "string"
                          },
                          "schema": {
                            "type": "string",
                            "description": "breaking needs the operator's acknowledgement to roll out",
                            "enum": [
                              "additive",
                              "breaking"
                            ]
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "build",
                              "upload"
                            ]
                          },
                          "source_build": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "the kept build it was registered from"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "desired",
                              "previous",
                              ""
                            ]
                          },
                          "traceparent": {
                            "type": "string",
                            "description": "W3C traceparent of the release's trace, only in what a target is handed"
                          }
                        },
                        "required": [
                          "id",
                          "commit",
                          "schema",
                          "builder",
                          "source",
                          "source_build",
                          "files",
                          "registered_by",
                          "registered_at",
                          "pruned",
                          "state",
                          "events"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Back office"
        ],
        "summary": "Register a release from a kept build of a project of the platform's own workspace.",
        "description": "The build must have kept manifest.json and the four files it names (tools/release/build.sh output). The files are copied into the console's own store, checked byte for byte against the manifest, and only then put in place; a manifest that names another file, a size or sha256 that differs, a commit that is not 40 hex digits or a schema other than additive or breaking is refused and nothing is kept. The newest five releases stay besides the desired and the previous; the rest are pruned. Registering a release that is in place answers 200 with it. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "registerConsoleRelease",
        "requestBody": {
          "description": "the project and the build's id",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "integer",
                    "example": 12
                  },
                  "project": {
                    "type": "string",
                    "example": "plans-backend"
                  }
                },
                "required": [
                  "build",
                  "project"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "already registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "builder": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "run": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "kind",
                        "name",
                        "run"
                      ]
                    },
                    "commit": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{40}$"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "id": {
                            "type": "integer"
                          },
                          "proofs": {
                            "type": "object",
                            "description": "what the host proved, free keys"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "revision": {
                            "type": "string",
                            "description": "the commit of the console that took the report; empty when it has no clean revision"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "started",
                              "proved",
                              "live",
                              "failed",
                              "rolled_back"
                            ]
                          },
                          "step": {
                            "type": "string"
                          },
                          "target": {
                            "type": "string",
                            "description": "the machine that reported"
                          }
                        },
                        "required": [
                          "at",
                          "id",
                          "proofs",
                          "reason",
                          "revision",
                          "state",
                          "step",
                          "target"
                        ]
                      }
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                          },
                          "sha256": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          },
                          "url": {
                            "type": "string",
                            "description": "where the host downloads the file; only in what a target is handed"
                          }
                        },
                        "required": [
                          "name",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "id": {
                      "type": "string",
                      "description": "the sha256 of manifest.json"
                    },
                    "pruned": {
                      "type": "boolean",
                      "description": "the files were taken away; the row stays"
                    },
                    "registered_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "registered_by": {
                      "type": "string"
                    },
                    "schema": {
                      "type": "string",
                      "description": "breaking needs the operator's acknowledgement to roll out",
                      "enum": [
                        "additive",
                        "breaking"
                      ]
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "build",
                        "upload"
                      ]
                    },
                    "source_build": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "the kept build it was registered from"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "desired",
                        "previous",
                        ""
                      ]
                    },
                    "traceparent": {
                      "type": "string",
                      "description": "W3C traceparent of the release's trace, only in what a target is handed"
                    }
                  },
                  "required": [
                    "id",
                    "commit",
                    "schema",
                    "builder",
                    "source",
                    "source_build",
                    "files",
                    "registered_by",
                    "registered_at",
                    "pruned",
                    "state",
                    "events"
                  ]
                }
              }
            }
          },
          "201": {
            "description": "registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "builder": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "run": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "kind",
                        "name",
                        "run"
                      ]
                    },
                    "commit": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{40}$"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "id": {
                            "type": "integer"
                          },
                          "proofs": {
                            "type": "object",
                            "description": "what the host proved, free keys"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "revision": {
                            "type": "string",
                            "description": "the commit of the console that took the report; empty when it has no clean revision"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "started",
                              "proved",
                              "live",
                              "failed",
                              "rolled_back"
                            ]
                          },
                          "step": {
                            "type": "string"
                          },
                          "target": {
                            "type": "string",
                            "description": "the machine that reported"
                          }
                        },
                        "required": [
                          "at",
                          "id",
                          "proofs",
                          "reason",
                          "revision",
                          "state",
                          "step",
                          "target"
                        ]
                      }
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                          },
                          "sha256": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          },
                          "url": {
                            "type": "string",
                            "description": "where the host downloads the file; only in what a target is handed"
                          }
                        },
                        "required": [
                          "name",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "id": {
                      "type": "string",
                      "description": "the sha256 of manifest.json"
                    },
                    "pruned": {
                      "type": "boolean",
                      "description": "the files were taken away; the row stays"
                    },
                    "registered_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "registered_by": {
                      "type": "string"
                    },
                    "schema": {
                      "type": "string",
                      "description": "breaking needs the operator's acknowledgement to roll out",
                      "enum": [
                        "additive",
                        "breaking"
                      ]
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "build",
                        "upload"
                      ]
                    },
                    "source_build": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "the kept build it was registered from"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "desired",
                        "previous",
                        ""
                      ]
                    },
                    "traceparent": {
                      "type": "string",
                      "description": "W3C traceparent of the release's trace, only in what a target is handed"
                    }
                  },
                  "required": [
                    "id",
                    "commit",
                    "schema",
                    "builder",
                    "source",
                    "source_build",
                    "files",
                    "registered_by",
                    "registered_at",
                    "pruned",
                    "state",
                    "events"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "the manifest or the build is refused; nothing was kept",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project or build",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "retention took the build's files away",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "507": {
            "description": "the store for built files is full",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/host": {
      "put": {
        "tags": [
          "Back office"
        ],
        "summary": "Mark the one machine that runs the console.",
        "description": "Only this machine is handed the desired release. Marking another machine replaces it (and the Activity line says so); the new host starts with nothing desired. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "markConsoleHost",
        "requestBody": {
          "description": "the machine's id, as GET /v1/targets lists it",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "example": "tg_1"
                  }
                },
                "required": [
                  "target"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "marked; replaced is the machine it replaced, or null"
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such machine in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/rollback": {
      "post": {
        "tags": [
          "Back office"
        ],
        "summary": "Make the previous release the desired one again.",
        "description": "There is no previous afterwards: a second rollback is 409, and the way forward again is a rollout. The acknowledgement belongs to the release being left: when the desired release is breaking, the rollback is 409 unless the body says `acknowledge_breaking: true`, because the previous binary may not run on the new schema and the dump taken before that release is the way back. Rolling back to a breaking previous from an additive release needs none. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "rollBackConsole",
        "requestBody": {
          "description": "whether the operator acknowledges a breaking schema; the body may be left out",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "acknowledge_breaking": {
                    "type": "boolean",
                    "description": "must be true to roll out a release whose schema is breaking, and to roll back from one (the release being left)"
                  }
                },
                "required": [
                  "acknowledge_breaking"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the console's desired state now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "approved_by": {
                      "type": "string"
                    },
                    "breaking_acknowledged": {
                      "type": "boolean"
                    },
                    "desired": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "previous": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "target": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "there is no previous release, no host is marked, or the desired release is breaking and leaving it is not acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "the previous release's files were pruned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/uploads": {
      "post": {
        "tags": [
          "Back office"
        ],
        "summary": "Open the upload of a release: send its manifest.json.",
        "description": "The body is manifest.json itself (at most 64 KiB), checked as registering is; the release's id is its sha256. The answer says how many parts each file comes in. The room is claimed now: a store that cannot hold the release refuses here, before any part is sent. An upload opened again resumes. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "openConsoleUpload",
        "requestBody": {
          "description": "manifest.json, as tools/release/build.sh wrote it",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "already registered: nothing to upload"
          },
          "201": {
            "description": "open: send the parts"
          },
          "400": {
            "description": "the manifest is refused",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "manifest.json is past 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "507": {
            "description": "the store for built files is full",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/uploads/{id}/files/{file}": {
      "put": {
        "tags": [
          "Back office"
        ],
        "summary": "Send one part of a file of an open upload.",
        "description": "Parts of 8 MiB (the last holds the rest); `X-Part-SHA256` is the hex sha256 of the body and `of` must be the number of parts the manifest's size makes. A part sent again lands where the first did. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "putConsoleUploadPart",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the release's id: the sha256 of its manifest.json, 64 lower-case hex digits",
            "schema": {
              "type": "string",
              "example": "9f2c0d1e5a7b4c3d8e6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
              "pattern": "^[0-9a-f]{64}$"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "the file's name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "part",
            "in": "query",
            "required": true,
            "description": "this part, from 1",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "of",
            "in": "query",
            "required": true,
            "description": "how many parts the file has",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "kept"
          },
          "400": {
            "description": "a part that is not one: past 8 MiB, short or long of the size the manifest fixes, a hash that differs, a name or a count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such upload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/uploads/{id}/finish": {
      "post": {
        "tags": [
          "Back office"
        ],
        "summary": "Finish an upload: every byte is checked against the manifest, then the release is put in place and registered.",
        "description": "A file whose size or sha256 differs refuses the finish and takes the whole upload away; nothing is kept. Finishing an upload that finished answers 200 with the release. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "finishConsoleUpload",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the release's id: the sha256 of its manifest.json, 64 lower-case hex digits",
            "schema": {
              "type": "string",
              "example": "9f2c0d1e5a7b4c3d8e6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "already registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "builder": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "run": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "kind",
                        "name",
                        "run"
                      ]
                    },
                    "commit": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{40}$"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "id": {
                            "type": "integer"
                          },
                          "proofs": {
                            "type": "object",
                            "description": "what the host proved, free keys"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "revision": {
                            "type": "string",
                            "description": "the commit of the console that took the report; empty when it has no clean revision"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "started",
                              "proved",
                              "live",
                              "failed",
                              "rolled_back"
                            ]
                          },
                          "step": {
                            "type": "string"
                          },
                          "target": {
                            "type": "string",
                            "description": "the machine that reported"
                          }
                        },
                        "required": [
                          "at",
                          "id",
                          "proofs",
                          "reason",
                          "revision",
                          "state",
                          "step",
                          "target"
                        ]
                      }
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                          },
                          "sha256": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          },
                          "url": {
                            "type": "string",
                            "description": "where the host downloads the file; only in what a target is handed"
                          }
                        },
                        "required": [
                          "name",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "id": {
                      "type": "string",
                      "description": "the sha256 of manifest.json"
                    },
                    "pruned": {
                      "type": "boolean",
                      "description": "the files were taken away; the row stays"
                    },
                    "registered_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "registered_by": {
                      "type": "string"
                    },
                    "schema": {
                      "type": "string",
                      "description": "breaking needs the operator's acknowledgement to roll out",
                      "enum": [
                        "additive",
                        "breaking"
                      ]
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "build",
                        "upload"
                      ]
                    },
                    "source_build": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "the kept build it was registered from"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "desired",
                        "previous",
                        ""
                      ]
                    },
                    "traceparent": {
                      "type": "string",
                      "description": "W3C traceparent of the release's trace, only in what a target is handed"
                    }
                  },
                  "required": [
                    "id",
                    "commit",
                    "schema",
                    "builder",
                    "source",
                    "source_build",
                    "files",
                    "registered_by",
                    "registered_at",
                    "pruned",
                    "state",
                    "events"
                  ]
                }
              }
            }
          },
          "201": {
            "description": "registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "builder": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "run": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "kind",
                        "name",
                        "run"
                      ]
                    },
                    "commit": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{40}$"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "id": {
                            "type": "integer"
                          },
                          "proofs": {
                            "type": "object",
                            "description": "what the host proved, free keys"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "revision": {
                            "type": "string",
                            "description": "the commit of the console that took the report; empty when it has no clean revision"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "started",
                              "proved",
                              "live",
                              "failed",
                              "rolled_back"
                            ]
                          },
                          "step": {
                            "type": "string"
                          },
                          "target": {
                            "type": "string",
                            "description": "the machine that reported"
                          }
                        },
                        "required": [
                          "at",
                          "id",
                          "proofs",
                          "reason",
                          "revision",
                          "state",
                          "step",
                          "target"
                        ]
                      }
                    },
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string",
                            "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                          },
                          "sha256": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          },
                          "url": {
                            "type": "string",
                            "description": "where the host downloads the file; only in what a target is handed"
                          }
                        },
                        "required": [
                          "name",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "id": {
                      "type": "string",
                      "description": "the sha256 of manifest.json"
                    },
                    "pruned": {
                      "type": "boolean",
                      "description": "the files were taken away; the row stays"
                    },
                    "registered_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "registered_by": {
                      "type": "string"
                    },
                    "schema": {
                      "type": "string",
                      "description": "breaking needs the operator's acknowledgement to roll out",
                      "enum": [
                        "additive",
                        "breaking"
                      ]
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "build",
                        "upload"
                      ]
                    },
                    "source_build": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "the kept build it was registered from"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "desired",
                        "previous",
                        ""
                      ]
                    },
                    "traceparent": {
                      "type": "string",
                      "description": "W3C traceparent of the release's trace, only in what a target is handed"
                    }
                  },
                  "required": [
                    "id",
                    "commit",
                    "schema",
                    "builder",
                    "source",
                    "source_build",
                    "files",
                    "registered_by",
                    "registered_at",
                    "pruned",
                    "state",
                    "events"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a file differs from the manifest; the upload was discarded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such upload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/console-releases/{id}/rollout": {
      "post": {
        "tags": [
          "Back office"
        ],
        "summary": "Make a release the one the console's host is to run.",
        "description": "The release that was desired becomes the previous one. A release whose schema is breaking is refused with 409 unless the body says `acknowledge_breaking`. Rolling out the desired release again is a new approval of it. Only a session of an owner of the workspace that runs the platform; everybody else is refused with 403 and the refusal is in their Activity.",
        "operationId": "rollOutConsole",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the release's id: the sha256 of its manifest.json, 64 lower-case hex digits",
            "schema": {
              "type": "string",
              "example": "9f2c0d1e5a7b4c3d8e6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "description": "whether the operator acknowledges a breaking schema; the body may be left out",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "acknowledge_breaking": {
                    "type": "boolean",
                    "description": "must be true to roll out a release whose schema is breaking, and to roll back from one (the release being left)"
                  }
                },
                "required": [
                  "acknowledge_breaking"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the console's desired state now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "approved_by": {
                      "type": "string"
                    },
                    "breaking_acknowledged": {
                      "type": "boolean"
                    },
                    "desired": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "previous": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "target": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "not an owner of the workspace that runs the platform, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such release",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "no machine is marked as the host, or the release is breaking and not acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "the release's files were pruned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "console releases are not kept here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/desktop": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "The newest nuved for Mac: its version, size and sha256.",
        "description": "For a person with a place in any workspace; everybody else is answered 404.",
        "operationId": "getDesktop",
        "responses": {
          "200": {
            "description": "the newest version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the newest nuved for Mac",
                  "properties": {
                    "file": {
                      "type": "string",
                      "example": "nuved-mac-0.8.2.zip"
                    },
                    "published_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-10-21T09:00:00Z"
                    },
                    "sha256": {
                      "type": "string",
                      "example": "3dff8ae5c418fe7956b4f2bacab1413d13c201fac928b2e7a87dd0aa7d407e93"
                    },
                    "size": {
                      "type": "integer",
                      "description": "bytes",
                      "example": 7903685
                    },
                    "url": {
                      "type": "string",
                      "description": "where the zip downloads from, with the same session",
                      "example": "/v1/desktop/latest"
                    },
                    "version": {
                      "type": "string",
                      "example": "0.8.2"
                    }
                  },
                  "required": [
                    "version",
                    "file",
                    "size",
                    "sha256",
                    "published_at",
                    "url"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not a person with a place in a workspace, an agent token, or nothing published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "nuved for Mac is not published on this installation, or it could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/desktop/latest": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Download the newest nuved for Mac: a zip of the app and the script that installs it.",
        "description": "`X-Nuved-Version` and `X-Nuved-SHA256` say what it is; check the sha256 before opening it. Byte ranges are answered, so a download resumes.",
        "operationId": "downloadDesktop",
        "responses": {
          "200": {
            "description": "the zip",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "the range asked for"
          },
          "404": {
            "description": "not a person with a place in a workspace, an agent token, or nothing published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "nuved for Mac is not published on this installation, or it could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/entitlements": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "What the workspace's plan allows: shares, reserved names, guests, traffic, and the plan's state.",
        "description": "Free or Pro, with the numbers of the plan in force and every reserved name, active or held: the same answer the tunnel relay reads, for every workspace but one that runs the platform (GET /v1/plan, `relay_limits_applied`). Pro is €4.00 a month from the balance and renews on the day it started; a balance that cannot pay drops the workspace to Free (`status` lapsed) with its reserved names held until `hold_until`. Hosting is charged from the balance on top of either plan. Times are UTC.",
        "operationId": "getEntitlements",
        "responses": {
          "200": {
            "description": "the entitlements",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entitlements"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the plan could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/environments/current": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The machine image the desktop app installs on a Mac: Mansoor OS Node, at the versions nuved's own servers run.",
        "description": "The manifest of the current version: Ubuntu, Podman and nuved-boxd versions, the disk image (zstd, with its sha256 and unpacked size) and the agent's binaries, each with a URL the same credential fetches. The app compares the version with the machine it runs and offers an update. The image is the platform's, the same for every workspace, and is served only with a nuved credential.",
        "operationId": "getCurrentEnvironment",
        "responses": {
          "200": {
            "description": "the current version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a machine image version: Mansoor OS Node, the local machine the desktop app sets up",
                  "properties": {
                    "arch": {
                      "type": "string",
                      "example": "arm64"
                    },
                    "boxd": {
                      "type": "string",
                      "description": "the commit nuved-boxd inside the image was built from"
                    },
                    "files": {
                      "type": "array",
                      "description": "the other files of the version: the agent's binaries, the manifest's signature later",
                      "items": {
                        "type": "object",
                        "description": "one file of a machine image version",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "compression": {
                            "type": "string",
                            "description": "zstd, or empty for a file served as it is"
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "description": "the disk image's size once unpacked"
                          },
                          "name": {
                            "type": "string",
                            "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                          },
                          "sha256": {
                            "type": "string",
                            "description": "of the file as served"
                          },
                          "url": {
                            "type": "string",
                            "description": "where to fetch it, with the same credential"
                          }
                        },
                        "required": [
                          "name",
                          "url",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "image": {
                      "type": "object",
                      "description": "one file of a machine image version",
                      "properties": {
                        "bytes": {
                          "type": "integer"
                        },
                        "compression": {
                          "type": "string",
                          "description": "zstd, or empty for a file served as it is"
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "description": "the disk image's size once unpacked"
                        },
                        "name": {
                          "type": "string",
                          "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                        },
                        "sha256": {
                          "type": "string",
                          "description": "of the file as served"
                        },
                        "url": {
                          "type": "string",
                          "description": "where to fetch it, with the same credential"
                        }
                      },
                      "required": [
                        "name",
                        "url",
                        "sha256",
                        "bytes"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "example": "Mansoor OS Node"
                    },
                    "notes": {
                      "type": "string"
                    },
                    "podman": {
                      "type": "string",
                      "example": "4.9.3"
                    },
                    "published_at": {
                      "type": "string"
                    },
                    "ubuntu": {
                      "type": "string",
                      "example": "24.04.3"
                    },
                    "version": {
                      "type": "string",
                      "example": "24.04.3-20261001-a1b2c3d"
                    }
                  },
                  "required": [
                    "version",
                    "name",
                    "arch",
                    "ubuntu",
                    "podman",
                    "boxd",
                    "published_at",
                    "image",
                    "files"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "no machine image is published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "machine images are not published on this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/environments/{version}/{file}": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "One file of a machine image version: the disk image or an agent binary, with byte ranges so a download resumes.",
        "description": "The names come from the version's manifest (`GET /v1/environments/current`). Check the sha256 the manifest gives before using a file.",
        "operationId": "getEnvironmentFile",
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "24.04.3-20261001-a1b2c3d"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "a file name from the manifest",
            "schema": {
              "type": "string",
              "example": "nuved-boxd-linux-arm64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "description": "the range asked for"
          },
          "404": {
            "description": "no such file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "machine images are not published on this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/exposures": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Everything the workspace exposes: its projects' addresses and every name the relay holds for it.",
        "description": "The projects' services that answer from outside, and every name the tunnel relay holds for the relay tenants linked to the workspace (a cluster's agentd, a machine's novid-share), live, offline or held, with the dates it is kept. A person with project roles and an agent token see their projects' services and none of the relay's names, which belong to the workspace. `relay` says whether the relay's half is in the list.",
        "operationId": "listExposures",
        "responses": {
          "200": {
            "description": "the exposures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exposures": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "address": {
                            "type": "string",
                            "example": "https://jaeger-tracing.nuved.app"
                          },
                          "agent": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "a relay name: the agent behind it",
                            "properties": {
                              "connected": {
                                "type": "boolean",
                                "example": true
                              },
                              "kind": {
                                "type": "string",
                                "example": "machine",
                                "enum": [
                                  "cluster",
                                  "machine"
                                ]
                              }
                            }
                          },
                          "auth": {
                            "type": "string",
                            "description": "login: the edge asks for a sign-in first (Authentik for a self-hosted project's nuved.app address, the console's own for a service of a project a machine runs); none; invite: the share's invited addresses; authentik: the relay asks Authentik; empty for a held relay name",
                            "example": "login"
                          },
                          "group": {
                            "type": "string",
                            "description": "the project's name, or the relay agent behind it (kind and tunnel address), or relay for a held name",
                            "example": "machine 10.77.0.7"
                          },
                          "held_until": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "a relay name: when a plan's hold on it ends",
                            "example": "2026-10-09T20:00:00Z"
                          },
                          "kept_until": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "a relay name: when it lapses unless its agent reports it again; null while it does not lapse",
                            "example": "2026-10-09T20:00:00Z"
                          },
                          "label": {
                            "type": "string",
                            "description": "a relay name: what its agent calls it",
                            "example": "Jaeger"
                          },
                          "name": {
                            "type": "string",
                            "example": "jaeger"
                          },
                          "open_paths": {
                            "type": "array",
                            "description": "an address of the platform's edge: what of it passes with no sign-in (git's smart-HTTP paths, path prefixes); absent when none",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "git",
                              "/api/"
                            ]
                          },
                          "project": {
                            "type": "string",
                            "description": "a project's service: the project",
                            "example": "tracing"
                          },
                          "public": {
                            "type": "boolean",
                            "description": "auth is none: anybody may open it, with no sign-in",
                            "example": false
                          },
                          "source": {
                            "type": "string",
                            "example": "relay",
                            "enum": [
                              "project",
                              "relay"
                            ]
                          },
                          "state": {
                            "type": "string",
                            "description": "a relay name: live, offline (its agent is away) or held (kept, not served); a self-hosted address: registered; an address of the platform's edge: exposed; a cluster route: the service's status",
                            "example": "live"
                          },
                          "tenant": {
                            "type": "string",
                            "description": "a relay name: the relay tenant that holds it, linked to this workspace",
                            "example": "novid"
                          }
                        },
                        "required": [
                          "source",
                          "group",
                          "name",
                          "address",
                          "auth",
                          "public",
                          "state",
                          "kept_until",
                          "held_until",
                          "agent"
                        ]
                      }
                    },
                    "relay": {
                      "type": "string",
                      "example": "read",
                      "enum": [
                        "read",
                        "off",
                        "unreachable",
                        "not yours"
                      ]
                    }
                  },
                  "required": [
                    "exposures",
                    "relay"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the projects could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/git/credential": {
      "post": {
        "tags": [
          "Tokens"
        ],
        "summary": "A person's git credential for one push over https: their own identity, fifteen minutes long.",
        "description": "What `nuved git-credential` hands git when it asks for a password. git-router sees the person, confined to this workspace and held to their own signing keys, as their ssh key is. It opens git and nothing else, and it is not in the token list. Only for a session that names its person and reaches the whole workspace.",
        "operationId": "createGitCredential",
        "requestBody": {
          "description": "the host git is asking about",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "the platform's git host; anything else is refused",
                    "example": "git.nuved.io"
                  }
                },
                "required": [
                  "host"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the credential, once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GitCredential"
                }
              }
            }
          },
          "400": {
            "description": "not the platform's git host",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, a session that names nobody, or a person with project roles only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "git is not switched on, or git-router could not be told",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/invitations": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "Invitations still outstanding, with when they expire, and every invitation to the platform this workspace sent, with how far that person has come.",
        "operationId": "listInvitations",
        "responses": {
          "200": {
            "description": "the invitations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "People"
        ],
        "summary": "Invite somebody: to this workspace, to some of its projects, or to the platform.",
        "description": "An owner's alone (decision 2026-10-21-roles-who-organises-people). kind=member (the default) names its scope: projects, which they reach only, each as a member (changes it) or a viewer (reads it), in one invitation and one letter that names every one; or scope=organisation, the whole workspace, a separate choice. A body with neither is refused, never read as the whole workspace. project and role are the same for one project. kind=platform invites somebody who has no workspace at all; they name one when they arrive. A workspace that has topped up at least once may send ten of those in 24 hours, and when the person it invited pays €10 or more for the first time, each side gets €5.",
        "operationId": "createInvitation",
        "requestBody": {
          "description": "who, and to what",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "stranger@example.com"
                  },
                  "kind": {
                    "type": "string",
                    "example": "member",
                    "enum": [
                      "member",
                      "platform"
                    ]
                  },
                  "project": {
                    "type": "string",
                    "description": "kind=member only: one project they will reach, instead of projects",
                    "example": "novid-shop"
                  },
                  "projects": {
                    "type": "array",
                    "description": "kind=member only: the projects they will reach, each once, and the role on each",
                    "items": {
                      "$ref": "#/components/schemas/ProjectRole"
                    }
                  },
                  "role": {
                    "type": "string",
                    "description": "with project: member changes the project, viewer reads it",
                    "example": "viewer",
                    "enum": [
                      "member",
                      "viewer"
                    ]
                  },
                  "scope": {
                    "type": "string",
                    "description": "kind=member only, instead of projects: the whole workspace (`workspace` is taken too)",
                    "example": "organisation",
                    "enum": [
                      "organisation"
                    ]
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the invitation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invitation"
                }
              }
            }
          },
          "400": {
            "description": "that is not an email address, not a kind of invitation, a project without a role, a project named twice, both projects and project, both projects and scope, or neither: `Name the projects, or choose the whole workspace.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner: `Only an owner of \u003corg\u003e invites people.`; or a platform invitation before one top-up",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that address is already in this workspace; or, for kind=platform, already signs in to Nuved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "ten platform invitations in 24 hours is the most",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "invitations are not configured on this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/invitations/{id}": {
      "delete": {
        "tags": [
          "People"
        ],
        "summary": "Take an invitation back. The link in the letter stops working at once. An owner's alone.",
        "operationId": "revokeInvitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the invitation's id, from GET /v1/invitations",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "revoked here and at the identity provider"
          },
          "403": {
            "description": "not an owner: `Only an owner of \u003corg\u003e takes invitations back.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such invitation in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the invitation was used; there is nothing to take back",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/keys": {
      "get": {
        "tags": [
          "Tokens"
        ],
        "summary": "The workspace's registered SSH keys, newest first.",
        "operationId": "listKeys",
        "responses": {
          "200": {
            "description": "the keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Key"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Tokens"
        ],
        "summary": "Register an SSH public key: the way in over ssh, and what a commit signature is checked against.",
        "description": "A key belongs to a person and reaches their workspace's repositories, not one project. Paste the contents of a .pub file. The principal recorded with it is the address of the person registering, because that is what git matches a signature's signer identity against.",
        "operationId": "addKey",
        "requestBody": {
          "description": "the public key, and what to call it",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string",
                    "example": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... novid@mac"
                  },
                  "name": {
                    "type": "string",
                    "description": "what to call it; the key's own comment when left out",
                    "example": "mac"
                  }
                },
                "required": [
                  "key"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the registered key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Key"
                }
              }
            }
          },
          "400": {
            "description": "that is not an SSH public key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not register keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that key is already registered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/keys/{id}": {
      "delete": {
        "tags": [
          "Tokens"
        ],
        "summary": "Retire a key. It stops opening the ssh door, and commits it signed stop verifying.",
        "operationId": "revokeKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the key's id, from GET /v1/keys",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "revoked"
          },
          "403": {
            "description": "an agent token may not revoke keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such key in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Who this credential belongs to, and what is left on the balance.",
        "description": "The answer says which workspace the caller is in, whether the credential is a person's session or one project's agent token, and what the prepaid balance is. Nothing in the request names a workspace.",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "the caller and their balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/me/addresses": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Your addresses: the one you sign in with, then those you added, verified or waiting.",
        "description": "The sign-in address is first: it was verified when the account was confirmed, and it stays. An added address is verified once its owner presses Confirm on the page its letter's link opens; until then it carries when that link stops working. Your commits may carry a verified address that the workspace accepts (GET /v1/me, commit_addresses). Addresses are yours, whichever workspace the session acts from.",
        "operationId": "listMyAddresses",
        "responses": {
          "200": {
            "description": "your addresses, the sign-in one first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MailAddress"
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a session that does not record who holds it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not read your addresses just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation does not keep addresses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Add an address: a letter goes to it, and confirming on the page its link opens verifies it.",
        "description": "The letter goes to the address itself and names your account. Its link works for 24 hours; asking again for an address still waiting sends a fresh link, and the first stops working. Another person waiting for the same address does not stand in the way: the first to confirm has it. The address is kept lower case. One person may ask for 10 letters an hour, refusals included.",
        "operationId": "addMyAddress",
        "requestBody": {
          "description": "the address",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "Dev+Nuved@Example.com"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the address, waiting to be confirmed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MailAddress"
                }
              }
            }
          },
          "400": {
            "description": "that is not an email address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a session that does not record who holds it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`\u003cemail\u003e` is already one of your verified addresses; or `\u003cemail\u003e` is somebody else's verified address (verified by them, or the address they sign in with)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "you asked for 10 letters in the last hour; open the ones on their way, or try again later",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the letter could not be sent, and nothing was kept; or the request could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "letters are not configured on this installation, so an address cannot be confirmed here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/addresses/{email}": {
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Remove an address you added, verified or waiting.",
        "description": "The sign-in address stays. An address a place of yours commits as stays until that place chooses another (PUT or DELETE /v1/me/commit-identity there).",
        "operationId": "removeMyAddress",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the address, URL-escaped; matched without regard to case",
            "schema": {
              "type": "string",
              "example": "dev%2Bnuved%40example.com"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "removed"
          },
          "400": {
            "description": "`\u003cemail\u003e` is the address you sign in with; it stays",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`\u003cemail\u003e` is not one of your addresses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`\u003cemail\u003e` is your commit address in `\u003cworkspaces\u003e`; choose another there first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/commit-identity": {
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Go back to the default commit identity in this workspace.",
        "description": "The default is your sign-in address, with its part before the @ for a name, where the workspace accepts it; otherwise there is none until you choose one.",
        "operationId": "clearMyCommitIdentity",
        "responses": {
          "204": {
            "description": "cleared"
          },
          "403": {
            "description": "an agent token, or a session that does not record who holds it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a guest of the workspace, with no place of their own to keep an identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Account"
        ],
        "summary": "Choose the name and address your commits in this workspace carry.",
        "description": "For your place in the workspace the session acts from. The address must be one of your verified addresses and one the workspace accepts (GET /v1/me, commit_domains). Nothing in git is changed by this: it is what the console and the desktop app set as your author there, and what GET /v1/me answers as commit_identity.",
        "operationId": "setMyCommitIdentity",
        "requestBody": {
          "description": "the identity",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "dev@acme.com"
                  },
                  "name": {
                    "type": "string",
                    "description": "up to 100 characters, without \u003c \u003e or line breaks",
                    "example": "Dev Example"
                  }
                },
                "required": [
                  "name",
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the identity now chosen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommitIdentity"
                }
              }
            }
          },
          "400": {
            "description": "`\u003cemail\u003e` is not a verified address of yours; add it under your addresses first; or `\u003corg\u003e` accepts commit addresses under `\u003cdomains\u003e` only; or a name or an address that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a session that does not record who holds it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a guest of the workspace, with no place of their own to keep an identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/keys": {
      "get": {
        "tags": [
          "Vault"
        ],
        "summary": "This person's devices and their public keys.",
        "description": "A person's session; the same list in every workspace they are in.",
        "operationId": "listHolderKeys",
        "responses": {
          "200": {
            "description": "the devices, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one device's public keys; the private halves never leave the device",
                    "properties": {
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "device": {
                        "type": "string",
                        "description": "what the person calls the machine, 1 to 80 characters, no slash; another device of theirs already called so gets \" (2)\", \" (3)\"…",
                        "example": "Novid's MacBook Pro"
                      },
                      "id": {
                        "type": "string",
                        "description": "the device's id: the first 16 hex of sha256 of its public key, the same whatever the device is called",
                        "example": "9f2c4e1a7b3d5c80"
                      },
                      "public_key": {
                        "type": "string",
                        "description": "the age X25519 recipient a vault share is encrypted to",
                        "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                      },
                      "signing_key": {
                        "type": "string",
                        "description": "the Ed25519 public key an unseal request is signed with, standard base64 of its 32 bytes",
                        "example": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
                      }
                    },
                    "required": [
                      "id",
                      "device",
                      "public_key",
                      "signing_key",
                      "created_at"
                    ]
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Vault"
        ],
        "summary": "Publish this device's public keys, so a vault share can be encrypted to it and an unseal signed by it.",
        "description": "A person's session. The desktop app makes the two pairs on first sign-in and keeps the private halves in the Keychain; the CLI keeps its own. A device is its keys (2026-10-21): keys published already are that device, answered as it is; new keys are another device, added beside the others and never in their place, under the name sent or, when another device of the person has it, that name with \" (2)\", \" (3)\"…. Only DELETE takes a device away. A share made after this is encrypted to every device of the holder; a vault made before keeps the keys it was made with.",
        "operationId": "putHolderKey",
        "requestBody": {
          "description": "the device and its two public keys",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device": {
                    "type": "string",
                    "description": "what the person calls the machine, 1 to 80 characters, no slash; another device of theirs already called so gets \" (2)\", \" (3)\"…",
                    "example": "Novid's MacBook Pro"
                  },
                  "public_key": {
                    "type": "string",
                    "description": "the age X25519 recipient a vault share is encrypted to",
                    "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                  },
                  "signing_key": {
                    "type": "string",
                    "description": "the Ed25519 public key an unseal request is signed with, standard base64 of its 32 bytes",
                    "example": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
                  }
                },
                "required": [
                  "device",
                  "public_key",
                  "signing_key"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the device's keys as kept",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one device's public keys; the private halves never leave the device",
                  "properties": {
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "device": {
                      "type": "string",
                      "description": "what the person calls the machine, 1 to 80 characters, no slash; another device of theirs already called so gets \" (2)\", \" (3)\"…",
                      "example": "Novid's MacBook Pro"
                    },
                    "id": {
                      "type": "string",
                      "description": "the device's id: the first 16 hex of sha256 of its public key, the same whatever the device is called",
                      "example": "9f2c4e1a7b3d5c80"
                    },
                    "public_key": {
                      "type": "string",
                      "description": "the age X25519 recipient a vault share is encrypted to",
                      "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                    },
                    "signing_key": {
                      "type": "string",
                      "description": "the Ed25519 public key an unseal request is signed with, standard base64 of its 32 bytes",
                      "example": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
                    }
                  },
                  "required": [
                    "id",
                    "device",
                    "public_key",
                    "signing_key",
                    "created_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a device name, public key or signing key that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the public key is published already with another signing key: remove that device first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/keys/{device}": {
      "delete": {
        "tags": [
          "Vault"
        ],
        "summary": "Forget one device's keys: no new share is encrypted to it.",
        "description": "A person's session. A vault already made keeps the snapshot of the keys it was made with; a share already encrypted to the device stays readable by it.",
        "operationId": "deleteHolderKey",
        "parameters": [
          {
            "name": "device",
            "in": "path",
            "required": true,
            "description": "the device's name, URL-escaped, or its id",
            "schema": {
              "type": "string",
              "example": "Novid's MacBook Pro"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "forgotten"
          },
          "404": {
            "description": "no such device",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/reach": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Who this credential is, and every project it reaches with the role there.",
        "description": "For the services beside the console that ask who a token is instead of keeping people or tokens (the task board). Unlike GET /v1/me, a person of the whole workspace gets every project of it listed. workspace_id is the workspace's fixed id, which such a service keys its rows by; org is its name, which may change. So that the board can stop a person's agents or a team's from taking new work, an agent token's answer says who minted it, their commit name and their teams (minted_by, minted_by_name, teams), and a session's says the person's commit name and the teams they own (person_name, owns_teams); each kind's fields are absent from the other's. All of it is this workspace's and read on every request, never cached. Any credential may ask about itself: no scope is needed. A credential that is unknown, revoked or expired is 401.",
        "operationId": "getMeReach",
        "responses": {
          "200": {
            "description": "the caller and what it reaches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "who a credential is, and every project it reaches with the role there",
                  "properties": {
                    "actor": {
                      "type": "string",
                      "description": "who the caller is on a record: the person's address, or `agent:\u003cproject\u003e/\u003ctoken name\u003e`, as a claim's `by` says it",
                      "example": "owner@example.com"
                    },
                    "email": {
                      "type": "string",
                      "description": "who signed in: present for a session, never for an agent token",
                      "example": "owner@example.com"
                    },
                    "kind": {
                      "type": "string",
                      "description": "a person's session, or one project's agent token",
                      "example": "session",
                      "enum": [
                        "session",
                        "agent"
                      ]
                    },
                    "minted_by": {
                      "type": "string",
                      "description": "an agent token only, absent for a session: the address of the person who minted it; empty for a token minted before makers were recorded",
                      "example": "owner@example.com"
                    },
                    "minted_by_name": {
                      "type": "string",
                      "description": "an agent token only, absent for a session: the minting person's commit name in this workspace; empty when they chose none, never made up from the address",
                      "example": "Sam Example"
                    },
                    "name": {
                      "type": "string",
                      "description": "the credential's own name (`console`, `cli`, an agent token's), never a person's; a person's commit name is `person_name`",
                      "example": "cli"
                    },
                    "org": {
                      "type": "string",
                      "description": "the workspace this credential belongs to, by its name",
                      "example": "novid"
                    },
                    "owns_teams": {
                      "type": "array",
                      "description": "a session only, absent for an agent token: the teams whose agents this person may stop, by name, sorted. Teams have no owners of their own yet (NPB-55), so it is every team of the workspace for a workspace owner and empty for anybody else",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "novid",
                        "pilots"
                      ]
                    },
                    "person_name": {
                      "type": "string",
                      "description": "a session only, absent for an agent token: the person's commit name in this workspace; empty when they chose none",
                      "example": "Sam Example"
                    },
                    "projects": {
                      "type": "array",
                      "description": "every project the credential reaches, by name, sorted. The whole workspace: every project GET /v1/projects lists, an owner as owner and anybody else as member. A person with project roles: those projects, as member or viewer. An agent token: its one project, as agent",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "novid-shop"
                          },
                          "role": {
                            "type": "string",
                            "example": "owner",
                            "enum": [
                              "owner",
                              "member",
                              "viewer",
                              "agent"
                            ]
                          }
                        },
                        "required": [
                          "name",
                          "role"
                        ]
                      }
                    },
                    "role": {
                      "type": "string",
                      "description": "the workspace role, owner or member, when the reach is the whole workspace; empty for a person with project roles and for an agent token",
                      "example": "owner"
                    },
                    "teams": {
                      "type": "array",
                      "description": "an agent token only, absent for a session: the teams of this workspace the minting person is in, by name, sorted; empty when none or when minted_by is empty",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "novid",
                        "pilots"
                      ]
                    },
                    "token_id": {
                      "type": "integer",
                      "description": "the credential's id",
                      "example": 42
                    },
                    "workspace_id": {
                      "type": "string",
                      "description": "the workspace's fixed id, `ws_` and 26 letters of base32 (a-z, 2-7): made once, never changed, while its name (org) may be. The task board files every row under it",
                      "example": "ws_qvalvswj3y2snw6efietdufj6o"
                    }
                  },
                  "required": [
                    "org",
                    "workspace_id",
                    "kind",
                    "token_id",
                    "actor",
                    "name",
                    "role",
                    "projects"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that is unknown, revoked or expired; a session that does not say who holds it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "what the caller reaches could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/me/security": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Your password, your second steps at sign-in, and the workspaces that require one.",
        "description": "Read from the identity provider, which holds them; `two_step.state` is unknown when it could not be asked. The links go to its pages, each of which asks the current password and the second step first. Open to a session its workspace refuses for want of a second step.",
        "operationId": "getSecurity",
        "responses": {
          "200": {
            "description": "your security",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the signed-in person's password, second steps and the workspaces that require one",
                  "properties": {
                    "email": {
                      "type": "string",
                      "example": "owner@example.com"
                    },
                    "password": {
                      "type": "object",
                      "properties": {
                        "change_url": {
                          "type": "string",
                          "description": "the identity provider's page that asks the current password, and the second step when there is one, then a new one; it comes back to the console's Security page. Empty when none is configured",
                          "example": "https://auth.nuved.io/if/flow/nuved-password-change/"
                        },
                        "changed_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "when the identity provider last saw it change; null when it cannot say"
                        }
                      },
                      "required": [
                        "changed_at",
                        "change_url"
                      ]
                    },
                    "refused_here": {
                      "type": "boolean",
                      "description": "this session's workspace requires two-step login and this session signed in without it: every route but the person's own answers 403 two_step_required"
                    },
                    "required_by": {
                      "type": "array",
                      "description": "the person's workspaces whose owners require two-step login",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "GrowGoddess"
                          },
                          "tenant": {
                            "type": "string",
                            "example": "growgoddess"
                          }
                        },
                        "required": [
                          "tenant",
                          "name"
                        ]
                      }
                    },
                    "this_session": {
                      "type": "object",
                      "properties": {
                        "two_step": {
                          "type": "boolean",
                          "description": "the session asking signed in with a second step"
                        }
                      },
                      "required": [
                        "two_step"
                      ]
                    },
                    "two_step": {
                      "type": "object",
                      "properties": {
                        "add_app_url": {
                          "type": "string",
                          "description": "the identity provider's page that adds an authenticator app, after the current password and second step"
                        },
                        "add_key_url": {
                          "type": "string",
                          "description": "the same for a security key or a passkey"
                        },
                        "methods": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "one second step at sign-in",
                            "properties": {
                              "created_at": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2026-10-26T15:50:00Z"
                              },
                              "id": {
                                "type": "string",
                                "description": "what DELETE /v1/me/two-step/{id} takes",
                                "example": "totp:3"
                              },
                              "kind": {
                                "type": "string",
                                "description": "app: a code from an authenticator app; key: a security key or a passkey; codes: recovery codes",
                                "example": "app",
                                "enum": [
                                  "app",
                                  "key",
                                  "codes",
                                  "other"
                                ]
                              },
                              "last_used_at": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time",
                                "description": "null for never"
                              },
                              "model": {
                                "type": "string",
                                "description": "what a security key says it is; empty otherwise",
                                "example": "YubiKey 5C NFC"
                              },
                              "name": {
                                "type": "string",
                                "example": "Phone"
                              }
                            },
                            "required": [
                              "id",
                              "kind",
                              "name",
                              "model",
                              "created_at",
                              "last_used_at"
                            ]
                          }
                        },
                        "state": {
                          "type": "string",
                          "description": "unknown when the identity provider could not be asked",
                          "example": "on",
                          "enum": [
                            "on",
                            "off",
                            "unknown"
                          ]
                        }
                      },
                      "required": [
                        "state",
                        "methods",
                        "add_app_url",
                        "add_key_url"
                      ]
                    }
                  },
                  "required": [
                    "email",
                    "password",
                    "two_step",
                    "this_session",
                    "required_by",
                    "refused_here"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token has no sign-in: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the workspaces could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/sessions": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Where you are signed in, in every workspace you are in, newest first.",
        "description": "Browsers and the CLI, each with its workspace and whether its sign-in carried a second step. `current` marks the one asking. Agent tokens are not sessions and are not listed.",
        "operationId": "listPersonSessions",
        "responses": {
          "200": {
            "description": "your sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one place the signed-in person is signed in, in any of their workspaces",
                    "properties": {
                      "client": {
                        "type": "string",
                        "description": "the browser and system its User-Agent named at sign-in; empty when not recorded",
                        "example": "Chrome on macOS"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-10-11T08:12:00Z"
                      },
                      "current": {
                        "type": "boolean",
                        "example": true
                      },
                      "device": {
                        "type": "string",
                        "example": "browser",
                        "enum": [
                          "browser",
                          "cli"
                        ]
                      },
                      "expires_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "id": {
                        "type": "integer",
                        "example": 41
                      },
                      "ip": {
                        "type": "string",
                        "description": "the address it signed in from; kept only while the session lives",
                        "example": "203.0.113.7"
                      },
                      "last_used_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "made_from": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "the session a workspace switch minted it from; null otherwise"
                      },
                      "place": {
                        "type": "string",
                        "description": "the city and country the edge said it signed in from; kept only while the session lives",
                        "example": "Berlin, DE"
                      },
                      "two_step": {
                        "type": "boolean",
                        "description": "its sign-in checked a second step"
                      },
                      "via": {
                        "type": "string",
                        "description": "password: a password; passkey: a passkey on its own; provider: the identity provider by another door (GitHub, a code by mail); cli: the CLI's device code; switch: minted from made_from for another workspace; claim: minted on the claim or welcome page a moment after a sign-in, without its methods; empty when not recorded",
                        "example": "password",
                        "enum": [
                          "",
                          "password",
                          "passkey",
                          "provider",
                          "cli",
                          "switch",
                          "claim"
                        ]
                      },
                      "workspace": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "example": "GrowGoddess"
                          },
                          "tenant": {
                            "type": "string",
                            "example": "growgoddess"
                          }
                        },
                        "required": [
                          "tenant",
                          "name"
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "device",
                      "created_at",
                      "last_used_at",
                      "expires_at",
                      "current",
                      "workspace",
                      "client",
                      "ip",
                      "place",
                      "via",
                      "two_step",
                      "made_from"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token has no sessions: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the sessions could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/sessions/end-others": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "summary": "End every one of your sessions, in every workspace, but the one asking.",
        "description": "Browsers and the CLI, with every sign-in on an exposed address made from them, and your sign-ins at the identity provider, so a browser signed in there with an old password has to sign in again. The session asking stays signed in. Agent tokens are not sessions and keep working.",
        "operationId": "endPersonOtherSessions",
        "responses": {
          "200": {
            "description": "what ended",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ended": {
                      "type": "integer",
                      "example": 3
                    },
                    "sign_ins_ended": {
                      "type": "boolean",
                      "description": "your sign-ins at the identity provider ended too; false when it could not be asked"
                    }
                  },
                  "required": [
                    "ended",
                    "sign_ins_ended"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token has no sessions: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the sessions could not be ended just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/me/two-step/{id}": {
      "delete": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Remove one of your second steps.",
        "description": "Needs a session that signed in with a second step. The last one stays while a workspace of yours requires two-step login.",
        "operationId": "removeTwoStep",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the second step's id, from GET /v1/me/security",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "removed"
          },
          "403": {
            "description": "this session signed in without a second step, or an agent token asked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such second step of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "your last second step, and a workspace of yours requires one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the identity provider did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation cannot change second steps",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/members": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "Everybody who can sign in to this workspace.",
        "operationId": "listMembers",
        "responses": {
          "200": {
            "description": "the people",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Member"
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token is not a person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "People"
        ],
        "summary": "Invite a teammate to the whole workspace: they get an email, and land in this workspace's console.",
        "description": "An owner's alone (decision 2026-10-21-roles-who-organises-people). Records them as a member, creates a single-use enrolment invitation at the identity provider unless they already have an account, and sends the letter. Somebody in another workspace is invited like anybody else: a person may be in several. The whole workspace is a choice the body states, scope=organisation; the same thing as POST /v1/invitations with kind=member and that scope, which also invites to projects.",
        "operationId": "inviteMember",
        "requestBody": {
          "description": "who to invite, and that it is to everything",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "teammate@example.com"
                  },
                  "scope": {
                    "type": "string",
                    "description": "the whole workspace; `workspace` is taken too",
                    "example": "organisation",
                    "enum": [
                      "organisation"
                    ]
                  }
                },
                "required": [
                  "email",
                  "scope"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the invitation, as the console lists it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invitation"
                }
              }
            }
          },
          "400": {
            "description": "that is not an email address; or no scope: `Name the projects, or choose the whole workspace.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner: `Only an owner of \u003corg\u003e invites people.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that address is already in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "invitations are not configured on this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/members/{email}": {
      "delete": {
        "tags": [
          "People"
        ],
        "summary": "Take somebody out of this workspace. They keep their account; they lose this console.",
        "operationId": "removeMember",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the person's address, as GET /v1/members lists it",
            "schema": {
              "type": "string",
              "example": "teammate@example.com"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "they are no longer in this workspace"
          },
          "403": {
            "description": "only an owner can remove somebody",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that is the last owner, and a workspace needs one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "People"
        ],
        "summary": "Give somebody projects, with a role on each: member (changes it) or viewer (reads it).",
        "description": "Each named project is added, or its role changed; projects not named stay as they are, and DELETE /v1/members/{email}/projects/{name} takes one away. A member who reached every project reaches only the projects they have a role on from then on. An owner reaches every project and is not narrowed. Only an owner may do this.",
        "operationId": "setMemberProjects",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the person's address, as GET /v1/members lists it",
            "schema": {
              "type": "string",
              "example": "teammate@example.com"
            }
          }
        ],
        "requestBody": {
          "description": "the projects, and the role on each",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "projects": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ProjectRole"
                    }
                  }
                },
                "required": [
                  "projects"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the person, as GET /v1/members lists them",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "no projects named, or a role that is not member or viewer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only an owner can change who is on a project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "nobody with that address here, or no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that person is an owner, who reaches every project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/members/{email}/projects/{name}": {
      "delete": {
        "tags": [
          "People"
        ],
        "summary": "Take somebody off one project. Their place in the workspace stays, even with no project left.",
        "operationId": "removeMemberFromProject",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the person's address, as GET /v1/members lists it",
            "schema": {
              "type": "string",
              "example": "teammate@example.com"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "they no longer reach that project"
          },
          "403": {
            "description": "only an owner can change who is on a project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or that person has no role on it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "The workspaces the signed-in person is in or invited to, the session's own marked current.",
        "description": "A person may be in any number of workspaces; a session is for one of them. An invitation not accepted yet is listed with status invited and who sent it. POST /v1/orgs/{tenant}/accept accepts one; POST /v1/orgs/{tenant}/session moves to an accepted one. Ordered by tenant.",
        "operationId": "listOrgs",
        "responses": {
          "200": {
            "description": "the workspaces",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "a workspace the signed-in person is in, or is invited to",
                    "properties": {
                      "current": {
                        "type": "boolean",
                        "description": "the workspace this session is for"
                      },
                      "invited_by": {
                        "type": "string",
                        "description": "who put the person there; empty when nobody is recorded",
                        "example": "owner@growgoddess.example"
                      },
                      "name": {
                        "type": "string",
                        "description": "what the workspace is called, as POST /v1/orgs named it; its tenant when it was given none",
                        "example": "GrowGoddess"
                      },
                      "require_two_step": {
                        "type": "boolean",
                        "description": "its owners require every session there to have signed in with two-step login (PATCH /v1/orgs/{tenant})"
                      },
                      "role": {
                        "type": "string",
                        "description": "the person's role there, as GET /v1/members says it; guest for a workspace they reach through a team of their own workspace's that it holds; empty for this session's workspace when it lists nobody by this address",
                        "enum": [
                          "",
                          "owner",
                          "member",
                          "project",
                          "guest"
                        ]
                      },
                      "status": {
                        "type": "string",
                        "description": "invited until the person accepts (POST /v1/orgs/{tenant}/accept) or first signs in there",
                        "enum": [
                          "",
                          "invited",
                          "active"
                        ]
                      },
                      "tenant": {
                        "type": "string",
                        "example": "growgoddess"
                      },
                      "via": {
                        "type": "string",
                        "description": "a guest's: the team there that holds them, `from-\u003ctheir workspace\u003e`; empty otherwise",
                        "example": ""
                      }
                    },
                    "required": [
                      "tenant",
                      "name",
                      "role",
                      "status",
                      "invited_by",
                      "via",
                      "current",
                      "require_two_step"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token is not a person",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not read your workspaces just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Make another workspace the signed-in person owns, and get a session there.",
        "description": "The same person, the same sign-in: the tenant goes through sign-up's rules (lower case, letters, digits and dashes, dashes at the ends trimmed, cut at 40) and is refused with sign-up's sentences when it is a word the platform keeps or somebody's already. The person is its owner, its own team (named after the tenant) holds them, and its Activity says who made it. It starts as every workspace does: balance 0, Free, its own bill. The answer is a session there, the same kind as the one that asked and ending when it does, as POST /v1/orgs/{tenant}/session gives; the session that asked stays valid. The token is shown once. An agent token cannot make one: it belongs to one workspace. A browser's session, sent as its cookie, moves too: the answer also sets the cookie to the new one.",
        "operationId": "createOrg",
        "requestBody": {
          "description": "the tenant and what it is called",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "what its people call it, at most 80 characters on one line; the tenant when left out",
                    "example": "GrowGoddess"
                  },
                  "tenant": {
                    "type": "string",
                    "description": "the workspace's name in addresses; lower-cased, trimmed and cut at 40 as sign-up does",
                    "example": "growgoddess"
                  }
                },
                "required": [
                  "tenant"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the session, shown once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenant": {
                      "type": "string",
                      "example": "growgoddess"
                    },
                    "token": {
                      "type": "string",
                      "description": "a session token for that workspace; store it, it is not shown again",
                      "example": "nvd_sess_…"
                    }
                  },
                  "required": [
                    "token",
                    "tenant"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`enter a name for your workspace`; `\"\u003ctenant\u003e\" is a name the platform keeps for itself: choose another`; `a workspace's name is at most 80 characters, on one line`; or not JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a session that does not say whose it is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that workspace name is taken; choose another",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not check that name just now, or could not make that workspace just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation does not list people, so nobody can own another workspace here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/current/commit-domains": {
      "put": {
        "tags": [
          "People"
        ],
        "summary": "Which domains this workspace accepts commit addresses under: an owner's.",
        "description": "An address is accepted when its domain is one listed or below one (eu.acme.com is under acme.com; evilacme.com is not). An empty list accepts any verified address. Each domain is kept lower case, without a leading @, once; at most 20. An identity chosen before that the new list does not accept is not rewritten: GET /v1/me stops answering it, and git-plane refuses its commits. Everybody in the workspace reads the list in GET /v1/me.",
        "operationId": "setCommitDomains",
        "requestBody": {
          "description": "the domains",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "acme.com"
                    ]
                  }
                },
                "required": [
                  "domains"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the list now in force",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommitDomains"
                }
              }
            }
          },
          "400": {
            "description": "no domains list, a domain that is not one (named), or more than 20",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only an owner can change which commit addresses `\u003corg\u003e` accepts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/invitations": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "What this workspace is invited to, newest first, and what became of each.",
        "description": "Invitations of this workspace, the session's, to other workspaces' projects. Its owner accepts or declines one; its members read them.",
        "operationId": "listGuestInvites",
        "responses": {
          "200": {
            "description": "the invitations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "an invitation of a workspace to a project",
                    "properties": {
                      "answered_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it was accepted, declined or revoked; null while pending"
                      },
                      "answered_by": {
                        "type": "string",
                        "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "fourteen days after it was sent"
                      },
                      "guest": {
                        "type": "string",
                        "description": "the workspace invited",
                        "example": "novid"
                      },
                      "host": {
                        "type": "string",
                        "description": "the workspace whose project it is",
                        "example": "growgoddess"
                      },
                      "id": {
                        "type": "integer",
                        "example": 12
                      },
                      "invited_by": {
                        "type": "string",
                        "example": "owner@growgoddess.example"
                      },
                      "project": {
                        "type": "string",
                        "example": "growgoddess-shop"
                      },
                      "role": {
                        "type": "string",
                        "description": "member changes the project, viewer reads it",
                        "enum": [
                          "member",
                          "viewer"
                        ]
                      },
                      "status": {
                        "type": "string",
                        "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                        "enum": [
                          "pending",
                          "expired",
                          "accepted",
                          "ended",
                          "declined",
                          "revoked"
                        ]
                      },
                      "team": {
                        "type": "string",
                        "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                        "example": "novid"
                      }
                    },
                    "required": [
                      "id",
                      "host",
                      "guest",
                      "project",
                      "role",
                      "team",
                      "invited_by",
                      "status",
                      "expires_at",
                      "created_at",
                      "answered_at",
                      "answered_by"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/invitations/{id}/accept": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Accept an invitation of this workspace to another's project. The owner's.",
        "description": "The team named goes: its active people reach the project with the invitation's role, and whoever this workspace puts in or takes out of the team later follows. Without a team, this workspace's own team goes. The host already holding another of this workspace's teams is refused.",
        "operationId": "acceptGuestInvite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the invitation's id, as its list gives it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "requestBody": {
          "description": "the team that goes, or nothing",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "team": {
                    "type": "string",
                    "description": "one of this workspace's teams; its own team when left out",
                    "example": "design"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the invitation, accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "an invitation of a workspace to a project",
                  "properties": {
                    "answered_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was accepted, declined or revoked; null while pending"
                    },
                    "answered_by": {
                      "type": "string",
                      "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "fourteen days after it was sent"
                    },
                    "guest": {
                      "type": "string",
                      "description": "the workspace invited",
                      "example": "novid"
                    },
                    "host": {
                      "type": "string",
                      "description": "the workspace whose project it is",
                      "example": "growgoddess"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "invited_by": {
                      "type": "string",
                      "example": "owner@growgoddess.example"
                    },
                    "project": {
                      "type": "string",
                      "example": "growgoddess-shop"
                    },
                    "role": {
                      "type": "string",
                      "description": "member changes the project, viewer reads it",
                      "enum": [
                        "member",
                        "viewer"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                      "enum": [
                        "pending",
                        "expired",
                        "accepted",
                        "ended",
                        "declined",
                        "revoked"
                      ]
                    },
                    "team": {
                      "type": "string",
                      "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                      "example": "novid"
                    }
                  },
                  "required": [
                    "id",
                    "host",
                    "guest",
                    "project",
                    "role",
                    "team",
                    "invited_by",
                    "status",
                    "expires_at",
                    "created_at",
                    "answered_at",
                    "answered_by"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "an invitation's id is a number",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such invitation, or `no such team: \u003cteam\u003e`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "answered or revoked already, expired, the project gone, or the host holding another team or a team of its own by that name; the sentence says which",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/invitations/{id}/decline": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Decline an invitation of this workspace to another's project. The owner's.",
        "operationId": "declineGuestInvite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the invitation's id, as its list gives it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the invitation, declined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "an invitation of a workspace to a project",
                  "properties": {
                    "answered_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was accepted, declined or revoked; null while pending"
                    },
                    "answered_by": {
                      "type": "string",
                      "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "fourteen days after it was sent"
                    },
                    "guest": {
                      "type": "string",
                      "description": "the workspace invited",
                      "example": "novid"
                    },
                    "host": {
                      "type": "string",
                      "description": "the workspace whose project it is",
                      "example": "growgoddess"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "invited_by": {
                      "type": "string",
                      "example": "owner@growgoddess.example"
                    },
                    "project": {
                      "type": "string",
                      "example": "growgoddess-shop"
                    },
                    "role": {
                      "type": "string",
                      "description": "member changes the project, viewer reads it",
                      "enum": [
                        "member",
                        "viewer"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                      "enum": [
                        "pending",
                        "expired",
                        "accepted",
                        "ended",
                        "declined",
                        "revoked"
                      ]
                    },
                    "team": {
                      "type": "string",
                      "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                      "example": "novid"
                    }
                  },
                  "required": [
                    "id",
                    "host",
                    "guest",
                    "project",
                    "role",
                    "team",
                    "invited_by",
                    "status",
                    "expires_at",
                    "created_at",
                    "answered_at",
                    "answered_by"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "an invitation's id is a number",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such invitation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "answered or revoked already",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/{tenant}": {
      "patch": {
        "tags": [
          "Account"
        ],
        "summary": "Give a workspace another name, or set its two-step rule: an owner's.",
        "description": "The name is what GET /v1/orgs and GET /v1/me call the workspace; its tenant, which names it in addresses and commands, and its id stay what they were. Only an active owner of that workspace renames it, from a session of any workspace they are in. The name is trimmed and is 1 to 80 characters on one line. Its Activity says who renamed it, from what to what; the same name again changes nothing and adds no line. An agent token cannot rename one: it belongs to one project. `require_two_step` turns on or off the rule that every session in the workspace signed in with two-step login; changing it, and changing anything of a workspace that has it on, needs a session that signed in with two-step login, so an owner cannot lock themselves out and a password alone cannot take the rule away. Activity says who turned it on or off.",
        "operationId": "renameOrg",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "the workspace's tenant, as GET /v1/orgs lists it",
            "schema": {
              "type": "string",
              "example": "growgoddess"
            }
          }
        ],
        "requestBody": {
          "description": "the new name, the two-step rule, or both",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "what its people call it, 1 to 80 characters on one line",
                    "example": "GrowGoddess"
                  },
                  "require_two_step": {
                    "type": "boolean",
                    "description": "every session in the workspace signed in with two-step login",
                    "example": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the workspace as GET /v1/orgs lists it, with its new name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a workspace the signed-in person is in, or is invited to",
                  "properties": {
                    "current": {
                      "type": "boolean",
                      "description": "the workspace this session is for"
                    },
                    "invited_by": {
                      "type": "string",
                      "description": "who put the person there; empty when nobody is recorded",
                      "example": "owner@growgoddess.example"
                    },
                    "name": {
                      "type": "string",
                      "description": "what the workspace is called, as POST /v1/orgs named it; its tenant when it was given none",
                      "example": "GrowGoddess"
                    },
                    "require_two_step": {
                      "type": "boolean",
                      "description": "its owners require every session there to have signed in with two-step login (PATCH /v1/orgs/{tenant})"
                    },
                    "role": {
                      "type": "string",
                      "description": "the person's role there, as GET /v1/members says it; guest for a workspace they reach through a team of their own workspace's that it holds; empty for this session's workspace when it lists nobody by this address",
                      "enum": [
                        "",
                        "owner",
                        "member",
                        "project",
                        "guest"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "description": "invited until the person accepts (POST /v1/orgs/{tenant}/accept) or first signs in there",
                      "enum": [
                        "",
                        "invited",
                        "active"
                      ]
                    },
                    "tenant": {
                      "type": "string",
                      "example": "growgoddess"
                    },
                    "via": {
                      "type": "string",
                      "description": "a guest's: the team there that holds them, `from-\u003ctheir workspace\u003e`; empty otherwise",
                      "example": ""
                    }
                  },
                  "required": [
                    "tenant",
                    "name",
                    "role",
                    "status",
                    "invited_by",
                    "via",
                    "current",
                    "require_two_step"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`enter a name for your workspace`; `a workspace's name is at most 80 characters, on one line`; or not JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only an owner of \u003ctenant\u003e renames the workspace.`; `you are not a member of \u003ctenant\u003e`; the two-step rule changed, or a workspace with it on changed, from a session that signed in without two-step login; or an agent token, or a session that does not say whose it is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "that workspace has no account yet, so there is no name to change, or no rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not check that just now, or could not rename that workspace just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation does not list people, so no owner can rename a workspace here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/{tenant}/accept": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Accept an invitation to a workspace, and get a session there.",
        "description": "The person's own act, from a session of any workspace they are in: their place in that workspace becomes active, its Activity gets an accepted line, and the answer is a session there, the same as POST /v1/orgs/{tenant}/session would give, so accepting and switching are one step. Only an invitation not accepted yet can be. A browser's session, sent as its cookie, moves too: the answer also sets the cookie to the new one.",
        "operationId": "acceptOrg",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "the workspace's tenant, as GET /v1/orgs lists it",
            "schema": {
              "type": "string",
              "example": "growgoddess"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "the session, shown once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenant": {
                      "type": "string",
                      "example": "growgoddess"
                    },
                    "token": {
                      "type": "string",
                      "description": "a session token for that workspace; store it, it is not shown again",
                      "example": "nvd_sess_…"
                    }
                  },
                  "required": [
                    "token",
                    "tenant"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "`you are not a member of \u003ctenant\u003e`; or an agent token, or a session that does not say whose it is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`you are already in \u003ctenant\u003e; switch to it with nuved org \u003ctenant\u003e`; or, for a guest, `you reach \u003ctenant\u003e as a guest through \u003cworkspace\u003e's team; there is nothing to accept`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not check that just now, or could not accept just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation does not list people, so there is no other workspace to switch to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/orgs/{tenant}/session": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "A session for another workspace the signed-in person is in.",
        "description": "The new session is held by the same person, is the same kind (console or cli) and ends when the session that asked does; the session that asked stays valid. A switch never accepts an invitation: a workspace the person is only invited to is refused, naming who invited them and how to accept. A guest, who reaches some of a workspace's projects through a team of their own workspace's that it holds, gets a session there too; what it reaches is decided on every request. The token is shown once. An agent token cannot switch: it belongs to one workspace. A browser's session, sent as its cookie, moves too: the answer also sets the cookie to the new one.",
        "operationId": "switchOrg",
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "the workspace's tenant, as GET /v1/orgs lists it",
            "schema": {
              "type": "string",
              "example": "growgoddess"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "the session, shown once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenant": {
                      "type": "string",
                      "example": "growgoddess"
                    },
                    "token": {
                      "type": "string",
                      "description": "a session token for that workspace; store it, it is not shown again",
                      "example": "nvd_sess_…"
                    }
                  },
                  "required": [
                    "token",
                    "tenant"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "`you are not a member of \u003ctenant\u003e`; `you are invited to \u003ctenant\u003e by \u003cwho\u003e; accept it first (nuved org accept \u003ctenant\u003e)`; or an agent token, or a session that does not say whose it is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not check that just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation does not list people, so there is no other workspace to switch to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/otlp/v1/traces": {
      "post": {
        "tags": [
          "Activity"
        ],
        "summary": "Send the spans of an agent's command to the platform's trace collector, under your workspace.",
        "description": "The nuved CLI and the desktop app post here what their own OpenTelemetry SDK exported, so an agent's command and the console's work on it are one trace in Jaeger. The body is an OTLP/HTTP ExportTraceServiceRequest, protobuf or JSON by Content-Type, at most 1 MiB and 500 spans. Every resource is stamped with `nuved.tenant` from the token, and its `service.name` becomes `nuved-cli/\u003cagent\u003e` or `nuved-desktop/\u003cagent\u003e` (any other name is `nuved-client`), the agent being the resource's `nuved.agent` or the `X-Nuved-Agent` header. `X-Nuved-Task` is stamped as `nuved.task` the same way. Sixty requests a minute per token.",
        "operationId": "relayTraces",
        "parameters": [
          {
            "name": "X-Nuved-Agent",
            "in": "header",
            "description": "the agent's name, as its claim says it: letters, digits, space and . _ - : / ( ) #, 64 at most",
            "schema": {
              "type": "string",
              "example": "T-17 executor"
            }
          },
          {
            "name": "X-Nuved-Task",
            "in": "header",
            "description": "the task the command is for, under the same rule",
            "schema": {
              "type": "string",
              "example": "T-20261022-17"
            }
          }
        ],
        "requestBody": {
          "description": "the spans, as OTLP/HTTP sends them",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "an OTLP ExportTraceServiceRequest, as OTLP/JSON encodes it"
              }
            },
            "application/x-protobuf": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "an OTLP ExportTraceServiceRequest, protobuf-encoded"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the collector took the spans; an empty ExportTraceServiceResponse in the request's encoding",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "an OTLP ExportTraceServiceRequest, as OTLP/JSON encodes it"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "an OTLP ExportTraceServiceRequest, protobuf-encoded"
                }
              }
            }
          },
          "204": {
            "description": "this installation has no trace collector: the spans were dropped, which is not an error"
          },
          "400": {
            "description": "not OTLP traces, or more than 500 spans in one request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token minted without the read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "more than 1 MiB in one request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "a Content-Type other than application/x-protobuf or application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "more than 60 requests a minute from one token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the collector did not take the spans",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/overview": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Where the workspace stands: the getting-set-up list, the project limit, and whether it exists yet.",
        "description": "The same state the console's Overview is drawn from. Every step is checked by what the platform holds (a ledger row, a token, a Deployment, a route), so a step is done exactly when the thing it asks for exists. The list counts the people and invitations, so an agent token is refused.",
        "operationId": "getOverview",
        "responses": {
          "200": {
            "description": "the overview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the Overview's own state: the getting-set-up list, the project limit, and where a new workspace stands",
                  "properties": {
                    "defaults": {
                      "$ref": "#/components/schemas/Caps",
                      "description": "the caps a new project starts at when none are chosen"
                    },
                    "first_top_up": {
                      "type": "boolean",
                      "description": "an account with nothing on it and nothing in the cluster yet: what it needs is a first top-up",
                      "example": false
                    },
                    "projects": {
                      "type": "object",
                      "properties": {
                        "at_limit": {
                          "type": "boolean",
                          "description": "every project the plan allows exists; POST /v1/projects answers 409",
                          "example": false
                        },
                        "by_status": {
                          "type": "object",
                          "description": "how many projects are in each state; every state is present",
                          "properties": {
                            "creating": {
                              "type": "integer",
                              "example": 0
                            },
                            "deleting": {
                              "type": "integer",
                              "example": 0
                            },
                            "failed": {
                              "type": "integer",
                              "example": 0
                            },
                            "ready": {
                              "type": "integer",
                              "example": 0
                            },
                            "waiting": {
                              "type": "integer",
                              "example": 0
                            }
                          },
                          "required": [
                            "creating",
                            "waiting",
                            "ready",
                            "failed",
                            "deleting"
                          ]
                        },
                        "count": {
                          "type": "integer",
                          "example": 2
                        },
                        "max": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "how many the plan allows; null when it names no limit",
                          "example": 3
                        }
                      },
                      "required": [
                        "count",
                        "max",
                        "at_limit",
                        "by_status"
                      ]
                    },
                    "provisioned": {
                      "type": "boolean",
                      "description": "the workspace exists in the cluster; false until its first top-up has been applied",
                      "example": true
                    },
                    "setup": {
                      "type": "object",
                      "properties": {
                        "current": {
                          "type": "string",
                          "description": "the step to do next; empty when every step is done",
                          "example": "agent",
                          "enum": [
                            "",
                            "org",
                            "topup",
                            "cli",
                            "deploy",
                            "address",
                            "agent",
                            "invite"
                          ]
                        },
                        "done": {
                          "type": "integer",
                          "example": 5
                        },
                        "project": {
                          "type": "string",
                          "description": "the project the next step names: an empty one to deploy into, or one with a service nobody can reach",
                          "example": ""
                        },
                        "steps": {
                          "type": "array",
                          "description": "in the order the page lists them",
                          "items": {
                            "type": "object",
                            "description": "one step of the list, and what the platform found once it is done; only the values that step has are present",
                            "properties": {
                              "amount": {
                                "type": "string",
                                "description": "topup: the same, as a person reads it",
                                "example": "€10.00"
                              },
                              "amount_micro": {
                                "type": "integer",
                                "description": "topup: the first paid top-up, in micro-euros",
                                "example": 10000000
                              },
                              "at": {
                                "type": "string",
                                "format": "date-time",
                                "description": "topup: when it was paid; cli: when the CLI first signed in; agent: when an agent token was last used",
                                "example": "2026-09-20T10:04:00Z"
                              },
                              "done": {
                                "type": "boolean",
                                "description": "done exactly when what it asks for exists; never by a click",
                                "example": true
                              },
                              "invitations": {
                                "type": "integer",
                                "description": "invite: invitations still out; done at 1",
                                "example": 0
                              },
                              "key": {
                                "type": "string",
                                "example": "deploy",
                                "enum": [
                                  "org",
                                  "topup",
                                  "cli",
                                  "deploy",
                                  "address",
                                  "agent",
                                  "invite"
                                ]
                              },
                              "name": {
                                "type": "string",
                                "description": "org: the workspace's name",
                                "example": "novid"
                              },
                              "people": {
                                "type": "integer",
                                "description": "invite: everybody in the workspace, the owner included; done at 2",
                                "example": 2
                              },
                              "project": {
                                "type": "string",
                                "description": "deploy: the project it runs in",
                                "example": "novid-shop"
                              },
                              "service": {
                                "type": "string",
                                "description": "deploy: the first service found",
                                "example": "hello"
                              },
                              "url": {
                                "type": "string",
                                "description": "address: the first service address found",
                                "example": "https://hello-novid.nuved.io"
                              }
                            },
                            "required": [
                              "key",
                              "done"
                            ]
                          }
                        },
                        "total": {
                          "type": "integer",
                          "example": 7
                        }
                      },
                      "required": [
                        "steps",
                        "done",
                        "total",
                        "current",
                        "project"
                      ]
                    }
                  },
                  "required": [
                    "provisioned",
                    "first_top_up",
                    "projects",
                    "defaults",
                    "setup"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the workspace's overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the cluster did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation cannot work out an overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/people/{email}/commit-addresses": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "git-plane's question at a push: which addresses may this person's commits carry here.",
        "description": "The push hook's call, with GITPLANE_SERVICE_TOKEN; no person or agent token is taken. The answer is a bare array of lower-cased addresses: the person's verified addresses (the sign-in address first) that the workspace accepts, when the person reaches the project there: a member of the workspace, a person with a role on the project, or a guest through a team of their own workspace's that the project's team list holds; only an active membership counts, never an invitation not yet accepted. Otherwise it is empty, and an address nobody signs in with is answered the same, so the answer never says whether a person exists. A push whose author or committer is not in the array is refused by the hook.",
        "operationId": "commitAddressesForPush",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the pusher's sign-in address, URL-escaped",
            "schema": {
              "type": "string",
              "example": "dev%40acme.com"
            }
          },
          {
            "name": "org",
            "in": "query",
            "required": true,
            "description": "the workspace the repository belongs to",
            "schema": {
              "type": "string",
              "example": "acme"
            }
          },
          {
            "name": "project",
            "in": "query",
            "required": true,
            "description": "the project the repository is, by its full name or its short one",
            "schema": {
              "type": "string",
              "example": "acme-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the addresses, lower case; empty when the person reaches nothing there",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "example": "teammate@example.com"
                  }
                }
              }
            }
          },
          "400": {
            "description": "org and project name the repository the push is to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "git-plane's service token is the credential here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "that is not git-plane's service token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "gitPlane": []
          }
        ]
      }
    },
    "/v1/placements": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "What runs where: every service of every project, on which target, from which commit, since when.",
        "description": "The one listing the console, the desktop app and `nuved` read. A person with project roles sees their projects' rows; an agent token sees its own project's rows and nothing else (`nuved brief`). `?project=` narrows it to one project, by its name or its short name.",
        "operationId": "listPlacements",
        "parameters": [
          {
            "name": "project",
            "in": "query",
            "description": "one project's rows only",
            "schema": {
              "type": "string",
              "example": "shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the placements, by project, service and environment",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one service of a project, in one environment, and where it runs",
                    "properties": {
                      "address": {
                        "type": "string",
                        "description": "where it answers from outside; empty for a service that does not",
                        "example": "shop-novid.nuved.app"
                      },
                      "adopted": {
                        "type": "boolean",
                        "description": "whether project is the platform's project that adopted compose_project on this target (POST /v1/projects/{name}/adoptions)",
                        "example": false
                      },
                      "applied": {
                        "type": "object",
                        "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                        "properties": {
                          "cpu_milli": {
                            "type": "integer",
                            "example": 500
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "example": 10737418240
                          },
                          "memory_bytes": {
                            "type": "integer",
                            "example": 536870912
                          },
                          "pids": {
                            "type": "integer",
                            "example": 256
                          }
                        }
                      },
                      "commit": {
                        "type": "string",
                        "description": "what runs, as a git commit",
                        "example": "abc123"
                      },
                      "compose_project": {
                        "type": "string",
                        "description": "the name the target reported it under: its compose project; the same as project unless adopted",
                        "example": "shop"
                      },
                      "deploy_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "the deploy that set these; null for a row no deploy made",
                        "example": 12
                      },
                      "egress": {
                        "type": "array",
                        "description": "the domains the definition declared it talks to (nuved.io/egress), or the one word open: the plan's open tier, every HTTPS domain but the deny-list",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "api.stripe.com",
                          "*.googleapis.com"
                        ]
                      },
                      "environment": {
                        "type": "string",
                        "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                        "example": "production",
                        "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                      },
                      "health": {
                        "type": "string",
                        "description": "what the target's health check says, in its words",
                        "example": "healthy"
                      },
                      "image": {
                        "type": "string",
                        "description": "the image built from it, when there is one",
                        "example": "shop-web:abc123"
                      },
                      "last_error": {
                        "type": "string"
                      },
                      "limits": {
                        "type": "object",
                        "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                        "properties": {
                          "cpu_milli": {
                            "type": "integer",
                            "example": 500
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "example": 10737418240
                          },
                          "memory_bytes": {
                            "type": "integer",
                            "example": 536870912
                          },
                          "pids": {
                            "type": "integer",
                            "example": 256
                          }
                        }
                      },
                      "project": {
                        "type": "string",
                        "description": "the platform's project when adopted, else the name the target reported",
                        "example": "shop"
                      },
                      "public": {
                        "type": "boolean",
                        "description": "whether it is the project's public service (nuved.io/public)",
                        "example": true
                      },
                      "replica": {
                        "type": "integer",
                        "description": "which replica of the service this row is: 0 on the home machine, where every service of the project has a row; 1 and up for a replicated public service's rows on the other machines of the workspace's mesh (docs/replicas.md)",
                        "example": 0
                      },
                      "replicas_desired": {
                        "type": "integer",
                        "description": "how many replicas the deploy asked for; 1 unless the definition said otherwise",
                        "example": 1
                      },
                      "reported_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last report that mentioned it, or the one that stopped it"
                      },
                      "restarts": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "how many times the engine restarted the service's running container (its RestartCount), as the machine last reported it; null from an older nuved-boxd, or for a row with no container",
                        "example": 2
                      },
                      "revision": {
                        "type": "string",
                        "description": "the target's own revision of it",
                        "example": "3"
                      },
                      "service": {
                        "type": "string",
                        "example": "web"
                      },
                      "since": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "since when this commit has run there"
                      },
                      "stateful": {
                        "type": "boolean",
                        "description": "whether it has a volume: one replica, moved with its volume",
                        "example": false
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "queued",
                          "deploying",
                          "live",
                          "failed",
                          "stopped"
                        ]
                      },
                      "target": {
                        "type": "object",
                        "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                        "properties": {
                          "arch": {
                            "type": "string",
                            "description": "the machine's CPU architecture",
                            "example": "aarch64"
                          },
                          "country": {
                            "type": "string",
                            "example": "DE"
                          },
                          "display_name": {
                            "type": "string",
                            "description": "told to an owner of the workspace only; empty for everyone else",
                            "example": "Hetzner"
                          },
                          "id": {
                            "type": "string",
                            "example": "tg_1"
                          },
                          "kernel": {
                            "type": "string",
                            "description": "the machine's kernel release, as its gate read it",
                            "example": "6.8.0-146-generic"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "compose",
                              "kubernetes"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                            "example": "nuved-box"
                          },
                          "provider": {
                            "type": "string",
                            "example": "Hetzner"
                          },
                          "region": {
                            "type": "string",
                            "example": "Falkenstein"
                          },
                          "runtime": {
                            "type": "string",
                            "description": "the container engine on a compose machine",
                            "example": "podman version 4.9.3"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "display_name",
                          "kind",
                          "provider",
                          "region",
                          "country"
                        ]
                      }
                    },
                    "required": [
                      "project",
                      "compose_project",
                      "adopted",
                      "service",
                      "environment",
                      "target",
                      "commit",
                      "image",
                      "revision",
                      "address",
                      "status",
                      "limits",
                      "applied",
                      "health",
                      "since",
                      "reported_at",
                      "last_error",
                      "replicas_desired",
                      "stateful",
                      "egress",
                      "public",
                      "deploy_id",
                      "replica",
                      "restarts"
                    ]
                  }
                }
              }
            }
          },
          "404": {
            "description": "no such project, for a ?project= the caller does not reach (an agent token's other projects included)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/plan": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "The workspace's plan, its next renewal and what it costs, and every plan with what it includes.",
        "description": "What the console's Plan page shows. `covered` says whether the balance pays the next renewal, or a switch to Pro now. `relay_limits_applied` is false only for a workspace that runs the platform, which pays for its plan like any other but whose shares the relay does not count against it.",
        "operationId": "getPlan",
        "responses": {
          "200": {
            "description": "the plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the workspace's plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the plan could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "Billing"
        ],
        "summary": "Switch between Free and Pro: Pro at once, Free at the end of the period.",
        "description": "An owner's alone (decision 2026-10-21-roles-who-organises-people). `pro` starts a full period now and takes €4.00 from the balance; nothing is prorated. `pro` while a switch to Free waits calls the switch off, for nothing. `free` on Pro takes effect when the period paid for ends, and nothing is refunded; reserved names are then held for 14 days. Asking for the plan already in force changes nothing.",
        "operationId": "putPlan",
        "requestBody": {
          "description": "the plan to be on",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "free",
                      "pro"
                    ]
                  }
                },
                "required": [
                  "plan"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the plan as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "400": {
            "description": "plan is not \"free\" or \"pro\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "the balance does not cover a month of Pro; top up first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner: `Only an owner of \u003corg\u003e changes the billing.`; or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the plan could not be changed just now; nothing was taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Every project in the workspace, with its caps, what it reserves and what is running in it.",
        "description": "An agent token sees only the project it was minted for.",
        "operationId": "listProjects",
        "responses": {
          "200": {
            "description": "the projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Project"
                  }
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Create a project: its own namespace, its own caps, its own network rules.",
        "description": "The project is recorded and the answer comes at once, with status `creating`; a job writes it into the cluster, retrying for about an hour when the cluster does not answer. GET /v1/projects says where it stands: `waiting` while the job retries, `ready` once the project is up, `failed` when the job stopped, with the last error. Asking again for a project already asked for queues nothing more. On a console with no cluster behind it the project is still created, as its repository alone: the answer is 201 with the project as listed, status `ready`, `unplaced` set, no services and no caps; where it runs is decided later by a placement, and until then caps, addresses, kubeconfig, logs and runs are refused for it.",
        "operationId": "createProject",
        "requestBody": {
          "description": "the project's name and caps",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cpu": {
                    "type": "string",
                    "description": "vCPU cap, Kubernetes quantity, 0.25 to 8",
                    "example": "1"
                  },
                  "mem": {
                    "type": "string",
                    "description": "memory cap, Kubernetes quantity, 0.25Gi to 32Gi",
                    "example": "2Gi"
                  },
                  "name": {
                    "type": "string",
                    "description": "the name as typed: the project becomes `organization-name`, made DNS-safe, and keeps what was typed as display_name. GET /v1/projects/preview says it first",
                    "example": "shop"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "on a console with no cluster: the project as listed, status ready and unplaced, its repository made",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "202": {
            "description": "the project as recorded, status creating",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "a cap outside its range, or a name that is not a name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "the balance is used up; top up first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not create projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the name belongs to another workspace, or a project of that name is being deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the request could not be recorded; nothing was asked of the cluster",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/preview": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "What a name becomes as a project, before it is made: its name, repository and an address.",
        "description": "A project's name is the workspace's, a dash and the name as typed, made DNS-safe (`ggg` in novid is `novid-ggg`): its namespace, its repository and its addresses. This answers it with nothing made, or the refusal POST /v1/projects would give. The example address is where a service called web in it would answer once exposed.",
        "operationId": "previewProject",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "the name as the person types it",
            "schema": {
              "type": "string",
              "example": "ggg"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "what the project would be",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "display_name": {
                      "type": "string",
                      "description": "the name as typed, as it is kept",
                      "example": "ggg"
                    },
                    "example_address": {
                      "type": "string",
                      "example": "https://web-novid-ggg.nuved.app"
                    },
                    "git": {
                      "type": "object",
                      "properties": {
                        "https": {
                          "type": "string",
                          "example": "https://git.nuved.io/novid/novid-ggg"
                        },
                        "ssh": {
                          "type": "string",
                          "example": "ssh://git@ssh.nuved.io/novid/novid-ggg"
                        }
                      },
                      "required": [
                        "https",
                        "ssh"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "example": "novid-ggg"
                    }
                  },
                  "required": [
                    "name",
                    "display_name",
                    "git",
                    "example_address"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a name that is not one: `Could not create that project: please choose a project name.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person with roles on some projects only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the name is another workspace's or the platform's (`That project name is already in use; choose another.`), this workspace's own (`This workspace already has a project called novid-ggg.`), or being deleted (`That project is being deleted; wait until it is gone.`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "whether the name is free could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/self-hosted": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Register a self-hosted project: one that runs on a machine of your own, with the services it exposes.",
        "description": "The project is listed by GET /v1/projects at once, with `self_hosted` set, status `ready` and its services at their addresses. Nothing is asked of a cluster and nothing is charged for it. Its name is also its repository's: a repository of that name in the workspace shows its history on the Code tab, and one is made, empty, when there is none. Caps, addresses, kubeconfig, logs and runs are refused for it. DELETE /v1/projects/{name} removes it. Only an owner may do this.",
        "operationId": "registerSelfHostedProject",
        "requestBody": {
          "description": "the project's name and its services",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "lowercase letters, digits and dashes, up to 63; unique in the workspace",
                    "example": "tracing"
                  },
                  "services": {
                    "type": "array",
                    "description": "at most 32; none is a project with only its code",
                    "items": {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string",
                          "description": "the hostname it answers on: one name under the domain customer services answer on, used by no other service",
                          "example": "jaeger-tracing.nuved.app"
                        },
                        "name": {
                          "type": "string",
                          "description": "lowercase letters, digits and dashes",
                          "example": "jaeger"
                        },
                        "target": {
                          "type": "string",
                          "description": "host:port where the machine's edge reaches the service; kept for the edge, never shown to a customer's visitors",
                          "example": "172.31.254.2:16686"
                        }
                      },
                      "required": [
                        "name",
                        "target",
                        "address"
                      ]
                    }
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the project as listed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "a name, a target or an address that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the workspace has a project of that name, an address is taken, or there is no account yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the registration could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}": {
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project and everything running in it.",
        "description": "An owner of the workspace only. A member who is not one is refused before the name is checked, and the refusal is written to Activity. The name is repeated as ?confirm=. The delete is recorded and answered at once, with status `deleting`; a job deletes the project in the cluster, and it leaves GET /v1/projects when it is gone.",
        "operationId": "deleteProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "the delete, as recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectState"
                }
              }
            }
          },
          "204": {
            "description": "a self-hosted or an unplaced project, removed at once with its services and roles; a machine it runs on is not touched, and its repository stays"
          },
          "400": {
            "description": "?confirm= missing or not the project's name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not delete projects; a member who is not an owner: only an owner can delete a project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "who owns the workspace could not be read just now; nothing was deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the delete could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Change a project's caps. What is left out is left alone.",
        "description": "The new caps are recorded and answered at once; a job writes them into the cluster. A project still being created is created with them.",
        "operationId": "patchProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the caps to change",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cpu": {
                    "type": "string",
                    "example": "2"
                  },
                  "mem": {
                    "type": "string",
                    "example": "4Gi"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "the project with its new caps, as recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "a cap outside its range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not change caps",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the project is being deleted, or is self-hosted or has no cluster yet, and has no caps here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the change could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/adoptions": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "Which compose projects on which targets are this project.",
        "description": "What was adopted into this project with POST /v1/projects/{name}/adoptions: each row names the target and the name the target reports under. GET /v1/placements lists those rows under this project, with `compose_project` keeping the reported name and `adopted` true.",
        "operationId": "listAdoptions",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the project's adoptions, by target and compose project",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "a compose project on a target that is this project",
                    "properties": {
                      "compose_project": {
                        "type": "string",
                        "description": "the name the target reports it under",
                        "example": "forgejo"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "created_by": {
                        "type": "string",
                        "description": "who adopted it; empty when the session did not say",
                        "example": "owner@example.com"
                      },
                      "project": {
                        "type": "string",
                        "description": "the platform's project",
                        "example": "riegel"
                      },
                      "target": {
                        "type": "object",
                        "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                        "properties": {
                          "arch": {
                            "type": "string",
                            "description": "the machine's CPU architecture",
                            "example": "aarch64"
                          },
                          "country": {
                            "type": "string",
                            "example": "DE"
                          },
                          "display_name": {
                            "type": "string",
                            "description": "told to an owner of the workspace only; empty for everyone else",
                            "example": "Hetzner"
                          },
                          "id": {
                            "type": "string",
                            "example": "tg_1"
                          },
                          "kernel": {
                            "type": "string",
                            "description": "the machine's kernel release, as its gate read it",
                            "example": "6.8.0-146-generic"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "compose",
                              "kubernetes"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                            "example": "nuved-box"
                          },
                          "provider": {
                            "type": "string",
                            "example": "Hetzner"
                          },
                          "region": {
                            "type": "string",
                            "example": "Falkenstein"
                          },
                          "runtime": {
                            "type": "string",
                            "description": "the container engine on a compose machine",
                            "example": "podman version 4.9.3"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "display_name",
                          "kind",
                          "provider",
                          "region",
                          "country"
                        ]
                      }
                    },
                    "required": [
                      "project",
                      "target",
                      "compose_project",
                      "created_by",
                      "created_at"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "adoptions are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Say that a compose project a target reports is this project.",
        "description": "A machine reports what runs under its compose projects' names; this says that one of them, on that target, is this project. From then on GET /v1/placements lists its rows under this project, with `compose_project` keeping the reported name. The machine is not touched and the rows stay as reported. One compose project on one target belongs to one project at a time: one another project holds is refused, naming it; one this project holds already is this project's still; one named like another of the workspace's projects is that project's and is refused. An owner or a member of the workspace may; a person with project roles may not, whatever their role on this project. A target of another workspace is not found.",
        "operationId": "adopt",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "which compose project on which target",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "compose_project": {
                    "type": "string",
                    "description": "the name the target reports it under: lowercase letters, digits and dashes, up to 63",
                    "example": "forgejo"
                  },
                  "target_id": {
                    "type": "string",
                    "description": "the target's id, as GET /v1/targets lists it",
                    "example": "tg_1"
                  }
                },
                "required": [
                  "target_id",
                  "compose_project"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the adoption",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a compose project on a target that is this project",
                  "properties": {
                    "compose_project": {
                      "type": "string",
                      "description": "the name the target reports it under",
                      "example": "forgejo"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_by": {
                      "type": "string",
                      "description": "who adopted it; empty when the session did not say",
                      "example": "owner@example.com"
                    },
                    "project": {
                      "type": "string",
                      "description": "the platform's project",
                      "example": "riegel"
                    },
                    "target": {
                      "type": "object",
                      "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "project",
                    "target",
                    "compose_project",
                    "created_by",
                    "created_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, no target_id, or a compose_project that is not a name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a person with project roles, or an agent token: adopting is an owner's or a member's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such target in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that compose project on that target belongs to another project, which the sentence names, or its name is another project of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the workspace's project names could not be checked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "adoptions are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/adoptions/{target_id}/{compose_project}": {
      "delete": {
        "tags": [
          "Targets"
        ],
        "summary": "Take an adoption back: the compose project is listed under its reported name again.",
        "description": "The machine is not touched. Only this project's own adoption is taken back; one held by another project is not found here. An owner or a member of the workspace may; a person with project roles may not.",
        "operationId": "unadopt",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "description": "the target's id, as GET /v1/targets lists it",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "compose_project",
            "in": "path",
            "required": true,
            "description": "the compose project's name, as the target reports it",
            "schema": {
              "type": "string",
              "example": "forgejo"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "taken back"
          },
          "403": {
            "description": "a person with project roles, or an agent token: unadopting is an owner's or a member's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or that compose project on that target is not adopted into it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "adoptions are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/build-mark": {
      "put": {
        "tags": [
          "Deploys"
        ],
        "summary": "Make this project a build project, or a service again.",
        "description": "A build project runs nothing: every deploy of it, and every push to it, waits for a build machine and never runs on a machine that runs services. A push cannot carry a flag, so this setting is how a push gets the mark; one deploy request may say `build: true` instead, which sets the same mark. A project that runs a service (queued, deploying or live) cannot take the mark. Taking it off makes the project a service again, even if a deploy of it once ended as a build. An owner or a member may, a member of the project, by a person's sign-in; a viewer may not, and an agent token is always refused (it changes production).",
        "operationId": "setBuildMark",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the mark",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "boolean",
                    "description": "true for a build project, false for a service",
                    "example": true
                  }
                },
                "required": [
                  "build"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the mark and what a push does now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "whether a project is a build project, and what a deploy and a push do now",
                  "properties": {
                    "build": {
                      "type": "boolean",
                      "description": "true: its deploys and pushes wait for a build machine and never run on a machine that runs services"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-ship"
                    },
                    "says": {
                      "type": "string",
                      "description": "what a deploy and a push do now, in a sentence"
                    }
                  },
                  "required": [
                    "project",
                    "build",
                    "says"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or no build",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project, or an agent token (always refused: this changes production, a person's sign-in only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the project runs a service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/builds": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "This project's builds, newest first, 50 at most, with the files each kept.",
        "description": "A build-only project (nuved.io/keep) builds and starts nothing; what it leaves are files. A viewer of the project reads this too.",
        "operationId": "listBuilds",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the builds, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "the deploy with `files`: name, bytes, sha256 and whether retention took the bytes (`pruned`)"
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such build of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "builds are not kept here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/builds/{id}": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "One build of this project and its files.",
        "operationId": "getBuild",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the listing has it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the build",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the deploy with `files`: name, bytes, sha256 and whether retention took the bytes (`pruned`)"
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such build of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "builds are not kept here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/builds/{id}/files/{file}": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "Download one file a build kept.",
        "description": "The bytes, as an attachment, with their sha256 in `X-Content-SHA256`. Members of the project and its viewers, with their session; nobody else. Range requests are answered.",
        "operationId": "downloadBuildFile",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the listing has it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "the file's name",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, build or file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "retention took the file away",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "builds are not kept here, or the disk did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/bypass-tokens": {
      "get": {
        "tags": [
          "Tokens"
        ],
        "summary": "The project's bypass tokens: its agent tokens with the scope open, which open its protected addresses.",
        "description": "A tool sends one to any of the project's addresses on the platform's edge as `Authorization: Bearer \u003ctoken\u003e` and passes without a sign-in, as Vercel's Protection Bypass for Automation. `X-Nuved-Token: \u003ctoken\u003e` works for nuved.app addresses only: a nuved.dev share's app would still receive it. A token with open alone is refused by every API route, the cluster, artifacts and git. Mint one with POST /v1/tokens and `scopes: [\"open\"]`, revoke it with DELETE /v1/tokens/{id}. Each says who made it, when it ends, when it was last used, and the address it opened last with when that hour's first pass was (the edge's access log keeps one row per address and token an hour, for 90 days). Newest first. A person who reaches the project reads it; an agent token does not.",
        "operationId": "listBypassTokens",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the project's bypass tokens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BypassTokenList"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the project's tokens this way",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/claims": {
      "post": {
        "tags": [
          "Claims"
        ],
        "summary": "Claim a task on a project: who works where, right now.",
        "description": "Before an agent or a person edits, it says what it is working on and which paths it owns. Others read the project's claims first and stop when a live one covers what they need. The platform records and shows; it refuses nothing on a claim. Heartbeat every twenty minutes; a claim with no heartbeat for thirty is stale. Say done with the commit that landed.",
        "operationId": "createClaim",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the task and the paths",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent": {
                    "type": "string",
                    "description": "what the claimant calls itself",
                    "example": "Claude Code (Opus 5.5)"
                  },
                  "feature": {
                    "type": "string",
                    "example": "checkout"
                  },
                  "note": {
                    "type": "string",
                    "description": "the test command, the shared files, anything the next reader needs"
                  },
                  "paths": {
                    "type": "array",
                    "description": "the paths the claimant owns",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "web/src/checkout/",
                      "api/coupons.go"
                    ]
                  },
                  "task": {
                    "type": "string",
                    "description": "what is being worked on",
                    "example": "checkout: the coupon field"
                  }
                },
                "required": [
                  "task"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the claim, live",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "type": "string",
                      "description": "what the claimant calls itself",
                      "example": "Claude Code (Opus 5.5)"
                    },
                    "by": {
                      "type": "string",
                      "description": "a person's address, or agent: followed by the project and the token's name",
                      "example": "agent:shop/claude"
                    },
                    "commit": {
                      "type": "string",
                      "description": "what landed, from done",
                      "example": "a1b2c3d"
                    },
                    "done_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "feature": {
                      "type": "string",
                      "example": "checkout"
                    },
                    "heartbeat_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "note": {
                      "type": "string",
                      "description": "the test command, the shared files, anything the next reader needs"
                    },
                    "paths": {
                      "type": "array",
                      "description": "the paths the claimant owns",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "web/src/checkout/",
                        "api/coupons.go"
                      ]
                    },
                    "project": {
                      "type": "string",
                      "example": "shop"
                    },
                    "started_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "state": {
                      "type": "string",
                      "description": "stale: no heartbeat for thirty minutes",
                      "enum": [
                        "live",
                        "stale",
                        "done"
                      ]
                    },
                    "task": {
                      "type": "string",
                      "description": "what is being worked on",
                      "example": "checkout: the coupon field"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "no task, or a field too long",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such claim in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "claims are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/commits": {
      "get": {
        "tags": [
          "Code"
        ],
        "summary": "The commits pushed to a project, newest first, with who pushed each and whether its signature was checked.",
        "description": "Read from the platform's git service after the credential and the project are checked. A project nobody has pushed to answers an empty list. An agent token may read its own project's commits, with `email` and `signer` empty: it never learns the people's addresses. The clone addresses are on the project (GET /v1/projects, `git`).",
        "operationId": "listCommits",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "how many commits, newest first: 1 or more, default 50; more than 200 is 200",
            "schema": {
              "type": "integer",
              "example": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the commits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one commit pushed to a project",
                    "properties": {
                      "add": {
                        "type": "integer",
                        "description": "lines added",
                        "example": 11
                      },
                      "agent": {
                        "type": "string",
                        "description": "the agent token that pushed it; empty when a person did",
                        "example": "claude-code"
                      },
                      "at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it was authored, UTC",
                        "example": "2026-09-29T14:07:02Z"
                      },
                      "author": {
                        "type": "string",
                        "description": "the author's name as the commit records it",
                        "example": "Novid"
                      },
                      "del": {
                        "type": "integer",
                        "description": "lines removed",
                        "example": 4
                      },
                      "email": {
                        "type": "string",
                        "description": "the author's address; empty for an agent token",
                        "example": "support@nimanou.com"
                      },
                      "files": {
                        "type": "integer",
                        "example": 1
                      },
                      "sha": {
                        "type": "string",
                        "example": "cc336668b4ad0e7a2b3f1c9d7e5a4b8c0d2e6f11"
                      },
                      "short": {
                        "type": "string",
                        "example": "cc33666"
                      },
                      "signed": {
                        "type": "boolean",
                        "description": "the commit carries a signature; nothing is said about whose",
                        "example": true
                      },
                      "signer": {
                        "type": "string",
                        "description": "the key's principal a verified signature matched; empty for an agent token",
                        "example": "support@nimanou.com"
                      },
                      "subject": {
                        "type": "string",
                        "description": "the message's first line; empty when there is none",
                        "example": "take the DSN from the environment, not argv"
                      },
                      "verified": {
                        "type": "boolean",
                        "description": "the signature checks against an SSH key registered to this workspace (POST /v1/keys)",
                        "example": false
                      }
                    },
                    "required": [
                      "sha",
                      "short",
                      "subject",
                      "author",
                      "email",
                      "at",
                      "signed",
                      "verified",
                      "signer",
                      "agent",
                      "files",
                      "add",
                      "del"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "limit is not a whole number of 1 or more",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the code service did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "code is not switched on for this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/commits/{sha}": {
      "get": {
        "tags": [
          "Code"
        ],
        "summary": "One commit: its message, its signature and its diff, file by file.",
        "description": "The diff is bounded: a file whose diff is over 800 lines, and every file after the commit's 5000th line, comes back with its counts and `cut` set instead of its hunks; `diff_lines_omitted` says how many lines that left out. An agent token may read its own project's commits, with `email` and `signer` empty.",
        "operationId": "getCommit",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "sha",
            "in": "path",
            "required": true,
            "description": "the commit's id, whole or at least its first 7 characters",
            "schema": {
              "type": "string",
              "example": "cc33666"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the commit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one commit with its diff, file by file; a diff too long to carry is cut, and says so",
                  "properties": {
                    "add": {
                      "type": "integer",
                      "description": "lines added",
                      "example": 11
                    },
                    "agent": {
                      "type": "string",
                      "description": "the agent token that pushed it; empty when a person did",
                      "example": "claude-code"
                    },
                    "at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was authored, UTC",
                      "example": "2026-09-29T14:07:02Z"
                    },
                    "author": {
                      "type": "string",
                      "description": "the author's name as the commit records it",
                      "example": "Novid"
                    },
                    "body": {
                      "type": "string",
                      "description": "the message under the subject",
                      "example": "argv is readable by anything that can see the process."
                    },
                    "cut": {
                      "type": "boolean",
                      "description": "some file was cut: diff_lines_omitted is more than 0",
                      "example": false
                    },
                    "del": {
                      "type": "integer",
                      "description": "lines removed",
                      "example": 4
                    },
                    "diff": {
                      "type": "array",
                      "description": "every file the commit touched, in git's order",
                      "items": {
                        "type": "object",
                        "properties": {
                          "add": {
                            "type": "integer",
                            "example": 11
                          },
                          "cut": {
                            "type": "boolean",
                            "description": "the file's hunks are left out: its diff is over 800 lines, or carrying it would pass 5000 for the commit. `git show \u003csha\u003e -- \u003cpath\u003e` prints it",
                            "example": false
                          },
                          "del": {
                            "type": "integer",
                            "example": 4
                          },
                          "hunks": {
                            "type": "array",
                            "description": "empty for a cut file, and for one git writes no diff for (renamed, or not text)",
                            "items": {
                              "type": "object",
                              "properties": {
                                "header": {
                                  "type": "string",
                                  "description": "git's own; its tail names the function the hunk sits in",
                                  "example": "@@ -20,9 +20,16 @@ func main() {"
                                },
                                "lines": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "kind": {
                                        "type": "string",
                                        "description": "unchanged, added or removed",
                                        "enum": [
                                          "ctx",
                                          "add",
                                          "del"
                                        ]
                                      },
                                      "new": {
                                        "type": [
                                          "integer",
                                          "null"
                                        ],
                                        "description": "the line's number in the new file; null when it is not in it"
                                      },
                                      "old": {
                                        "type": [
                                          "integer",
                                          "null"
                                        ],
                                        "description": "the line's number in the old file; null when it is not in it"
                                      },
                                      "text": {
                                        "type": "string",
                                        "description": "the line, without a + or - in front",
                                        "example": "\tflag.Parse()"
                                      }
                                    },
                                    "required": [
                                      "kind",
                                      "old",
                                      "new",
                                      "text"
                                    ]
                                  }
                                }
                              },
                              "required": [
                                "header",
                                "lines"
                              ]
                            }
                          },
                          "lines": {
                            "type": "integer",
                            "description": "lines in this file's diff, carried or not",
                            "example": 9
                          },
                          "path": {
                            "type": "string",
                            "example": "cmd/git-router/main.go"
                          }
                        },
                        "required": [
                          "path",
                          "add",
                          "del",
                          "lines",
                          "cut",
                          "hunks"
                        ]
                      }
                    },
                    "diff_lines": {
                      "type": "integer",
                      "description": "lines of diff in the whole commit",
                      "example": 11
                    },
                    "diff_lines_omitted": {
                      "type": "integer",
                      "description": "how many of those are in cut files and not in this answer",
                      "example": 0
                    },
                    "email": {
                      "type": "string",
                      "description": "the author's address; empty for an agent token",
                      "example": "support@nimanou.com"
                    },
                    "files": {
                      "type": "integer",
                      "example": 1
                    },
                    "sha": {
                      "type": "string",
                      "example": "cc336668b4ad0e7a2b3f1c9d7e5a4b8c0d2e6f11"
                    },
                    "short": {
                      "type": "string",
                      "example": "cc33666"
                    },
                    "signed": {
                      "type": "boolean",
                      "description": "the commit carries a signature; nothing is said about whose",
                      "example": true
                    },
                    "signer": {
                      "type": "string",
                      "description": "the key's principal a verified signature matched; empty for an agent token",
                      "example": "support@nimanou.com"
                    },
                    "subject": {
                      "type": "string",
                      "description": "the message's first line; empty when there is none",
                      "example": "take the DSN from the environment, not argv"
                    },
                    "verified": {
                      "type": "boolean",
                      "description": "the signature checks against an SSH key registered to this workspace (POST /v1/keys)",
                      "example": false
                    }
                  },
                  "required": [
                    "sha",
                    "short",
                    "subject",
                    "author",
                    "email",
                    "at",
                    "signed",
                    "verified",
                    "signer",
                    "agent",
                    "files",
                    "add",
                    "del",
                    "body",
                    "diff",
                    "diff_lines",
                    "diff_lines_omitted",
                    "cut"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such commit in it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the code service did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "code is not switched on for this installation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/db": {
      "get": {
        "tags": [
          "Secrets"
        ],
        "summary": "The project's databases: the connections the machine's agent configured in the vault, and their roles.",
        "description": "A database is a compose service declared `nuved.secrets.db: postgres`; after its deploy the agent made its root password, handed it to the database and to the vault, and wrote the roles app and admin. (~/claude/pm/decisions/2026-10-11-riegel-secrets.md §2)",
        "operationId": "listDatabases",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the databases",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one database of the project: a connection the machine's agent configured in the vault",
                    "properties": {
                      "engine": {
                        "type": "string",
                        "enum": [
                          "postgres"
                        ]
                      },
                      "name": {
                        "type": "string",
                        "example": "db"
                      },
                      "roles": {
                        "type": "array",
                        "description": "the roles that can be minted",
                        "items": {
                          "type": "string",
                          "enum": [
                            "app",
                            "admin"
                          ]
                        }
                      }
                    },
                    "required": [
                      "name",
                      "engine",
                      "roles"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "the vault's policy for this credential does not reach the project: an agent token of another project, or a viewer writing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/db/{db}/creds": {
      "post": {
        "tags": [
          "Secrets"
        ],
        "summary": "Mint a short database user: app (an hour, renewable to a day) for programs and agents, admin (fifteen minutes, superuser) for an owner's psql.",
        "description": "The user exists until the lease ends or is revoked; revoking ends its sessions. An agent token mints app only (the vault's policy and this API both say so); admin is an owner's. The password is in this answer and nowhere else. Written to the activity as `minted db/\u003cproject\u003e/\u003cdb\u003e`.",
        "operationId": "mintCredential",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          },
          {
            "name": "db",
            "in": "path",
            "required": true,
            "description": "the database's name: the compose service declared with nuved.secrets.db, as GET /v1/projects/{name}/db lists it",
            "schema": {
              "type": "string",
              "example": "db"
            }
          }
        ],
        "requestBody": {
          "description": "the role",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "description": "app when empty",
                    "enum": [
                      "app",
                      "admin"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the user, shown once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a minted database user, shown this once",
                  "properties": {
                    "database": {
                      "type": "string",
                      "example": "db"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the user is dropped unless the lease is renewed"
                    },
                    "issued_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "lease_id": {
                      "type": "string",
                      "description": "what to renew or revoke",
                      "example": "novid-shop/db/creds/app-db/5fd3…"
                    },
                    "password": {
                      "type": "string",
                      "description": "the only answer that carries it"
                    },
                    "renewable": {
                      "type": "boolean"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "app",
                        "admin"
                      ]
                    },
                    "username": {
                      "type": "string",
                      "example": "v-jwt-app-x7k2…"
                    }
                  },
                  "required": [
                    "database",
                    "role",
                    "username",
                    "password",
                    "lease_id",
                    "issued_at",
                    "expires_at",
                    "renewable"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a role that is neither app nor admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token asking for admin, a member asking for admin, or the vault's policy refusing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, no such database, or its roles not written yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/db/{db}/rotate-root": {
      "post": {
        "tags": [
          "Secrets"
        ],
        "summary": "Rotate a database's root password to one only the vault knows. An owner's action.",
        "description": "After this nobody can administer the database with the password the compose file or the agent gave it; admin credentials (fifteen minutes, superuser) are the way in. The agent does this on deploy for a service declared `nuved.secrets.rotate-root: \"true\"`; this is the same by hand. Written to the activity as `rotated db/\u003cproject\u003e/\u003cdb\u003e`.",
        "operationId": "rotateRoot",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          },
          {
            "name": "db",
            "in": "path",
            "required": true,
            "description": "the database's name: the compose service declared with nuved.secrets.db, as GET /v1/projects/{name}/db lists it",
            "schema": {
              "type": "string",
              "example": "db"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "rotated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "database": {
                      "type": "string"
                    },
                    "rotated": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "database",
                    "rotated"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token, a member or a viewer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/deploys": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "This project's deploys, newest first, 50 at most.",
        "description": "What was deployed, by whom, where, and what became of it. A viewer of the project reads it too.",
        "operationId": "listDeploys",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the deploys, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "a request to run a project's definition at a commit on a target, and what became of it",
                    "properties": {
                      "branch": {
                        "type": "string",
                        "example": "main"
                      },
                      "commit": {
                        "type": "string",
                        "description": "the full commit, or empty when the agent resolves the branch's tip",
                        "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "end_reason": {
                        "type": "string",
                        "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                        "enum": [
                          "",
                          "machine_hours",
                          "machine_silent",
                          "machine_gone",
                          "machine_stopped"
                        ]
                      },
                      "environment": {
                        "type": "string",
                        "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                        "example": "production",
                        "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                      },
                      "finished_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it went live, failed or was cancelled"
                      },
                      "id": {
                        "type": "integer",
                        "example": 12
                      },
                      "kind": {
                        "type": "string",
                        "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                        "enum": [
                          "deploy",
                          "remove",
                          "build"
                        ]
                      },
                      "message": {
                        "type": "string",
                        "description": "the agent's one sentence on progress or failure; empty until it says"
                      },
                      "project": {
                        "type": "string",
                        "example": "novid-pol"
                      },
                      "requested_by": {
                        "type": "string",
                        "description": "the person's address; git-plane for a push the hook could not name a person for",
                        "example": "owner@example.com"
                      },
                      "started_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when the agent took it"
                      },
                      "status": {
                        "type": "string",
                        "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                        "enum": [
                          "waiting",
                          "queued",
                          "running",
                          "live",
                          "failed",
                          "cancelled",
                          "removed"
                        ]
                      },
                      "target": {
                        "type": "object",
                        "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                        "properties": {
                          "arch": {
                            "type": "string",
                            "description": "the machine's CPU architecture",
                            "example": "aarch64"
                          },
                          "country": {
                            "type": "string",
                            "example": "DE"
                          },
                          "display_name": {
                            "type": "string",
                            "description": "told to an owner of the workspace only; empty for everyone else",
                            "example": "Hetzner"
                          },
                          "id": {
                            "type": "string",
                            "example": "tg_1"
                          },
                          "kernel": {
                            "type": "string",
                            "description": "the machine's kernel release, as its gate read it",
                            "example": "6.8.0-146-generic"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "compose",
                              "kubernetes"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                            "example": "nuved-box"
                          },
                          "provider": {
                            "type": "string",
                            "example": "Hetzner"
                          },
                          "region": {
                            "type": "string",
                            "example": "Falkenstein"
                          },
                          "runtime": {
                            "type": "string",
                            "description": "the container engine on a compose machine",
                            "example": "podman version 4.9.3"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "display_name",
                          "kind",
                          "provider",
                          "region",
                          "country"
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "project",
                      "environment",
                      "branch",
                      "commit",
                      "target",
                      "status",
                      "requested_by",
                      "message",
                      "created_at",
                      "started_at",
                      "finished_at",
                      "end_reason"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Deploys"
        ],
        "summary": "Queue a deploy of this project at a commit, in production or preview.",
        "description": "The platform picks the machine: the one the project runs on (in that environment first), else the machine chosen for its first deploy (PUT /v1/projects/{name}/machine) and no other, else the workspace's one online compose machine; with none, 409 says to enrol one. The target's agent takes the deploy from its work queue and reports on it; GET reads what became of it. An owner or a member of the workspace may, and a member of the project; a viewer may not. git-plane's push hook calls this too, with the service token the two share instead of a session, and then names the workspace (`org`) and, when it knows one, the person who pushed (`pusher`), who is recorded as having asked.",
        "operationId": "queueDeploy",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "what to deploy; an empty body deploys main to production",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "before": {
                    "type": "string",
                    "description": "git-plane only: the branch's commit before the push, git's zeros when the push made the branch. Each pushed commit whose message names a board item in a `Task: GG-142` trailer is linked to it",
                    "example": "0000000000000000000000000000000000000000"
                  },
                  "branch": {
                    "type": "string",
                    "description": "main unless said",
                    "example": "main"
                  },
                  "build": {
                    "type": "boolean",
                    "description": "the project is build-only (nuved.io/keep): with no machine to run it, the build waits for one (202, status waiting) instead of being refused. The console remembers it for the project's later pushes. Production only"
                  },
                  "commit": {
                    "type": "string",
                    "description": "the full 40-character commit; left out, the agent deploys the branch's tip",
                    "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                  },
                  "environment": {
                    "type": "string",
                    "description": "production unless said",
                    "enum": [
                      "production",
                      "preview"
                    ]
                  },
                  "org": {
                    "type": "string",
                    "description": "git-plane only: the workspace the repository belongs to",
                    "example": "novid"
                  },
                  "pusher": {
                    "type": "string",
                    "description": "git-plane only: the address of the person who pushed, when known",
                    "example": "owner@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "queued; the deploy as recorded. A build-only project with no machine to run on is accepted as a waiting build (status waiting, kind build) with the reason in message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a request to run a project's definition at a commit on a target, and what became of it",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "example": "main"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the full commit, or empty when the agent resolves the branch's tip",
                      "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_reason": {
                      "type": "string",
                      "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                      "enum": [
                        "",
                        "machine_hours",
                        "machine_silent",
                        "machine_gone",
                        "machine_stopped"
                      ]
                    },
                    "environment": {
                      "type": "string",
                      "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                      "example": "production",
                      "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                    },
                    "finished_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it went live, failed or was cancelled"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "kind": {
                      "type": "string",
                      "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                      "enum": [
                        "deploy",
                        "remove",
                        "build"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "the agent's one sentence on progress or failure; empty until it says"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-pol"
                    },
                    "requested_by": {
                      "type": "string",
                      "description": "the person's address; git-plane for a push the hook could not name a person for",
                      "example": "owner@example.com"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the agent took it"
                    },
                    "status": {
                      "type": "string",
                      "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                      "enum": [
                        "waiting",
                        "queued",
                        "running",
                        "live",
                        "failed",
                        "cancelled",
                        "removed"
                      ]
                    },
                    "target": {
                      "type": "object",
                      "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "environment",
                    "branch",
                    "commit",
                    "target",
                    "status",
                    "requested_by",
                    "message",
                    "created_at",
                    "started_at",
                    "finished_at",
                    "end_reason"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or an environment, branch or commit that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a viewer of the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "no machine to run it on, none online, several and nothing to say which, or the chosen machine cannot take it now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/deploys/{id}": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "One deploy of this project.",
        "operationId": "getDeploy",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the listing has it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the deploy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a request to run a project's definition at a commit on a target, and what became of it",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "example": "main"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the full commit, or empty when the agent resolves the branch's tip",
                      "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_reason": {
                      "type": "string",
                      "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                      "enum": [
                        "",
                        "machine_hours",
                        "machine_silent",
                        "machine_gone",
                        "machine_stopped"
                      ]
                    },
                    "environment": {
                      "type": "string",
                      "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                      "example": "production",
                      "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                    },
                    "finished_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it went live, failed or was cancelled"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "kind": {
                      "type": "string",
                      "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                      "enum": [
                        "deploy",
                        "remove",
                        "build"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "the agent's one sentence on progress or failure; empty until it says"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-pol"
                    },
                    "requested_by": {
                      "type": "string",
                      "description": "the person's address; git-plane for a push the hook could not name a person for",
                      "example": "owner@example.com"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the agent took it"
                    },
                    "status": {
                      "type": "string",
                      "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                      "enum": [
                        "waiting",
                        "queued",
                        "running",
                        "live",
                        "failed",
                        "cancelled",
                        "removed"
                      ]
                    },
                    "target": {
                      "type": "object",
                      "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "environment",
                    "branch",
                    "commit",
                    "target",
                    "status",
                    "requested_by",
                    "message",
                    "created_at",
                    "started_at",
                    "finished_at",
                    "end_reason"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such deploy of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/deploys/{id}/cancel": {
      "post": {
        "tags": [
          "Deploys"
        ],
        "summary": "Cancel a deploy: take it back while queued, or stop it while it runs.",
        "description": "A queued deploy is taken off the queue. A running one is marked cancelled at once and its machine stops it: boxd asks the console about the deploy every fifteen seconds or so, is told it has ended, kills the build or the compose up under way, and leaves the project as the last good deploy had it (the next check puts the services back); the next queued deploy of that machine then runs. A running deploy that reaches live in the moment before boxd learns of the cancel stays up on the machine and is answered as cancelled here. A deploy that has ended already, and a removal under way, cannot be cancelled. A viewer may not.",
        "operationId": "cancelDeploy",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the listing has it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "cancelled; the deploy as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a request to run a project's definition at a commit on a target, and what became of it",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "example": "main"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the full commit, or empty when the agent resolves the branch's tip",
                      "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_reason": {
                      "type": "string",
                      "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                      "enum": [
                        "",
                        "machine_hours",
                        "machine_silent",
                        "machine_gone",
                        "machine_stopped"
                      ]
                    },
                    "environment": {
                      "type": "string",
                      "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                      "example": "production",
                      "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                    },
                    "finished_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it went live, failed or was cancelled"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "kind": {
                      "type": "string",
                      "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                      "enum": [
                        "deploy",
                        "remove",
                        "build"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "the agent's one sentence on progress or failure; empty until it says"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-pol"
                    },
                    "requested_by": {
                      "type": "string",
                      "description": "the person's address; git-plane for a push the hook could not name a person for",
                      "example": "owner@example.com"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the agent took it"
                    },
                    "status": {
                      "type": "string",
                      "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                      "enum": [
                        "waiting",
                        "queued",
                        "running",
                        "live",
                        "failed",
                        "cancelled",
                        "removed"
                      ]
                    },
                    "target": {
                      "type": "object",
                      "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "environment",
                    "branch",
                    "commit",
                    "target",
                    "status",
                    "requested_by",
                    "message",
                    "created_at",
                    "started_at",
                    "finished_at",
                    "end_reason"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a viewer of the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such deploy of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the deploy has ended already, or it is a removal under way",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/deploys/{id}/output": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "What a build printed, as Server-Sent Events: the end of a failed one, or the last part of a running one so far.",
        "description": "The machine that ran the build keeps the end of a failed build's output (1 MiB at most) and reads it back masked, and holds what a running build has printed so far (its last 512 KiB; one read shows the last 256 KiB of it, whole lines only): every value it rendered for the project and the shapes of credentials are replaced by [masked] before a byte leaves the machine. Events: `state` first, {kept: running|output|none|pruned, bytes, truncated}; then `data:` lines carrying {text}; then `end`. `error` carries why the machine could not read it. `running` means the build is still going: the text is all that is shown so far (not a part to add to the last read), and the answer to ask again, every three seconds, one read at a time, until `state` says anything else; then the build has ended and that answer is its kept state (`output` for a build that failed, `none` for one that did not keep output, such as a build that went live). A build that is queued has not started (409). For a member of the project; a viewer is refused.",
        "operationId": "getBuildOutput",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the listing has it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the output",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a viewer of the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such build of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the build is queued: no machine has taken it yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "the machine that ran the build is not the workspace's any more",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the machine that ran the build is offline, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "the machine did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/exec": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Run one command in a running replica's container, its output as Server-Sent Events.",
        "description": "For a project on machines: the console asks the machine that runs the replica, which runs the command in that container as the project's own user, never as root, with no shell (the list is the argv). `data:` lines carry {stream, text}; `event: exit` carries {code}; `event: error` says why it could not run. Every run is an Activity line. The `nuved exec` command calls this.",
        "operationId": "execInProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "command": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "wget",
                      "-qO-",
                      "http://127.0.0.1:8080/"
                    ]
                  },
                  "environment": {
                    "type": "string",
                    "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                    "example": "production",
                    "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                  },
                  "replica": {
                    "type": "integer",
                    "example": 0
                  },
                  "service": {
                    "type": "string",
                    "example": "web"
                  },
                  "timeout": {
                    "type": "integer",
                    "description": "seconds, at most 300; 60 when absent",
                    "example": 30
                  }
                },
                "required": [
                  "command"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the output",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "no command, or one too long",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no running replica by that number on a machine",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "the machine did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/flow": {
      "get": {
        "tags": [
          "Flow"
        ],
        "summary": "How code reaches main in this project: the preset, the rules, a sentence per rule, and what an agent does next.",
        "description": "The one source of the flow: git-router enforces it on every push, and nothing in the repository overrides it. An agent's token may read its own project's, so it knows where to push; the version (also the ETag) moves with every change, so it can tell when the rules moved. can_edit and why_not say whether the caller may change it.",
        "operationId": "getFlow",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the flow",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Flow"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "git-router could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "git is not switched on",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "put": {
        "tags": [
          "Flow"
        ],
        "summary": "Pick how code reaches main: a preset, or advanced rules. Owner only; every change is in Activity.",
        "description": "direct (agents push main themselves), branch-gate (agents push their branches, a branch reaches main through a merge) or branch-dev-main (feature branches merge into dev, main moves only from dev), or advanced with its rules. The version is the one the change was made against: in the body, or as If-Match. A refused field comes back in fields, as the sentence a person reads.",
        "operationId": "setFlow",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "the change",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preset": {
                    "type": "string",
                    "enum": [
                      "direct",
                      "branch-gate",
                      "branch-dev-main",
                      "advanced"
                    ]
                  },
                  "rules": {
                    "type": "object",
                    "properties": {
                      "agent_refs": {
                        "type": "array",
                        "description": "what an agent's credential may write; feature/* is refs/heads/feature/*",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "refs/heads/feature/*",
                          "refs/heads/agent/*"
                        ]
                      },
                      "agents_unrestricted": {
                        "type": "boolean",
                        "description": "the explicit yes for a list that covers every branch, or an empty one"
                      },
                      "merge_into": {
                        "type": "string",
                        "description": "where agents' branches merge; empty when agents push main",
                        "enum": [
                          "main",
                          "dev",
                          ""
                        ]
                      },
                      "person_https": {
                        "type": "boolean",
                        "description": "a person's https credential may write main and dev"
                      },
                      "protected_refs": {
                        "type": "array",
                        "description": "refs that only move forward",
                        "items": {
                          "type": "string"
                        }
                      },
                      "signed_commits": {
                        "type": "boolean",
                        "description": "a person's commits must be signed by their own key"
                      }
                    }
                  },
                  "version": {
                    "type": "string",
                    "description": "the version the change was made against; or send If-Match"
                  }
                },
                "required": [
                  "preset"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the flow, at its new version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Flow"
                }
              }
            }
          },
          "403": {
            "description": "only an owner changes the flow",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the flow changed since it was read; flow is how it is now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowRefusal"
                }
              }
            }
          },
          "422": {
            "description": "not changed: fields says what is wrong with each",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowRefusal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/flow/preview": {
      "post": {
        "tags": [
          "Flow"
        ],
        "summary": "What a change to the flow would mean, in the same sentences, before anybody saves it.",
        "description": "widens is true when the change gives agents or people more than they have, or takes a protection away, and widenings says each in a sentence: a page asks a person to confirm those, in those words.",
        "operationId": "previewFlow",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "the change",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preset": {
                    "type": "string",
                    "enum": [
                      "direct",
                      "branch-gate",
                      "branch-dev-main",
                      "advanced"
                    ]
                  },
                  "rules": {
                    "type": "object",
                    "properties": {
                      "agent_refs": {
                        "type": "array",
                        "description": "what an agent's credential may write; feature/* is refs/heads/feature/*",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "refs/heads/feature/*",
                          "refs/heads/agent/*"
                        ]
                      },
                      "agents_unrestricted": {
                        "type": "boolean",
                        "description": "the explicit yes for a list that covers every branch, or an empty one"
                      },
                      "merge_into": {
                        "type": "string",
                        "description": "where agents' branches merge; empty when agents push main",
                        "enum": [
                          "main",
                          "dev",
                          ""
                        ]
                      },
                      "person_https": {
                        "type": "boolean",
                        "description": "a person's https credential may write main and dev"
                      },
                      "protected_refs": {
                        "type": "array",
                        "description": "refs that only move forward",
                        "items": {
                          "type": "string"
                        }
                      },
                      "signed_commits": {
                        "type": "boolean",
                        "description": "a person's commits must be signed by their own key"
                      }
                    }
                  },
                  "version": {
                    "type": "string",
                    "description": "the version the change was made against; or send If-Match"
                  }
                },
                "required": [
                  "preset"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the flow it would be, with widens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Flow"
                }
              }
            }
          },
          "422": {
            "description": "fields says what is wrong with each",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowRefusal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/invite-org": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "The workspaces invited to this project, newest first, and what became of each.",
        "operationId": "listOrgInvites",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the invitations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "an invitation of a workspace to a project",
                    "properties": {
                      "answered_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it was accepted, declined or revoked; null while pending"
                      },
                      "answered_by": {
                        "type": "string",
                        "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "fourteen days after it was sent"
                      },
                      "guest": {
                        "type": "string",
                        "description": "the workspace invited",
                        "example": "novid"
                      },
                      "host": {
                        "type": "string",
                        "description": "the workspace whose project it is",
                        "example": "growgoddess"
                      },
                      "id": {
                        "type": "integer",
                        "example": 12
                      },
                      "invited_by": {
                        "type": "string",
                        "example": "owner@growgoddess.example"
                      },
                      "project": {
                        "type": "string",
                        "example": "growgoddess-shop"
                      },
                      "role": {
                        "type": "string",
                        "description": "member changes the project, viewer reads it",
                        "enum": [
                          "member",
                          "viewer"
                        ]
                      },
                      "status": {
                        "type": "string",
                        "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                        "enum": [
                          "pending",
                          "expired",
                          "accepted",
                          "ended",
                          "declined",
                          "revoked"
                        ]
                      },
                      "team": {
                        "type": "string",
                        "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                        "example": "novid"
                      }
                    },
                    "required": [
                      "id",
                      "host",
                      "guest",
                      "project",
                      "role",
                      "team",
                      "invited_by",
                      "status",
                      "expires_at",
                      "created_at",
                      "answered_at",
                      "answered_by"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "People"
        ],
        "summary": "Invite another workspace to this project, as member or viewer. An owner's.",
        "description": "The invitation waits fourteen days for the other workspace's owner, who accepts or declines in their own session (POST /v1/orgs/invitations/{id}/accept). On acceptance this workspace gets a team called `from-\u003corg\u003e` holding the other workspace's team, and the project gets that team with the role. Activity gets a line here and one there.",
        "operationId": "inviteOrg",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the workspace and the role",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "org": {
                    "type": "string",
                    "description": "the workspace's tenant",
                    "example": "novid"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "viewer"
                    ]
                  }
                },
                "required": [
                  "org",
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the invitation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "an invitation of a workspace to a project",
                  "properties": {
                    "answered_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it was accepted, declined or revoked; null while pending"
                    },
                    "answered_by": {
                      "type": "string",
                      "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "fourteen days after it was sent"
                    },
                    "guest": {
                      "type": "string",
                      "description": "the workspace invited",
                      "example": "novid"
                    },
                    "host": {
                      "type": "string",
                      "description": "the workspace whose project it is",
                      "example": "growgoddess"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "invited_by": {
                      "type": "string",
                      "example": "owner@growgoddess.example"
                    },
                    "project": {
                      "type": "string",
                      "example": "growgoddess-shop"
                    },
                    "role": {
                      "type": "string",
                      "description": "member changes the project, viewer reads it",
                      "enum": [
                        "member",
                        "viewer"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                      "enum": [
                        "pending",
                        "expired",
                        "accepted",
                        "ended",
                        "declined",
                        "revoked"
                      ]
                    },
                    "team": {
                      "type": "string",
                      "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                      "example": "novid"
                    }
                  },
                  "required": [
                    "id",
                    "host",
                    "guest",
                    "project",
                    "role",
                    "team",
                    "invited_by",
                    "status",
                    "expires_at",
                    "created_at",
                    "answered_at",
                    "answered_by"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "no org, a role that is not member or viewer, or this workspace itself",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or `no workspace called \u003corg\u003e`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "invited already and not answered, reaching the project already, or a team of this workspace's own is called `from-\u003corg\u003e`; the sentence says which",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/invite-org/{id}": {
      "delete": {
        "tags": [
          "People"
        ],
        "summary": "Take an invitation of a workspace back. An owner's.",
        "description": "A pending invitation is revoked. An accepted one takes `from-\u003corg\u003e` off the project, and deletes `from-\u003corg\u003e` when it reaches no other project. When somebody would lose the project, nothing is written unless the request carries `?confirm=true`: the 409 says how many and who.",
        "operationId": "revokeOrgInvite",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the invitation's id, as its list gives it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "description": "true to go on when somebody loses the project by it",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "revoked: the invitation, and who lost the project",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invitation": {
                      "type": "object",
                      "description": "an invitation of a workspace to a project",
                      "properties": {
                        "answered_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "when it was accepted, declined or revoked; null while pending"
                        },
                        "answered_by": {
                          "type": "string",
                          "description": "who accepted, declined or revoked it; on the host's side, said only for a revocation"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "fourteen days after it was sent"
                        },
                        "guest": {
                          "type": "string",
                          "description": "the workspace invited",
                          "example": "novid"
                        },
                        "host": {
                          "type": "string",
                          "description": "the workspace whose project it is",
                          "example": "growgoddess"
                        },
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "invited_by": {
                          "type": "string",
                          "example": "owner@growgoddess.example"
                        },
                        "project": {
                          "type": "string",
                          "example": "growgoddess-shop"
                        },
                        "role": {
                          "type": "string",
                          "description": "member changes the project, viewer reads it",
                          "enum": [
                            "member",
                            "viewer"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "description": "expired is pending past expires_at; ended is accepted, and the host has since taken the team off the project",
                          "enum": [
                            "pending",
                            "expired",
                            "accepted",
                            "ended",
                            "declined",
                            "revoked"
                          ]
                        },
                        "team": {
                          "type": "string",
                          "description": "the guest's team that goes, or went; the guest's own team unless its owner named another. Empty on the host's side",
                          "example": "novid"
                        }
                      },
                      "required": [
                        "id",
                        "host",
                        "guest",
                        "project",
                        "role",
                        "team",
                        "invited_by",
                        "status",
                        "expires_at",
                        "created_at",
                        "answered_at",
                        "answered_by"
                      ]
                    },
                    "lose": {
                      "type": "integer",
                      "example": 0
                    },
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "invitation",
                    "lose",
                    "people"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "an invitation's id is a number",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such invitation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "somebody would lose the project and confirm=true was not sent, or `that invitation was declined already`; nothing was written",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "2 people lose novid-shop (a@example.com, b@example.com): send confirm=true to go on"
                    },
                    "lose": {
                      "type": "integer",
                      "example": 2
                    },
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "inviting workspaces is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/kubeconfig": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "A kubeconfig for one project, pointing kubectl at kube.nuved.io.",
        "description": "It carries the credential the request was made with, so it reaches exactly what that credential reaches. The cluster's own address is never in it.",
        "operationId": "getKubeconfig",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the kubeconfig file",
            "content": {
              "application/x-yaml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "the credential could not be read back to put in the file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the project is self-hosted or has no cluster yet: it has no namespace here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/leases": {
      "get": {
        "tags": [
          "Secrets"
        ],
        "summary": "The project's live database credentials: which role, since when, until when; never a password.",
        "description": "Every minted user, a program's and a person's alike, as the vault holds the lease. Who minted it is in the activity.",
        "operationId": "listLeases",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the leases, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one live credential: who holds it is not recorded here (the mint is in the activity); never the password",
                    "properties": {
                      "database": {
                        "type": "string"
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "id": {
                        "type": "string",
                        "example": "novid-shop/db/creds/app-db/5fd3…"
                      },
                      "issued_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "renewable": {
                        "type": "boolean"
                      },
                      "role": {
                        "type": "string"
                      },
                      "ttl_seconds": {
                        "type": "integer",
                        "description": "what is left"
                      }
                    },
                    "required": [
                      "id",
                      "database",
                      "role",
                      "issued_at",
                      "expires_at",
                      "renewable",
                      "ttl_seconds"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "the vault's policy for this credential does not reach the project: an agent token of another project, or a viewer writing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/leases/renew": {
      "post": {
        "tags": [
          "Secrets"
        ],
        "summary": "Extend a credential's lease, what a program's package does at two thirds of its life.",
        "description": "The vault grants up to the role's longest life (a day for app); past it the answer is 409 and the program mints a new credential, checks a connection with it, swaps, then revokes this one (the order of decision §7.2; sdk/).",
        "operationId": "renewLease",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          }
        ],
        "requestBody": {
          "description": "the lease and how much longer",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "increment": {
                    "type": "string",
                    "description": "1m to 24h; 1h when empty",
                    "example": "1h"
                  },
                  "lease_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "lease_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "renewed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "lease_id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "lease_id",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "an increment out of range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the vault's policy for this credential does not reach the project: an agent token of another project, or a viewer writing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the lease cannot be renewed any more: mint a new credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/leases/revoke": {
      "post": {
        "tags": [
          "Secrets"
        ],
        "summary": "Revoke a credential now: its sessions end within seconds and the user is dropped.",
        "description": "A person's, or the project's agent token's (a program's package revokes its own old lease after it swapped; the vault's policy reaches the project's leases and no other's). A program holding it fails on its next query; one the agent delivered gets a fresh credential at the agent's next tick. The lease id carries slashes, so it travels in the body. Written to the activity as `revoked lease/\u003cid\u003e`.",
        "operationId": "revokeLease",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          }
        ],
        "requestBody": {
          "description": "the lease",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lease_id": {
                    "type": "string",
                    "description": "as GET /v1/projects/{name}/leases lists it",
                    "example": "novid-shop/db/creds/app-db/5fd3…"
                  }
                },
                "required": [
                  "lease_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lease_id": {
                      "type": "string"
                    },
                    "revoked": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "lease_id",
                    "revoked"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "a viewer, or the vault's policy refusing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/logs": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "What a project's services write to stdout and stderr, as Server-Sent Events.",
        "description": "One `data:` line per log line, a LogLine object. Pods are found through each Deployment's selector. Only what the cluster still holds is here: the running container of each pod, gone when the pod is replaced or restarts. Without `follow` the stream ends with `event: end`; with it, new lines keep coming and a comment every 25 seconds keeps an idle connection open. `event: notice` names a pod whose lines are missing, and why. An agent token may read its own project. The `nuved logs` command reads this.",
        "operationId": "streamLogs",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "query",
            "description": "one service; absent means every service in the project",
            "schema": {
              "type": "string",
              "example": "api"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "how far back to start, as a duration; at most 24h, 1h when absent",
            "schema": {
              "type": "string",
              "example": "15m"
            }
          },
          {
            "name": "follow",
            "in": "query",
            "description": "1 keeps the stream open for new lines",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "keep only lines containing this text, ignoring case; at most 200 characters",
            "schema": {
              "type": "string",
              "example": "/orders"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/LogLine"
                }
              }
            }
          },
          "400": {
            "description": "a since, a service name or a filter this cannot read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no service by that name in it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the project is self-hosted or has no cluster yet: no logs are read here for it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the cluster did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this installation cannot read logs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/machine": {
      "delete": {
        "tags": [
          "Deploys"
        ],
        "summary": "Clear the machine chosen for this project's first deploy: the platform picks again.",
        "description": "Clearing a project with no choice answers the same. Who may: as for choosing one, a person's sign-in only; an agent token is always refused.",
        "operationId": "clearProjectMachine",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the choice and where the next deploy goes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the machine a project's first deploy goes to, and where its next deploy goes",
                  "properties": {
                    "chosen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "chosen_by": {
                      "type": "string",
                      "description": "who chose it: a person's address or an agent token's name; empty when none is chosen",
                      "example": "owner@example.com"
                    },
                    "placed": {
                      "type": "boolean",
                      "description": "whether the project runs somewhere already; it then stays there, and the choice decides only a project's first deploy"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-preview-check"
                    },
                    "says": {
                      "type": "string",
                      "description": "where the next deploy goes, in a sentence",
                      "example": "The first deploy of novid-preview-check goes to tg_12."
                    },
                    "target": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "the chosen machine, null when none is chosen; its id alone when the project may no longer deploy to it (removed, or its share taken away)",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "project",
                    "target",
                    "chosen_by",
                    "chosen_at",
                    "placed",
                    "says"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project, or an agent token (always refused: this changes production, a person's sign-in only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "The machine this project's first deploy goes to, if one was chosen, and where its next deploy goes.",
        "description": "A viewer of the project reads it too, and the project's agent token with the read scope. The machine's own name is told to an owner of the workspace only.",
        "operationId": "getProjectMachine",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the choice and where the next deploy goes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the machine a project's first deploy goes to, and where its next deploy goes",
                  "properties": {
                    "chosen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "chosen_by": {
                      "type": "string",
                      "description": "who chose it: a person's address or an agent token's name; empty when none is chosen",
                      "example": "owner@example.com"
                    },
                    "placed": {
                      "type": "boolean",
                      "description": "whether the project runs somewhere already; it then stays there, and the choice decides only a project's first deploy"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-preview-check"
                    },
                    "says": {
                      "type": "string",
                      "description": "where the next deploy goes, in a sentence",
                      "example": "The first deploy of novid-preview-check goes to tg_12."
                    },
                    "target": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "the chosen machine, null when none is chosen; its id alone when the project may no longer deploy to it (removed, or its share taken away)",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "project",
                    "target",
                    "chosen_by",
                    "chosen_at",
                    "placed",
                    "says"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token without the read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "put": {
        "tags": [
          "Deploys"
        ],
        "summary": "Choose the machine this project's first deploy goes to.",
        "description": "One of the compose machines the project may deploy to: the workspace's own, or one another workspace shares with the project. While the project runs nowhere, its deploys go there and nowhere else, ahead of the mesh pool and of several online machines; a chosen machine that is offline, cordoned or not ready yet refuses the deploy with a sentence naming it, and the deploy never goes to another machine. A project that runs somewhere already stays there: the choice is kept and `says` tells so. Named by its id; an owner's session may name one of the workspace's own machines by its name. An owner or a member may, a member of the project, by a person's sign-in; a viewer may not, and an agent token is always refused (it changes production). Activity records who chose which machine.",
        "operationId": "setProjectMachine",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the machine",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "the machine's id, as GET /v1/targets lists it",
                    "example": "tg_12"
                  }
                },
                "required": [
                  "target"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the choice and where the next deploy goes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "the machine a project's first deploy goes to, and where its next deploy goes",
                  "properties": {
                    "chosen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "chosen_by": {
                      "type": "string",
                      "description": "who chose it: a person's address or an agent token's name; empty when none is chosen",
                      "example": "owner@example.com"
                    },
                    "placed": {
                      "type": "boolean",
                      "description": "whether the project runs somewhere already; it then stays there, and the choice decides only a project's first deploy"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-preview-check"
                    },
                    "says": {
                      "type": "string",
                      "description": "where the next deploy goes, in a sentence",
                      "example": "The first deploy of novid-preview-check goes to tg_12."
                    },
                    "target": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "the chosen machine, null when none is chosen; its id alone when the project may no longer deploy to it (removed, or its share taken away)",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "project",
                    "target",
                    "chosen_by",
                    "chosen_at",
                    "placed",
                    "says"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or no target",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project, or an agent token (always refused: this changes production, a person's sign-in only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such machine the project may deploy to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a machine that is not a compose machine, or a name several machines have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/move": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Move a project to another workspace the signed-in person owns: the plan, then with confirm the move.",
        "description": "Without `confirm` the answer is the plan and nothing is written: the code's address before and after, its GitHub copy (kept), the share names released, how many secrets move, the machines it runs on and the access the new workspace gets to each, the agent tokens that stop working, and who loses it. With `confirm: true` the move is carried out and the same plan comes back with `done`: git-plane moves the repository, the secrets are copied to the new workspace's vault, every record of the project moves in one transaction (who reaches it is the new workspace's own team; its agent tokens are revoked; its share names are released), and the old vault mount is removed. A move that stops part way (git-plane busy, the vault away) answers 503 and is finished by asking again. Only an active owner of both workspaces may; a project the cluster runs cannot move yet.",
        "operationId": "moveProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "where to, and whether to do it",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "boolean",
                    "description": "carry the move out; without it the answer is the plan alone"
                  },
                  "org": {
                    "type": "string",
                    "description": "the workspace to move it to, as GET /v1/orgs lists it",
                    "example": "growgoddess"
                  }
                },
                "required": [
                  "org"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the plan; with confirm, the move done",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "what moving the project changes; with `done` the move as it was carried out",
                  "properties": {
                    "addresses_released": {
                      "type": "array",
                      "description": "the share names that go back: their label names the workspace the project leaves",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "ggstage-novid.nuved.dev"
                      ]
                    },
                    "code": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "string",
                          "description": "where the repository is cloned from now",
                          "example": "https://git.nuved.io/novid/novid-growgoddess-auth-stage"
                        },
                        "github_copy": {
                          "type": "string",
                          "description": "the copy to GitHub, which moves with the repository to the same GitHub repository: `kept: github.com/\u003crepository\u003e`; empty when the project has none",
                          "example": "kept: github.com/nuved/growgoddess-auth"
                        },
                        "to": {
                          "type": "string",
                          "description": "where it is cloned from after the move",
                          "example": "https://git.nuved.io/growgoddess/novid-growgoddess-auth-stage"
                        }
                      },
                      "required": [
                        "from",
                        "to",
                        "github_copy"
                      ]
                    },
                    "done": {
                      "type": "boolean",
                      "description": "set on the answer to confirm: the move was carried out"
                    },
                    "from": {
                      "type": "string",
                      "example": "novid"
                    },
                    "machines": {
                      "type": "array",
                      "description": "the machines it is placed on, adopted on or being deployed to",
                      "items": {
                        "type": "object",
                        "properties": {
                          "access": {
                            "type": "string",
                            "example": "growgoddess gets access to this machine for the project"
                          },
                          "name": {
                            "type": "string",
                            "example": "Novid's MacBook Pro"
                          },
                          "owner": {
                            "type": "string",
                            "description": "the workspace whose machine it is",
                            "example": "novid"
                          }
                        },
                        "required": [
                          "name",
                          "owner",
                          "access"
                        ]
                      }
                    },
                    "project": {
                      "type": "string",
                      "description": "its name, which it keeps",
                      "example": "novid-growgoddess-auth-stage"
                    },
                    "secrets": {
                      "type": "object",
                      "description": "how many secrets move to the new workspace's vault, at their current values",
                      "properties": {
                        "keys": {
                          "type": "integer",
                          "example": 7
                        },
                        "moved_to": {
                          "type": "string",
                          "example": "growgoddess"
                        }
                      },
                      "required": [
                        "keys",
                        "moved_to"
                      ]
                    },
                    "sentence": {
                      "type": "string",
                      "description": "the plan in one line",
                      "example": "novid-growgoddess-auth-stage moves to growgoddess; its code address changes, 1 share name is released, 2 agent tokens stop working, 1 person loses it"
                    },
                    "teams": {
                      "type": "string",
                      "description": "who reaches it after the move, and who loses it",
                      "example": "reached by growgoddess's team only; novid's teams and people with their own access lose it: 1 person (cy@example.com)"
                    },
                    "to": {
                      "type": "string",
                      "example": "growgoddess"
                    },
                    "tokens_revoked": {
                      "type": "integer",
                      "description": "the project's live agent tokens, which stop working: they name the workspace it leaves",
                      "example": 2
                    }
                  },
                  "required": [
                    "project",
                    "from",
                    "to",
                    "code",
                    "addresses_released",
                    "secrets",
                    "machines",
                    "tokens_revoked",
                    "teams",
                    "sentence"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "org is the workspace to move the project to; or not JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`only an owner of both \u003cfrom\u003e and \u003cto\u003e can move a project between them`; an agent token; or a session that does not say whose it is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`it is already in \u003corg\u003e`; `a move of \u003cname\u003e is running`; `revoke \u003corg\u003e's invitation to \u003cname\u003e first`; `\u003corg\u003e has a project named \u003cname\u003e already`; `\u003cname\u003e is being moved to \u003corg\u003e; ask to move it to \u003corg\u003e again to finish that move first`; `\u003cname\u003e runs on the cluster: moving it to another workspace is not available yet`; or git-plane's own sentence when the new address is taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "could not check that, or read what moving it changes, just now, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`moving code is not available yet`; `\u003cname\u003e's code is busy; ask again in a minute`; or a move that stopped part way (`...: ask again to finish the move`); or moving projects is not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/network-check": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "The project's last network check, as it was answered.",
        "operationId": "lastNetworkCheck",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the last check",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "never checked, or no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Check, now, that the machines running a project's replicas reach each other's replicas over the machine mesh and that nothing else does.",
        "description": "Each replica's machine runs fixed checks as root: the tunnel to every other replica's machine, the other replica's port, and that port from an unprivileged process, which must be refused. The console adds the replica's port on its machine's public address from outside the mesh, which must be closed. Expectations are fixed and results measured; no address is in the answer.",
        "operationId": "networkCheck",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the checks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/placements": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "Where this project's services run, per environment.",
        "description": "The same rows as GET /v1/placements?project=, for a project the caller reaches.",
        "operationId": "listProjectPlacements",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the placements, by project, service and environment",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one service of a project, in one environment, and where it runs",
                    "properties": {
                      "address": {
                        "type": "string",
                        "description": "where it answers from outside; empty for a service that does not",
                        "example": "shop-novid.nuved.app"
                      },
                      "adopted": {
                        "type": "boolean",
                        "description": "whether project is the platform's project that adopted compose_project on this target (POST /v1/projects/{name}/adoptions)",
                        "example": false
                      },
                      "applied": {
                        "type": "object",
                        "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                        "properties": {
                          "cpu_milli": {
                            "type": "integer",
                            "example": 500
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "example": 10737418240
                          },
                          "memory_bytes": {
                            "type": "integer",
                            "example": 536870912
                          },
                          "pids": {
                            "type": "integer",
                            "example": 256
                          }
                        }
                      },
                      "commit": {
                        "type": "string",
                        "description": "what runs, as a git commit",
                        "example": "abc123"
                      },
                      "compose_project": {
                        "type": "string",
                        "description": "the name the target reported it under: its compose project; the same as project unless adopted",
                        "example": "shop"
                      },
                      "deploy_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "the deploy that set these; null for a row no deploy made",
                        "example": 12
                      },
                      "egress": {
                        "type": "array",
                        "description": "the domains the definition declared it talks to (nuved.io/egress), or the one word open: the plan's open tier, every HTTPS domain but the deny-list",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "api.stripe.com",
                          "*.googleapis.com"
                        ]
                      },
                      "environment": {
                        "type": "string",
                        "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                        "example": "production",
                        "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                      },
                      "health": {
                        "type": "string",
                        "description": "what the target's health check says, in its words",
                        "example": "healthy"
                      },
                      "image": {
                        "type": "string",
                        "description": "the image built from it, when there is one",
                        "example": "shop-web:abc123"
                      },
                      "last_error": {
                        "type": "string"
                      },
                      "limits": {
                        "type": "object",
                        "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                        "properties": {
                          "cpu_milli": {
                            "type": "integer",
                            "example": 500
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "example": 10737418240
                          },
                          "memory_bytes": {
                            "type": "integer",
                            "example": 536870912
                          },
                          "pids": {
                            "type": "integer",
                            "example": 256
                          }
                        }
                      },
                      "project": {
                        "type": "string",
                        "description": "the platform's project when adopted, else the name the target reported",
                        "example": "shop"
                      },
                      "public": {
                        "type": "boolean",
                        "description": "whether it is the project's public service (nuved.io/public)",
                        "example": true
                      },
                      "replica": {
                        "type": "integer",
                        "description": "which replica of the service this row is: 0 on the home machine, where every service of the project has a row; 1 and up for a replicated public service's rows on the other machines of the workspace's mesh (docs/replicas.md)",
                        "example": 0
                      },
                      "replicas_desired": {
                        "type": "integer",
                        "description": "how many replicas the deploy asked for; 1 unless the definition said otherwise",
                        "example": 1
                      },
                      "reported_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last report that mentioned it, or the one that stopped it"
                      },
                      "restarts": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "how many times the engine restarted the service's running container (its RestartCount), as the machine last reported it; null from an older nuved-boxd, or for a row with no container",
                        "example": 2
                      },
                      "revision": {
                        "type": "string",
                        "description": "the target's own revision of it",
                        "example": "3"
                      },
                      "service": {
                        "type": "string",
                        "example": "web"
                      },
                      "since": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "since when this commit has run there"
                      },
                      "stateful": {
                        "type": "boolean",
                        "description": "whether it has a volume: one replica, moved with its volume",
                        "example": false
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "queued",
                          "deploying",
                          "live",
                          "failed",
                          "stopped"
                        ]
                      },
                      "target": {
                        "type": "object",
                        "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                        "properties": {
                          "arch": {
                            "type": "string",
                            "description": "the machine's CPU architecture",
                            "example": "aarch64"
                          },
                          "country": {
                            "type": "string",
                            "example": "DE"
                          },
                          "display_name": {
                            "type": "string",
                            "description": "told to an owner of the workspace only; empty for everyone else",
                            "example": "Hetzner"
                          },
                          "id": {
                            "type": "string",
                            "example": "tg_1"
                          },
                          "kernel": {
                            "type": "string",
                            "description": "the machine's kernel release, as its gate read it",
                            "example": "6.8.0-146-generic"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "compose",
                              "kubernetes"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                            "example": "nuved-box"
                          },
                          "provider": {
                            "type": "string",
                            "example": "Hetzner"
                          },
                          "region": {
                            "type": "string",
                            "example": "Falkenstein"
                          },
                          "runtime": {
                            "type": "string",
                            "description": "the container engine on a compose machine",
                            "example": "podman version 4.9.3"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "display_name",
                          "kind",
                          "provider",
                          "region",
                          "country"
                        ]
                      }
                    },
                    "required": [
                      "project",
                      "compose_project",
                      "adopted",
                      "service",
                      "environment",
                      "target",
                      "commit",
                      "image",
                      "revision",
                      "address",
                      "status",
                      "limits",
                      "applied",
                      "health",
                      "since",
                      "reported_at",
                      "last_error",
                      "replicas_desired",
                      "stateful",
                      "egress",
                      "public",
                      "deploy_id",
                      "replica",
                      "restarts"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/previews": {
      "get": {
        "tags": [
          "Previews"
        ],
        "summary": "This project's previews: every branch up, on its way up or on its way down, by branch.",
        "description": "Each with its address, its status, its services and its newest deploy or removal. A viewer of the project reads it too.",
        "operationId": "listPreviews",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the previews, by branch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Preview"
                  }
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Previews"
        ],
        "summary": "Put a branch up as a preview: its own containers, network, new database and address beside production.",
        "description": "The branch runs in its own environment on the machine the project runs on, its network isolated from every other project's and from production's, its database new: empty, filled by the branch's own migrations as the program starts, then the project's seed file (docs/previews.md). Its address is a random nuved.dev name behind the workspace's sign-in. Asked again for the same branch, it deploys the branch's tip again at the same address, keeping its database. With `task` the address is linked to that board item when it is live. An owner, a member, a member of the project or an agent token with the deploy scope may; a viewer may not. Production is untouched.",
        "operationId": "startPreview",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the branch to put up",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "branch": {
                    "type": "string",
                    "example": "feature/login"
                  },
                  "commit": {
                    "type": "string",
                    "description": "the full 40-character commit; left out, the machine deploys the branch's tip"
                  },
                  "task": {
                    "type": "string",
                    "description": "a board item of this project, GG-142 or 142, to link the address to",
                    "example": "GG-142"
                  }
                },
                "required": [
                  "branch"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "queued; the preview as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Preview"
                }
              }
            }
          },
          "400": {
            "description": "no branch, or a branch, commit or task that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project, or a token without the deploy scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "no machine to run it on, none online, or several and nothing to say which",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/previews/{preview}": {
      "delete": {
        "tags": [
          "Previews"
        ],
        "summary": "Take a preview down: its containers, its database, its checkout and its address go.",
        "description": "Its queued deploys are cancelled and a removal is queued on the machine that runs it; the machine stops its containers, deletes its volumes and its checkout, ends its address and forgets it. Production and the other previews are left as they are. 202 with the preview on its way down; 200 when nothing of it had run yet. Who may put one up may take it down.",
        "operationId": "takeDownPreview",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "preview",
            "in": "path",
            "required": true,
            "description": "the preview's environment, as the listing has it",
            "schema": {
              "type": "string",
              "example": "preview-feature-login-3fa9c1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the preview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Preview"
                }
              }
            }
          },
          "202": {
            "description": "the preview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Preview"
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project, or a token without the deploy scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such preview of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/repo": {
      "get": {
        "tags": [
          "GitHub"
        ],
        "summary": "Where a project lives, and the state of its copy to GitHub, ref by ref.",
        "description": "The state is what the copy last recorded; reading it never touches GitHub. When the copy cannot be asked (not switched on for this installation, git-router down, or slower than 5 seconds) the answer is still 200, with state `unavailable` and `reason` and `detail` saying why. An agent token may read its own project's state; its answer leaves out `public_key`. Nothing in either answer says who connected the repository: Activity does, for a person.",
        "operationId": "getRepo",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "where a project lives, and its copy to GitHub",
                  "properties": {
                    "github": {
                      "type": "object",
                      "description": "the project's copy to GitHub",
                      "properties": {
                        "copies": {
                          "type": "object",
                          "description": "what is copied; branches always",
                          "properties": {
                            "branches": {
                              "type": "boolean",
                              "example": true
                            },
                            "checkpoints": {
                              "type": "boolean",
                              "description": "`refs/nuved/run/*` and `wip/*`, as branches `nuved/run/\u003cid\u003e`",
                              "example": false
                            },
                            "tags": {
                              "type": "boolean",
                              "example": true
                            }
                          },
                          "required": [
                            "branches",
                            "tags",
                            "checkpoints"
                          ]
                        },
                        "detail": {
                          "type": "string",
                          "description": "the connection's own trouble in words, or why it is unavailable",
                          "example": ""
                        },
                        "last_copy": {
                          "type": "object",
                          "properties": {
                            "at": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2026-09-29T14:00:04Z"
                            },
                            "seconds_after_push": {
                              "type": "integer",
                              "example": 4
                            }
                          },
                          "required": [
                            "at",
                            "seconds_after_push"
                          ]
                        },
                        "public_key": {
                          "type": "string",
                          "description": "the deploy key's public half, to add on GitHub with write access. Left out for an agent token: connecting is a person's act and the key is what the person adds, so an agent has no use for it",
                          "example": "ssh-ed25519 AAAAC3Nz...oTj2K nuved:acme/shop"
                        },
                        "reason": {
                          "type": "string",
                          "description": "state unavailable only: not_configured (the operator has not switched the copy on), unreachable, or timeout",
                          "example": "not_configured",
                          "enum": [
                            "not_configured",
                            "unreachable",
                            "timeout"
                          ]
                        },
                        "refs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "one ref of the project and its copy",
                            "properties": {
                              "brought_in": {
                                "type": "string",
                                "description": "where GitHub's commits were fetched to, for an apart ref; never merged by nuved",
                                "example": "refs/heads/github/main"
                              },
                              "detail": {
                                "type": "string",
                                "description": "what happened, in words; GitHub's own message for a failed ref",
                                "example": "GitHub has 2 commits this project does not have"
                              },
                              "github": {
                                "type": "string",
                                "description": "the commit GitHub was last seen to hold; absent when none",
                                "example": "9d2f7aa"
                              },
                              "next_attempt": {
                                "type": "string",
                                "format": "date-time",
                                "description": "a failed ref only: the next automatic retry; absent while a changed host key waits for a person",
                                "example": "2026-09-29T14:00:05Z"
                              },
                              "nuved": {
                                "type": "string",
                                "description": "the commit the ref names on nuved",
                                "example": "a41c9e2"
                              },
                              "ref": {
                                "type": "string",
                                "example": "refs/heads/main"
                              },
                              "state": {
                                "type": "string",
                                "example": "apart",
                                "enum": [
                                  "pending",
                                  "in_step",
                                  "apart",
                                  "failed",
                                  "not_copied"
                                ]
                              },
                              "waiting": {
                                "type": "integer",
                                "description": "a failed ref only: commits GitHub does not have yet",
                                "example": 3
                              }
                            },
                            "required": [
                              "ref",
                              "nuved",
                              "state"
                            ]
                          }
                        },
                        "repository": {
                          "type": "string",
                          "description": "owner/name on GitHub; absent when not connected",
                          "example": "acme-corp/shop"
                        },
                        "running": {
                          "type": "string",
                          "description": "a check or an import started from here and still running",
                          "example": "check",
                          "enum": [
                            "check",
                            "import"
                          ]
                        },
                        "state": {
                          "type": "string",
                          "description": "the worst of the refs (failed \u003e apart \u003e pending \u003e in_step); waiting_for_key until a check passes; unavailable when the copy cannot be asked on this installation just now, with reason and detail saying why",
                          "example": "apart",
                          "enum": [
                            "not_connected",
                            "waiting_for_key",
                            "pending",
                            "in_step",
                            "apart",
                            "failed",
                            "unavailable"
                          ]
                        },
                        "waiting": {
                          "type": "integer",
                          "description": "commits GitHub does not have yet, over the failed refs",
                          "example": 0
                        }
                      },
                      "required": [
                        "state",
                        "waiting",
                        "refs"
                      ]
                    },
                    "home": {
                      "type": "string",
                      "description": "the only place main is written",
                      "example": "nuved",
                      "enum": [
                        "nuved"
                      ]
                    }
                  },
                  "required": [
                    "home",
                    "github"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/repo/github": {
      "delete": {
        "tags": [
          "GitHub"
        ],
        "summary": "Stop copying and delete the private key. Nothing is deleted on either side.",
        "description": "Remove the deploy key on GitHub too: nuved can no longer use it, but GitHub still lists it. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`. An owner of the workspace only: a member who is not one gets 403 with code owner_only, recorded in Activity like the agent's refusal.",
        "operationId": "disconnectGitHub",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "disconnected"
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token (agent_token), or a member who is not an owner (owner_only); either way a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "who owns the workspace could not be read just now (owner_unknown); nothing was changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "GitHub"
        ],
        "summary": "Change what is copied besides branches: tags, checkpoints.",
        "description": "A field left out stays as it is. Switching a class on copies the refs that already exist; switching it off deletes nothing on GitHub. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`.",
        "operationId": "setGitHubCopies",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "what to change",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "copies": {
                    "type": "object",
                    "description": "what to copy besides branches, which are always copied; nothing else is accepted",
                    "properties": {
                      "checkpoints": {
                        "type": "boolean",
                        "example": false
                      },
                      "tags": {
                        "type": "boolean",
                        "example": true
                      }
                    }
                  }
                },
                "required": [
                  "copies"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the state, as GET …/repo answers it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "where a project lives, and its copy to GitHub",
                  "properties": {
                    "github": {
                      "type": "object",
                      "description": "the project's copy to GitHub",
                      "properties": {
                        "copies": {
                          "type": "object",
                          "description": "what is copied; branches always",
                          "properties": {
                            "branches": {
                              "type": "boolean",
                              "example": true
                            },
                            "checkpoints": {
                              "type": "boolean",
                              "description": "`refs/nuved/run/*` and `wip/*`, as branches `nuved/run/\u003cid\u003e`",
                              "example": false
                            },
                            "tags": {
                              "type": "boolean",
                              "example": true
                            }
                          },
                          "required": [
                            "branches",
                            "tags",
                            "checkpoints"
                          ]
                        },
                        "detail": {
                          "type": "string",
                          "description": "the connection's own trouble in words, or why it is unavailable",
                          "example": ""
                        },
                        "last_copy": {
                          "type": "object",
                          "properties": {
                            "at": {
                              "type": "string",
                              "format": "date-time",
                              "example": "2026-09-29T14:00:04Z"
                            },
                            "seconds_after_push": {
                              "type": "integer",
                              "example": 4
                            }
                          },
                          "required": [
                            "at",
                            "seconds_after_push"
                          ]
                        },
                        "public_key": {
                          "type": "string",
                          "description": "the deploy key's public half, to add on GitHub with write access. Left out for an agent token: connecting is a person's act and the key is what the person adds, so an agent has no use for it",
                          "example": "ssh-ed25519 AAAAC3Nz...oTj2K nuved:acme/shop"
                        },
                        "reason": {
                          "type": "string",
                          "description": "state unavailable only: not_configured (the operator has not switched the copy on), unreachable, or timeout",
                          "example": "not_configured",
                          "enum": [
                            "not_configured",
                            "unreachable",
                            "timeout"
                          ]
                        },
                        "refs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "one ref of the project and its copy",
                            "properties": {
                              "brought_in": {
                                "type": "string",
                                "description": "where GitHub's commits were fetched to, for an apart ref; never merged by nuved",
                                "example": "refs/heads/github/main"
                              },
                              "detail": {
                                "type": "string",
                                "description": "what happened, in words; GitHub's own message for a failed ref",
                                "example": "GitHub has 2 commits this project does not have"
                              },
                              "github": {
                                "type": "string",
                                "description": "the commit GitHub was last seen to hold; absent when none",
                                "example": "9d2f7aa"
                              },
                              "next_attempt": {
                                "type": "string",
                                "format": "date-time",
                                "description": "a failed ref only: the next automatic retry; absent while a changed host key waits for a person",
                                "example": "2026-09-29T14:00:05Z"
                              },
                              "nuved": {
                                "type": "string",
                                "description": "the commit the ref names on nuved",
                                "example": "a41c9e2"
                              },
                              "ref": {
                                "type": "string",
                                "example": "refs/heads/main"
                              },
                              "state": {
                                "type": "string",
                                "example": "apart",
                                "enum": [
                                  "pending",
                                  "in_step",
                                  "apart",
                                  "failed",
                                  "not_copied"
                                ]
                              },
                              "waiting": {
                                "type": "integer",
                                "description": "a failed ref only: commits GitHub does not have yet",
                                "example": 3
                              }
                            },
                            "required": [
                              "ref",
                              "nuved",
                              "state"
                            ]
                          }
                        },
                        "repository": {
                          "type": "string",
                          "description": "owner/name on GitHub; absent when not connected",
                          "example": "acme-corp/shop"
                        },
                        "running": {
                          "type": "string",
                          "description": "a check or an import started from here and still running",
                          "example": "check",
                          "enum": [
                            "check",
                            "import"
                          ]
                        },
                        "state": {
                          "type": "string",
                          "description": "the worst of the refs (failed \u003e apart \u003e pending \u003e in_step); waiting_for_key until a check passes; unavailable when the copy cannot be asked on this installation just now, with reason and detail saying why",
                          "example": "apart",
                          "enum": [
                            "not_connected",
                            "waiting_for_key",
                            "pending",
                            "in_step",
                            "apart",
                            "failed",
                            "unavailable"
                          ]
                        },
                        "waiting": {
                          "type": "integer",
                          "description": "commits GitHub does not have yet, over the failed refs",
                          "example": 0
                        }
                      },
                      "required": [
                        "state",
                        "waiting",
                        "refs"
                      ]
                    },
                    "home": {
                      "type": "string",
                      "description": "the only place main is written",
                      "example": "nuved",
                      "enum": [
                        "nuved"
                      ]
                    }
                  },
                  "required": [
                    "home",
                    "github"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "nothing to change, a value that is not true or false (bad_copies), or a field this does not take (unknown_field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token: code agent_token, and a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "a body over 4 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "GitHub"
        ],
        "summary": "Connect a GitHub repository: nuved makes a deploy key for it, and copies to it once the key is added.",
        "description": "Answers the public half of a new ed25519 key. Add it to the repository under Settings, Deploy keys, with write access, then POST …/repo/github/check. Nothing is sent to GitHub before that. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`.",
        "operationId": "connectGitHub",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the repository, and what to copy besides branches",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "copies": {
                    "type": "object",
                    "description": "what to copy besides branches, which are always copied; nothing else is accepted",
                    "properties": {
                      "checkpoints": {
                        "type": "boolean",
                        "example": false
                      },
                      "tags": {
                        "type": "boolean",
                        "example": true
                      }
                    }
                  },
                  "repository": {
                    "type": "string",
                    "description": "owner/name, the way GitHub shows it: not a URL, no .git",
                    "example": "acme-corp/shop"
                  }
                },
                "required": [
                  "repository"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the key to add on GitHub",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "public_key": {
                      "type": "string",
                      "example": "ssh-ed25519 AAAAC3Nz...oTj2K nuved:acme/shop"
                    },
                    "state": {
                      "type": "string",
                      "example": "waiting_for_key",
                      "enum": [
                        "waiting_for_key"
                      ]
                    }
                  },
                  "required": [
                    "public_key",
                    "state"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not owner/name (bad_repository), a field this does not take (unknown_field), or copies that are not true or false (bad_copies)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token: code agent_token, and a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already connected (already_connected)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "a body over 4 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/repo/github/check": {
      "post": {
        "tags": [
          "GitHub"
        ],
        "summary": "Check that GitHub accepts the key and that it may write, then copy.",
        "description": "The first copy runs while the request waits, for up to 25 seconds; past that the answer is 202 and the check carries on (for up to 11 minutes), with its outcome in GET …/repo and in Activity. One check or import at a time per project. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`.",
        "operationId": "checkGitHub",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "checked and copied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "copied": {
                      "type": "object",
                      "properties": {
                        "branches": {
                          "type": "integer",
                          "example": 3
                        },
                        "seconds": {
                          "type": "integer",
                          "example": 3
                        },
                        "tags": {
                          "type": "integer",
                          "example": 1
                        }
                      },
                      "required": [
                        "branches",
                        "tags",
                        "seconds"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "example": "in_step"
                    }
                  },
                  "required": [
                    "state",
                    "copied"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "still running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "still running after 25 seconds: it carries on, GET …/repo shows the state and Activity how it ended",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "The check is still running on git-router; it may take minutes."
                    },
                    "operation": {
                      "type": "string",
                      "example": "check",
                      "enum": [
                        "check",
                        "import"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "example": "running",
                      "enum": [
                        "running"
                      ]
                    }
                  },
                  "required": [
                    "state",
                    "operation",
                    "message"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token: code agent_token, and a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "key_not_found, key_read_only, repository_not_found, unrelated_history, copy_running, or operation_running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "GitHub did not answer as expected (github_unreachable, host_key_changed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/repo/github/copy": {
      "post": {
        "tags": [
          "GitHub"
        ],
        "summary": "Copy now, without waiting for the next retry; also clears a hold after a changed host key.",
        "description": "Queued, not waited for: GET …/repo shows the copy land. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`.",
        "operationId": "copyToGitHub",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token: code agent_token, and a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/repo/import": {
      "post": {
        "tags": [
          "GitHub"
        ],
        "summary": "Bring an existing GitHub repository's history into a project that has none, then copy to it.",
        "description": "Only while the project has no commits, and only through a connection to the same repository whose key GitHub accepts. Commits before the import are unattributed. Waits up to 25 seconds, then answers 202 and carries on, as a check does. A person's session only. An agent token gets 403 with code agent_token and its own sentence, and the refusal is written to Activity; so is every other refusal. Refusals carry the spec's code under `code`. An owner of the workspace only: a member who is not one gets 403 with code owner_only, recorded in Activity like the agent's refusal.",
        "operationId": "importRepo",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the repository",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "repository": {
                    "type": "string",
                    "description": "owner/name, the way GitHub shows it: not a URL, no .git",
                    "example": "acme-corp/shop"
                  }
                },
                "required": [
                  "repository"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "imported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "baseline": {
                      "type": "string",
                      "description": "the commit refs/nuved/baseline names",
                      "example": "b4e1d2c"
                    },
                    "imported": {
                      "type": "object",
                      "properties": {
                        "branches": {
                          "type": "integer",
                          "example": 2
                        },
                        "tags": {
                          "type": "integer",
                          "example": 1
                        }
                      },
                      "required": [
                        "branches",
                        "tags"
                      ]
                    },
                    "skipped": {
                      "type": "array",
                      "description": "refs whose names nuved does not take",
                      "items": {
                        "type": "string"
                      }
                    },
                    "state": {
                      "type": "string",
                      "example": "in_step"
                    }
                  },
                  "required": [
                    "state",
                    "imported"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "still running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "still running after 25 seconds: it carries on, GET …/repo shows the state and Activity how it ended",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "The check is still running on git-router; it may take minutes."
                    },
                    "operation": {
                      "type": "string",
                      "example": "check",
                      "enum": [
                        "check",
                        "import"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "example": "running",
                      "enum": [
                        "running"
                      ]
                    }
                  },
                  "required": [
                    "state",
                    "operation",
                    "message"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not owner/name (bad_repository), or a field this does not take (unknown_field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one that has been revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token (agent_token), or a member who is not an owner (owner_only); either way a refused row in Activity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours; or not_connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "project_has_refs, not_connected, other_repository, replicated, or operation_running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "a body over 4 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "who owns the workspace could not be read just now (owner_unknown); nothing was changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the copy is not switched on for this installation (not_configured, naming what the operator sets), or git-router did not answer (unreachable)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "git-router did not answer in time (timeout); it may still have done it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/retry": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Try a failed project's last change again: its create, caps or delete.",
        "description": "For a project whose status is `failed`, or `deleting` with a last error and no next attempt: the job stopped and a person decides. This queues it once more, with its attempts counted from zero.",
        "operationId": "retryProject",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "the project, its change queued again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not retry changes; trying a delete again is an owner's, as the delete is",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the project's last change did not fail, or is still being carried out, or the project is self-hosted or has no cluster yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the retry could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/runs": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "A project's runs, newest first: state, where each runs, cost so far and the three checks.",
        "operationId": "listRuns",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the latest 50 runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one agent run, and how far it got",
                    "properties": {
                      "branch": {
                        "type": "string",
                        "description": "where the run's commits go: `agent/`, the task in a few words, and the run's id",
                        "example": "agent/add-a-healthz-endpoint-and-a-test-12"
                      },
                      "cap_eur": {
                        "type": "number",
                        "description": "the most the run may spend, in euros",
                        "example": 2
                      },
                      "checks": {
                        "type": "object",
                        "description": "what done is claimed on; null is not known yet",
                        "properties": {
                          "commits_ok": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "its branch has commits the default branch does not"
                          },
                          "exit_ok": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "the run's process exited 0"
                          },
                          "preview_ok": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "its preview answered 200; null when no preview was asked for"
                          }
                        },
                        "required": [
                          "exit_ok",
                          "commits_ok",
                          "preview_ok"
                        ]
                      },
                      "class": {
                        "type": "string",
                        "enum": [
                          "small",
                          "medium",
                          "large"
                        ]
                      },
                      "cloud_micro": {
                        "type": "integer",
                        "description": "the cloud part: cost_micro less tokens_micro, so the two always add up",
                        "example": 100000
                      },
                      "cloud_minutes": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "whole minutes the run's pod has run, to now or to its end (0 is under a minute); null when no pod has started",
                        "example": 29
                      },
                      "cost_eur": {
                        "type": "number",
                        "description": "cost so far: cloud minutes at the class rate, plus tokens",
                        "example": 0.02
                      },
                      "cost_micro": {
                        "type": "integer",
                        "description": "the same, in millionths of a euro",
                        "example": 20000
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "ended_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it finished, either way"
                      },
                      "id": {
                        "type": "integer",
                        "example": 12
                      },
                      "lane": {
                        "type": "string",
                        "enum": [
                          "ours",
                          "byo"
                        ]
                      },
                      "last_checkpoint_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last checkpoint the run pushed; null until it pushes one"
                      },
                      "model": {
                        "type": "string",
                        "description": "the model asked for; empty for the default"
                      },
                      "project": {
                        "type": "string",
                        "example": "novid-shop"
                      },
                      "reason": {
                        "type": "string",
                        "description": "why it failed, or what it is waiting for; empty otherwise",
                        "example": "waiting for capacity"
                      },
                      "runner": {
                        "type": "string",
                        "description": "where it runs: `cloud` for our pods; empty while queued",
                        "example": "cloud"
                      },
                      "started_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when a runner took it"
                      },
                      "state": {
                        "type": "string",
                        "description": "queued until a runner takes it; done only when the platform saw it exit 0 with new commits on its branch",
                        "enum": [
                          "queued",
                          "running",
                          "pushed",
                          "previewed",
                          "handing-off",
                          "done",
                          "failed"
                        ]
                      },
                      "task": {
                        "type": "string",
                        "example": "add a /healthz endpoint and a test"
                      },
                      "task_id": {
                        "type": "string",
                        "description": "the board item the run was started for, as it was given (GG-142 or 142); empty for none. The item's links carry the run and its pushed commit",
                        "example": "GG-142"
                      },
                      "tokens_micro": {
                        "type": "integer",
                        "description": "the tokens part of cost_micro, in millionths of a euro; 0 for a run on the workspace's own key",
                        "example": 380000
                      }
                    },
                    "required": [
                      "id",
                      "project",
                      "task",
                      "task_id",
                      "branch",
                      "model",
                      "lane",
                      "class",
                      "cap_eur",
                      "state",
                      "reason",
                      "runner",
                      "cost_eur",
                      "cost_micro",
                      "created_at",
                      "started_at",
                      "ended_at",
                      "last_checkpoint_at",
                      "checks",
                      "tokens_micro",
                      "cloud_micro",
                      "cloud_minutes"
                    ]
                  }
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "runs are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Start an agent run on a project. It is queued at once; a pod runs it moments later.",
        "description": "The run works on its own branch and stops at its cap (default €2) or after 30 minutes, whichever comes first. A project may have 2 runs in progress and a workspace 5; the next one is refused with 409 and the reason, never queued. A run needs a balance above zero, and the project's runs of one UTC day may commit no more than its daily ceiling. A session or the project's agent token may start one; a run's own token may not.",
        "operationId": "startRun",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "what the run should do, and on what",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cap_eur": {
                    "type": "number",
                    "description": "the most it may spend, in euros: more than 0, at most 100; default 2",
                    "example": 2
                  },
                  "class": {
                    "type": "string",
                    "description": "small 250m/512Mi, medium 1 CPU/2Gi, large 2 CPU/4Gi reserved",
                    "enum": [
                      "small",
                      "medium",
                      "large"
                    ]
                  },
                  "lane": {
                    "type": "string",
                    "description": "ours: our keys, metered; byo: your workspace's own key",
                    "enum": [
                      "ours",
                      "byo"
                    ]
                  },
                  "model": {
                    "type": "string",
                    "description": "which model; empty for the default",
                    "example": "claude-sonnet-5"
                  },
                  "task": {
                    "type": "string",
                    "description": "what the agent should do, in words",
                    "example": "add a /healthz endpoint and a test"
                  },
                  "task_id": {
                    "type": "string",
                    "description": "the board item of this project it works on, by its id (GG-142) or its number (142): the run links itself to the item now and its pushed commit at pushed. An id of another project, or one the project does not have, is not linked, and the run starts all the same",
                    "example": "GG-142"
                  }
                },
                "required": [
                  "task",
                  "lane",
                  "class"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the run, queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one agent run, and how far it got",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "description": "where the run's commits go: `agent/`, the task in a few words, and the run's id",
                      "example": "agent/add-a-healthz-endpoint-and-a-test-12"
                    },
                    "cap_eur": {
                      "type": "number",
                      "description": "the most the run may spend, in euros",
                      "example": 2
                    },
                    "checks": {
                      "type": "object",
                      "description": "what done is claimed on; null is not known yet",
                      "properties": {
                        "commits_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "its branch has commits the default branch does not"
                        },
                        "exit_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "the run's process exited 0"
                        },
                        "preview_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "its preview answered 200; null when no preview was asked for"
                        }
                      },
                      "required": [
                        "exit_ok",
                        "commits_ok",
                        "preview_ok"
                      ]
                    },
                    "class": {
                      "type": "string",
                      "enum": [
                        "small",
                        "medium",
                        "large"
                      ]
                    },
                    "cloud_micro": {
                      "type": "integer",
                      "description": "the cloud part: cost_micro less tokens_micro, so the two always add up",
                      "example": 100000
                    },
                    "cloud_minutes": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "whole minutes the run's pod has run, to now or to its end (0 is under a minute); null when no pod has started",
                      "example": 29
                    },
                    "cost_eur": {
                      "type": "number",
                      "description": "cost so far: cloud minutes at the class rate, plus tokens",
                      "example": 0.02
                    },
                    "cost_micro": {
                      "type": "integer",
                      "description": "the same, in millionths of a euro",
                      "example": 20000
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ended_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it finished, either way"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "lane": {
                      "type": "string",
                      "enum": [
                        "ours",
                        "byo"
                      ]
                    },
                    "last_checkpoint_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last checkpoint the run pushed; null until it pushes one"
                    },
                    "model": {
                      "type": "string",
                      "description": "the model asked for; empty for the default"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-shop"
                    },
                    "reason": {
                      "type": "string",
                      "description": "why it failed, or what it is waiting for; empty otherwise",
                      "example": "waiting for capacity"
                    },
                    "runner": {
                      "type": "string",
                      "description": "where it runs: `cloud` for our pods; empty while queued",
                      "example": "cloud"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when a runner took it"
                    },
                    "state": {
                      "type": "string",
                      "description": "queued until a runner takes it; done only when the platform saw it exit 0 with new commits on its branch",
                      "enum": [
                        "queued",
                        "running",
                        "pushed",
                        "previewed",
                        "handing-off",
                        "done",
                        "failed"
                      ]
                    },
                    "task": {
                      "type": "string",
                      "example": "add a /healthz endpoint and a test"
                    },
                    "task_id": {
                      "type": "string",
                      "description": "the board item the run was started for, as it was given (GG-142 or 142); empty for none. The item's links carry the run and its pushed commit",
                      "example": "GG-142"
                    },
                    "tokens_micro": {
                      "type": "integer",
                      "description": "the tokens part of cost_micro, in millionths of a euro; 0 for a run on the workspace's own key",
                      "example": 380000
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "task",
                    "task_id",
                    "branch",
                    "model",
                    "lane",
                    "class",
                    "cap_eur",
                    "state",
                    "reason",
                    "runner",
                    "cost_eur",
                    "cost_micro",
                    "created_at",
                    "started_at",
                    "ended_at",
                    "last_checkpoint_at",
                    "checks",
                    "tokens_micro",
                    "cloud_micro",
                    "cloud_minutes"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "no task, a task_id that is neither an item's id nor its number, or a lane, class or cap that is not one of the choices",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a run's own token may not start runs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "refused by a cap: runs in progress, balance, or the daily ceiling; the error says which; or the project is self-hosted or has no cluster yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "runs are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/secrets": {
      "get": {
        "tags": [
          "Secrets"
        ],
        "summary": "The project's secrets by name: version, who set it last and when; never a value.",
        "description": "The project's own KV in the platform's vault (OpenBao), read as the caller: a person who reaches the project, or the project's agent token. An agent token of another project is refused by the vault's policy (403), not by a rule of this API. Values are read one by one with GET of a key. (~/claude/pm/decisions/2026-10-11-riegel-secrets.md)",
        "operationId": "listSecrets",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the names, sorted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one secret by name: its versions and who set it last; never its value",
                    "properties": {
                      "created_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "key": {
                        "type": "string",
                        "description": "the name: letters, digits, _ . and -, starting with a letter or _, up to 128",
                        "example": "DATABASE_URL"
                      },
                      "updated_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when the current version was written"
                      },
                      "updated_by": {
                        "type": "string",
                        "description": "who wrote the current version: the person's address (agents never write)",
                        "example": "me@example.com"
                      },
                      "version": {
                        "type": "integer",
                        "description": "the current version, from 1",
                        "example": 3
                      },
                      "versions": {
                        "type": "integer",
                        "description": "how many versions are still readable",
                        "example": 3
                      }
                    },
                    "required": [
                      "key",
                      "version",
                      "versions",
                      "created_at",
                      "updated_at",
                      "updated_by"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "the vault's policy for this credential does not reach the project: an agent token of another project, or a viewer writing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/secrets/{key}": {
      "delete": {
        "tags": [
          "Secrets"
        ],
        "summary": "Remove a secret and every version of it.",
        "description": "A person's session only. Programs that read it fail on their next start. Written to the project's activity as `deleted secret/\u003cproject\u003e/\u003ckey\u003e`.",
        "operationId": "deleteSecret",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "the secret's name, as GET /v1/projects/{name}/secrets lists it",
            "schema": {
              "type": "string",
              "example": "DATABASE_URL"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "gone",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "key": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "key",
                    "deleted"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a viewer of the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "get": {
        "tags": [
          "Secrets"
        ],
        "summary": "One secret's value: the current version, or the one asked for.",
        "description": "What a program or an agent reads where it runs, with the project's agent token; the one answer that carries a value. `?version=N` reads an earlier version while it is kept. A viewer of the project lists the names and reads no value (2026-10-21): 403 `Viewers see a secret's name, not its value.`",
        "operationId": "getSecret",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "the secret's name, as GET /v1/projects/{name}/secrets lists it",
            "schema": {
              "type": "string",
              "example": "DATABASE_URL"
            }
          },
          {
            "name": "version",
            "in": "query",
            "description": "an earlier version, from 1; the current one when absent",
            "schema": {
              "type": "integer",
              "example": 2
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the value",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one version's value: the only answer that carries one",
                  "properties": {
                    "key": {
                      "type": "string",
                      "example": "DATABASE_URL"
                    },
                    "updated_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "updated_by": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string",
                      "description": "the value as it was set, up to 64 KiB"
                    },
                    "version": {
                      "type": "integer",
                      "example": 3
                    }
                  },
                  "required": [
                    "key",
                    "version",
                    "value",
                    "updated_at",
                    "updated_by"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "version is not a whole number from 1",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a viewer of the project: `Viewers see a secret's name, not its value.`; or the vault's policy does not reach the project (an agent token of another project)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "put": {
        "tags": [
          "Secrets"
        ],
        "summary": "Set a secret: a new version of the name, by a person.",
        "description": "A person's session only: agents read, people write. A name that exists gets a new version and the old one stays readable by version; a new name starts at 1. The answer carries the version and the writer, never the value. Written to the project's activity as `set secret/\u003cproject\u003e/\u003ckey\u003e`.",
        "operationId": "setSecret",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "env",
            "in": "query",
            "description": "the project's environment (its declared set; default when absent). Each environment is its own KV and database mount with its own roles: a credential of one never opens another.",
            "schema": {
              "type": "string",
              "example": "default"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "the secret's name, as GET /v1/projects/{name}/secrets lists it",
            "schema": {
              "type": "string",
              "example": "DATABASE_URL"
            }
          }
        ],
        "requestBody": {
          "description": "the value",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string",
                    "description": "up to 64 KiB; not empty"
                  }
                },
                "required": [
                  "value"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the secret, with its new version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one secret by name: its versions and who set it last; never its value",
                  "properties": {
                    "created_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "key": {
                      "type": "string",
                      "description": "the name: letters, digits, _ . and -, starting with a letter or _, up to 128",
                      "example": "DATABASE_URL"
                    },
                    "updated_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the current version was written"
                    },
                    "updated_by": {
                      "type": "string",
                      "description": "who wrote the current version: the person's address (agents never write)",
                      "example": "me@example.com"
                    },
                    "version": {
                      "type": "integer",
                      "description": "the current version, from 1",
                      "example": 3
                    },
                    "versions": {
                      "type": "integer",
                      "description": "how many versions are still readable",
                      "example": 3
                    }
                  },
                  "required": [
                    "key",
                    "version",
                    "versions",
                    "created_at",
                    "updated_at",
                    "updated_by"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, an empty value, or a name that is not a secret's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a viewer of the project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project for this credential, or no such secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "a value over 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here, or the project's vault is not ready yet (a project just created gets its policies within a minute)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "What is running in one project, with its address and hourly rate.",
        "operationId": "listServices",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the services",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Service"
                  }
                }
              }
            }
          },
          "404": {
            "description": "no such project, or not one of yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/access": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Who opened a service's addresses at the platform's edge, newest first.",
        "description": "One row per address and who an hour at most, and one per sign-in: a person by their e-mail (how session), a tool by its token's name (`token:\u003cname\u003e`, how token), anybody on an open path (open, how open, by client address). The client's address and the first path asked, never a query, cookie or token. Rows are kept 90 days; the newest 100 are listed. A person who reaches the project reads them; an agent token does not.",
        "operationId": "listEdgeAccess",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "who opened it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EdgeAccessList"
                }
              }
            }
          },
          "400": {
            "description": "that is not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this console keeps no access log",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/domains": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "A service's own hostnames (custom domains), and where each stands.",
        "description": "Every hostname a customer connected to the service, with its state as the DNS and Cloudflare last said it: pending_dns while the TXT record in `verify` (the proof that the hostname is the customer's) or the CNAME to `cname_target` is missing, `message` naming which; issuing while the certificate is; active once it serves; failed with `message` saying what to fix. The state is refreshed every minute. A service that is not exposed, or has no hostnames, answers an empty list.",
        "operationId": "listDomains",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the hostnames",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Domains"
                }
              }
            }
          },
          "400": {
            "description": "that is not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "this console keeps no domains",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Connect a hostname of the customer's own to an exposed service.",
        "description": "The service must be exposed: the hostname goes where its nuved.app address goes, behind the sign-in until the workspace's owner makes it public. Adding it is a claim: the customer adds two records at their DNS provider, the TXT record in `verify`, which proves the hostname theirs, and a CNAME from the hostname to `cname_target`. Until the TXT record matches, Cloudflare is not asked and the edge does not serve the hostname; a claim never proven lapses after seven days. Proven, Cloudflare issues and renews the certificate. Adding the same hostname to the same service again is the same claim, record and all. A hostname another workspace holds answers 409 with code `taken` and this workspace's record in `verify`: it moves here once that record matches (now, if it is there already, answering 201). A subdomain only: an apex takes no CNAME, so the 400 (code `apex`) says to connect www and redirect the apex there.",
        "operationId": "addDomain",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "requestBody": {
          "description": "the hostname",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hostname": {
                    "type": "string",
                    "description": "lower case is kept; a trailing dot is dropped",
                    "example": "shop.example.com"
                  }
                },
                "required": [
                  "hostname"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the claim, pending its records (or proven already)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Domain"
                }
              }
            }
          },
          "400": {
            "description": "not a hostname, one under nuved.app, nuved.io or nuved.dev, or an apex (code apex; the message says to use www)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the hostname is connected already in this workspace, the service is not exposed (expose the service first), another workspace holds the hostname (code taken, with verify), or 20 hostnames of the workspace wait for their TXT record (code too_many_claims)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainConflict"
                }
              }
            }
          },
          "503": {
            "description": "custom domains are not switched on yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/domains/{hostname}": {
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Disconnect a hostname. The edge stops serving it at once; its certificate is deleted.",
        "operationId": "removeDomain",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          },
          {
            "name": "hostname",
            "in": "path",
            "required": true,
            "description": "the customer's hostname, as GET .../domains lists it",
            "schema": {
              "type": "string",
              "example": "shop.example.com"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "disconnected"
          },
          "404": {
            "description": "no such project, or no such hostname on the service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the change could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Make a hostname public, or put it behind the sign-in again.",
        "description": "Only the workspace's owner makes an address public: the account holder answers for what anybody can open there. Anybody who may change the project puts it behind the sign-in.",
        "operationId": "setDomainPublic",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          },
          {
            "name": "hostname",
            "in": "path",
            "required": true,
            "description": "the customer's hostname, as GET .../domains lists it",
            "schema": {
              "type": "string",
              "example": "shop.example.com"
            }
          }
        ],
        "requestBody": {
          "description": "who may open the address",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "public": {
                    "type": "boolean",
                    "description": "true: anybody, with no sign-in (the workspace's owner only); false: the workspace's people, after the sign-in",
                    "example": true
                  }
                },
                "required": [
                  "public"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the hostname as it is now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Domain"
                }
              }
            }
          },
          "400": {
            "description": "public is missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only the workspace's owner can make an address public",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such hostname on the service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/expose": {
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Take a service's public address away. It keeps running; the internet stops reaching it.",
        "description": "Queued, and answered at once. Only an address this platform created or adopted is removed: a route somebody wrote by hand is theirs. For a project a machine's agent runs, the edge stops serving the address at once and a deploy of the commit that runs is queued; the service's relay name stays closed until that deploy is live.",
        "operationId": "unexposeService",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "the address going, status unexposing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exposure"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the address was written by hand, not by the platform, and is not taken away, or the project is self-hosted or has no cluster yet (on a console without the edge)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the change could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Projects"
        ],
        "summary": "Make a service's nuved.app address public, open some of its paths, or put it behind the sign-in again.",
        "description": "For a service the platform's edge serves (a project a machine runs). `public` and `open_paths` each change only when given. Only the workspace's owner makes an address public or opens a path; anybody who may change the project puts it behind the sign-in (`public` false, `open_paths` empty). An open path passes with no sign-in, for a service with its own login and tokens: `git` (git's smart-HTTP paths, `/\u003cowner\u003e/\u003crepo\u003e/info/refs`, `git-upload-pack` and `git-receive-pack`) or a path prefix that starts and ends with / (`/api/`), at most 16. Requests on them that carry Basic credentials are limited to 30 a minute per client address, with bursts of 60, then 429. Tools pass anywhere on the address with the header `X-Nuved-Token: \u003can agent token of this project\u003e`, which the edge strips (this concerns opening the exposed address, not calling this API; an agent token is refused here); never `Authorization`, which is the service's own. An unexpose closes every path again. Its hostnames (custom domains) are each made public on their own, and open the service's paths.",
        "operationId": "setExposePublic",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "requestBody": {
          "description": "who may open the address: public, open paths, or both",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "open_paths": {
                    "type": "array",
                    "description": "the paths that pass with no sign-in, replacing the ones there were: git, or a prefix that starts and ends with /; at most 16; empty for none. Any but empty is the owner's",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "git",
                      "/api/"
                    ]
                  },
                  "public": {
                    "type": "boolean",
                    "description": "true: anybody, with no sign-in (the workspace's owner only); false: the workspace's people, after the sign-in",
                    "example": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the address, status exposing, whether it is public, and its open paths",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExposureOnEdge"
                }
              }
            }
          },
          "400": {
            "description": "neither public nor open_paths, an open path that is not one (code bad_open_path), more than 16 (code too_many_open_paths), or that is not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only the workspace's owner can make an address public, or open paths of it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the service is not exposed on the edge",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Give a service a public address, so the internet can reach it.",
        "description": "Queues the route the platform's gateway needs and answers with the address it will carry, `\u003cservice\u003e-\u003cworkspace\u003e.nuved.io` for the default project and `\u003cservice\u003e-\u003cworkspace\u003e-\u003cproject\u003e.nuved.io` for any other. Exposing twice is exposing once, and a route that already existed is adopted rather than replaced. A missing service or a taken address is refused at once when the cluster answers. For a project with no cluster that a machine's agent (nuved-boxd) runs, the address is `\u003cservice\u003e-\u003cproject\u003e.nuved.app`, served by the platform's edge behind the workspace's login, and the service is the project's public one, or one shared already, in production; a deploy of the commit that runs is queued to tell the machine (none when one is queued already). A person's sign-in only: an agent token is always refused, because this changes production.",
        "operationId": "exposeService",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "where it will answer, status exposing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exposure"
                }
              }
            }
          },
          "400": {
            "description": "that is not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no service by that name in it (for a project a machine runs: none placed in production as its public service)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "another route or service already answers on that address, or the project is self-hosted or has no cluster yet (on a console without the edge)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the change could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/guests": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "The people outside the workspace who may open a service's addresses.",
        "description": "Each guest opens the service's nuved.app address and its hostnames with a code sent to their inbox, until ends_at, and nothing else of the workspace. Ended invitations stay listed, live false, until they are revoked or invited again.",
        "operationId": "listGuests",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the guests, by address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "guests": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "a person outside the workspace who may open one service's addresses until ends_at",
                        "properties": {
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "example": "2026-10-22T12:00:00Z"
                          },
                          "email": {
                            "type": "string",
                            "description": "lower case",
                            "example": "ada@example.com"
                          },
                          "ends_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the invitation ends",
                            "example": "2026-10-29T12:00:00Z"
                          },
                          "invited_by": {
                            "type": "string",
                            "description": "who invited them, last",
                            "example": "owner@example.com"
                          },
                          "live": {
                            "type": "boolean",
                            "description": "false once ends_at has passed; the row stays until it is revoked or invited again",
                            "example": true
                          }
                        },
                        "required": [
                          "email",
                          "ends_at",
                          "live",
                          "invited_by",
                          "created_at"
                        ]
                      }
                    }
                  },
                  "required": [
                    "guests"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "that is not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "guests are not switched on yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Let one e-mail address open an exposed service's addresses for a while (the owner only).",
        "description": "The guest signs in on the address with a 6-digit code sent to their inbox, good once for 10 minutes; the sign-in lasts 24 hours at most and never past the invitation. `days` is 7 unless said, 90 at most. Inviting the same address again sets the new end. Only the workspace's owner invites: the account holder answers for who opens the address. A person's session only: an agent token is refused.",
        "operationId": "addGuest",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          }
        ],
        "requestBody": {
          "description": "who, and for how long",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "integer",
                    "description": "1 to 90; 7 when left out",
                    "example": 7
                  },
                  "email": {
                    "type": "string",
                    "description": "one address; kept in lower case",
                    "example": "ada@example.com"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the guest as now stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a person outside the workspace who may open one service's addresses until ends_at",
                  "properties": {
                    "created_at": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-10-22T12:00:00Z"
                    },
                    "email": {
                      "type": "string",
                      "description": "lower case",
                      "example": "ada@example.com"
                    },
                    "ends_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the invitation ends",
                      "example": "2026-10-29T12:00:00Z"
                    },
                    "invited_by": {
                      "type": "string",
                      "description": "who invited them, last",
                      "example": "owner@example.com"
                    },
                    "live": {
                      "type": "boolean",
                      "description": "false once ends_at has passed; the row stays until it is revoked or invited again",
                      "example": true
                    }
                  },
                  "required": [
                    "email",
                    "ends_at",
                    "live",
                    "invited_by",
                    "created_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "not an e-mail address, days outside 1 to 90, or not a service name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only the workspace's owner can invite guests to an address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the service is not exposed: expose it first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "guests are not switched on yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/services/{service}/guests/{email}": {
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Revoke a guest (the owner only): their code and their sign-ins end at once.",
        "operationId": "revokeGuest",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "the service's name, as GET /v1/projects/{name}/services lists it",
            "schema": {
              "type": "string",
              "example": "hello"
            }
          },
          {
            "name": "email",
            "in": "path",
            "required": true,
            "description": "the guest's address, as GET .../guests lists it",
            "schema": {
              "type": "string",
              "example": "ada@example.com"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "revoked"
          },
          "400": {
            "description": "not an e-mail address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only the workspace's owner can revoke a guest",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such guest on the service",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the change could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/sizing": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "How each service of this project is sized: measured, recommended, applied, manual, and the project's mode.",
        "description": "The machine's agent samples every container's cgroup every 15 s and sends the percentiles once a minute; the console recommends memory = max(p99 peak × 1.3, 64 MiB) and CPU = max(p95 × 1.5, 0.1 core), rounded to 16 MiB and 0.05 core and capped by the plan, with a first-deploy default by the image's kind. The project page's service row reads this (docs/automatic-sizing.md).",
        "operationId": "projectSizing",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the project's sizing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a project's mode and every service's sizing",
                  "properties": {
                    "mode": {
                      "type": "string",
                      "description": "observe: measure, recommend and show, apply nothing (the default); apply: the machine sets the numbers",
                      "enum": [
                        "observe",
                        "apply"
                      ]
                    },
                    "services": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "one service's sizing: what runs, what is recommended, what was measured",
                        "properties": {
                          "applied": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "basis": {
                            "type": "string",
                            "description": "where the recommendation came from",
                            "example": "measured over 3 days"
                          },
                          "days": {
                            "type": "integer",
                            "description": "days of the last week that hold samples",
                            "example": 3
                          },
                          "decision": {
                            "type": "string",
                            "description": "what the applier said this minute: \"would raise …\" in observe mode",
                            "example": "would lower memory to 192 MiB (was 512 MiB): measured over 3 days"
                          },
                          "environment": {
                            "type": "string",
                            "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                            "example": "production",
                            "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                          },
                          "hours": {
                            "type": "integer",
                            "description": "hours of the last day that hold samples",
                            "example": 24
                          },
                          "image": {
                            "type": "string",
                            "example": "localhost/shop-web:latest"
                          },
                          "kind": {
                            "type": "string",
                            "description": "the image's kind, told from its environment and name; the first-deploy default comes from it",
                            "enum": [
                              "static",
                              "go",
                              "node",
                              "jvm",
                              "python",
                              "ruby",
                              "database",
                              "unknown"
                            ]
                          },
                          "last_change": {
                            "type": "string",
                            "description": "the last change the applier made",
                            "example": "raised memory to 768 MiB (was 512 MiB): OOM killed at 12:03"
                          },
                          "last_change_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "managed": {
                            "type": "boolean",
                            "description": "deployed by the machine's agent, so the applier may resize it; false for a container only observed"
                          },
                          "manual": {
                            "type": "object",
                            "description": "a field a person set (a nuved.io/memory or nuved.io/cpu label, or explicit limits in the definition); the applier never touches it",
                            "properties": {
                              "cpu": {
                                "type": "boolean"
                              },
                              "memory": {
                                "type": "boolean"
                              }
                            },
                            "required": [
                              "memory",
                              "cpu"
                            ]
                          },
                          "recommended": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "service": {
                            "type": "string",
                            "example": "web"
                          },
                          "since": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "the first sample"
                          },
                          "state": {
                            "type": "string",
                            "description": "manual: a person set both fields; automatic: the applier set the numbers; measuring: under a day of samples; recommended: a day or more, shown and not applied; unmeasured: no samples",
                            "enum": [
                              "manual",
                              "measuring",
                              "automatic",
                              "recommended",
                              "unmeasured"
                            ]
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "the last report that carried it"
                          },
                          "windows": {
                            "type": "object",
                            "description": "the three windows, 1h, 24h and 7d",
                            "properties": {
                              "1h": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              },
                              "24h": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              },
                              "7d": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              }
                            }
                          }
                        },
                        "required": [
                          "service",
                          "environment",
                          "state",
                          "image",
                          "kind",
                          "managed",
                          "manual",
                          "applied",
                          "recommended",
                          "basis",
                          "since",
                          "hours",
                          "days",
                          "windows",
                          "decision",
                          "last_change",
                          "last_change_at",
                          "updated_at"
                        ]
                      }
                    }
                  },
                  "required": [
                    "mode",
                    "services"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "automatic sizing is not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Targets"
        ],
        "summary": "Switch a project between observe and apply.",
        "description": "The owner's. In observe the numbers are measured, recommended and shown, and the applier only says what it would do; in apply the machine's agent raises a service's memory at once on an OOM kill or five minutes above 90 % of its ceiling (× 1.5, capped by the plan) and lowers after a day of samples, never under the week's peak + 30 %, one service per project per hour. A field a person set is never touched.",
        "operationId": "setProjectSizing",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "requestBody": {
          "description": "the mode",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "observe",
                      "apply"
                    ]
                  }
                },
                "required": [
                  "mode"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the project's sizing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a project's mode and every service's sizing",
                  "properties": {
                    "mode": {
                      "type": "string",
                      "description": "observe: measure, recommend and show, apply nothing (the default); apply: the machine sets the numbers",
                      "enum": [
                        "observe",
                        "apply"
                      ]
                    },
                    "services": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "one service's sizing: what runs, what is recommended, what was measured",
                        "properties": {
                          "applied": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "basis": {
                            "type": "string",
                            "description": "where the recommendation came from",
                            "example": "measured over 3 days"
                          },
                          "days": {
                            "type": "integer",
                            "description": "days of the last week that hold samples",
                            "example": 3
                          },
                          "decision": {
                            "type": "string",
                            "description": "what the applier said this minute: \"would raise …\" in observe mode",
                            "example": "would lower memory to 192 MiB (was 512 MiB): measured over 3 days"
                          },
                          "environment": {
                            "type": "string",
                            "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                            "example": "production",
                            "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                          },
                          "hours": {
                            "type": "integer",
                            "description": "hours of the last day that hold samples",
                            "example": 24
                          },
                          "image": {
                            "type": "string",
                            "example": "localhost/shop-web:latest"
                          },
                          "kind": {
                            "type": "string",
                            "description": "the image's kind, told from its environment and name; the first-deploy default comes from it",
                            "enum": [
                              "static",
                              "go",
                              "node",
                              "jvm",
                              "python",
                              "ruby",
                              "database",
                              "unknown"
                            ]
                          },
                          "last_change": {
                            "type": "string",
                            "description": "the last change the applier made",
                            "example": "raised memory to 768 MiB (was 512 MiB): OOM killed at 12:03"
                          },
                          "last_change_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "managed": {
                            "type": "boolean",
                            "description": "deployed by the machine's agent, so the applier may resize it; false for a container only observed"
                          },
                          "manual": {
                            "type": "object",
                            "description": "a field a person set (a nuved.io/memory or nuved.io/cpu label, or explicit limits in the definition); the applier never touches it",
                            "properties": {
                              "cpu": {
                                "type": "boolean"
                              },
                              "memory": {
                                "type": "boolean"
                              }
                            },
                            "required": [
                              "memory",
                              "cpu"
                            ]
                          },
                          "recommended": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "service": {
                            "type": "string",
                            "example": "web"
                          },
                          "since": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "the first sample"
                          },
                          "state": {
                            "type": "string",
                            "description": "manual: a person set both fields; automatic: the applier set the numbers; measuring: under a day of samples; recommended: a day or more, shown and not applied; unmeasured: no samples",
                            "enum": [
                              "manual",
                              "measuring",
                              "automatic",
                              "recommended",
                              "unmeasured"
                            ]
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "the last report that carried it"
                          },
                          "windows": {
                            "type": "object",
                            "description": "the three windows, 1h, 24h and 7d",
                            "properties": {
                              "1h": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              },
                              "24h": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              },
                              "7d": {
                                "type": "object",
                                "description": "percentiles of the per-bucket peaks over one window",
                                "properties": {
                                  "cpu_p50": {
                                    "type": "integer",
                                    "description": "millicores",
                                    "example": 100
                                  },
                                  "cpu_p95": {
                                    "type": "integer",
                                    "example": 180
                                  },
                                  "cpu_p99": {
                                    "type": "integer",
                                    "example": 300
                                  },
                                  "mem_p50": {
                                    "type": "integer",
                                    "example": 94371840
                                  },
                                  "mem_p95": {
                                    "type": "integer",
                                    "example": 104857600
                                  },
                                  "mem_p99": {
                                    "type": "integer",
                                    "example": 146800640
                                  },
                                  "oom": {
                                    "type": "integer",
                                    "description": "OOM kills in the window",
                                    "example": 0
                                  },
                                  "peak_bytes": {
                                    "type": "integer",
                                    "example": 157286400
                                  },
                                  "samples": {
                                    "type": "integer",
                                    "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                    "example": 1440
                                  }
                                },
                                "required": [
                                  "samples",
                                  "mem_p50",
                                  "mem_p95",
                                  "mem_p99",
                                  "cpu_p50",
                                  "cpu_p95",
                                  "cpu_p99",
                                  "peak_bytes",
                                  "oom"
                                ]
                              }
                            }
                          }
                        },
                        "required": [
                          "service",
                          "environment",
                          "state",
                          "image",
                          "kind",
                          "managed",
                          "manual",
                          "applied",
                          "recommended",
                          "basis",
                          "since",
                          "hours",
                          "days",
                          "windows",
                          "decision",
                          "last_change",
                          "last_change_at",
                          "updated_at"
                        ]
                      }
                    }
                  },
                  "required": [
                    "mode",
                    "services"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a mode that is not observe or apply",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not the workspace's owner, or an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "automatic sizing is not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/sizing/tickets": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The project's OOM tickets, newest first.",
        "description": "One per hour per service: the service, when, the peak, the ceiling it hit, what the recommender says and the last lines it logged. The run of the project's own agent that works a ticket is a later task; status stays open.",
        "operationId": "listSizingTickets",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the tickets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "an OOM kill as a ticket on the project",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the kernel killed it"
                          },
                          "ceiling_bytes": {
                            "type": "integer",
                            "description": "memory.max at the time",
                            "example": 536870912
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "environment": {
                            "type": "string",
                            "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                            "example": "production",
                            "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                          },
                          "id": {
                            "type": "integer",
                            "example": 12
                          },
                          "log_lines": {
                            "type": "string",
                            "description": "the last 50 lines it logged, up to 16 KiB; empty for a container the agent could not read"
                          },
                          "outcome": {
                            "type": "string",
                            "description": "what was done about it; empty while open"
                          },
                          "peak_bytes": {
                            "type": "integer",
                            "description": "the most it held",
                            "example": 536870912
                          },
                          "recommended": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "service": {
                            "type": "string",
                            "example": "web"
                          },
                          "status": {
                            "type": "string",
                            "description": "open until the project's agent works it (a later task)",
                            "enum": [
                              "open",
                              "done"
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "service",
                          "environment",
                          "at",
                          "peak_bytes",
                          "ceiling_bytes",
                          "recommended",
                          "log_lines",
                          "status",
                          "outcome",
                          "created_at"
                        ]
                      }
                    }
                  },
                  "required": [
                    "tickets"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "automatic sizing is not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/projects/{name}/teams": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "The teams that reach this project, and the role of each.",
        "operationId": "listProjectTeams",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the teams, the workspace's own first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one team on a project, and its role there",
                    "properties": {
                      "role": {
                        "type": "string",
                        "description": "member changes the project, viewer reads it",
                        "enum": [
                          "member",
                          "viewer"
                        ]
                      },
                      "team": {
                        "type": "string",
                        "example": "design"
                      }
                    },
                    "required": [
                      "team",
                      "role"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "People"
        ],
        "summary": "Make this project's teams exactly these, each as member or viewer. An owner's.",
        "description": "When somebody would lose the project by the change, nothing is written unless the request carries `?confirm=true`: the 409 says how many and who, and the same count is what applies when confirmed. Owners and members of the workspace reach every project and are never counted; a person narrowed to some projects reaches their own projects and those of their teams. An empty list takes every team off the project.",
        "operationId": "setProjectTeams",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "the project's full name, as GET /v1/projects lists it",
            "schema": {
              "type": "string",
              "example": "novid-shop"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "description": "true to go on when somebody loses the project by the change",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "requestBody": {
          "description": "the project's teams",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "teams": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "one team on a project, and its role there",
                      "properties": {
                        "role": {
                          "type": "string",
                          "description": "member changes the project, viewer reads it",
                          "enum": [
                            "member",
                            "viewer"
                          ]
                        },
                        "team": {
                          "type": "string",
                          "example": "design"
                        }
                      },
                      "required": [
                        "team",
                        "role"
                      ]
                    }
                  }
                },
                "required": [
                  "teams"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "applied: who lost and who gained the project, and its teams now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "gain": {
                      "type": "array",
                      "description": "who reaches it now and did not",
                      "items": {
                        "type": "string"
                      }
                    },
                    "lose": {
                      "type": "integer",
                      "description": "how many people lost the project",
                      "example": 0
                    },
                    "people": {
                      "type": "array",
                      "description": "who lost it",
                      "items": {
                        "type": "string"
                      }
                    },
                    "teams": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "one team on a project, and its role there",
                        "properties": {
                          "role": {
                            "type": "string",
                            "description": "member changes the project, viewer reads it",
                            "enum": [
                              "member",
                              "viewer"
                            ]
                          },
                          "team": {
                            "type": "string",
                            "example": "design"
                          }
                        },
                        "required": [
                          "team",
                          "role"
                        ]
                      }
                    }
                  },
                  "required": [
                    "lose",
                    "people",
                    "gain",
                    "teams"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "no teams list, a role that is not member or viewer, or a team named twice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project, or no such team",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "somebody would lose the project and confirm=true was not sent; nothing was written",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "2 people lose novid-shop (a@example.com, b@example.com): send confirm=true to go on"
                    },
                    "lose": {
                      "type": "integer",
                      "example": 2
                    },
                    "people": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error",
                    "lose",
                    "people"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/rates": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "What this platform charges, and what that price was derived from.",
        "description": "The three prices are not chosen: they are yesterday's AWS bill for the machines, divided by the capacity the cluster can sell after the platform's own pods have taken theirs, converted at the ECB rate of the day. `derived_from` is the summary of those measurements, and `since` is when the card was worked out. A card is only replaced when a rate moves more than 5%, so this number is stable from day to day. `shared_max` is the most one container may reserve on a shared machine: a quarter of the smallest shared machine's sellable capacity, worked out again every day. Admission refuses a container that asks for more, so read it before writing a manifest. No credential: what something costs is not a secret.",
        "operationId": "getRates",
        "responses": {
          "200": {
            "description": "the rate card in force",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Rates"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/runners": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Offer this machine as the place the workspace's runs execute first; the cloud takes them when it is off.",
        "description": "`nuved runner start` sends this once, at every start. A name the workspace already has keeps its id. A session offers a runner that takes any of the workspace's runs; a project's agent token one that takes only that project's; a run's own token none. The policy is kept to be shown: the runner applies it itself and says so in every heartbeat.",
        "operationId": "registerRunner",
        "requestBody": {
          "description": "what this machine has, and when it takes a run",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cores": {
                    "type": "integer",
                    "example": 10
                  },
                  "has_local_models": {
                    "type": "boolean",
                    "description": "Ollama or LM Studio answers on the machine"
                  },
                  "memory_gb": {
                    "type": "integer",
                    "description": "whole GiB, rounded",
                    "example": 32
                  },
                  "name": {
                    "type": "string",
                    "description": "what the console calls it: lower case, letters, digits and dashes (anything else becomes a dash); not `cloud`",
                    "example": "novids-macbook"
                  },
                  "policy": {
                    "type": "object",
                    "description": "when the runner takes a run; it applies this itself",
                    "properties": {
                      "never_after": {
                        "type": "string",
                        "description": "local time on the runner, 24-hour; none from then until 06:00; empty for none",
                        "example": "22:00"
                      },
                      "only_idle": {
                        "type": "boolean",
                        "description": "only when nobody is using the machine"
                      },
                      "only_on_ac": {
                        "type": "boolean",
                        "description": "only on mains power"
                      }
                    }
                  }
                },
                "required": [
                  "name",
                  "cores",
                  "memory_gb"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the runner, registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "rn_1"
                    },
                    "name": {
                      "type": "string",
                      "description": "the name as the console shows it",
                      "example": "novids-macbook"
                    }
                  },
                  "required": [
                    "id",
                    "name"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, no name, or no cores and memory",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a run's own token may not offer a runner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "runners are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/runners/{id}/heartbeat": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Say where this runner is and what its runs did; hear back a run to start, or one to drop.",
        "description": "Every 30 seconds, and at once after anything changes. A runner not heard from for 75 seconds is gone: a run it was handed resumes in the cloud from its last checkpoint, after its git token is revoked. A run is handed over only in the answer to a heartbeat that says idle. `runs` is the run in progress and every run that ended since the last answered heartbeat; a run this runner was handed and does not report is taken to be lost, and resumes in the cloud.",
        "operationId": "runnerHeartbeat",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the runner's id, as registering it answers",
            "schema": {
              "type": "string",
              "example": "rn_1"
            }
          }
        ],
        "requestBody": {
          "description": "what the runner is, and what its runs did",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "which part of the policy says not now, or why it let a run go",
                    "example": "on battery power"
                  },
                  "runs": {
                    "type": "array",
                    "description": "never null; [] when there are none",
                    "items": {
                      "type": "object",
                      "properties": {
                        "checkpoint": {
                          "type": "object",
                          "description": "with handing-off: what the hand-off pushed",
                          "properties": {
                            "at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "run": {
                              "type": "string",
                              "description": "`refs/nuved/run/\u003cid\u003e`"
                            },
                            "wip": {
                              "type": "string",
                              "description": "`refs/nuved/wip/\u003cid\u003e`"
                            }
                          }
                        },
                        "error": {
                          "type": "string",
                          "description": "why no checkpoint could be pushed, or why the run did not start"
                        },
                        "exit_code": {
                          "type": "integer",
                          "description": "with exited"
                        },
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "state": {
                          "type": "string",
                          "description": "running here; exited by itself (exit_code); handing-off, checkpointed (checkpoint, or error); stopped because the platform said so; not-started (error)",
                          "enum": [
                            "running",
                            "exited",
                            "handing-off",
                            "stopped",
                            "not-started"
                          ]
                        }
                      },
                      "required": [
                        "id",
                        "state"
                      ]
                    }
                  },
                  "state": {
                    "type": "string",
                    "description": "idle takes a run now; busy runs one; paused: its policy says not now; handing-off let its run go; offline is stopping",
                    "enum": [
                      "idle",
                      "busy",
                      "paused",
                      "handing-off",
                      "offline"
                    ]
                  }
                },
                "required": [
                  "state",
                  "runs"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "nothing to do (both null), a run to start, or a run to drop",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "a run placed on this runner, to start now, with the credentials its pod would get",
                      "properties": {
                        "branch": {
                          "type": "string",
                          "example": "agent/add-a-healthz-endpoint-and-a-test-12"
                        },
                        "class": {
                          "type": "string",
                          "enum": [
                            "small",
                            "medium",
                            "large"
                          ]
                        },
                        "gateway_key": {
                          "type": "string",
                          "description": "the run's own key at the gateway; a secret"
                        },
                        "gateway_url": {
                          "type": "string",
                          "example": "https://gateway.nuved.io"
                        },
                        "git_token": {
                          "type": "string",
                          "description": "the run's own git token, sent this once; a secret"
                        },
                        "git_url": {
                          "type": "string",
                          "example": "https://git.nuved.io/novid/shop"
                        },
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "lane": {
                          "type": "string",
                          "enum": [
                            "ours",
                            "byo"
                          ]
                        },
                        "model": {
                          "type": "string",
                          "description": "empty for the platform's default"
                        },
                        "task": {
                          "type": "string",
                          "example": "add a /healthz endpoint and a test"
                        }
                      },
                      "required": [
                        "id",
                        "task",
                        "branch",
                        "model",
                        "lane",
                        "class",
                        "gateway_url",
                        "gateway_key",
                        "git_url",
                        "git_token"
                      ]
                    },
                    "stop": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "a run this runner must drop without a checkpoint: it belongs elsewhere now",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "reason": {
                          "type": "string",
                          "example": "run 12 moved to the cloud while this machine was quiet"
                        }
                      },
                      "required": [
                        "id",
                        "reason"
                      ]
                    }
                  },
                  "required": [
                    "run",
                    "stop"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or a state that is not one of the five",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a run's own token may not speak for a runner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such runner in this workspace, or not one of the agent token's project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "runners are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/runs/{id}": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "One run: state, where it runs, cost so far, last checkpoint and the three checks.",
        "operationId": "getRun",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the run's id, as starting it or listing runs answers",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one agent run, and how far it got",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "description": "where the run's commits go: `agent/`, the task in a few words, and the run's id",
                      "example": "agent/add-a-healthz-endpoint-and-a-test-12"
                    },
                    "cap_eur": {
                      "type": "number",
                      "description": "the most the run may spend, in euros",
                      "example": 2
                    },
                    "checks": {
                      "type": "object",
                      "description": "what done is claimed on; null is not known yet",
                      "properties": {
                        "commits_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "its branch has commits the default branch does not"
                        },
                        "exit_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "the run's process exited 0"
                        },
                        "preview_ok": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "its preview answered 200; null when no preview was asked for"
                        }
                      },
                      "required": [
                        "exit_ok",
                        "commits_ok",
                        "preview_ok"
                      ]
                    },
                    "class": {
                      "type": "string",
                      "enum": [
                        "small",
                        "medium",
                        "large"
                      ]
                    },
                    "cloud_micro": {
                      "type": "integer",
                      "description": "the cloud part: cost_micro less tokens_micro, so the two always add up",
                      "example": 100000
                    },
                    "cloud_minutes": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "whole minutes the run's pod has run, to now or to its end (0 is under a minute); null when no pod has started",
                      "example": 29
                    },
                    "cost_eur": {
                      "type": "number",
                      "description": "cost so far: cloud minutes at the class rate, plus tokens",
                      "example": 0.02
                    },
                    "cost_micro": {
                      "type": "integer",
                      "description": "the same, in millionths of a euro",
                      "example": 20000
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ended_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it finished, either way"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "lane": {
                      "type": "string",
                      "enum": [
                        "ours",
                        "byo"
                      ]
                    },
                    "last_checkpoint_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last checkpoint the run pushed; null until it pushes one"
                    },
                    "model": {
                      "type": "string",
                      "description": "the model asked for; empty for the default"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-shop"
                    },
                    "reason": {
                      "type": "string",
                      "description": "why it failed, or what it is waiting for; empty otherwise",
                      "example": "waiting for capacity"
                    },
                    "runner": {
                      "type": "string",
                      "description": "where it runs: `cloud` for our pods; empty while queued",
                      "example": "cloud"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when a runner took it"
                    },
                    "state": {
                      "type": "string",
                      "description": "queued until a runner takes it; done only when the platform saw it exit 0 with new commits on its branch",
                      "enum": [
                        "queued",
                        "running",
                        "pushed",
                        "previewed",
                        "handing-off",
                        "done",
                        "failed"
                      ]
                    },
                    "task": {
                      "type": "string",
                      "example": "add a /healthz endpoint and a test"
                    },
                    "task_id": {
                      "type": "string",
                      "description": "the board item the run was started for, as it was given (GG-142 or 142); empty for none. The item's links carry the run and its pushed commit",
                      "example": "GG-142"
                    },
                    "tokens_micro": {
                      "type": "integer",
                      "description": "the tokens part of cost_micro, in millionths of a euro; 0 for a run on the workspace's own key",
                      "example": 380000
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "task",
                    "task_id",
                    "branch",
                    "model",
                    "lane",
                    "class",
                    "cap_eur",
                    "state",
                    "reason",
                    "runner",
                    "cost_eur",
                    "cost_micro",
                    "created_at",
                    "started_at",
                    "ended_at",
                    "last_checkpoint_at",
                    "checks",
                    "tokens_micro",
                    "cloud_micro",
                    "cloud_minutes"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "no such run in this workspace, or not in the agent token's project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "runs are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/services": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The services the caller reaches: their state, and whether the newest change of each project went live.",
        "description": "One row per service of every project the caller reaches, in one environment, production unless `?environment=` says. A person reaches what GET /v1/placements shows them (an owner or member every project, a person with project roles theirs); an agent token its own project; another workspace nothing. Replicas are one row: the worst state, the restarts summed. No machine is named. Newest change first. A bare array, [] when the caller reaches no service. The console's Services page and the desktop app read it every 10 s; the git plane is read at most once per project per 10 s.",
        "operationId": "listReachedServices",
        "parameters": [
          {
            "name": "environment",
            "in": "query",
            "description": "production unless said",
            "schema": {
              "type": "string",
              "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
              "example": "production",
              "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the services, newest change first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one service the caller reaches, in one environment: its replicas folded into one row; no machine is named",
                    "properties": {
                      "address": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "where it answers from outside; null for a service that does not",
                        "example": "shop-novid.nuved.app"
                      },
                      "change": {
                        "type": "string",
                        "description": "what became of the newest change: live, every replica runs it; deploying or queued, a deploy of it is on its way; failed, its deploy failed (change_error says why); not_deployed, pushed and not running with nothing on its way (the push hook never queued it, its deploy was cancelled, or a later deploy put another commit back); none, nothing was pushed; unknown, the git plane cannot be read and the newest deploy is not on its way, so the head may be ahead. Never live on a guess. A preview never says not_deployed for a push it was not told of",
                        "enum": [
                          "live",
                          "deploying",
                          "queued",
                          "failed",
                          "not_deployed",
                          "none",
                          "unknown"
                        ]
                      },
                      "change_error": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "the failed deploy's one line, when change is failed; else null",
                        "example": "web: exited 1"
                      },
                      "environment": {
                        "type": "string",
                        "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                        "example": "production",
                        "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                      },
                      "health": {
                        "type": "string",
                        "description": "the health word of the replica that decided the state, as its machine reported it: a health check's word, the container's state, or \"2/3 up\"",
                        "example": "healthy"
                      },
                      "latest_change": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "description": "the project's newest change: in production the head of main, read from the git plane and kept about 10 s; for a preview the environment's newest deploy (a push to a preview branch that was never deployed is not seen); while the git plane cannot be read, production's newest deploy. Null when nothing was pushed, or nothing is known",
                        "properties": {
                          "at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "when it was committed; a deploy's queueing time when it is a deploy"
                          },
                          "by": {
                            "type": "string",
                            "description": "who made it, as the board's timeline names them: agent: followed by the project and the agent's name for an agent's push, else the author's address; empty for a person's address read with an agent token, and when nobody is known",
                            "example": "agent:novid-plans-backend/builder-services-1"
                          },
                          "commit": {
                            "type": "string",
                            "example": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2"
                          },
                          "deploy_id": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "the deploy that took it; null while none did",
                            "example": 12
                          },
                          "message": {
                            "type": "string",
                            "description": "the commit's first line; empty when it could not be read",
                            "example": "Services: list what the caller reaches"
                          },
                          "task": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "the board item its Task: trailer names, else null",
                            "example": "NPB-216"
                          }
                        },
                        "required": [
                          "commit",
                          "message",
                          "by",
                          "at",
                          "deploy_id",
                          "task"
                        ]
                      },
                      "live_commit": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "the commit it runs: the home replica's, of those that went live or failed; null while it runs none",
                        "example": "9f8e7d6c5b4a39281706f5e4d3c2b1a098765432"
                      },
                      "project": {
                        "type": "string",
                        "example": "novid-shop"
                      },
                      "restarts": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "the restarts of every replica summed, stopped ones included (a restart is history); null when no machine counted any",
                        "example": 2
                      },
                      "service": {
                        "type": "string",
                        "example": "web"
                      },
                      "since": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "since when the replica that decided the state has run; null when its machine did not say"
                      },
                      "state": {
                        "type": "string",
                        "description": "the worst state of its replicas that are not stopped; stopped when every one is",
                        "enum": [
                          "running",
                          "starting",
                          "unhealthy",
                          "failed",
                          "stopped"
                        ]
                      }
                    },
                    "required": [
                      "project",
                      "service",
                      "environment",
                      "state",
                      "health",
                      "address",
                      "since",
                      "restarts",
                      "live_commit",
                      "latest_change",
                      "change",
                      "change_error"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "an environment that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "what the caller reaches could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "no database here, or it did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      }
    },
    "/v1/sessions": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "summary": "Where you are signed in to this workspace, newest first: browsers and the CLI.",
        "description": "Your own sessions only: this workspace's that carry your address, or this one alone when it carries none. `current` marks the one asking. A browser's ends 30 days after it signed in; a CLI's lasts until it is ended.",
        "operationId": "listSessions",
        "responses": {
          "200": {
            "description": "your sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one place the signed-in person is signed in to this workspace",
                    "properties": {
                      "client": {
                        "type": "string",
                        "description": "the browser and system its User-Agent named at sign-in; empty when not recorded",
                        "example": "Chrome on macOS"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "when it signed in",
                        "example": "2026-10-11T08:12:00Z"
                      },
                      "current": {
                        "type": "boolean",
                        "description": "the session this request was made with",
                        "example": true
                      },
                      "device": {
                        "type": "string",
                        "description": "browser: a sign-in on the console, 30 days from created_at; cli: the nuved CLI's or nuved for Mac's, until it is ended",
                        "example": "browser",
                        "enum": [
                          "browser",
                          "cli"
                        ]
                      },
                      "expires_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "when it ends by itself; null for a cli session, which lasts until it is ended",
                        "example": "2026-11-10T08:12:00Z"
                      },
                      "id": {
                        "type": "integer",
                        "example": 41
                      },
                      "ip": {
                        "type": "string",
                        "description": "the address it signed in from; kept only while the session lives",
                        "example": "203.0.113.7"
                      },
                      "last_used_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last request it made; null for none"
                      },
                      "made_from": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "the session a workspace switch minted it from; null otherwise"
                      },
                      "place": {
                        "type": "string",
                        "description": "the city and country the edge said it signed in from; kept only while the session lives",
                        "example": "Berlin, DE"
                      },
                      "two_step": {
                        "type": "boolean",
                        "description": "its sign-in checked a second step"
                      },
                      "via": {
                        "type": "string",
                        "description": "password: a password; passkey: a passkey on its own; provider: the identity provider by another door (GitHub, a code by mail); cli: the CLI's device code; switch: minted from made_from for another workspace; claim: minted on the claim or welcome page a moment after a sign-in, without its methods; empty when not recorded",
                        "example": "password",
                        "enum": [
                          "",
                          "password",
                          "passkey",
                          "provider",
                          "cli",
                          "switch",
                          "claim"
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "device",
                      "created_at",
                      "last_used_at",
                      "expires_at",
                      "current",
                      "client",
                      "ip",
                      "place",
                      "via",
                      "two_step",
                      "made_from"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token has no sessions: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the sessions could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/sessions/others": {
      "delete": {
        "tags": [
          "Sign-in"
        ],
        "summary": "End every one of your sessions in this workspace but the one asking.",
        "description": "Each ends at once, with every sign-in on an exposed address made from it. The session asking stays signed in.",
        "operationId": "endOtherSessions",
        "responses": {
          "200": {
            "description": "how many sessions ended",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ended": {
                      "type": "integer",
                      "example": 2
                    }
                  },
                  "required": [
                    "ended"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "an agent token has no sessions: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the sessions could not be ended just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/sessions/{id}": {
      "delete": {
        "tags": [
          "Sign-in"
        ],
        "summary": "End one of your sessions: whatever holds it is signed out at once.",
        "description": "Every sign-in on an exposed address made from it ends with it. Ending the session asking signs this browser or CLI out.",
        "operationId": "endSession",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the session's id, from GET /v1/sessions",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "ended"
          },
          "403": {
            "description": "an agent token has no sessions: a person's session is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such session of yours: another person's, ended already, or none",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the session could not be ended just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The machines this workspace's services are deployed to, by name.",
        "description": "What a customer is shown of a machine: its display name, provider, region, country and kind, whether it is online, when it was last heard from, and what it has. Never its host, address or port.",
        "operationId": "listTargets",
        "responses": {
          "200": {
            "description": "the targets, by name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "a machine services are deployed to, as a customer sees it: never its host, address or port",
                    "properties": {
                      "agent_revision": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "the full commit the machine's nuved-boxd was built from, as its last heartbeat that said one said it; null when it never did (an older nuved-boxd, or a build outside a clean checkout)",
                        "example": "56ae6750c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
                      },
                      "capacity": {
                        "type": "object",
                        "description": "what the machine has, as its agent last said it; {} before it said"
                      },
                      "cert_fingerprint": {
                        "type": "string",
                        "description": "the SHA-256 (lower-case hex) of the certificate the machine's unseal endpoint serves, which the app pins; empty until published"
                      },
                      "checks": {
                        "type": "object",
                        "description": "every host check's last result by name: {ok, required, read, wanted, at}; a required one that is not ok cordons; {} before the agent said",
                        "example": {
                          "cgroup v2 unified": {
                            "at": "2026-10-22T22:00:00Z",
                            "ok": true,
                            "read": "cpuset cpu io memory hugetlb pids rdma misc",
                            "required": true,
                            "wanted": "cgroup2 at /sys/fs/cgroup listing cpu memory pids io, no v1 memory hierarchy"
                          }
                        }
                      },
                      "cordon_reason": {
                        "type": "string",
                        "description": "the check that cordoned the machine, with what was read and wanted; empty when it is not cordoned",
                        "example": "check failed: customers filesystem has project quota (read: ext4 rw,relatime on /dev/sda1: mounted without prjquota; wanted: /srv/customers mounted, xfs or ext4, with prjquota)"
                      },
                      "country": {
                        "type": "string",
                        "description": "ISO 3166-1 alpha-2, or empty",
                        "example": "DE"
                      },
                      "declared": {
                        "type": "object",
                        "description": "what the person or the provisioner said at the join: provider, location, country, type, server_id, arch, managed; {} for a machine enrolled the old way"
                      },
                      "display_name": {
                        "type": "string",
                        "description": "what listings call it",
                        "example": "Hetzner"
                      },
                      "endpoint": {
                        "type": "string",
                        "description": "on a fleet machine (labelled `nuved.io/fleet`) and only to an owner of the platform's own organisation: where its WireGuard listens, the public address its agent published; the key is absent for any other machine, for a member, and before the agent said",
                        "example": "203.0.113.10:51820"
                      },
                      "facts": {
                        "type": "object",
                        "description": "what the agent discovered: arch, kernel, os, cpus, memory, boot, the provider's metadata (instance id, location, zone), disks, the customers filesystem, swap, time, subuid ranges, runtime versions, the cgroup tree's numbers; {} before it said"
                      },
                      "id": {
                        "type": "string",
                        "example": "tg_1"
                      },
                      "kind": {
                        "type": "string",
                        "enum": [
                          "compose",
                          "kubernetes"
                        ]
                      },
                      "labels": {
                        "type": "object",
                        "description": "the labels the machine joined with: general key and value pairs, `nuved.io/fleet` and `nuved.io/purpose` (build or service, what it is for) among them; {} for a machine with none. A machine with no purpose is nobody's to automate: nothing that creates or removes machines acts on it, and only a build machine is ever removed by automation",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "example": {
                          "example.com/team": "core",
                          "nuved.io/fleet": "build"
                        }
                      },
                      "last_seen_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "the last heartbeat; null before one"
                      },
                      "lifecycle": {
                        "type": "string",
                        "description": "where the machine stands in the platform's handling of it: a machine that joined with declared facts is enrolled until its first heartbeat's checks pass, ready while they pass, cordoned (with cordon_reason) while a required one fails, and ready again after three clean rounds; only ready machines take deploys",
                        "example": "ready",
                        "enum": [
                          "requested",
                          "creating",
                          "bootstrapping",
                          "enrolled",
                          "verifying",
                          "ready",
                          "cordoned",
                          "dead",
                          "draining",
                          "retired",
                          "failed"
                        ]
                      },
                      "name": {
                        "type": "string",
                        "description": "lowercase letters, digits and dashes; unique in the workspace",
                        "example": "nuved-box"
                      },
                      "profile": {
                        "type": "string",
                        "description": "the profile version the agent held the host to; empty before it said",
                        "example": "v1"
                      },
                      "protected": {
                        "type": "boolean",
                        "description": "the operator's flag: while it is on, nothing removes, replaces, powers off, drains or rescales the machine; only the operator sets or clears it",
                        "example": false
                      },
                      "provider": {
                        "type": "string",
                        "example": "Hetzner"
                      },
                      "public_key": {
                        "type": "string",
                        "description": "the machine's own age X25519 recipient: an unseal share sent to this machine is encrypted to it; empty until its agent published it",
                        "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                      },
                      "region": {
                        "type": "string",
                        "description": "the datacenter or region",
                        "example": "Falkenstein"
                      },
                      "shared_with": {
                        "type": "array",
                        "description": "other workspaces whose project may run here: a project moved away from this workspace keeps running where it ran (POST /v1/projects/{name}/move); an owner takes it away with DELETE /v1/targets/{id}/access/{tenant}/{project}",
                        "items": {
                          "type": "object",
                          "properties": {
                            "project": {
                              "type": "string",
                              "example": "novid-growgoddess-auth-stage"
                            },
                            "tenant": {
                              "type": "string",
                              "example": "growgoddess"
                            }
                          },
                          "required": [
                            "tenant",
                            "project"
                          ]
                        }
                      },
                      "state": {
                        "type": "string",
                        "description": "what the last heartbeat said; empty before one",
                        "enum": [
                          "",
                          "online",
                          "offline"
                        ]
                      },
                      "usage": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "description": "how loaded the machine is, its agent's last reading, refreshed by every heartbeat (30 s); null when the machine's agent never sent one (an older nuved-boxd)",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the agent read it, by the machine's clock"
                          },
                          "cpu_milli": {
                            "type": "integer",
                            "description": "millicores busy since the heartbeat before, from the kernel's CPU counters (1000 is one CPU wholly busy)",
                            "example": 1250
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "description": "bytes used of the filesystem the customers' homes are on",
                            "example": 42949672960
                          },
                          "disk_total_bytes": {
                            "type": "integer",
                            "example": 107374182400
                          },
                          "memory_bytes": {
                            "type": "integer",
                            "description": "memory in use: MemTotal less MemAvailable",
                            "example": 3221225472
                          },
                          "memory_total_bytes": {
                            "type": "integer",
                            "example": 8589934592
                          }
                        },
                        "required": [
                          "at",
                          "cpu_milli",
                          "memory_bytes",
                          "memory_total_bytes",
                          "disk_bytes",
                          "disk_total_bytes"
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "name",
                      "display_name",
                      "kind",
                      "provider",
                      "region",
                      "country",
                      "state",
                      "last_seen_at",
                      "capacity",
                      "shared_with",
                      "public_key",
                      "cert_fingerprint",
                      "lifecycle",
                      "protected",
                      "cordon_reason",
                      "declared",
                      "facts",
                      "checks",
                      "profile",
                      "usage",
                      "agent_revision",
                      "labels"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person with project roles: targets are the workspace's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the database did not answer, or who else has access to the machines could not be read (`could not read who else has access to your machines just now, try again`): never a list that says nobody",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets/enroll": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Enrol this machine as a target, with an owner's enrollment token; get its own credential back, once.",
        "description": "What `nuved-boxd enroll \u003ctoken\u003e` sends. No session: the token is the consent, and it is used up here, so it enrols one machine. A name the workspace already has is that machine enrolled again (reinstalled, re-keyed): it keeps its id and its placements and gets the new credential; the old one stops working. The answer's `bearer` is what the machine sends on its heartbeats and reports, and it is shown this once.",
        "operationId": "enrollTarget",
        "requestBody": {
          "description": "the token, and what this machine is",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "capacity": {
                    "type": "object",
                    "description": "what the machine has, in the agent's own keys; at most 4 KiB",
                    "example": {
                      "cores": 8,
                      "memory_bytes": 34359738368
                    }
                  },
                  "cert_fingerprint": {
                    "type": "string",
                    "description": "the SHA-256 (lower-case hex) of the certificate its unseal endpoint serves"
                  },
                  "country": {
                    "type": "string",
                    "description": "ISO 3166-1 alpha-2, or empty",
                    "example": "DE"
                  },
                  "declared": {
                    "type": "object",
                    "description": "what the person or the provisioner said the machine is (`nuved-boxd join`): provider, location, country, type, server_id, arch, managed; at most 2 KiB. With it the machine starts enrolled and takes no work until its first heartbeat's checks pass; without it (the old `enroll`) it is ready as before",
                    "example": {
                      "arch": "x86_64",
                      "country": "DE",
                      "location": "fsn1",
                      "managed": true,
                      "provider": "hetzner",
                      "type": "cx22"
                    }
                  },
                  "display_name": {
                    "type": "string",
                    "description": "what listings call it; the name when empty; at most 80 characters",
                    "example": "Hetzner"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "compose",
                      "kubernetes"
                    ]
                  },
                  "name": {
                    "type": "string",
                    "description": "lowercase letters, digits and dashes, up to 63",
                    "example": "nuved-box"
                  },
                  "provider": {
                    "type": "string",
                    "description": "at most 40 characters",
                    "example": "Hetzner"
                  },
                  "public_key": {
                    "type": "string",
                    "description": "the machine's own age X25519 recipient, made by the agent beside its state file"
                  },
                  "region": {
                    "type": "string",
                    "description": "the datacenter or region; at most 60 characters",
                    "example": "Falkenstein"
                  },
                  "token": {
                    "type": "string",
                    "description": "the enrollment token",
                    "example": "nvd_enroll_…"
                  }
                },
                "required": [
                  "token",
                  "name",
                  "kind"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the target, and its credential",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bearer": {
                      "type": "string",
                      "description": "the target's own credential, shown this once; a secret",
                      "example": "nvd_target_…"
                    },
                    "target": {
                      "type": "object",
                      "description": "a machine services are deployed to, as a customer sees it: never its host, address or port",
                      "properties": {
                        "agent_revision": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "the full commit the machine's nuved-boxd was built from, as its last heartbeat that said one said it; null when it never did (an older nuved-boxd, or a build outside a clean checkout)",
                          "example": "56ae6750c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
                        },
                        "capacity": {
                          "type": "object",
                          "description": "what the machine has, as its agent last said it; {} before it said"
                        },
                        "cert_fingerprint": {
                          "type": "string",
                          "description": "the SHA-256 (lower-case hex) of the certificate the machine's unseal endpoint serves, which the app pins; empty until published"
                        },
                        "checks": {
                          "type": "object",
                          "description": "every host check's last result by name: {ok, required, read, wanted, at}; a required one that is not ok cordons; {} before the agent said",
                          "example": {
                            "cgroup v2 unified": {
                              "at": "2026-10-22T22:00:00Z",
                              "ok": true,
                              "read": "cpuset cpu io memory hugetlb pids rdma misc",
                              "required": true,
                              "wanted": "cgroup2 at /sys/fs/cgroup listing cpu memory pids io, no v1 memory hierarchy"
                            }
                          }
                        },
                        "cordon_reason": {
                          "type": "string",
                          "description": "the check that cordoned the machine, with what was read and wanted; empty when it is not cordoned",
                          "example": "check failed: customers filesystem has project quota (read: ext4 rw,relatime on /dev/sda1: mounted without prjquota; wanted: /srv/customers mounted, xfs or ext4, with prjquota)"
                        },
                        "country": {
                          "type": "string",
                          "description": "ISO 3166-1 alpha-2, or empty",
                          "example": "DE"
                        },
                        "declared": {
                          "type": "object",
                          "description": "what the person or the provisioner said at the join: provider, location, country, type, server_id, arch, managed; {} for a machine enrolled the old way"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "what listings call it",
                          "example": "Hetzner"
                        },
                        "endpoint": {
                          "type": "string",
                          "description": "on a fleet machine (labelled `nuved.io/fleet`) and only to an owner of the platform's own organisation: where its WireGuard listens, the public address its agent published; the key is absent for any other machine, for a member, and before the agent said",
                          "example": "203.0.113.10:51820"
                        },
                        "facts": {
                          "type": "object",
                          "description": "what the agent discovered: arch, kernel, os, cpus, memory, boot, the provider's metadata (instance id, location, zone), disks, the customers filesystem, swap, time, subuid ranges, runtime versions, the cgroup tree's numbers; {} before it said"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "labels": {
                          "type": "object",
                          "description": "the labels the machine joined with: general key and value pairs, `nuved.io/fleet` and `nuved.io/purpose` (build or service, what it is for) among them; {} for a machine with none. A machine with no purpose is nobody's to automate: nothing that creates or removes machines acts on it, and only a build machine is ever removed by automation",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "example": {
                            "example.com/team": "core",
                            "nuved.io/fleet": "build"
                          }
                        },
                        "last_seen_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "the last heartbeat; null before one"
                        },
                        "lifecycle": {
                          "type": "string",
                          "description": "where the machine stands in the platform's handling of it: a machine that joined with declared facts is enrolled until its first heartbeat's checks pass, ready while they pass, cordoned (with cordon_reason) while a required one fails, and ready again after three clean rounds; only ready machines take deploys",
                          "example": "ready",
                          "enum": [
                            "requested",
                            "creating",
                            "bootstrapping",
                            "enrolled",
                            "verifying",
                            "ready",
                            "cordoned",
                            "dead",
                            "draining",
                            "retired",
                            "failed"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "lowercase letters, digits and dashes; unique in the workspace",
                          "example": "nuved-box"
                        },
                        "profile": {
                          "type": "string",
                          "description": "the profile version the agent held the host to; empty before it said",
                          "example": "v1"
                        },
                        "protected": {
                          "type": "boolean",
                          "description": "the operator's flag: while it is on, nothing removes, replaces, powers off, drains or rescales the machine; only the operator sets or clears it",
                          "example": false
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "public_key": {
                          "type": "string",
                          "description": "the machine's own age X25519 recipient: an unseal share sent to this machine is encrypted to it; empty until its agent published it",
                          "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                        },
                        "region": {
                          "type": "string",
                          "description": "the datacenter or region",
                          "example": "Falkenstein"
                        },
                        "shared_with": {
                          "type": "array",
                          "description": "other workspaces whose project may run here: a project moved away from this workspace keeps running where it ran (POST /v1/projects/{name}/move); an owner takes it away with DELETE /v1/targets/{id}/access/{tenant}/{project}",
                          "items": {
                            "type": "object",
                            "properties": {
                              "project": {
                                "type": "string",
                                "example": "novid-growgoddess-auth-stage"
                              },
                              "tenant": {
                                "type": "string",
                                "example": "growgoddess"
                              }
                            },
                            "required": [
                              "tenant",
                              "project"
                            ]
                          }
                        },
                        "state": {
                          "type": "string",
                          "description": "what the last heartbeat said; empty before one",
                          "enum": [
                            "",
                            "online",
                            "offline"
                          ]
                        },
                        "usage": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "how loaded the machine is, its agent's last reading, refreshed by every heartbeat (30 s); null when the machine's agent never sent one (an older nuved-boxd)",
                          "properties": {
                            "at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "when the agent read it, by the machine's clock"
                            },
                            "cpu_milli": {
                              "type": "integer",
                              "description": "millicores busy since the heartbeat before, from the kernel's CPU counters (1000 is one CPU wholly busy)",
                              "example": 1250
                            },
                            "disk_bytes": {
                              "type": "integer",
                              "description": "bytes used of the filesystem the customers' homes are on",
                              "example": 42949672960
                            },
                            "disk_total_bytes": {
                              "type": "integer",
                              "example": 107374182400
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "description": "memory in use: MemTotal less MemAvailable",
                              "example": 3221225472
                            },
                            "memory_total_bytes": {
                              "type": "integer",
                              "example": 8589934592
                            }
                          },
                          "required": [
                            "at",
                            "cpu_milli",
                            "memory_bytes",
                            "memory_total_bytes",
                            "disk_bytes",
                            "disk_total_bytes"
                          ]
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country",
                        "state",
                        "last_seen_at",
                        "capacity",
                        "shared_with",
                        "public_key",
                        "cert_fingerprint",
                        "lifecycle",
                        "protected",
                        "cordon_reason",
                        "declared",
                        "facts",
                        "checks",
                        "profile",
                        "usage",
                        "agent_revision",
                        "labels"
                      ]
                    }
                  },
                  "required": [
                    "target",
                    "bearer"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, no token, or a name, kind, country or capacity that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the token was used already or has expired; or it is for another machine name or another provider server than this machine says; or the machine has other labels than the token (its purpose in particular is fixed for its life); or the machine is protected and the token is not one the operator minted with its name (the token is not used up by these)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the enrolment could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/targets/join/manifest": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The machine image's manifest for a machine that is joining: which nuved-boxd to install, by sha256, with only its enrollment token.",
        "description": "What `join.sh` reads first on a new machine (docs/machine-join.md). The credential is the enrollment token an owner minted, as the bearer, unused and unexpired; the manifest is `GET /v1/environments/current` with each file's URL pointing at `GET /v1/targets/join/{version}/{file}`, which takes the same token. Nothing else is open to that token.",
        "operationId": "joinManifest",
        "responses": {
          "200": {
            "description": "the current version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a machine image version: Mansoor OS Node, the local machine the desktop app sets up",
                  "properties": {
                    "arch": {
                      "type": "string",
                      "example": "arm64"
                    },
                    "boxd": {
                      "type": "string",
                      "description": "the commit nuved-boxd inside the image was built from"
                    },
                    "files": {
                      "type": "array",
                      "description": "the other files of the version: the agent's binaries, the manifest's signature later",
                      "items": {
                        "type": "object",
                        "description": "one file of a machine image version",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "compression": {
                            "type": "string",
                            "description": "zstd, or empty for a file served as it is"
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "description": "the disk image's size once unpacked"
                          },
                          "name": {
                            "type": "string",
                            "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                          },
                          "sha256": {
                            "type": "string",
                            "description": "of the file as served"
                          },
                          "url": {
                            "type": "string",
                            "description": "where to fetch it, with the same credential"
                          }
                        },
                        "required": [
                          "name",
                          "url",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "image": {
                      "type": "object",
                      "description": "one file of a machine image version",
                      "properties": {
                        "bytes": {
                          "type": "integer"
                        },
                        "compression": {
                          "type": "string",
                          "description": "zstd, or empty for a file served as it is"
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "description": "the disk image's size once unpacked"
                        },
                        "name": {
                          "type": "string",
                          "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                        },
                        "sha256": {
                          "type": "string",
                          "description": "of the file as served"
                        },
                        "url": {
                          "type": "string",
                          "description": "where to fetch it, with the same credential"
                        }
                      },
                      "required": [
                        "name",
                        "url",
                        "sha256",
                        "bytes"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "example": "Mansoor OS Node"
                    },
                    "notes": {
                      "type": "string"
                    },
                    "podman": {
                      "type": "string",
                      "example": "4.9.3"
                    },
                    "published_at": {
                      "type": "string"
                    },
                    "ubuntu": {
                      "type": "string",
                      "example": "24.04.3"
                    },
                    "version": {
                      "type": "string",
                      "example": "24.04.3-20261001-a1b2c3d"
                    }
                  },
                  "required": [
                    "version",
                    "name",
                    "arch",
                    "ubuntu",
                    "podman",
                    "boxd",
                    "published_at",
                    "image",
                    "files"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no enrollment token as the bearer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the token was used already or has expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no machine image is published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets or machine images are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/targets/join/{version}/{file}": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "One file of the machine image version named by the join manifest, for a joining machine: the agent's binary, by sha256.",
        "description": "The same file as `GET /v1/environments/{version}/{file}`, served to an unused, unexpired enrollment token as the bearer. `join.sh` checks the manifest's sha256 before installing it.",
        "operationId": "joinFile",
        "parameters": [
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "24.04.3-20261001-a1b2c3d"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "a file name from the manifest",
            "schema": {
              "type": "string",
              "example": "nuved-boxd-linux-amd64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "description": "the range asked for"
          },
          "401": {
            "description": "no enrollment token as the bearer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the token was used already or has expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets or machine images are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/targets/service-tokens": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "The machine service's call: mint the join token of the one machine it is about to create.",
        "description": "With MACHINE_SERVICE_TOKEN, the secret the machine service and this backend share; no person, agent or machine holds it, and a backend started without it (or with one under 32 characters) answers 403 to everybody. The token enrols the one `name` in `org`'s workspace with the `labels` given (the fleet and the purpose among them), and nothing else; it is in this answer and nowhere else, and it is good for 24 hours. The service is never the operator: a protected machine, a machine of another fleet and a machine in none are refused here and again at the enrolment. It mints build machines only: a `nuved.io/purpose` of service is refused, and so is the main machine's name; a machine for a customer's services is enrolled with a token a person mints. At most 5 of the service's tokens are open (not used, not expired) in one workspace at a time; the next is answered 429 until one is used or expires.",
        "operationId": "mintMachineEnrollmentToken",
        "requestBody": {
          "description": "the workspace, the machine, its fleet",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "labels": {
                    "type": "object",
                    "description": "the machine's labels, as the service creates it with them; `nuved.io/fleet` and `nuved.io/purpose` are needed, and the purpose is build (this route refuses service). The row gets them and keeps them",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "example": {
                      "example.com/team": "core",
                      "nuved.io/fleet": "build"
                    }
                  },
                  "name": {
                    "type": "string",
                    "description": "the one machine name the token enrols: lowercase letters, digits and dashes, up to 63",
                    "example": "build-1"
                  },
                  "org": {
                    "type": "string",
                    "description": "the workspace the machine joins",
                    "example": "acme"
                  },
                  "server_id": {
                    "type": "string",
                    "description": "the provider's id for the one server it enrols, when the service knows it",
                    "example": "200000001"
                  }
                },
                "required": [
                  "org",
                  "name",
                  "labels"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the token, shown this once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "token": {
                      "type": "string",
                      "description": "a secret",
                      "example": "nvd_enroll_…"
                    }
                  },
                  "required": [
                    "token",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or no org, name, fleet or purpose, or labels that are not any",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "the machine service's credential is the credential here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "that is not the machine service's credential; or the purpose is not build, or the name is the main machine's or a protected machine's, or a machine of another fleet or of none",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "that workspace has no account yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "the service already has 5 open join tokens in that workspace: one must be used or expire first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the token could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "machineService": []
          }
        ]
      }
    },
    "/v1/targets/tokens": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Mint a token that enrols one machine as a target of this workspace, within 24 hours.",
        "description": "Only an owner may. The token is shown this once; `nuved-boxd enroll \u003ctoken\u003e` on the machine sends it to POST /v1/targets/enroll, which uses it up. A token not used within 24 hours is dead. With `name` it enrols that machine only (and, with `server_id`, only a machine that says it is that provider server); without it, any name that is not protected. A protected machine is enrolled again only by a token the operator minted with its name, so naming one is the operator's alone.",
        "operationId": "mintEnrollmentToken",
        "requestBody": {
          "description": "what the token is for; the body may be left out",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "labels": {
                    "type": "object",
                    "description": "the machine's labels, up to 16 key and value pairs as Kubernetes has them (`example.com/team`: `core`); the key `nuved.io/fleet` takes a lowercase DNS label, `nuved.io/purpose` takes `build` or `service` (what the machine is for, fixed for its life: a machine is never enrolled into another purpose, nor given one it had none of), and no other key under `nuved.io/` is kept. The machine's row gets them, `GET /v1/targets` shows them, and a machine is enrolled again only by a token with exactly the labels it has",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "example": {
                      "example.com/team": "core",
                      "nuved.io/fleet": "build"
                    }
                  },
                  "name": {
                    "type": "string",
                    "description": "the one machine name this token enrols: lowercase letters, digits and dashes, up to 63",
                    "example": "lab-a"
                  },
                  "server_id": {
                    "type": "string",
                    "description": "the provider's id for the one server it enrols (a machine says it with `nuved-boxd join --server-id`); needs `name`",
                    "example": "200000001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the token, shown this once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "token": {
                      "type": "string",
                      "description": "a secret",
                      "example": "nvd_enroll_…"
                    }
                  },
                  "required": [
                    "token",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a name, a server_id or labels that are not any, or a server_id with no name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, or an agent token, or the name is a protected machine's and the caller is not the operator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the workspace has no account yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "whether the caller is the operator could not be checked just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the token could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets/{id}": {
      "delete": {
        "tags": [
          "Targets"
        ],
        "summary": "Take a machine off the workspace: its placements and adoptions go, its credential stops working, its name is free again.",
        "description": "An owner's action. What the machine ran is not stopped by this: the machine, if it still exists, keeps the containers and is refused when it reports. The desktop app calls it after deleting a local machine's disks; `nuved machine rm` calls it for any machine.",
        "operationId": "removeTarget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_7"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the machine, as it was",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a machine services are deployed to, as a customer sees it: never its host, address or port",
                  "properties": {
                    "agent_revision": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "the full commit the machine's nuved-boxd was built from, as its last heartbeat that said one said it; null when it never did (an older nuved-boxd, or a build outside a clean checkout)",
                      "example": "56ae6750c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
                    },
                    "capacity": {
                      "type": "object",
                      "description": "what the machine has, as its agent last said it; {} before it said"
                    },
                    "cert_fingerprint": {
                      "type": "string",
                      "description": "the SHA-256 (lower-case hex) of the certificate the machine's unseal endpoint serves, which the app pins; empty until published"
                    },
                    "checks": {
                      "type": "object",
                      "description": "every host check's last result by name: {ok, required, read, wanted, at}; a required one that is not ok cordons; {} before the agent said",
                      "example": {
                        "cgroup v2 unified": {
                          "at": "2026-10-22T22:00:00Z",
                          "ok": true,
                          "read": "cpuset cpu io memory hugetlb pids rdma misc",
                          "required": true,
                          "wanted": "cgroup2 at /sys/fs/cgroup listing cpu memory pids io, no v1 memory hierarchy"
                        }
                      }
                    },
                    "cordon_reason": {
                      "type": "string",
                      "description": "the check that cordoned the machine, with what was read and wanted; empty when it is not cordoned",
                      "example": "check failed: customers filesystem has project quota (read: ext4 rw,relatime on /dev/sda1: mounted without prjquota; wanted: /srv/customers mounted, xfs or ext4, with prjquota)"
                    },
                    "country": {
                      "type": "string",
                      "description": "ISO 3166-1 alpha-2, or empty",
                      "example": "DE"
                    },
                    "declared": {
                      "type": "object",
                      "description": "what the person or the provisioner said at the join: provider, location, country, type, server_id, arch, managed; {} for a machine enrolled the old way"
                    },
                    "display_name": {
                      "type": "string",
                      "description": "what listings call it",
                      "example": "Hetzner"
                    },
                    "endpoint": {
                      "type": "string",
                      "description": "on a fleet machine (labelled `nuved.io/fleet`) and only to an owner of the platform's own organisation: where its WireGuard listens, the public address its agent published; the key is absent for any other machine, for a member, and before the agent said",
                      "example": "203.0.113.10:51820"
                    },
                    "facts": {
                      "type": "object",
                      "description": "what the agent discovered: arch, kernel, os, cpus, memory, boot, the provider's metadata (instance id, location, zone), disks, the customers filesystem, swap, time, subuid ranges, runtime versions, the cgroup tree's numbers; {} before it said"
                    },
                    "id": {
                      "type": "string",
                      "example": "tg_1"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "compose",
                        "kubernetes"
                      ]
                    },
                    "labels": {
                      "type": "object",
                      "description": "the labels the machine joined with: general key and value pairs, `nuved.io/fleet` and `nuved.io/purpose` (build or service, what it is for) among them; {} for a machine with none. A machine with no purpose is nobody's to automate: nothing that creates or removes machines acts on it, and only a build machine is ever removed by automation",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "example.com/team": "core",
                        "nuved.io/fleet": "build"
                      }
                    },
                    "last_seen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last heartbeat; null before one"
                    },
                    "lifecycle": {
                      "type": "string",
                      "description": "where the machine stands in the platform's handling of it: a machine that joined with declared facts is enrolled until its first heartbeat's checks pass, ready while they pass, cordoned (with cordon_reason) while a required one fails, and ready again after three clean rounds; only ready machines take deploys",
                      "example": "ready",
                      "enum": [
                        "requested",
                        "creating",
                        "bootstrapping",
                        "enrolled",
                        "verifying",
                        "ready",
                        "cordoned",
                        "dead",
                        "draining",
                        "retired",
                        "failed"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "description": "lowercase letters, digits and dashes; unique in the workspace",
                      "example": "nuved-box"
                    },
                    "profile": {
                      "type": "string",
                      "description": "the profile version the agent held the host to; empty before it said",
                      "example": "v1"
                    },
                    "protected": {
                      "type": "boolean",
                      "description": "the operator's flag: while it is on, nothing removes, replaces, powers off, drains or rescales the machine; only the operator sets or clears it",
                      "example": false
                    },
                    "provider": {
                      "type": "string",
                      "example": "Hetzner"
                    },
                    "public_key": {
                      "type": "string",
                      "description": "the machine's own age X25519 recipient: an unseal share sent to this machine is encrypted to it; empty until its agent published it",
                      "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                    },
                    "region": {
                      "type": "string",
                      "description": "the datacenter or region",
                      "example": "Falkenstein"
                    },
                    "shared_with": {
                      "type": "array",
                      "description": "other workspaces whose project may run here: a project moved away from this workspace keeps running where it ran (POST /v1/projects/{name}/move); an owner takes it away with DELETE /v1/targets/{id}/access/{tenant}/{project}",
                      "items": {
                        "type": "object",
                        "properties": {
                          "project": {
                            "type": "string",
                            "example": "novid-growgoddess-auth-stage"
                          },
                          "tenant": {
                            "type": "string",
                            "example": "growgoddess"
                          }
                        },
                        "required": [
                          "tenant",
                          "project"
                        ]
                      }
                    },
                    "state": {
                      "type": "string",
                      "description": "what the last heartbeat said; empty before one",
                      "enum": [
                        "",
                        "online",
                        "offline"
                      ]
                    },
                    "usage": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "how loaded the machine is, its agent's last reading, refreshed by every heartbeat (30 s); null when the machine's agent never sent one (an older nuved-boxd)",
                      "properties": {
                        "at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "when the agent read it, by the machine's clock"
                        },
                        "cpu_milli": {
                          "type": "integer",
                          "description": "millicores busy since the heartbeat before, from the kernel's CPU counters (1000 is one CPU wholly busy)",
                          "example": 1250
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "description": "bytes used of the filesystem the customers' homes are on",
                          "example": 42949672960
                        },
                        "disk_total_bytes": {
                          "type": "integer",
                          "example": 107374182400
                        },
                        "memory_bytes": {
                          "type": "integer",
                          "description": "memory in use: MemTotal less MemAvailable",
                          "example": 3221225472
                        },
                        "memory_total_bytes": {
                          "type": "integer",
                          "example": 8589934592
                        }
                      },
                      "required": [
                        "at",
                        "cpu_milli",
                        "memory_bytes",
                        "memory_total_bytes",
                        "disk_bytes",
                        "disk_total_bytes"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "display_name",
                    "kind",
                    "provider",
                    "region",
                    "country",
                    "state",
                    "last_seen_at",
                    "capacity",
                    "shared_with",
                    "public_key",
                    "cert_fingerprint",
                    "lifecycle",
                    "protected",
                    "cordon_reason",
                    "declared",
                    "facts",
                    "checks",
                    "profile",
                    "usage",
                    "agent_revision",
                    "labels"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "only an owner can remove a machine",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such machine in your workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "nothing was removed, and the sentence says why, in this order: the machine is protected (`\u003cname\u003e is protected; the operator unprotects it first`); it holds an organisation's vault (`\u003cname\u003e holds an organisation's vault; move or retire the vault before removing the machine`); or it holds other workspaces' services, running or stopped, or their deploys under way, a project moved away or one the machine is shared with, counted and never named (`\u003cname\u003e holds 3 services of 2 other workspaces, running or stopped, not yours; those workspaces move them off the machine or remove them first`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the store did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets/{id}/access/{tenant}/{project}": {
      "delete": {
        "tags": [
          "Targets"
        ],
        "summary": "Take away another workspace's access to one of this workspace's machines, for one project.",
        "description": "A project moved to another workspace keeps running on the machines it ran on, by access its new workspace was given there; GET /v1/targets lists it as `shared_with`. An owner of the machine's workspace takes it away here: the project cannot be deployed there any more. What runs is not stopped, and the machine's reports name the project under the machine's own workspace from then on.",
        "operationId": "revokeTargetAccess",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_7"
            }
          },
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "description": "the workspace that has the access",
            "schema": {
              "type": "string",
              "example": "growgoddess"
            }
          },
          {
            "name": "project",
            "in": "path",
            "required": true,
            "description": "the project it has it for",
            "schema": {
              "type": "string",
              "example": "novid-growgoddess-auth-stage"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "taken away",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "type": "string",
                      "example": "novid-growgoddess-auth-stage"
                    },
                    "revoked": {
                      "type": "boolean"
                    },
                    "target_id": {
                      "type": "string",
                      "example": "tg_7"
                    },
                    "tenant": {
                      "type": "string",
                      "example": "growgoddess"
                    }
                  },
                  "required": [
                    "target_id",
                    "tenant",
                    "project",
                    "revoked"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "only an owner can take a machine's access away",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such machine in your workspace, or no such access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the store did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets/{id}/agent": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The current machine image's manifest for a joined machine: which nuved-boxd it should run, by commit and sha256.",
        "description": "What a running agent reads to keep itself current (docs/boxd.md). The credential is the target's own; every file's URL points at `GET /v1/targets/{id}/agent/{version}/{file}`, which takes the same credential.",
        "operationId": "agentManifest",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_4"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the current version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a machine image version: Mansoor OS Node, the local machine the desktop app sets up",
                  "properties": {
                    "arch": {
                      "type": "string",
                      "example": "arm64"
                    },
                    "boxd": {
                      "type": "string",
                      "description": "the commit nuved-boxd inside the image was built from"
                    },
                    "files": {
                      "type": "array",
                      "description": "the other files of the version: the agent's binaries, the manifest's signature later",
                      "items": {
                        "type": "object",
                        "description": "one file of a machine image version",
                        "properties": {
                          "bytes": {
                            "type": "integer"
                          },
                          "compression": {
                            "type": "string",
                            "description": "zstd, or empty for a file served as it is"
                          },
                          "disk_bytes": {
                            "type": "integer",
                            "description": "the disk image's size once unpacked"
                          },
                          "name": {
                            "type": "string",
                            "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                          },
                          "sha256": {
                            "type": "string",
                            "description": "of the file as served"
                          },
                          "url": {
                            "type": "string",
                            "description": "where to fetch it, with the same credential"
                          }
                        },
                        "required": [
                          "name",
                          "url",
                          "sha256",
                          "bytes"
                        ]
                      }
                    },
                    "image": {
                      "type": "object",
                      "description": "one file of a machine image version",
                      "properties": {
                        "bytes": {
                          "type": "integer"
                        },
                        "compression": {
                          "type": "string",
                          "description": "zstd, or empty for a file served as it is"
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "description": "the disk image's size once unpacked"
                        },
                        "name": {
                          "type": "string",
                          "example": "mansoor-node-24.04.3-20261001-a1b2c3d-arm64.raw.zst"
                        },
                        "sha256": {
                          "type": "string",
                          "description": "of the file as served"
                        },
                        "url": {
                          "type": "string",
                          "description": "where to fetch it, with the same credential"
                        }
                      },
                      "required": [
                        "name",
                        "url",
                        "sha256",
                        "bytes"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "example": "Mansoor OS Node"
                    },
                    "notes": {
                      "type": "string"
                    },
                    "podman": {
                      "type": "string",
                      "example": "4.9.3"
                    },
                    "published_at": {
                      "type": "string"
                    },
                    "ubuntu": {
                      "type": "string",
                      "example": "24.04.3"
                    },
                    "version": {
                      "type": "string",
                      "example": "24.04.3-20261001-a1b2c3d"
                    }
                  },
                  "required": [
                    "version",
                    "name",
                    "arch",
                    "ubuntu",
                    "podman",
                    "boxd",
                    "published_at",
                    "image",
                    "files"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "not this target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no machine image is published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "machine images are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/targets/{id}/agent/{version}/{file}": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "One file of the machine image version the agent manifest names: nuved-boxd for this machine's arch.",
        "operationId": "agentFile",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_4"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "24.04.5-20261023-ea25bb2"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "nuved-boxd-linux-arm64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "not this target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/targets/{id}/console-release": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The release the console's host is to run, and the one before it.",
        "description": "Answered only to the machine marked as the console host with its own credential; every other target gets 404, the same as for a route that does not exist. Each release lists its files with the sha256, the size and the URL to download them from.",
        "operationId": "getTargetConsoleRelease",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "desired and previous",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved_at": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "approved_by": {
                      "type": "string"
                    },
                    "breaking_acknowledged": {
                      "type": "boolean"
                    },
                    "desired": {
                      "type": "object",
                      "properties": {
                        "builder": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "run": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "kind",
                            "name",
                            "run"
                          ]
                        },
                        "commit": {
                          "type": "string",
                          "pattern": "^[0-9a-f]{40}$"
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "id": {
                                "type": "integer"
                              },
                              "proofs": {
                                "type": "object",
                                "description": "what the host proved, free keys"
                              },
                              "reason": {
                                "type": "string"
                              },
                              "revision": {
                                "type": "string",
                                "description": "the commit of the console that took the report; empty when it has no clean revision"
                              },
                              "state": {
                                "type": "string",
                                "enum": [
                                  "started",
                                  "proved",
                                  "live",
                                  "failed",
                                  "rolled_back"
                                ]
                              },
                              "step": {
                                "type": "string"
                              },
                              "target": {
                                "type": "string",
                                "description": "the machine that reported"
                              }
                            },
                            "required": [
                              "at",
                              "id",
                              "proofs",
                              "reason",
                              "revision",
                              "state",
                              "step",
                              "target"
                            ]
                          }
                        },
                        "files": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "bytes": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                              },
                              "sha256": {
                                "type": "string",
                                "pattern": "^[0-9a-f]{64}$"
                              },
                              "url": {
                                "type": "string",
                                "description": "where the host downloads the file; only in what a target is handed"
                              }
                            },
                            "required": [
                              "name",
                              "sha256",
                              "bytes"
                            ]
                          }
                        },
                        "id": {
                          "type": "string",
                          "description": "the sha256 of manifest.json"
                        },
                        "pruned": {
                          "type": "boolean",
                          "description": "the files were taken away; the row stays"
                        },
                        "registered_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "registered_by": {
                          "type": "string"
                        },
                        "schema": {
                          "type": "string",
                          "description": "breaking needs the operator's acknowledgement to roll out",
                          "enum": [
                            "additive",
                            "breaking"
                          ]
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "build",
                            "upload"
                          ]
                        },
                        "source_build": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "the kept build it was registered from"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "desired",
                            "previous",
                            ""
                          ]
                        },
                        "traceparent": {
                          "type": "string",
                          "description": "W3C traceparent of the release's trace, only in what a target is handed"
                        }
                      },
                      "required": [
                        "id",
                        "commit",
                        "schema",
                        "builder",
                        "source",
                        "source_build",
                        "files",
                        "registered_by",
                        "registered_at",
                        "pruned",
                        "state",
                        "events"
                      ]
                    },
                    "previous": {
                      "type": "object",
                      "properties": {
                        "builder": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "run": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "kind",
                            "name",
                            "run"
                          ]
                        },
                        "commit": {
                          "type": "string",
                          "pattern": "^[0-9a-f]{40}$"
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "id": {
                                "type": "integer"
                              },
                              "proofs": {
                                "type": "object",
                                "description": "what the host proved, free keys"
                              },
                              "reason": {
                                "type": "string"
                              },
                              "revision": {
                                "type": "string",
                                "description": "the commit of the console that took the report; empty when it has no clean revision"
                              },
                              "state": {
                                "type": "string",
                                "enum": [
                                  "started",
                                  "proved",
                                  "live",
                                  "failed",
                                  "rolled_back"
                                ]
                              },
                              "step": {
                                "type": "string"
                              },
                              "target": {
                                "type": "string",
                                "description": "the machine that reported"
                              }
                            },
                            "required": [
                              "at",
                              "id",
                              "proofs",
                              "reason",
                              "revision",
                              "state",
                              "step",
                              "target"
                            ]
                          }
                        },
                        "files": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "bytes": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string",
                                "description": "manifest.json, or one of plans-backend, kube-proxy, backfill-teams, nuved-boxd-linux-amd64"
                              },
                              "sha256": {
                                "type": "string",
                                "pattern": "^[0-9a-f]{64}$"
                              },
                              "url": {
                                "type": "string",
                                "description": "where the host downloads the file; only in what a target is handed"
                              }
                            },
                            "required": [
                              "name",
                              "sha256",
                              "bytes"
                            ]
                          }
                        },
                        "id": {
                          "type": "string",
                          "description": "the sha256 of manifest.json"
                        },
                        "pruned": {
                          "type": "boolean",
                          "description": "the files were taken away; the row stays"
                        },
                        "registered_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "registered_by": {
                          "type": "string"
                        },
                        "schema": {
                          "type": "string",
                          "description": "breaking needs the operator's acknowledgement to roll out",
                          "enum": [
                            "additive",
                            "breaking"
                          ]
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "build",
                            "upload"
                          ]
                        },
                        "source_build": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "the kept build it was registered from"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "desired",
                            "previous",
                            ""
                          ]
                        },
                        "traceparent": {
                          "type": "string",
                          "description": "W3C traceparent of the release's trace, only in what a target is handed"
                        }
                      },
                      "required": [
                        "id",
                        "commit",
                        "schema",
                        "builder",
                        "source",
                        "source_build",
                        "files",
                        "registered_by",
                        "registered_at",
                        "pruned",
                        "state",
                        "events"
                      ]
                    }
                  },
                  "required": [
                    "approved_at",
                    "approved_by",
                    "breaking_acknowledged",
                    "desired",
                    "previous"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "no target credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "this machine is not the console host, or nothing is desired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/console-release/{release}/files/{file}": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "Download one file of the desired or the previous release.",
        "description": "The bytes, with their sha256 in `X-Content-SHA256`. Only the console's host, and only for the desired or the previous release. Range requests are answered.",
        "operationId": "getTargetConsoleReleaseFile",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "release",
            "in": "path",
            "required": true,
            "description": "the release's id, as GET .../console-release names it",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "the file's name",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "no target credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not the console host, or not the desired or previous release, or no such file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "the file is no longer kept",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/console-release/{release}/report": {
      "put": {
        "tags": [
          "Targets"
        ],
        "summary": "Report a step of rolling out a release.",
        "description": "The console keeps the report with the commit it was built from and answers that commit in `X-Nuved-Revision` (absent when the build has no clean revision): that is how a host that sends a report learns which console took it. A host that only wants the revision asks GET .../revision, which stores nothing. Only the console's host, only for the desired or the previous release, at most 100 reports a release.",
        "operationId": "putTargetConsoleReleaseReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "release",
            "in": "path",
            "required": true,
            "description": "the release's id, as GET .../console-release names it",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "description": "the step",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "proofs": {
                    "type": "object"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "started",
                      "proved",
                      "live",
                      "failed",
                      "rolled_back"
                    ]
                  },
                  "step": {
                    "type": "string"
                  }
                },
                "required": [
                  "proofs",
                  "reason",
                  "state",
                  "step"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "kept",
            "headers": {
              "X-Nuved-Revision": {
                "description": "the commit the answering console was built from; absent when it has none",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "a state that is not one, or text that is too long",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not the console host, or not the desired or previous release",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "the release has all the reports it keeps",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/console-release/{release}/revision": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "Ask which commit the answering console was built from.",
        "description": "The revision probe of the console's host after it swapped the console: the answer is the commit in `X-Nuved-Revision` (absent when the build has no clean revision) and nothing else, and it stores nothing, so any number of probes leave the release's reports as they were (a report is kept, at most 100 a release). Only the machine marked as the console host with its own credential, and only for the desired or the previous release; every other request gets 404, the same as for a route that does not exist.",
        "operationId": "getTargetConsoleReleaseRevision",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "release",
            "in": "path",
            "required": true,
            "description": "the release's id, as GET .../console-release names it",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "the revision, in the header",
            "headers": {
              "X-Nuved-Revision": {
                "description": "the commit the answering console was built from; absent when it has none",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "no target credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not the console host, or not the desired or previous release",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/deploys/{deploy_id}": {
      "post": {
        "tags": [
          "Deploys"
        ],
        "summary": "Say how a deploy of this target is going: running, live or failed, and the services it placed.",
        "description": "Sent by the target's agent with the target's own credential, for its own deploys only. `running` carries a sentence on progress and may be sent again; `live` and `failed` end the deploy, revoke its read credential and are recorded. `placements` are written as a report's rows are, with this deploy's id on them, and say nothing about the rest of the machine: the next full report does.",
        "operationId": "targetDeployReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "deploy_id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the work answered it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "requestBody": {
          "description": "how it is going",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "one sentence on progress or failure; cut at 1000 characters"
                  },
                  "placements": {
                    "type": "array",
                    "description": "the services this deploy placed; may be left out while running",
                    "items": {
                      "type": "object",
                      "description": "one service the deploy placed, as a report names it (POST /v1/targets/{id}/report), with what the definition asked of it",
                      "properties": {
                        "address": {
                          "type": "string",
                          "example": "pol-novid.nuved.app"
                        },
                        "applied": {
                          "type": "object",
                          "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                          "properties": {
                            "cpu_milli": {
                              "type": "integer",
                              "example": 500
                            },
                            "disk_bytes": {
                              "type": "integer",
                              "example": 10737418240
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "example": 536870912
                            },
                            "pids": {
                              "type": "integer",
                              "example": 256
                            }
                          }
                        },
                        "commit": {
                          "type": "string",
                          "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                        },
                        "egress": {
                          "type": "array",
                          "description": "at most 64 domains, each a hostname or *. in front of one, or the one word open (the plan's open tier); left out keeps the row's",
                          "items": {
                            "type": "string"
                          }
                        },
                        "environment": {
                          "type": "string",
                          "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                          "example": "production",
                          "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                        },
                        "health": {
                          "type": "string",
                          "example": "healthy"
                        },
                        "image": {
                          "type": "string",
                          "example": "novid-pol-web:4a3c2b1"
                        },
                        "last_error": {
                          "type": "string",
                          "description": "cut at 1000 characters"
                        },
                        "limits": {
                          "type": "object",
                          "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                          "properties": {
                            "cpu_milli": {
                              "type": "integer",
                              "example": 500
                            },
                            "disk_bytes": {
                              "type": "integer",
                              "example": 10737418240
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "example": 536870912
                            },
                            "pids": {
                              "type": "integer",
                              "example": 256
                            }
                          }
                        },
                        "project": {
                          "type": "string",
                          "example": "novid-pol"
                        },
                        "public": {
                          "type": "boolean",
                          "description": "left out keeps the row's"
                        },
                        "replicas_desired": {
                          "type": "integer",
                          "description": "0 to 64; left out keeps the row's",
                          "example": 1
                        },
                        "revision": {
                          "type": "string",
                          "example": "3"
                        },
                        "service": {
                          "type": "string",
                          "example": "web"
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "stateful": {
                          "type": "boolean",
                          "description": "left out keeps the row's"
                        },
                        "status": {
                          "type": "string",
                          "description": "what the target is doing with it",
                          "enum": [
                            "deploying",
                            "live",
                            "failed",
                            "stopped"
                          ]
                        }
                      },
                      "required": [
                        "project",
                        "service",
                        "environment",
                        "status"
                      ]
                    }
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "running",
                      "live",
                      "failed"
                    ]
                  }
                },
                "required": [
                  "status"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the deploy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a request to run a project's definition at a commit on a target, and what became of it",
                  "properties": {
                    "branch": {
                      "type": "string",
                      "example": "main"
                    },
                    "commit": {
                      "type": "string",
                      "description": "the full commit, or empty when the agent resolves the branch's tip",
                      "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_reason": {
                      "type": "string",
                      "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                      "enum": [
                        "",
                        "machine_hours",
                        "machine_silent",
                        "machine_gone",
                        "machine_stopped"
                      ]
                    },
                    "environment": {
                      "type": "string",
                      "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                      "example": "production",
                      "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                    },
                    "finished_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when it went live, failed or was cancelled"
                    },
                    "id": {
                      "type": "integer",
                      "example": 12
                    },
                    "kind": {
                      "type": "string",
                      "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                      "enum": [
                        "deploy",
                        "remove",
                        "build"
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "the agent's one sentence on progress or failure; empty until it says"
                    },
                    "project": {
                      "type": "string",
                      "example": "novid-pol"
                    },
                    "requested_by": {
                      "type": "string",
                      "description": "the person's address; git-plane for a push the hook could not name a person for",
                      "example": "owner@example.com"
                    },
                    "started_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the agent took it"
                    },
                    "status": {
                      "type": "string",
                      "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                      "enum": [
                        "waiting",
                        "queued",
                        "running",
                        "live",
                        "failed",
                        "cancelled",
                        "removed"
                      ]
                    },
                    "target": {
                      "type": "object",
                      "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                      "properties": {
                        "arch": {
                          "type": "string",
                          "description": "the machine's CPU architecture",
                          "example": "aarch64"
                        },
                        "country": {
                          "type": "string",
                          "example": "DE"
                        },
                        "display_name": {
                          "type": "string",
                          "description": "told to an owner of the workspace only; empty for everyone else",
                          "example": "Hetzner"
                        },
                        "id": {
                          "type": "string",
                          "example": "tg_1"
                        },
                        "kernel": {
                          "type": "string",
                          "description": "the machine's kernel release, as its gate read it",
                          "example": "6.8.0-146-generic"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "compose",
                            "kubernetes"
                          ]
                        },
                        "name": {
                          "type": "string",
                          "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                          "example": "nuved-box"
                        },
                        "provider": {
                          "type": "string",
                          "example": "Hetzner"
                        },
                        "region": {
                          "type": "string",
                          "example": "Falkenstein"
                        },
                        "runtime": {
                          "type": "string",
                          "description": "the container engine on a compose machine",
                          "example": "podman version 4.9.3"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "display_name",
                        "kind",
                        "provider",
                        "region",
                        "country"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "project",
                    "environment",
                    "branch",
                    "commit",
                    "target",
                    "status",
                    "requested_by",
                    "message",
                    "created_at",
                    "started_at",
                    "finished_at",
                    "end_reason"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, a status that is not one, or a placement row that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such deploy of this target",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the deploy ended already",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/deploys/{deploy_id}/files/{file}": {
      "put": {
        "tags": [
          "Deploys"
        ],
        "summary": "Send one part of a file a build kept.",
        "description": "Sent by the target's agent with the target's own credential, for its own running deploys only. A file comes in parts of 8 MiB (the last holds the rest); `X-Part-SHA256` is the hex sha256 of the body. A part sent again lands where the first did. The file is put together, and checked against the hash the closing `live` word names, when that word comes.",
        "operationId": "targetBuildPart",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "deploy_id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the work answered it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "the file's name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "part",
            "in": "query",
            "required": true,
            "description": "this part, from 1",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "of",
            "in": "query",
            "required": true,
            "description": "how many parts the file has",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "kept"
          },
          "400": {
            "description": "a part that is not one: its size, its hash, a name or a count that is not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such deploy of this target",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the deploy ended already",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "a body past 8 MiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "builds are not kept here, or the disk did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/deploys/{deploy_id}/output": {
      "put": {
        "tags": [
          "Deploys"
        ],
        "summary": "Send the masked output of a build, so it outlives the machine.",
        "description": "Sent by a build machine's agent with the target's own credential, for its own builds only, at most every 30 seconds while the build runs and once more with a closing word. The body is the whole masked tail so far (the agent masks on the machine before it sends), not a part to add: `seq` counts the sends and an older one never replaces a newer. `closing` is empty while the build runs, `ended` when it ended by itself, `stopped` when the machine was stopped or taken away; after a closing word nothing changes. A `stopped` word ends a build that is still running as `The machine this build ran on was taken away`, with `end_reason` `machine_stopped`; a build the console ended already keeps its own reason. The console keeps at most 256 KiB per build, with the retention of the build's files.",
        "operationId": "targetBuildOutput",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "deploy_id",
            "in": "path",
            "required": true,
            "description": "the deploy's id, as the work answered it",
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "requestBody": {
          "description": "the tail and the closing word",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bytes": {
                    "type": "integer",
                    "description": "how much the build has printed in all"
                  },
                  "closing": {
                    "type": "string",
                    "enum": [
                      "",
                      "ended",
                      "stopped"
                    ]
                  },
                  "seq": {
                    "type": "integer",
                    "description": "the number of this send, from 1",
                    "example": 3
                  },
                  "text": {
                    "type": "string",
                    "description": "the masked tail, whole lines, at most 256 KiB"
                  },
                  "truncated": {
                    "type": "boolean",
                    "description": "the start of the output was cut"
                  }
                },
                "required": [
                  "seq",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "whether it was kept",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stored": {
                      "type": "boolean",
                      "description": "false: an older send than the last, or one after the closing word"
                    }
                  },
                  "required": [
                    "stored"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, a closing word nobody defined, or a deploy that is not a build",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such deploy on this target",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "more than 256 KiB of text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "build output is not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/heartbeat": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Say that this target is here, and what it has; a machine in the mesh is answered with its peers.",
        "description": "Sent by the target's agent with the target's own credential, for itself only. `capacity` replaces what was last said when present; left out, the last stays. A machine that joined the machine mesh (`nuved-boxd join --mesh`) publishes its WireGuard public key and endpoint as `mesh` and is answered 200 with its own mesh address and every peer: the other machines of its workspace with an address and a key, never itself, never another workspace's. Any other machine is answered 204.",
        "operationId": "targetHeartbeat",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "requestBody": {
          "description": "what the target is",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "abandoned": {
                    "type": "array",
                    "description": "with state offline, at most 10 deploys the agent took and stopped before their last word, each with the workspace and project that find the row; one the console has running on this very target is queued again, so the machine takes it first when it is back; any other, and a removal (left to the ten-minute stale retake), is logged and ignored",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 41
                        },
                        "project": {
                          "type": "string",
                          "example": "novid-shop"
                        },
                        "tenant": {
                          "type": "string",
                          "example": "novid"
                        }
                      },
                      "required": [
                        "id",
                        "tenant",
                        "project"
                      ]
                    }
                  },
                  "agent_revision": {
                    "type": "string",
                    "description": "the full commit the agent was built from, 7 to 64 hex digits; kept as the target's agent_revision. Left out (an older agent, a dirty build), the last stays; a value that is not a commit is left out the same way and logged, and the beat is taken",
                    "example": "56ae6750c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
                  },
                  "capacity": {
                    "type": "object",
                    "description": "what the machine has, in the agent's own keys; at most 4 KiB"
                  },
                  "cert_fingerprint": {
                    "type": "string",
                    "description": "the SHA-256 (lower-case hex) of its unseal endpoint's certificate; sent with public_key"
                  },
                  "checks": {
                    "type": "object",
                    "description": "every host check's result by name: {ok, required, read, wanted, at}; at most 40 checks, 16 KiB. A required check that is not ok cordons the machine with the reason naming it; every required check ok makes an enrolled machine ready at once and a cordoned one ready after three clean beats in a row. Left out, the lifecycle does not move"
                  },
                  "facts": {
                    "type": "object",
                    "description": "what the agent discovered about the machine (the readiness gate); at most 16 KiB; replaces the last"
                  },
                  "forced": {
                    "type": "array",
                    "description": "with state offline, at most 50 services the drain had to kill (still running at the end of their grace); the machine's Activity line counts them, and each is said in its own workspace's Activity when this target holds placements of its project, named to nobody else",
                    "items": {
                      "type": "object",
                      "properties": {
                        "environment": {
                          "type": "string",
                          "example": "production"
                        },
                        "project": {
                          "type": "string",
                          "example": "novid-shop"
                        },
                        "service": {
                          "type": "string",
                          "example": "db"
                        },
                        "tenant": {
                          "type": "string",
                          "example": "novid"
                        }
                      },
                      "required": [
                        "tenant",
                        "project",
                        "environment",
                        "service"
                      ]
                    }
                  },
                  "mesh": {
                    "type": "object",
                    "description": "the machine's mesh identity, from an agent in the machine mesh: the WireGuard public key boxd made (recorded, and its first sighting timed, when it differs from the one on file: the machine re-keyed) and the endpoint it listens on; left out, the last stays",
                    "properties": {
                      "endpoint": {
                        "type": "string",
                        "description": "where its WireGuard listens, host:port with a literal address; empty for a machine behind NAT, which its peers learn on its first packet",
                        "example": "203.0.113.10:51820"
                      },
                      "public_key": {
                        "type": "string",
                        "description": "the WireGuard public key: 32 bytes, base64",
                        "example": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="
                      }
                    },
                    "required": [
                      "public_key"
                    ]
                  },
                  "profile": {
                    "type": "string",
                    "description": "the profile version the agent held the host to",
                    "example": "v1"
                  },
                  "public_key": {
                    "type": "string",
                    "description": "the machine's own age X25519 recipient; recorded when it differs from the one on file (an agent updated past its enrolment publishes it on its first beat)"
                  },
                  "reason": {
                    "type": "string",
                    "description": "with state offline, the goodbye after the agent drained: restarting when the machine itself goes down (a reboot, a power-off, the app's Restart or Stop; Activity says \"nuved-box is restarting\"), agent-restarting when only its agent stops (an update, a restart of its unit, its services stay up; Activity says \"nuved-box's agent is restarting\"). Neither is a failure. A reason this console does not know is logged and the beat taken; an older console ignores the key",
                    "enum": [
                      "restarting",
                      "agent-restarting"
                    ]
                  },
                  "state": {
                    "type": "string",
                    "description": "offline is stopping",
                    "enum": [
                      "online",
                      "offline"
                    ]
                  },
                  "usage": {
                    "type": "object",
                    "description": "how loaded the machine is: the CPU since the beat before, the memory and the customers' filesystem; kept as the target's usage until the next one. Left out (an older agent, a reading that failed, the goodbye), the last stays; a usage without every key, with a number below zero or a time outside the years 1 to 9999 is left out the same way and logged, and the beat is taken",
                    "properties": {
                      "at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "when the agent read it, by the machine's clock"
                      },
                      "cpu_milli": {
                        "type": "integer",
                        "description": "millicores busy since the heartbeat before, from the kernel's CPU counters (1000 is one CPU wholly busy)",
                        "example": 1250
                      },
                      "disk_bytes": {
                        "type": "integer",
                        "description": "bytes used of the filesystem the customers' homes are on",
                        "example": 42949672960
                      },
                      "disk_total_bytes": {
                        "type": "integer",
                        "example": 107374182400
                      },
                      "memory_bytes": {
                        "type": "integer",
                        "description": "memory in use: MemTotal less MemAvailable",
                        "example": 3221225472
                      },
                      "memory_total_bytes": {
                        "type": "integer",
                        "example": 8589934592
                      }
                    },
                    "required": [
                      "at",
                      "cpu_milli",
                      "memory_bytes",
                      "memory_total_bytes",
                      "disk_bytes",
                      "disk_total_bytes"
                    ]
                  }
                },
                "required": [
                  "state"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "taken; this machine is in the mesh, and these are its peers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mesh": {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string",
                          "description": "this machine's mesh address, given once at the join",
                          "example": "10.9.0.1"
                        },
                        "names": {
                          "type": "array",
                          "description": "for every project placed on this machine, each of its services placed on another machine with a mesh port, with the project it belongs to: the agent gives that project's containers the name and sends it over the mesh to the address and port, and nothing else resolves it (docs/project-names.md). Never null",
                          "items": {
                            "type": "object",
                            "properties": {
                              "address": {
                                "type": "string",
                                "description": "the mesh address of the machine that runs it",
                                "example": "10.9.0.1"
                              },
                              "environment": {
                                "type": "string",
                                "description": "in the heartbeat's answer only",
                                "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                              },
                              "name": {
                                "type": "string",
                                "description": "what the project's containers call the service: its name for a service that runs once, `\u003cservice\u003e-\u003creplica\u003e` for a replica",
                                "example": "db"
                              },
                              "port": {
                                "type": "integer",
                                "description": "the mesh port it binds there",
                                "example": 30001
                              },
                              "project": {
                                "type": "string",
                                "description": "in the heartbeat's answer only",
                                "example": "novid-shop"
                              },
                              "tenant": {
                                "type": "string",
                                "description": "the project's workspace; in the heartbeat's answer only",
                                "example": "novid"
                              }
                            },
                            "required": [
                              "name",
                              "address",
                              "port"
                            ]
                          }
                        },
                        "network": {
                          "type": "string",
                          "description": "the workspace's mesh",
                          "example": "10.9.0.0/16"
                        },
                        "peers": {
                          "type": "array",
                          "description": "every other machine of the workspace with a mesh address and a key, by name; the agent applies them to its WireGuard interface, allowed-ips the address alone",
                          "items": {
                            "type": "object",
                            "properties": {
                              "address": {
                                "type": "string",
                                "example": "10.9.0.2"
                              },
                              "endpoint": {
                                "type": "string",
                                "description": "host:port, or empty for a machine behind NAT, which this machine addresses once it has heard from it",
                                "example": "203.0.113.11:51820"
                              },
                              "name": {
                                "type": "string",
                                "example": "lab-2"
                              },
                              "public_key": {
                                "type": "string",
                                "example": "CRfExLfS9sq6/F3TsbyIZ+j7W5BwE6Fqmi3nKRjM5XI="
                              }
                            },
                            "required": [
                              "name",
                              "public_key",
                              "endpoint",
                              "address"
                            ]
                          }
                        },
                        "version": {
                          "type": "string",
                          "description": "names the peer list and the names: the same lists, the same version; a changed key, endpoint, address, member or name, another",
                          "example": "0a1b2c3d4e5f"
                        }
                      },
                      "required": [
                        "address",
                        "network",
                        "peers",
                        "names",
                        "version"
                      ]
                    }
                  },
                  "required": [
                    "mesh"
                  ]
                }
              }
            }
          },
          "204": {
            "description": "taken; this machine is not in the mesh"
          },
          "400": {
            "description": "a body that cannot be read, a state that is not online or offline, keys that are not keys, a mesh key or endpoint that is not one, or facts or checks that are not the shape or too big; never a usage, an agent_revision or a goodbye's reason, abandoned or forced, which are left out instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/pools": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "The balancer's pools: every replicated service of this machine's workspace with its live replicas' mesh addresses.",
        "description": "Polled by lbedge on the lab edge machine every five seconds with the machine's own target credential; only a machine that joined with role edge reads it. The members come from the console's placements and the machines' rows alone: nothing on a machine registers itself, and a container the console never placed is in no pool. The answer carries an ETag naming the list; a request with If-None-Match of the same value is answered 304.",
        "operationId": "lbedgePools",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "the ETag of the last answer applied",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the pools",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "one replicated service as the balancer sees it",
                        "properties": {
                          "environment": {
                            "type": "string",
                            "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                            "example": "production",
                            "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                          },
                          "hosts": {
                            "type": "array",
                            "description": "the hosts the frontend routes to this backend: the service's address under the apps domain, for production; none for a preview",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "web-novid-shop.nuved.app"
                            ]
                          },
                          "members": {
                            "type": "array",
                            "description": "every live and healthy replica on an online machine of the mesh, by replica",
                            "items": {
                              "type": "object",
                              "properties": {
                                "address": {
                                  "type": "string",
                                  "description": "the machine's mesh address, from its row",
                                  "example": "10.9.0.2"
                                },
                                "commit": {
                                  "type": "string",
                                  "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                                },
                                "machine": {
                                  "type": "string",
                                  "example": "lab-2"
                                },
                                "port": {
                                  "type": "integer",
                                  "example": 30000
                                },
                                "replica": {
                                  "type": "integer",
                                  "example": 1
                                }
                              },
                              "required": [
                                "replica",
                                "machine",
                                "address",
                                "port",
                                "commit"
                              ]
                            }
                          },
                          "name": {
                            "type": "string",
                            "description": "the backend's name in HAProxy: project, service and environment",
                            "example": "novid-shop_web_production"
                          },
                          "note": {
                            "type": "string",
                            "description": "the scheduler's last word on placing the replicas: what is missing, or empty when every replica is placed"
                          },
                          "port": {
                            "type": "integer",
                            "description": "the port every replica binds on its machine's mesh address",
                            "example": 30000
                          },
                          "project": {
                            "type": "string",
                            "example": "novid-shop"
                          },
                          "replicas": {
                            "type": "integer",
                            "description": "how many replicas the definition asks for",
                            "example": 2
                          },
                          "service": {
                            "type": "string",
                            "example": "web"
                          }
                        },
                        "required": [
                          "name",
                          "project",
                          "service",
                          "environment",
                          "port",
                          "replicas",
                          "hosts",
                          "note",
                          "members"
                        ]
                      }
                    },
                    "version": {
                      "type": "string",
                      "description": "names the list; the ETag without its quotes",
                      "example": "0a1b2c3d4e5f6a7b"
                    }
                  },
                  "required": [
                    "version",
                    "pools"
                  ]
                }
              }
            }
          },
          "304": {
            "description": "the same list as the one named by If-None-Match"
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential, or a machine that did not join with role edge",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "replicas are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/protected": {
      "put": {
        "tags": [
          "Targets"
        ],
        "summary": "Set or clear a machine's protected flag: while it is on, nothing removes, replaces, powers off, drains or rescales it. The operator's only.",
        "description": "Only an owner of the workspace that runs the platform, signed in, whichever workspace the machine is in; everybody else is refused and the refusal is in their Activity. `reason` is required and recorded with the change, who made it and the old and new value, in the machine's log and its workspace's Activity. Asking for the value the machine has already changes nothing and records nothing. Enrolling the machine again never clears the flag.",
        "operationId": "protectTarget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "requestBody": {
          "description": "the flag, and why",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "protected": {
                    "type": "boolean",
                    "example": true
                  },
                  "reason": {
                    "type": "string",
                    "description": "why, at most 200 characters",
                    "example": "the day-one box is billed at the old prices"
                  }
                },
                "required": [
                  "protected",
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the machine, as it is now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "a machine services are deployed to, as a customer sees it: never its host, address or port",
                  "properties": {
                    "agent_revision": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "the full commit the machine's nuved-boxd was built from, as its last heartbeat that said one said it; null when it never did (an older nuved-boxd, or a build outside a clean checkout)",
                      "example": "56ae6750c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
                    },
                    "capacity": {
                      "type": "object",
                      "description": "what the machine has, as its agent last said it; {} before it said"
                    },
                    "cert_fingerprint": {
                      "type": "string",
                      "description": "the SHA-256 (lower-case hex) of the certificate the machine's unseal endpoint serves, which the app pins; empty until published"
                    },
                    "checks": {
                      "type": "object",
                      "description": "every host check's last result by name: {ok, required, read, wanted, at}; a required one that is not ok cordons; {} before the agent said",
                      "example": {
                        "cgroup v2 unified": {
                          "at": "2026-10-22T22:00:00Z",
                          "ok": true,
                          "read": "cpuset cpu io memory hugetlb pids rdma misc",
                          "required": true,
                          "wanted": "cgroup2 at /sys/fs/cgroup listing cpu memory pids io, no v1 memory hierarchy"
                        }
                      }
                    },
                    "cordon_reason": {
                      "type": "string",
                      "description": "the check that cordoned the machine, with what was read and wanted; empty when it is not cordoned",
                      "example": "check failed: customers filesystem has project quota (read: ext4 rw,relatime on /dev/sda1: mounted without prjquota; wanted: /srv/customers mounted, xfs or ext4, with prjquota)"
                    },
                    "country": {
                      "type": "string",
                      "description": "ISO 3166-1 alpha-2, or empty",
                      "example": "DE"
                    },
                    "declared": {
                      "type": "object",
                      "description": "what the person or the provisioner said at the join: provider, location, country, type, server_id, arch, managed; {} for a machine enrolled the old way"
                    },
                    "display_name": {
                      "type": "string",
                      "description": "what listings call it",
                      "example": "Hetzner"
                    },
                    "endpoint": {
                      "type": "string",
                      "description": "on a fleet machine (labelled `nuved.io/fleet`) and only to an owner of the platform's own organisation: where its WireGuard listens, the public address its agent published; the key is absent for any other machine, for a member, and before the agent said",
                      "example": "203.0.113.10:51820"
                    },
                    "facts": {
                      "type": "object",
                      "description": "what the agent discovered: arch, kernel, os, cpus, memory, boot, the provider's metadata (instance id, location, zone), disks, the customers filesystem, swap, time, subuid ranges, runtime versions, the cgroup tree's numbers; {} before it said"
                    },
                    "id": {
                      "type": "string",
                      "example": "tg_1"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "compose",
                        "kubernetes"
                      ]
                    },
                    "labels": {
                      "type": "object",
                      "description": "the labels the machine joined with: general key and value pairs, `nuved.io/fleet` and `nuved.io/purpose` (build or service, what it is for) among them; {} for a machine with none. A machine with no purpose is nobody's to automate: nothing that creates or removes machines acts on it, and only a build machine is ever removed by automation",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "example": {
                        "example.com/team": "core",
                        "nuved.io/fleet": "build"
                      }
                    },
                    "last_seen_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the last heartbeat; null before one"
                    },
                    "lifecycle": {
                      "type": "string",
                      "description": "where the machine stands in the platform's handling of it: a machine that joined with declared facts is enrolled until its first heartbeat's checks pass, ready while they pass, cordoned (with cordon_reason) while a required one fails, and ready again after three clean rounds; only ready machines take deploys",
                      "example": "ready",
                      "enum": [
                        "requested",
                        "creating",
                        "bootstrapping",
                        "enrolled",
                        "verifying",
                        "ready",
                        "cordoned",
                        "dead",
                        "draining",
                        "retired",
                        "failed"
                      ]
                    },
                    "name": {
                      "type": "string",
                      "description": "lowercase letters, digits and dashes; unique in the workspace",
                      "example": "nuved-box"
                    },
                    "profile": {
                      "type": "string",
                      "description": "the profile version the agent held the host to; empty before it said",
                      "example": "v1"
                    },
                    "protected": {
                      "type": "boolean",
                      "description": "the operator's flag: while it is on, nothing removes, replaces, powers off, drains or rescales the machine; only the operator sets or clears it",
                      "example": false
                    },
                    "provider": {
                      "type": "string",
                      "example": "Hetzner"
                    },
                    "public_key": {
                      "type": "string",
                      "description": "the machine's own age X25519 recipient: an unseal share sent to this machine is encrypted to it; empty until its agent published it",
                      "example": "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
                    },
                    "region": {
                      "type": "string",
                      "description": "the datacenter or region",
                      "example": "Falkenstein"
                    },
                    "shared_with": {
                      "type": "array",
                      "description": "other workspaces whose project may run here: a project moved away from this workspace keeps running where it ran (POST /v1/projects/{name}/move); an owner takes it away with DELETE /v1/targets/{id}/access/{tenant}/{project}",
                      "items": {
                        "type": "object",
                        "properties": {
                          "project": {
                            "type": "string",
                            "example": "novid-growgoddess-auth-stage"
                          },
                          "tenant": {
                            "type": "string",
                            "example": "growgoddess"
                          }
                        },
                        "required": [
                          "tenant",
                          "project"
                        ]
                      }
                    },
                    "state": {
                      "type": "string",
                      "description": "what the last heartbeat said; empty before one",
                      "enum": [
                        "",
                        "online",
                        "offline"
                      ]
                    },
                    "usage": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "how loaded the machine is, its agent's last reading, refreshed by every heartbeat (30 s); null when the machine's agent never sent one (an older nuved-boxd)",
                      "properties": {
                        "at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "when the agent read it, by the machine's clock"
                        },
                        "cpu_milli": {
                          "type": "integer",
                          "description": "millicores busy since the heartbeat before, from the kernel's CPU counters (1000 is one CPU wholly busy)",
                          "example": 1250
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "description": "bytes used of the filesystem the customers' homes are on",
                          "example": 42949672960
                        },
                        "disk_total_bytes": {
                          "type": "integer",
                          "example": 107374182400
                        },
                        "memory_bytes": {
                          "type": "integer",
                          "description": "memory in use: MemTotal less MemAvailable",
                          "example": 3221225472
                        },
                        "memory_total_bytes": {
                          "type": "integer",
                          "example": 8589934592
                        }
                      },
                      "required": [
                        "at",
                        "cpu_milli",
                        "memory_bytes",
                        "memory_total_bytes",
                        "disk_bytes",
                        "disk_total_bytes"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "display_name",
                    "kind",
                    "provider",
                    "region",
                    "country",
                    "state",
                    "last_seen_at",
                    "capacity",
                    "shared_with",
                    "public_key",
                    "cert_fingerprint",
                    "lifecycle",
                    "protected",
                    "cordon_reason",
                    "declared",
                    "facts",
                    "checks",
                    "profile",
                    "usage",
                    "agent_revision",
                    "labels"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, no protected, or no reason",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "only the operator protects or unprotects a machine: an owner of the workspace that runs the platform, signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such machine",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "whether the caller is the operator could not be checked just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the store did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/targets/{id}/report": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Say what runs on this target: all of it, every time.",
        "description": "Sent by the target's agent with the target's own credential, for itself only. Each row named is recorded on this target; a row this target held and leaves out is stopped. At most 256 rows. Which replica a row is stays the platform's word: a deploy's rows take the deploy's replica, a report's rows keep the replica the machine holds, and a replicated service reported by a machine that was never given a replica of it is left out and that machine is told to stop it. After the report the replicas' scheduler places what is missing, one replica at a time (docs/replicas.md).",
        "operationId": "targetReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "requestBody": {
          "description": "what runs on the target",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "placements": {
                    "type": "array",
                    "description": "never null; [] when nothing runs",
                    "items": {
                      "type": "object",
                      "properties": {
                        "address": {
                          "type": "string",
                          "example": "shop-novid.nuved.app"
                        },
                        "applied": {
                          "type": "object",
                          "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                          "properties": {
                            "cpu_milli": {
                              "type": "integer",
                              "example": 500
                            },
                            "disk_bytes": {
                              "type": "integer",
                              "example": 10737418240
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "example": 536870912
                            },
                            "pids": {
                              "type": "integer",
                              "example": 256
                            }
                          }
                        },
                        "commit": {
                          "type": "string",
                          "example": "abc123"
                        },
                        "egress": {
                          "type": "array",
                          "description": "at most 64 domains, each a hostname or *. in front of one, or the one word open (the plan's open tier); left out keeps the row's",
                          "items": {
                            "type": "string"
                          }
                        },
                        "environment": {
                          "type": "string",
                          "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                          "example": "production",
                          "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                        },
                        "health": {
                          "type": "string",
                          "example": "healthy"
                        },
                        "image": {
                          "type": "string",
                          "example": "shop-web:abc123"
                        },
                        "last_error": {
                          "type": "string",
                          "description": "cut at 1000 characters"
                        },
                        "limits": {
                          "type": "object",
                          "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                          "properties": {
                            "cpu_milli": {
                              "type": "integer",
                              "example": 500
                            },
                            "disk_bytes": {
                              "type": "integer",
                              "example": 10737418240
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "example": 536870912
                            },
                            "pids": {
                              "type": "integer",
                              "example": 256
                            }
                          }
                        },
                        "project": {
                          "type": "string",
                          "example": "shop"
                        },
                        "public": {
                          "type": "boolean",
                          "description": "left out keeps the row's"
                        },
                        "replicas_desired": {
                          "type": "integer",
                          "description": "0 to 64; left out keeps the row's",
                          "example": 1
                        },
                        "restarts": {
                          "type": "integer",
                          "description": "how many times the engine restarted the running container (its RestartCount); left out by an older agent, and the row then has none. A value that is not a whole number from 0 to 2147483647 is taken as none for that row and logged; the report stands",
                          "example": 2
                        },
                        "revision": {
                          "type": "string",
                          "example": "3"
                        },
                        "service": {
                          "type": "string",
                          "example": "web"
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "stateful": {
                          "type": "boolean",
                          "description": "left out keeps the row's"
                        },
                        "status": {
                          "type": "string",
                          "description": "what the target is doing with it; queued is the platform's word and is refused",
                          "enum": [
                            "deploying",
                            "live",
                            "failed",
                            "stopped"
                          ]
                        }
                      },
                      "required": [
                        "project",
                        "service",
                        "environment",
                        "status"
                      ]
                    }
                  }
                },
                "required": [
                  "placements"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "recorded"
          },
          "400": {
            "description": "a body that cannot be read, or a row with a name, environment, status or limit that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "targets are not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/requests": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "A machine's long poll for the logs and commands people asked of it.",
        "operationId": "targetRequests",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_4"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the next request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "204": {
            "description": "none within 25 seconds"
          }
        },
        "security": []
      }
    },
    "/v1/targets/{id}/requests/{rid}/output": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "A machine's answer to a request, one JSON frame a line, streamed for as long as it runs.",
        "operationId": "targetRequestOutput",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "tg_4"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "example": 12
            }
          }
        ],
        "responses": {
          "204": {
            "description": "taken"
          },
          "410": {
            "description": "nobody waits for it any more"
          }
        },
        "security": []
      }
    },
    "/v1/targets/{id}/secrets/token": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "The JWT a machine's agent logs in to the vault with, for one project and environment it is deploying or running.",
        "description": "A target's own credential only. The JWT lives two minutes and is bound to the project's machine role for the environment (`machine-\u003cproject\u003e-\u003cenv\u003e`), whose policy configures that environment's databases, mints app credentials for its services and keeps their leases. env is the environment the machine deploys (its compose project is `\u003cproject\u003e-\u003cenv\u003e`): a project that declares it gets it; a project that declares default alone gets default for every deploy (one project per environment); any other is 403. The answer names the vault environment and its two mounts. The agent logs in to the vault itself, over the machine's own path to it; a machine with no path (a Mac behind NAT) gets no secrets delivered yet.",
        "operationId": "machineSecretsToken",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "requestBody": {
          "description": "the project and environment",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "env": {
                    "type": "string",
                    "description": "the environment the machine deploys; default when absent",
                    "example": "production"
                  },
                  "project": {
                    "type": "string",
                    "example": "novid-shop"
                  }
                },
                "required": [
                  "project"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the JWT and where it logs in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "db_mount": {
                      "type": "string",
                      "description": "where its databases are; a project from before environments keeps novid-shop/db for default",
                      "example": "novid-shop/default/db"
                    },
                    "env": {
                      "type": "string",
                      "description": "the vault environment the token is for",
                      "example": "default"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "jwt": {
                      "type": "string"
                    },
                    "kv_mount": {
                      "type": "string",
                      "description": "where the project's secrets are in it",
                      "example": "novid-shop/default/kv"
                    },
                    "lease_role": {
                      "type": "string",
                      "description": "the role the same JWT logs in to for the token the agent's database credentials hang on: a credential is revoked with the token that minted it, and the machine role's token lives fifteen minutes; the lease role's lives two days and may mint app credentials only",
                      "example": "lease-novid-shop-default"
                    },
                    "namespace": {
                      "type": "string",
                      "description": "empty when the login goes to the workspace's own vault",
                      "example": "nuved/novid"
                    },
                    "org": {
                      "type": "string"
                    },
                    "project": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "example": "machine-novid-shop-default"
                    },
                    "vault": {
                      "type": "string",
                      "description": "shared: the platform's vault inside namespace; own: the workspace's own vault on this machine, at the vaults directory, then the workspace's name, then run/bao.sock (S-9)",
                      "enum": [
                        "shared",
                        "own"
                      ]
                    }
                  },
                  "required": [
                    "jwt",
                    "role",
                    "namespace",
                    "project",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "not a target's credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "a target speaking for another, a project this machine is not deploying or running, or an environment the project does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such project in the machine's workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "secrets are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/usage": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "Say what each service on this target used: the sampler's rollups, once a minute.",
        "description": "Sent by the target's agent with the target's own credential, beside its report. Each row replaces the service's windows; an OOM kill opens a ticket once an hour per service; a change the applier made goes to the project's Activity. The answer is each reported project's mode and plan, which is what the applier reads.",
        "operationId": "targetUsage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "requestBody": {
          "description": "the rollups",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "usage": {
                    "type": "array",
                    "description": "at most 256 rows; [] when nothing runs",
                    "items": {
                      "type": "object",
                      "properties": {
                        "ceiling": {
                          "type": "object",
                          "description": "memory_bytes and cpu_milli; a key absent is 0",
                          "properties": {
                            "cpu_milli": {
                              "type": "integer",
                              "example": 300
                            },
                            "memory_bytes": {
                              "type": "integer",
                              "example": 201326592
                            }
                          }
                        },
                        "changed": {
                          "type": "string",
                          "format": "date-time",
                          "description": "when the applier changed a number this minute; the console writes the decision to Activity"
                        },
                        "days": {
                          "type": "integer"
                        },
                        "decision": {
                          "type": "string",
                          "description": "what the applier said this minute"
                        },
                        "environment": {
                          "type": "string",
                          "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                          "example": "production",
                          "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                        },
                        "hours": {
                          "type": "integer"
                        },
                        "image": {
                          "type": "string",
                          "example": "localhost/shop-web:latest"
                        },
                        "kind": {
                          "type": "string",
                          "example": "go"
                        },
                        "managed": {
                          "type": "boolean"
                        },
                        "manual": {
                          "type": "object",
                          "properties": {
                            "cpu": {
                              "type": "boolean"
                            },
                            "memory": {
                              "type": "boolean"
                            }
                          }
                        },
                        "oom": {
                          "type": "object",
                          "description": "the last OOM kill within a day; the console opens a ticket once an hour per service",
                          "properties": {
                            "at": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "log_lines": {
                              "type": "string",
                              "description": "the last 50 lines, cut to 16 KiB"
                            },
                            "peak_bytes": {
                              "type": "integer"
                            }
                          }
                        },
                        "project": {
                          "type": "string",
                          "example": "shop"
                        },
                        "service": {
                          "type": "string",
                          "example": "web"
                        },
                        "since": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "windows": {
                          "type": "object",
                          "description": "the three windows, 1h, 24h and 7d",
                          "properties": {
                            "1h": {
                              "type": "object",
                              "description": "percentiles of the per-bucket peaks over one window",
                              "properties": {
                                "cpu_p50": {
                                  "type": "integer",
                                  "description": "millicores",
                                  "example": 100
                                },
                                "cpu_p95": {
                                  "type": "integer",
                                  "example": 180
                                },
                                "cpu_p99": {
                                  "type": "integer",
                                  "example": 300
                                },
                                "mem_p50": {
                                  "type": "integer",
                                  "example": 94371840
                                },
                                "mem_p95": {
                                  "type": "integer",
                                  "example": 104857600
                                },
                                "mem_p99": {
                                  "type": "integer",
                                  "example": 146800640
                                },
                                "oom": {
                                  "type": "integer",
                                  "description": "OOM kills in the window",
                                  "example": 0
                                },
                                "peak_bytes": {
                                  "type": "integer",
                                  "example": 157286400
                                },
                                "samples": {
                                  "type": "integer",
                                  "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                  "example": 1440
                                }
                              },
                              "required": [
                                "samples",
                                "mem_p50",
                                "mem_p95",
                                "mem_p99",
                                "cpu_p50",
                                "cpu_p95",
                                "cpu_p99",
                                "peak_bytes",
                                "oom"
                              ]
                            },
                            "24h": {
                              "type": "object",
                              "description": "percentiles of the per-bucket peaks over one window",
                              "properties": {
                                "cpu_p50": {
                                  "type": "integer",
                                  "description": "millicores",
                                  "example": 100
                                },
                                "cpu_p95": {
                                  "type": "integer",
                                  "example": 180
                                },
                                "cpu_p99": {
                                  "type": "integer",
                                  "example": 300
                                },
                                "mem_p50": {
                                  "type": "integer",
                                  "example": 94371840
                                },
                                "mem_p95": {
                                  "type": "integer",
                                  "example": 104857600
                                },
                                "mem_p99": {
                                  "type": "integer",
                                  "example": 146800640
                                },
                                "oom": {
                                  "type": "integer",
                                  "description": "OOM kills in the window",
                                  "example": 0
                                },
                                "peak_bytes": {
                                  "type": "integer",
                                  "example": 157286400
                                },
                                "samples": {
                                  "type": "integer",
                                  "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                  "example": 1440
                                }
                              },
                              "required": [
                                "samples",
                                "mem_p50",
                                "mem_p95",
                                "mem_p99",
                                "cpu_p50",
                                "cpu_p95",
                                "cpu_p99",
                                "peak_bytes",
                                "oom"
                              ]
                            },
                            "7d": {
                              "type": "object",
                              "description": "percentiles of the per-bucket peaks over one window",
                              "properties": {
                                "cpu_p50": {
                                  "type": "integer",
                                  "description": "millicores",
                                  "example": 100
                                },
                                "cpu_p95": {
                                  "type": "integer",
                                  "example": 180
                                },
                                "cpu_p99": {
                                  "type": "integer",
                                  "example": 300
                                },
                                "mem_p50": {
                                  "type": "integer",
                                  "example": 94371840
                                },
                                "mem_p95": {
                                  "type": "integer",
                                  "example": 104857600
                                },
                                "mem_p99": {
                                  "type": "integer",
                                  "example": 146800640
                                },
                                "oom": {
                                  "type": "integer",
                                  "description": "OOM kills in the window",
                                  "example": 0
                                },
                                "peak_bytes": {
                                  "type": "integer",
                                  "example": 157286400
                                },
                                "samples": {
                                  "type": "integer",
                                  "description": "buckets in the window: a minute each for 1h and 24h, five minutes for 7d",
                                  "example": 1440
                                }
                              },
                              "required": [
                                "samples",
                                "mem_p50",
                                "mem_p95",
                                "mem_p99",
                                "cpu_p50",
                                "cpu_p95",
                                "cpu_p99",
                                "peak_bytes",
                                "oom"
                              ]
                            }
                          }
                        }
                      },
                      "required": [
                        "project",
                        "service",
                        "environment"
                      ]
                    }
                  }
                },
                "required": [
                  "usage"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "recorded; each project's mode and plan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sizing": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "mode": {
                            "type": "string",
                            "enum": [
                              "observe",
                              "apply"
                            ]
                          },
                          "plan": {
                            "type": "object",
                            "description": "memory_bytes and cpu_milli; a key absent is 0",
                            "properties": {
                              "cpu_milli": {
                                "type": "integer",
                                "example": 300
                              },
                              "memory_bytes": {
                                "type": "integer",
                                "example": 201326592
                              }
                            }
                          },
                          "project": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "project",
                          "mode",
                          "plan"
                        ]
                      }
                    }
                  },
                  "required": [
                    "sizing"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a body that cannot be read, or a row with a name, environment or number that is not one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "automatic sizing is not switched on here, or the database did not answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/vaults": {
      "get": {
        "tags": [
          "Targets"
        ],
        "summary": "The workspaces' vaults this machine runs, with what a ceremony needs: shares, threshold, holders and their devices' public keys, the console's JWT key.",
        "description": "A target's own credential. The agent writes each vault's stack from its templates, starts or removes it; a vault being made gets its ceremony on the machine and its ciphertext is posted with POST /v1/targets/{id}/vaults/{org}/keys. Devices are active members' only: the agent refuses an unseal signed by anyone else. Public keys only.",
        "operationId": "targetVaults",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the vaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "console": {
                        "type": "object",
                        "properties": {
                          "issuer": {
                            "type": "string"
                          },
                          "jwt_public_key": {
                            "type": "string"
                          }
                        }
                      },
                      "devices": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "device": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "public_key": {
                              "type": "string"
                            },
                            "signing_key": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "holders": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "made_by": {
                        "type": "string"
                      },
                      "org": {
                        "type": "string"
                      },
                      "port": {
                        "type": "integer"
                      },
                      "shares": {
                        "type": "integer"
                      },
                      "status": {
                        "type": "string"
                      },
                      "stay_unsealed": {
                        "type": "boolean"
                      },
                      "threshold": {
                        "type": "integer"
                      },
                      "wanted": {
                        "type": "string",
                        "enum": [
                          "running",
                          "removed"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/vaults/{org}": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "The agent's word on one vault: running, error (a ceremony that failed), removed, or its seal (sealed; unsealed, with the holder who gave the last share on the machine).",
        "description": "A target's own credential, for its own workspace's vault. sealed and unsealed are taken for a vault that is made; by names the holder when the share came to the machine's own endpoint.",
        "operationId": "targetVaultReport",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "org",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "the state",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "by": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string",
                    "enum": [
                      "running",
                      "error",
                      "removed",
                      "sealed",
                      "unsealed"
                    ]
                  }
                },
                "required": [
                  "state"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the vault's status now"
          },
          "404": {
            "description": "no such vault on this machine",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/vaults/{org}/keys": {
      "post": {
        "tags": [
          "Targets"
        ],
        "summary": "The ceremony's ciphertext, once: one share row per share and device of its holder.",
        "description": "A target's own credential, for its own workspace's vault being made. Exactly the rows the vault's holders' devices call for, every one an age ciphertext; status says whether the agent left the vault sealed or unsealed. The console stores them unread; a second post is refused with 409. The root token is never posted (a body with root rows is refused): the agent revokes it after the setup.",
        "operationId": "targetVaultKeys",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          },
          {
            "name": "org",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "the ciphertext",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "shares": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "ciphertext": {
                          "type": "string"
                        },
                        "device": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "index": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "sealed",
                      "unsealed"
                    ]
                  }
                },
                "required": [
                  "shares",
                  "status"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "taken; the vault is made"
          },
          "400": {
            "description": "rows that do not match the vault's holders, a row that is not an age ciphertext, or root rows",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such vault on this machine",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the vault is not waiting for keys: taken already, or in error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/work": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "Take the oldest queued deploy for this target, with what running it needs.",
        "description": "Sent by the target's agent with the target's own credential, for itself only. The deploy is marked running and handed to one taker: two agents asking at once never get the same one. The answer carries the repository's https address and a credential for it that lives 15 minutes and is revoked when the deploy ends, the plan's limits per service (empty is no limit), the plan, the egress tier, and the services the platform's edge fronts (`deploy.exposed`). 204 when nothing is queued; ask again later.",
        "operationId": "targetWork",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "a deploy to run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deploy": {
                      "type": "object",
                      "description": "a request to run a project's definition at a commit on a target, and what became of it",
                      "properties": {
                        "branch": {
                          "type": "string",
                          "example": "main"
                        },
                        "commit": {
                          "type": "string",
                          "description": "the full commit, or empty when the agent resolves the branch's tip",
                          "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_reason": {
                          "type": "string",
                          "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                          "enum": [
                            "",
                            "machine_hours",
                            "machine_silent",
                            "machine_gone",
                            "machine_stopped"
                          ]
                        },
                        "environment": {
                          "type": "string",
                          "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                          "example": "production",
                          "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                        },
                        "exposed": {
                          "type": "array",
                          "description": "the services of the project the platform's edge fronts at `\u003cservice\u003e-\u003cproject\u003e.nuved.app`, behind the workspace's login, by name: share each of them for the edge alone (it proxies to the service's relay name, whose own address answers 404 meanwhile), the others as before. Read when the deploy is handed out. A production deploy's; empty for a preview and a removal. Never null.",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "web"
                          ]
                        },
                        "finished_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "when it went live, failed or was cancelled"
                        },
                        "id": {
                          "type": "integer",
                          "example": 12
                        },
                        "kind": {
                          "type": "string",
                          "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                          "enum": [
                            "deploy",
                            "remove",
                            "build"
                          ]
                        },
                        "mesh_port": {
                          "type": "integer",
                          "description": "the port the public service binds on this machine's mesh address, when the service is replicated; 0 binds 127.0.0.1 as before",
                          "example": 30000
                        },
                        "mesh_ports": {
                          "type": "object",
                          "description": "the named services' mesh ports on this machine, by service: each binds the machine's mesh address on its port, for the project's containers on other machines, which reach it by the service's name (docs/project-names.md). Never null; {} for a deploy that names nothing, and for a replica's",
                          "example": {
                            "db": 30001
                          }
                        },
                        "message": {
                          "type": "string",
                          "description": "the agent's one sentence on progress or failure; empty until it says"
                        },
                        "names": {
                          "type": "array",
                          "description": "the project's services on other machines, as its containers here call them: the agent gives each container the name and sends it over the mesh to the address and port. Read when the deploy is handed out. Never null; [] for a removal",
                          "items": {
                            "type": "object",
                            "properties": {
                              "address": {
                                "type": "string",
                                "description": "the mesh address of the machine that runs it",
                                "example": "10.9.0.1"
                              },
                              "environment": {
                                "type": "string",
                                "description": "in the heartbeat's answer only",
                                "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
                              },
                              "name": {
                                "type": "string",
                                "description": "what the project's containers call the service: its name for a service that runs once, `\u003cservice\u003e-\u003creplica\u003e` for a replica",
                                "example": "db"
                              },
                              "port": {
                                "type": "integer",
                                "description": "the mesh port it binds there",
                                "example": 30001
                              },
                              "project": {
                                "type": "string",
                                "description": "in the heartbeat's answer only",
                                "example": "novid-shop"
                              },
                              "tenant": {
                                "type": "string",
                                "description": "the project's workspace; in the heartbeat's answer only",
                                "example": "novid"
                              }
                            },
                            "required": [
                              "name",
                              "address",
                              "port"
                            ]
                          }
                        },
                        "project": {
                          "type": "string",
                          "example": "novid-pol"
                        },
                        "replica": {
                          "type": "integer",
                          "description": "which replica this deploy is; 0 for the home",
                          "example": 1
                        },
                        "requested_by": {
                          "type": "string",
                          "description": "the person's address; git-plane for a push the hook could not name a person for",
                          "example": "owner@example.com"
                        },
                        "service": {
                          "type": "string",
                          "description": "a replica's deploy: the one service to run; empty for a deploy of the whole project (docs/replicas.md)",
                          "example": "web"
                        },
                        "started_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "when the agent took it"
                        },
                        "status": {
                          "type": "string",
                          "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                          "enum": [
                            "waiting",
                            "queued",
                            "running",
                            "live",
                            "failed",
                            "cancelled",
                            "removed"
                          ]
                        },
                        "target": {
                          "type": "object",
                          "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                          "properties": {
                            "arch": {
                              "type": "string",
                              "description": "the machine's CPU architecture",
                              "example": "aarch64"
                            },
                            "country": {
                              "type": "string",
                              "example": "DE"
                            },
                            "display_name": {
                              "type": "string",
                              "description": "told to an owner of the workspace only; empty for everyone else",
                              "example": "Hetzner"
                            },
                            "id": {
                              "type": "string",
                              "example": "tg_1"
                            },
                            "kernel": {
                              "type": "string",
                              "description": "the machine's kernel release, as its gate read it",
                              "example": "6.8.0-146-generic"
                            },
                            "kind": {
                              "type": "string",
                              "enum": [
                                "compose",
                                "kubernetes"
                              ]
                            },
                            "name": {
                              "type": "string",
                              "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                              "example": "nuved-box"
                            },
                            "provider": {
                              "type": "string",
                              "example": "Hetzner"
                            },
                            "region": {
                              "type": "string",
                              "example": "Falkenstein"
                            },
                            "runtime": {
                              "type": "string",
                              "description": "the container engine on a compose machine",
                              "example": "podman version 4.9.3"
                            }
                          },
                          "required": [
                            "id",
                            "name",
                            "display_name",
                            "kind",
                            "provider",
                            "region",
                            "country"
                          ]
                        },
                        "tenant": {
                          "type": "string",
                          "description": "the workspace the project belongs to",
                          "example": "novid"
                        }
                      },
                      "required": [
                        "id",
                        "project",
                        "environment",
                        "branch",
                        "commit",
                        "target",
                        "status",
                        "requested_by",
                        "message",
                        "created_at",
                        "started_at",
                        "finished_at",
                        "end_reason",
                        "tenant",
                        "exposed",
                        "service",
                        "replica",
                        "mesh_port",
                        "mesh_ports",
                        "names"
                      ]
                    },
                    "egress_tier": {
                      "type": "string",
                      "description": "declared: only the domains the definition names; open: every HTTPS domain but the deny-list",
                      "enum": [
                        "declared",
                        "open"
                      ]
                    },
                    "limits": {
                      "type": "object",
                      "description": "memory_bytes, cpu_milli, pids, disk_bytes; a key absent is unset",
                      "properties": {
                        "cpu_milli": {
                          "type": "integer",
                          "example": 500
                        },
                        "disk_bytes": {
                          "type": "integer",
                          "example": 10737418240
                        },
                        "memory_bytes": {
                          "type": "integer",
                          "example": 536870912
                        },
                        "pids": {
                          "type": "integer",
                          "example": 256
                        }
                      }
                    },
                    "plan": {
                      "type": "string",
                      "description": "the workspace's plan; limits holds its numbers, and the operator's are 4 GiB, 4 CPUs and 2048 processes for one service",
                      "enum": [
                        "free",
                        "pro",
                        "operator"
                      ]
                    },
                    "project": {
                      "type": "object",
                      "properties": {
                        "git_https": {
                          "type": "string",
                          "description": "where to fetch the repository",
                          "example": "https://git.nuved.io/novid/novid-pol"
                        },
                        "name": {
                          "type": "string",
                          "example": "novid-pol"
                        },
                        "read_token": {
                          "type": "string",
                          "description": "a credential for this repository alone, dead after 15 minutes; a secret, shown this once",
                          "example": "nvd_agent_…"
                        }
                      },
                      "required": [
                        "name",
                        "git_https",
                        "read_token"
                      ]
                    }
                  },
                  "required": [
                    "deploy",
                    "project",
                    "limits",
                    "plan",
                    "egress_tier"
                  ]
                }
              }
            }
          },
          "204": {
            "description": "nothing is queued for this target"
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "deploys are not switched on here, or the deploy could not be handed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/targets/{id}/work-events": {
      "get": {
        "tags": [
          "Deploys"
        ],
        "summary": "Hold a stream that says when this target has work to take.",
        "description": "Sent by the target's agent with the target's own credential, for itself only. A server-sent event stream: `hello` first, with `fallback_seconds` (how often to ask GET /v1/targets/{id}/work anyway while the stream is up); then `work`, with no payload, each time work is queued for this target, upon which the agent asks GET /v1/targets/{id}/work; a `: keepalive` comment every 25 seconds; and `bye` when the console ends the stream: after 30 minutes ± 5 (`reason` rotate), when it shuts down (`reason` shutdown), or when the target's credential changed or the target is gone (no reason). A stream carries no work and no credential: the take is GET /v1/targets/{id}/work's. 404 where the console sends no stream (WORK_BELL or WORK_STREAM off, or this target not on WORK_STREAM's list): ask GET /work every 10 seconds as before.",
        "operationId": "targetWorkEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the target's id, as enrolling it answers",
            "schema": {
              "type": "string",
              "example": "tg_1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the stream: events hello, work and bye, and keepalive comments",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "no target credential, or one that is not a target's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "another target's credential: a target speaks for itself only",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "this console sends no work stream; ask GET /v1/targets/{id}/work",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "this machine holds four work streams already",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the console holds 1000 work streams, or is shutting down",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "target": []
          }
        ]
      }
    },
    "/v1/teams": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "The workspace's teams, its own first, with how many people and projects each has.",
        "description": "Every workspace has a team named after it, which its owners and members are in. A person narrowed to some projects is refused, as on the people routes.",
        "operationId": "listTeams",
        "responses": {
          "200": {
            "description": "the teams",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one team of the workspace",
                    "properties": {
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "from": {
                        "type": "string",
                        "description": "the other workspace whose team this one holds, made when it accepted an invitation to a project: its people are that workspace's to change; empty for a team of this workspace's own people",
                        "example": ""
                      },
                      "members": {
                        "type": "integer",
                        "description": "how many people are in it",
                        "example": 3
                      },
                      "name": {
                        "type": "string",
                        "example": "design"
                      },
                      "organization": {
                        "type": "boolean",
                        "description": "the workspace's own team, named after it: its owners and members are in it, and it is never deleted"
                      },
                      "projects": {
                        "type": "integer",
                        "description": "how many projects it reaches",
                        "example": 2
                      }
                    },
                    "required": [
                      "name",
                      "organization",
                      "from",
                      "members",
                      "projects",
                      "created_at"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "People"
        ],
        "summary": "Make an empty team. An owner's.",
        "operationId": "createTeam",
        "requestBody": {
          "description": "the team's name",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "lowercase letters, digits and hyphens, up to 63 characters",
                    "example": "design"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the team",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "one team of the workspace",
                  "properties": {
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "from": {
                      "type": "string",
                      "description": "the other workspace whose team this one holds, made when it accepted an invitation to a project: its people are that workspace's to change; empty for a team of this workspace's own people",
                      "example": ""
                    },
                    "members": {
                      "type": "integer",
                      "description": "how many people are in it",
                      "example": 3
                    },
                    "name": {
                      "type": "string",
                      "example": "design"
                    },
                    "organization": {
                      "type": "boolean",
                      "description": "the workspace's own team, named after it: its owners and members are in it, and it is never deleted"
                    },
                    "projects": {
                      "type": "integer",
                      "description": "how many projects it reaches",
                      "example": 2
                    }
                  },
                  "required": [
                    "name",
                    "organization",
                    "from",
                    "members",
                    "projects",
                    "created_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "no name, or a name a team cannot have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a team of that name exists, or it is the workspace's own name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/teams/{team}": {
      "delete": {
        "tags": [
          "People"
        ],
        "summary": "Delete a team. An owner's; the workspace's own team stays.",
        "description": "Its people stay in the workspace; the projects it reached are no longer reached through it.",
        "operationId": "deleteTeam",
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "description": "the team's name, as GET /v1/teams lists it",
            "schema": {
              "type": "string",
              "example": "design"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "deleted"
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such team",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the workspace's own team, which stays",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/teams/{team}/members": {
      "get": {
        "tags": [
          "People"
        ],
        "summary": "The people in one team, by address.",
        "operationId": "listTeamMembers",
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "description": "the team's name, as GET /v1/teams lists it",
            "schema": {
              "type": "string",
              "example": "design"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the people",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "one person in a team",
                    "properties": {
                      "added_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "when they were put in the team"
                      },
                      "email": {
                        "type": "string",
                        "example": "teammate@example.com"
                      },
                      "from": {
                        "type": "string",
                        "description": "a guest's workspace; empty for this workspace's own people",
                        "example": ""
                      },
                      "role": {
                        "type": "string",
                        "description": "their role in the workspace, as GET /v1/members has it; guest for a person of another workspace, in a team that holds its team",
                        "enum": [
                          "owner",
                          "member",
                          "project",
                          "guest"
                        ]
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "invited",
                          "active"
                        ]
                      }
                    },
                    "required": [
                      "email",
                      "role",
                      "status",
                      "added_at",
                      "from"
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such team",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "put": {
        "tags": [
          "People"
        ],
        "summary": "Make a team's people exactly these addresses. An owner's.",
        "description": "Everyone named must be in the workspace. The workspace's own team holds its owners and members: nobody is taken out of it but by leaving the workspace, and a person narrowed to some projects is not put in it. A team that holds another workspace's team is that workspace's to change. A refusal changes nothing.",
        "operationId": "setTeamMembers",
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "description": "the team's name, as GET /v1/teams lists it",
            "schema": {
              "type": "string",
              "example": "design"
            }
          }
        ],
        "requestBody": {
          "description": "the team's people",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emails": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "teammate@example.com"
                    }
                  }
                },
                "required": [
                  "emails"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "who was added and taken out, and the team's people now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "example": "teammate@example.com"
                      }
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "one person in a team",
                        "properties": {
                          "added_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when they were put in the team"
                          },
                          "email": {
                            "type": "string",
                            "example": "teammate@example.com"
                          },
                          "from": {
                            "type": "string",
                            "description": "a guest's workspace; empty for this workspace's own people",
                            "example": ""
                          },
                          "role": {
                            "type": "string",
                            "description": "their role in the workspace, as GET /v1/members has it; guest for a person of another workspace, in a team that holds its team",
                            "enum": [
                              "owner",
                              "member",
                              "project",
                              "guest"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "invited",
                              "active"
                            ]
                          }
                        },
                        "required": [
                          "email",
                          "role",
                          "status",
                          "added_at",
                          "from"
                        ]
                      }
                    },
                    "removed": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "example": "teammate@example.com"
                      }
                    }
                  },
                  "required": [
                    "added",
                    "removed",
                    "members"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "no emails list, or something in it that is not an address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner, an agent token, or a person who reaches only some projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such team, or an address that is not in the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a change the workspace's own team does not take, or a team that holds another workspace's team: `\u003cteam\u003e is \u003cworkspace\u003e's people; change it in \u003cworkspace\u003e, or revoke the invitation here`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "teams are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/tokens": {
      "get": {
        "tags": [
          "Tokens"
        ],
        "summary": "The workspace's live credentials, newest first. Never the tokens themselves.",
        "operationId": "listTokens",
        "responses": {
          "200": {
            "description": "the credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Token"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Tokens"
        ],
        "summary": "Mint an agent token for one project. The token itself is in this answer and nowhere else, ever.",
        "description": "What is stored is a SHA-256 hash, so no later read can show the token again. An agent token reaches one project and never people, billing or another project; `scopes` narrows what it may do there (read, code, deploy, logs, artifacts, secrets; all when left out) and `expires_in_days` ends it (1 to 365; never when left out). In kubectl, with a kubeconfig carrying it, `read` or `logs` is the project's view role (get, list, watch and pods' logs; never a Secret, `secrets` beside it or not), `deploy` or no scopes changes what runs, and a token with only `code`, `artifacts` or `secrets` is refused there with the scope it needs. Every live token of a project opens its protected addresses on the platform's edge; one minted with `open` alone does that and nothing else (the project's bypass tokens, GET /v1/projects/{name}/bypass-tokens).",
        "operationId": "createToken",
        "requestBody": {
          "description": "which project, what is using it, and how far it may go",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_in_days": {
                    "type": "integer",
                    "description": "1 to 365; the token never ends when left out",
                    "example": 30
                  },
                  "name": {
                    "type": "string",
                    "description": "what holds it, for the token list",
                    "example": "claude-code"
                  },
                  "project": {
                    "type": "string",
                    "example": "novid-shop"
                  },
                  "scopes": {
                    "type": "array",
                    "description": "what it may do; every scope when left out",
                    "items": {
                      "type": "string",
                      "enum": [
                        "read",
                        "code",
                        "deploy",
                        "logs",
                        "artifacts",
                        "secrets",
                        "open"
                      ]
                    }
                  }
                },
                "required": [
                  "project",
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the credential, with its plain text once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Token"
                }
              }
            }
          },
          "400": {
            "description": "no name, no such project, a scope that is not one, or days out of 1 to 365",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not mint credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/tokens/{id}": {
      "delete": {
        "tags": [
          "Tokens"
        ],
        "summary": "Revoke a credential. Whatever is using it stops at once.",
        "operationId": "revokeToken",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the credential's id, from GET /v1/tokens",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "revoked"
          },
          "403": {
            "description": "an agent token may not revoke credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no such credential in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "What each project cost, day by day, and when the balance runs out.",
        "description": "One entry per UTC day of the window, oldest first, including days with nothing charged. Each day lists what every project was charged and the day's total; days charged before per-project lines existed have a total and no projects. `hourly_now` is what is reserved right now, per hour, and `runs_out_on` the day the balance runs out at that rate: empty when nothing is reserved.",
        "operationId": "getUsage",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "how many days back, today included: 1 to 90, default 30",
            "schema": {
              "type": "integer",
              "example": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "400": {
            "description": "days is not a whole number from 1 to 90",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token may not read the workspace's spending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/usage/report": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "What each project reserved and paid for, day by day, over any window up to a year.",
        "description": "Read back from the meter's own records: every ten minutes it charges each project for the CPU, memory and disk it reserves, and writes the ledger debit and one line per project together. A day's total `debited_micro` is the ledger's usage debits for that UTC day, to the micro-euro; the projects' lines add up to it, and `unattributed_micro` is whatever was charged before lines existed. Reservations are given held over time (`cpu_milli_hours`, `mem_byte_hours`, `disk_byte_hours`) so they add up across projects and days. Every day of the window is listed, oldest first, days with nothing charged included. Traffic and the number of published shares are not recorded; `missing` says so rather than the report inventing them. `GET /v1/usage` is the Billing page's shorter answer, with the runway.",
        "operationId": "getUsageReport",
        "parameters": [
          {
            "name": "org",
            "in": "query",
            "description": "the caller's own workspace, which is also the default; any other is not found",
            "schema": {
              "type": "string",
              "example": "novid"
            }
          },
          {
            "name": "project",
            "in": "query",
            "description": "one project, by its full name or the part it was named; every project when left out",
            "schema": {
              "type": "string",
              "example": "shop"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "the first UTC day, included; default 29 days before to",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-10-08"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "the last UTC day, included; default today",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-10-08"
            }
          },
          {
            "name": "by",
            "in": "query",
            "description": "the grouping: day, the only one so far",
            "schema": {
              "type": "string",
              "example": "day",
              "enum": [
                "day"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "by": {
                      "type": "string",
                      "example": "day",
                      "enum": [
                        "day"
                      ]
                    },
                    "days": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-10-08"
                          },
                          "projects": {
                            "type": "array",
                            "description": "every project charged that day, by name",
                            "items": {
                              "type": "object",
                              "properties": {
                                "cpu_milli_hours": {
                                  "type": "integer",
                                  "description": "CPU reserved over time: millicores held for an hour",
                                  "example": 2880
                                },
                                "debited": {
                                  "type": "string",
                                  "example": "€1.39"
                                },
                                "debited_micro": {
                                  "type": "integer",
                                  "description": "money debited, in micro-euros",
                                  "example": 1390000
                                },
                                "disk_byte_hours": {
                                  "type": "integer",
                                  "description": "disk reserved over time: bytes of volume claims held for an hour",
                                  "example": 240000000000
                                },
                                "mem_byte_hours": {
                                  "type": "integer",
                                  "description": "memory reserved over time: bytes held for an hour; divide by 1073741824 for GiB-hours",
                                  "example": 25769803776
                                },
                                "project": {
                                  "type": "string",
                                  "example": "novid-shop"
                                },
                                "ticks": {
                                  "type": "integer",
                                  "description": "meter intervals charged (interval_seconds each)",
                                  "example": 144
                                }
                              },
                              "required": [
                                "project",
                                "ticks",
                                "cpu_milli_hours",
                                "mem_byte_hours",
                                "disk_byte_hours",
                                "debited_micro",
                                "debited"
                              ]
                            }
                          },
                          "total": {
                            "type": "object",
                            "properties": {
                              "cpu_milli_hours": {
                                "type": "integer",
                                "description": "CPU reserved over time: millicores held for an hour",
                                "example": 2880
                              },
                              "debited": {
                                "type": "string",
                                "example": "€1.39"
                              },
                              "debited_micro": {
                                "type": "integer",
                                "description": "money debited, in micro-euros",
                                "example": 1390000
                              },
                              "disk_byte_hours": {
                                "type": "integer",
                                "description": "disk reserved over time: bytes of volume claims held for an hour",
                                "example": 240000000000
                              },
                              "mem_byte_hours": {
                                "type": "integer",
                                "description": "memory reserved over time: bytes held for an hour; divide by 1073741824 for GiB-hours",
                                "example": 25769803776
                              },
                              "ticks": {
                                "type": "integer",
                                "description": "meter intervals charged (interval_seconds each)",
                                "example": 144
                              },
                              "unattributed_micro": {
                                "type": "integer",
                                "description": "the part of debited_micro no project line is under: ticks charged before the meter wrote one line per project. 0 for a report of one project",
                                "example": 0
                              }
                            },
                            "required": [
                              "unattributed_micro",
                              "ticks",
                              "cpu_milli_hours",
                              "mem_byte_hours",
                              "disk_byte_hours",
                              "debited_micro",
                              "debited"
                            ]
                          }
                        },
                        "required": [
                          "date",
                          "projects",
                          "total"
                        ]
                      }
                    },
                    "from": {
                      "type": "string",
                      "format": "date",
                      "description": "the first day",
                      "example": "2026-10-08"
                    },
                    "interval_seconds": {
                      "type": "integer",
                      "description": "how long one tick stands for: the meter's interval",
                      "example": 600
                    },
                    "missing": {
                      "type": "array",
                      "description": "what the meter does not record, and why",
                      "items": {
                        "type": "object",
                        "properties": {
                          "what": {
                            "type": "string",
                            "example": "traffic"
                          },
                          "why": {
                            "type": "string",
                            "example": "the meter charges for reserved CPU, memory and disk only"
                          }
                        },
                        "required": [
                          "what",
                          "why"
                        ]
                      }
                    },
                    "org": {
                      "type": "string",
                      "example": "novid"
                    },
                    "project": {
                      "type": "string",
                      "description": "the project asked for, as asked; empty for every project",
                      "example": ""
                    },
                    "projects": {
                      "type": "array",
                      "description": "each project over the whole window, by name",
                      "items": {
                        "type": "object",
                        "properties": {
                          "cpu_milli_hours": {
                            "type": "integer",
                            "description": "CPU reserved over time: millicores held for an hour",
                            "example": 2880
                          },
                          "debited": {
                            "type": "string",
                            "example": "€1.39"
                          },
                          "debited_micro": {
                            "type": "integer",
                            "description": "money debited, in micro-euros",
                            "example": 1390000
                          },
                          "disk_byte_hours": {
                            "type": "integer",
                            "description": "disk reserved over time: bytes of volume claims held for an hour",
                            "example": 240000000000
                          },
                          "mem_byte_hours": {
                            "type": "integer",
                            "description": "memory reserved over time: bytes held for an hour; divide by 1073741824 for GiB-hours",
                            "example": 25769803776
                          },
                          "project": {
                            "type": "string",
                            "example": "novid-shop"
                          },
                          "ticks": {
                            "type": "integer",
                            "description": "meter intervals charged (interval_seconds each)",
                            "example": 144
                          }
                        },
                        "required": [
                          "project",
                          "ticks",
                          "cpu_milli_hours",
                          "mem_byte_hours",
                          "disk_byte_hours",
                          "debited_micro",
                          "debited"
                        ]
                      }
                    },
                    "to": {
                      "type": "string",
                      "format": "date",
                      "description": "the last day",
                      "example": "2026-10-08"
                    },
                    "total": {
                      "type": "object",
                      "properties": {
                        "cpu_milli_hours": {
                          "type": "integer",
                          "description": "CPU reserved over time: millicores held for an hour",
                          "example": 2880
                        },
                        "debited": {
                          "type": "string",
                          "example": "€1.39"
                        },
                        "debited_micro": {
                          "type": "integer",
                          "description": "money debited, in micro-euros",
                          "example": 1390000
                        },
                        "disk_byte_hours": {
                          "type": "integer",
                          "description": "disk reserved over time: bytes of volume claims held for an hour",
                          "example": 240000000000
                        },
                        "mem_byte_hours": {
                          "type": "integer",
                          "description": "memory reserved over time: bytes held for an hour; divide by 1073741824 for GiB-hours",
                          "example": 25769803776
                        },
                        "ticks": {
                          "type": "integer",
                          "description": "meter intervals charged (interval_seconds each)",
                          "example": 144
                        },
                        "unattributed_micro": {
                          "type": "integer",
                          "description": "the part of debited_micro no project line is under: ticks charged before the meter wrote one line per project. 0 for a report of one project",
                          "example": 0
                        }
                      },
                      "required": [
                        "unattributed_micro",
                        "ticks",
                        "cpu_milli_hours",
                        "mem_byte_hours",
                        "disk_byte_hours",
                        "debited_micro",
                        "debited"
                      ]
                    }
                  },
                  "required": [
                    "org",
                    "project",
                    "from",
                    "to",
                    "by",
                    "interval_seconds",
                    "days",
                    "projects",
                    "total",
                    "missing"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a date that is not YYYY-MM-DD, from after to, a window over 366 days, or a grouping other than day",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "an agent token, or a person who reaches only some projects: spending is the workspace's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "org names another workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the usage could not be read just now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vault": {
      "get": {
        "tags": [
          "Vault"
        ],
        "summary": "The workspace's vault: its machine, who holds its shares and who has kept theirs, whether it is sealed, who unsealed it last.",
        "description": "One OpenBao per workspace on its machine, keyed by the holders' devices (the vault ceremony). Never a key or a token. Status none means no vault yet: secrets and databases wait for it.",
        "operationId": "getVault",
        "responses": {
          "200": {
            "description": "the vault",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "active": {
                      "type": "boolean",
                      "description": "unsealed and serving"
                    },
                    "holders": {
                      "type": "array",
                      "description": "every device a share was encrypted to, with its holder; empty before a vault",
                      "items": {
                        "type": "object",
                        "properties": {
                          "device": {
                            "type": "string",
                            "description": "a device their share was encrypted to at the ceremony",
                            "example": "Novid's MacBook Pro"
                          },
                          "email": {
                            "type": "string",
                            "example": "owner@example.com"
                          },
                          "has_share": {
                            "type": "boolean",
                            "description": "they said I have kept it on this device for every share of theirs encrypted to it"
                          }
                        },
                        "required": [
                          "email",
                          "device",
                          "has_share"
                        ]
                      }
                    },
                    "holders_with_share": {
                      "type": "integer",
                      "description": "how many holders have kept every share of theirs on at least one device"
                    },
                    "last_unsealed_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "last_unsealed_by": {
                      "type": "string"
                    },
                    "machine": {
                      "type": "string",
                      "description": "that machine by provider and place (\"Hetzner in Falkenstein\"), never its name"
                    },
                    "made_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "made_by": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "key",
                        "shares"
                      ]
                    },
                    "progress": {
                      "type": "integer",
                      "description": "shares given since the vault sealed"
                    },
                    "rekey_needed": {
                      "type": "boolean",
                      "description": "the holders must change: a holder left the workspace"
                    },
                    "rekey_reason": {
                      "type": "string"
                    },
                    "shares": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "none",
                        "making",
                        "sealed",
                        "unsealed",
                        "error",
                        "removing"
                      ]
                    },
                    "stay_unsealed": {
                      "type": "boolean",
                      "description": "the maker left it unsealed after the ceremony, until its next restart"
                    },
                    "target_id": {
                      "type": "string",
                      "description": "the machine that holds the vault"
                    },
                    "threshold": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "status",
                    "holders",
                    "holders_with_share",
                    "stay_unsealed",
                    "progress",
                    "active",
                    "rekey_needed"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "agent": []
          }
        ]
      },
      "post": {
        "tags": [
          "Vault"
        ],
        "summary": "Make the workspace's vault: the machine's agent runs the ceremony and encrypts every share to its holder's devices.",
        "description": "An owner's session. holders one address per share, in share order, and shares the number of holders (1 to 10; a spec where they differ is refused); threshold 1 to shares. One key is shares 1, threshold 1, holders the maker's address (or nothing at all). The maker is always a holder. Every holder is an active member of the workspace with at least one published device key (PUT /v1/me/keys); a holder without one is named in the 400 (has not opened the app yet). target_id is the machine (empty: the workspace's online compose machine). The answer comes at once with status making; the agent initialises the vault, encrypts share i to every device of holder i in its own memory, sets the vault up with the root token, revokes it, seals the vault unless stay_unsealed, and posts only the shares' ciphertext. The root token is never kept by anyone: break glass is a new root from the shares (generate-root), the holders' act. Each holder then reads theirs from GET /v1/vault/keys/mine. The console never holds a key.",
        "operationId": "makeVault",
        "requestBody": {
          "description": "how it is keyed",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "holders": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "owner@example.com",
                      "ana@example.com",
                      "owner@example.com"
                    ]
                  },
                  "shares": {
                    "type": "integer",
                    "example": 3
                  },
                  "stay_unsealed": {
                    "type": "boolean",
                    "description": "leave the vault unsealed after the ceremony, until its next restart"
                  },
                  "target_id": {
                    "type": "string",
                    "example": "tg_1"
                  },
                  "threshold": {
                    "type": "integer",
                    "example": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "being made",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "active": {
                      "type": "boolean",
                      "description": "unsealed and serving"
                    },
                    "holders": {
                      "type": "array",
                      "description": "every device a share was encrypted to, with its holder; empty before a vault",
                      "items": {
                        "type": "object",
                        "properties": {
                          "device": {
                            "type": "string",
                            "description": "a device their share was encrypted to at the ceremony",
                            "example": "Novid's MacBook Pro"
                          },
                          "email": {
                            "type": "string",
                            "example": "owner@example.com"
                          },
                          "has_share": {
                            "type": "boolean",
                            "description": "they said I have kept it on this device for every share of theirs encrypted to it"
                          }
                        },
                        "required": [
                          "email",
                          "device",
                          "has_share"
                        ]
                      }
                    },
                    "holders_with_share": {
                      "type": "integer",
                      "description": "how many holders have kept every share of theirs on at least one device"
                    },
                    "last_unsealed_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "last_unsealed_by": {
                      "type": "string"
                    },
                    "machine": {
                      "type": "string",
                      "description": "that machine by provider and place (\"Hetzner in Falkenstein\"), never its name"
                    },
                    "made_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "made_by": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "key",
                        "shares"
                      ]
                    },
                    "progress": {
                      "type": "integer",
                      "description": "shares given since the vault sealed"
                    },
                    "rekey_needed": {
                      "type": "boolean",
                      "description": "the holders must change: a holder left the workspace"
                    },
                    "rekey_reason": {
                      "type": "string"
                    },
                    "shares": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "none",
                        "making",
                        "sealed",
                        "unsealed",
                        "error",
                        "removing"
                      ]
                    },
                    "stay_unsealed": {
                      "type": "boolean",
                      "description": "the maker left it unsealed after the ceremony, until its next restart"
                    },
                    "target_id": {
                      "type": "string",
                      "description": "the machine that holds the vault"
                    },
                    "threshold": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "status",
                    "holders",
                    "holders_with_share",
                    "stay_unsealed",
                    "progress",
                    "active",
                    "rekey_needed"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a bad spec; the maker not among the holders; a holder who is not an active member or has not opened the app yet; §10's mode (the CLI from before the ceremony)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not an owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "a vault exists; no machine to put it on; the failed one is being taken off first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "vaults are not switched on here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vault/keys/mine": {
      "get": {
        "tags": [
          "Vault"
        ],
        "summary": "The caller's own share ciphertexts not kept yet: one per device they were encrypted to.",
        "description": "A person's session. The app opens each with the device's private key and shows it once; the console cannot read them. Empty once everything is kept.",
        "operationId": "myVaultKeys",
        "responses": {
          "200": {
            "description": "the rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "ciphertext": {
                        "type": "string",
                        "description": "an age file in ASCII armor to that device's public key; absent once kept",
                        "example": "-----BEGIN AGE ENCRYPTED FILE-----\n…\n-----END AGE ENCRYPTED FILE-----"
                      },
                      "collected_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "device": {
                        "type": "string",
                        "description": "the device whose public key it is encrypted to"
                      },
                      "id": {
                        "type": "integer"
                      },
                      "index": {
                        "type": "integer",
                        "description": "the share's number, 1 to shares"
                      },
                      "kind": {
                        "type": "string",
                        "description": "always a share: the root token is revoked on the machine after the setup and never kept; break glass is generate-root from the shares",
                        "enum": [
                          "share"
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "kind",
                      "index",
                      "device",
                      "created_at"
                    ]
                  }
                }
              }
            }
          },
          "404": {
            "description": "no vault",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vault/keys/{id}/kept": {
      "post": {
        "tags": [
          "Vault"
        ],
        "summary": "I have kept it: the row is marked collected and its ciphertext deleted.",
        "description": "A person's session, their own row only. The vault's page then counts them as having their share. Written to the activity without the value.",
        "operationId": "keptVaultKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the row, kept",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ciphertext": {
                      "type": "string",
                      "description": "an age file in ASCII armor to that device's public key; absent once kept",
                      "example": "-----BEGIN AGE ENCRYPTED FILE-----\n…\n-----END AGE ENCRYPTED FILE-----"
                    },
                    "collected_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "device": {
                      "type": "string",
                      "description": "the device whose public key it is encrypted to"
                    },
                    "id": {
                      "type": "integer"
                    },
                    "index": {
                      "type": "integer",
                      "description": "the share's number, 1 to shares"
                    },
                    "kind": {
                      "type": "string",
                      "description": "always a share: the root token is revoked on the machine after the setup and never kept; break glass is generate-root from the shares",
                      "enum": [
                        "share"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "kind",
                    "index",
                    "device",
                    "created_at"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not the caller's, or kept already",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vault/unseal": {
      "post": {
        "tags": [
          "Vault"
        ],
        "summary": "Give the sealed vault one share, encrypted to its machine's key and signed by your device; nuved relays it unread.",
        "description": "A holder's session. share is an age ciphertext to the vault's machine's public_key (GET /v1/targets); signature is the device's Ed25519 key over org|share|timestamp (timestamp RFC 3339 UTC, within five minutes). The console forwards it to the machine's agent, which checks the signature against the holder's published keys, decrypts it and unseals; the console never reads it. A plaintext share (§10's key) is refused. Five attempts a minute per workspace. A vault on a machine nuved does not reach (a Mac's own, a LAN machine) is unsealed by the app directly on its network.",
        "operationId": "unsealVault",
        "requestBody": {
          "description": "the encrypted share",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "share": {
                    "type": "string",
                    "description": "age ciphertext in ASCII armor to the machine's public key"
                  },
                  "signature": {
                    "type": "string",
                    "description": "standard base64 of the Ed25519 signature over org|share|timestamp"
                  },
                  "timestamp": {
                    "type": "string",
                    "format": "date-time"
                  }
                },
                "required": [
                  "share",
                  "timestamp",
                  "signature"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the vault's progress",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "active": {
                      "type": "boolean"
                    },
                    "progress": {
                      "type": "integer"
                    },
                    "sealed": {
                      "type": "boolean"
                    },
                    "threshold": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "sealed",
                    "progress",
                    "threshold",
                    "active"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a plaintext share, or not a ciphertext with a signature; the machine could not decrypt it or the vault refused it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the signature does not verify against the caller's published keys, or the caller is not a holder",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "no vault",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the vault is not in a state a share can be given to, or its machine is not reachable through nuved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "more than five attempts in a minute",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vpn/peers": {
      "get": {
        "tags": [
          "VPN"
        ],
        "summary": "The caller's VPN devices, connected or when last seen, with their traffic and until when they work.",
        "description": "Every signed-in person of the workspace the VPN is for has their own devices; an owner of the workspace sees everybody's, each with its owner. The feature does not exist for anybody else, who is answered 404.",
        "operationId": "listVPNPeers",
        "responses": {
          "200": {
            "description": "the devices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "peers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "blocked_reason": {
                            "type": "string",
                            "description": "why a blocked device was blocked; empty otherwise",
                            "example": ""
                          },
                          "connected": {
                            "type": "boolean",
                            "description": "the last handshake was under 180 seconds ago",
                            "example": true
                          },
                          "created_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "when the device was added; null for one from before the VPN server kept it (2026-10-21)",
                            "example": "2026-10-20T13:40:00Z"
                          },
                          "endpoint": {
                            "type": "string",
                            "description": "where the device last came from; empty when never",
                            "example": "[2a00:20:6403:f2ed::1]:56951"
                          },
                          "endpoint_family": {
                            "type": "string",
                            "example": "ipv6",
                            "enum": [
                              "ipv6",
                              "ipv4",
                              ""
                            ]
                          },
                          "expires_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "when the device's key stops working: a device in use renews itself (nuved-vpn docs/API.md), so it stops here only if it is not used again in time",
                            "example": "2026-11-19T13:40:00Z"
                          },
                          "id": {
                            "type": "string",
                            "description": "the device on the VPN server, unique there: what renew and remove take",
                            "example": "phone-c8cd3c"
                          },
                          "last_handshake": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "the device's last handshake, kept across a restart of the VPN server; null when never",
                            "example": "2026-10-20T13:40:02Z"
                          },
                          "name": {
                            "type": "string",
                            "description": "its owner's own name for it; two people may both have a phone",
                            "example": "phone"
                          },
                          "owner": {
                            "type": "string",
                            "description": "the address of the person who added it",
                            "example": "owner@example.com"
                          },
                          "rx_bytes": {
                            "type": "integer",
                            "example": 48211
                          },
                          "status": {
                            "type": "string",
                            "example": "active",
                            "enum": [
                              "active",
                              "blocked",
                              "expired"
                            ]
                          },
                          "stops_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "when the device stops if it is not used again, by the VPN server's own renewal rule and settings; null for a blocked one or one from before leases, which do not stop by themselves",
                            "example": "2026-12-12T13:40:00Z"
                          },
                          "tunnel_ipv4": {
                            "type": "string",
                            "example": "10.66.66.2"
                          },
                          "tunnel_ipv6": {
                            "type": "string",
                            "example": "fd66:66:66::2"
                          },
                          "tx_bytes": {
                            "type": "integer",
                            "example": 6021
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "tunnel_ipv4",
                          "tunnel_ipv6",
                          "connected",
                          "last_handshake",
                          "endpoint",
                          "endpoint_family",
                          "rx_bytes",
                          "tx_bytes",
                          "owner",
                          "status",
                          "blocked_reason",
                          "expires_at"
                        ]
                      }
                    }
                  },
                  "required": [
                    "peers"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not a person of the workspace the VPN is for, an agent token, or no VPN on this installation; on a device, one that is not the caller's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the VPN server refused the console, or answered something the contract does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the VPN server is not answering",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "tags": [
          "VPN"
        ],
        "summary": "Add a device: the answer is its config, the only time it exists. Only the nuved app's own session may ask.",
        "description": "The config carries the device's private key; neither the console nor the VPN server keeps it. A lost config is a device removed and added again. The device's owner is the caller, and the most devices is per person. A VPN device is made by the nuved app on the person's own Mac and by nothing else (2026-11-09): the session must be the app's, which the console marks when the app's device sign-in (POST /auth/device, started as the desktop) mints it, or a switch from such a session does. A browser's session, the CLI's and any other is answered 403 with `code: app_only`, and the VPN server is asked nothing. An app signed in before the mark existed signs out and in once. Listing and removing devices stay open to every session of the person; neither carries a key.",
        "operationId": "addVPNPeer",
        "requestBody": {
          "description": "the device",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "endpoint_family": {
                    "type": "string",
                    "description": "how the device reaches the server; ipv6 when left out",
                    "example": "ipv6",
                    "enum": [
                      "ipv6",
                      "ipv4"
                    ]
                  },
                  "name": {
                    "type": "string",
                    "description": "lower-case letters, digits and dashes, 1 to 25: the caller's own, another person's phone is no clash",
                    "example": "phone"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the device's config",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "config": {
                      "type": "string",
                      "description": "the client's .conf text, private key included"
                    },
                    "id": {
                      "type": "string",
                      "example": "phone-c8cd3c"
                    },
                    "name": {
                      "type": "string",
                      "example": "phone"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "config"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "a name that is not 1 to 25 lower-case letters, digits or dashes, or a family the VPN server does not take",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "the session is not the nuved app's (`code` is `app_only`: `A VPN device is made by the nuved app on your Mac. Open the app and turn the VPN on there; if it says it cannot, sign out of the app and sign in again.`), or the caller has every device a person may have (no `code`): `You already have 3 devices, the most allowed; remove one first.`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "present on the app-only refusal only",
                      "example": "app_only",
                      "enum": [
                        "app_only"
                      ]
                    },
                    "error": {
                      "type": "string",
                      "example": "A VPN device is made by the nuved app on your Mac. Open the app and turn the VPN on there; if it says it cannot, sign out of the app and sign in again."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not a person of the workspace the VPN is for, an agent token, or no VPN on this installation; on a device, one that is not the caller's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the caller has a device by that name, one made before names were per person included: `A device with this name already exists.`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the VPN server refused the console, or answered something the contract does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the VPN server is not answering",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vpn/peers/{id}": {
      "delete": {
        "tags": [
          "VPN"
        ],
        "summary": "Remove a device: its config stops working. The caller's own, or any for an owner of the workspace.",
        "operationId": "removeVPNPeer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the device's id, as GET /v1/vpn/peers lists it",
            "schema": {
              "type": "string",
              "example": "phone-c8cd3c"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "removed"
          },
          "404": {
            "description": "not a person of the workspace the VPN is for, an agent token, or no VPN on this installation; on a device, one that is not the caller's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the VPN server refused the console, or answered something the contract does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the VPN server is not answering",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vpn/peers/{id}/renew": {
      "post": {
        "tags": [
          "VPN"
        ],
        "summary": "Let a device's key work longer. The caller's own, or any for an owner of the workspace.",
        "description": "From now, for `days` or the VPN server's default; an expiry is never brought closer. Since 2026-10-21 a device in use renews itself (the VPN server, under 7 days left and used within its idle days), so nothing in the product calls this; it stays for owner tools.",
        "operationId": "renewVPNPeer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the device's id, as GET /v1/vpn/peers lists it",
            "schema": {
              "type": "string",
              "example": "phone-c8cd3c"
            }
          }
        ],
        "requestBody": {
          "description": "how long, or nothing for the default",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "integer",
                    "example": 30
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the device, with its new expiry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "blocked_reason": {
                      "type": "string",
                      "description": "why a blocked device was blocked; empty otherwise",
                      "example": ""
                    },
                    "connected": {
                      "type": "boolean",
                      "description": "the last handshake was under 180 seconds ago",
                      "example": true
                    },
                    "created_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the device was added; null for one from before the VPN server kept it (2026-10-21)",
                      "example": "2026-10-20T13:40:00Z"
                    },
                    "endpoint": {
                      "type": "string",
                      "description": "where the device last came from; empty when never",
                      "example": "[2a00:20:6403:f2ed::1]:56951"
                    },
                    "endpoint_family": {
                      "type": "string",
                      "example": "ipv6",
                      "enum": [
                        "ipv6",
                        "ipv4",
                        ""
                      ]
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "when the device's key stops working: a device in use renews itself (nuved-vpn docs/API.md), so it stops here only if it is not used again in time",
                      "example": "2026-11-19T13:40:00Z"
                    },
                    "id": {
                      "type": "string",
                      "description": "the device on the VPN server, unique there: what renew and remove take",
                      "example": "phone-c8cd3c"
                    },
                    "last_handshake": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "the device's last handshake, kept across a restart of the VPN server; null when never",
                      "example": "2026-10-20T13:40:02Z"
                    },
                    "name": {
                      "type": "string",
                      "description": "its owner's own name for it; two people may both have a phone",
                      "example": "phone"
                    },
                    "owner": {
                      "type": "string",
                      "description": "the address of the person who added it",
                      "example": "owner@example.com"
                    },
                    "rx_bytes": {
                      "type": "integer",
                      "example": 48211
                    },
                    "status": {
                      "type": "string",
                      "example": "active",
                      "enum": [
                        "active",
                        "blocked",
                        "expired"
                      ]
                    },
                    "stops_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "when the device stops if it is not used again, by the VPN server's own renewal rule and settings; null for a blocked one or one from before leases, which do not stop by themselves",
                      "example": "2026-12-12T13:40:00Z"
                    },
                    "tunnel_ipv4": {
                      "type": "string",
                      "example": "10.66.66.2"
                    },
                    "tunnel_ipv6": {
                      "type": "string",
                      "example": "fd66:66:66::2"
                    },
                    "tx_bytes": {
                      "type": "integer",
                      "example": 6021
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "tunnel_ipv4",
                    "tunnel_ipv6",
                    "connected",
                    "last_handshake",
                    "endpoint",
                    "endpoint_family",
                    "rx_bytes",
                    "tx_bytes",
                    "owner",
                    "status",
                    "blocked_reason",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "days the VPN server does not take",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not a person of the workspace the VPN is for, an agent token, or no VPN on this installation; on a device, one that is not the caller's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "the device is blocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the VPN server refused the console, or answered something the contract does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the VPN server is not answering",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/v1/vpn/server": {
      "get": {
        "tags": [
          "VPN"
        ],
        "summary": "Where devices reach the VPN server, by family, so a device whose config names one can try the other.",
        "description": "The VPN server's own values, the ones every config it hands out is written with: IPv6 first, each address as a config's `Endpoint =` line has it. For the same people as the devices; 503 while the VPN server does not say them.",
        "operationId": "getVPNServer",
        "responses": {
          "200": {
            "description": "the addresses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "address": {
                            "type": "string",
                            "description": "host and port, IPv6 in brackets",
                            "example": "[2a01:4f8:1c18:3823::1]:51830"
                          },
                          "family": {
                            "type": "string",
                            "example": "ipv6",
                            "enum": [
                              "ipv6",
                              "ipv4"
                            ]
                          }
                        },
                        "required": [
                          "family",
                          "address"
                        ]
                      }
                    }
                  },
                  "required": [
                    "endpoints"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not a person of the workspace the VPN is for, an agent token, or no VPN on this installation; on a device, one that is not the caller's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "the VPN server refused the console, or answered something the contract does not have",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "the VPN server is not answering, or does not say its addresses yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/webhook": {
      "post": {
        "tags": [
          "Billing webhook"
        ],
        "summary": "Stripe's notification that a payment completed or was disputed. Credits the balance and provisions the tenant.",
        "description": "The signature is verified against the raw body before anything is read. Idempotent twice over: by Stripe's event id, and by the ledger's unique reference. checkout.session.completed and checkout.session.async_payment_succeeded credit a top-up (and an invite credit on an invited workspace's first one); charge.dispute.created on that first top-up takes the inviter's credit back.",
        "operationId": "stripeWebhook",
        "responses": {
          "200": {
            "description": "handled, or deliberately ignored"
          },
          "400": {
            "description": "the signature did not verify"
          },
          "500": {
            "description": "the credit failed; Stripe should deliver it again"
          }
        },
        "security": [
          {
            "stripeSignature": []
          }
        ]
      }
    },
    "/welcome": {
      "get": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Where an invitation lands: join the workspace, download nuved for Mac, open it and sign in.",
        "description": "After the confirmation link and after a sign-in with an invitation not joined yet. Signed out, it offers the sign-in; invited, the Join; in the workspace, the download and where the app's first sign-in opens.",
        "operationId": "welcome",
        "parameters": [
          {
            "name": "org",
            "in": "query",
            "required": true,
            "description": "the workspace the invitation is to",
            "schema": {
              "type": "string",
              "example": "growgoddess"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the three steps, at the one the person is on",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no such workspace in the link, or no invitation to it for the person signed in",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/welcome/join": {
      "post": {
        "tags": [
          "Sign-in",
          "Browser"
        ],
        "summary": "Join the workspace an invitation is to, by the person's own click.",
        "description": "The invitation becomes a place, the workspace's Activity says who came in, and the browser's session moves there. With no session, the one-time proof the sign-in put on the page stands for the address.",
        "operationId": "welcomeJoin",
        "requestBody": {
          "description": "the form the welcome page posts",
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "with a proof: the address the sign-in verified"
                  },
                  "org": {
                    "type": "string",
                    "example": "growgoddess"
                  },
                  "proof": {
                    "type": "string",
                    "description": "the one-time proof from the page, when there is no session"
                  }
                },
                "required": [
                  "org"
                ]
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "the welcome page again, signed in to the workspace",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "no session and no proof, or a spent one",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "no invitation to that workspace for the address",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "too many attempts",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/workspaces": {
      "post": {
        "tags": [
          "Browser"
        ],
        "summary": "Create a project from the console's form; the no-JavaScript path of POST /v1/projects.",
        "operationId": "createWorkspaceForm",
        "requestBody": {
          "description": "name and caps",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "cpu": {
                    "type": "string"
                  },
                  "mem": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "services": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "/console",
            "headers": {
              "Location": {
                "description": "where the browser goes next",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "a cap outside its range, or a name that is not a name"
          },
          "402": {
            "description": "the balance is used up"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Access": {
        "type": "object",
        "description": "what a person reaches: the whole workspace, or some of its projects",
        "properties": {
          "projects": {
            "type": "array",
            "description": "scope projects: what they reach, and the role on each; empty for the workspace",
            "items": {
              "$ref": "#/components/schemas/ProjectRole"
            }
          },
          "role": {
            "type": "string",
            "description": "scope organization only, when known: the workspace role",
            "example": "member",
            "enum": [
              "owner",
              "member"
            ]
          },
          "scope": {
            "type": "string",
            "example": "projects",
            "enum": [
              "organization",
              "projects"
            ]
          }
        },
        "required": [
          "scope",
          "projects"
        ]
      },
      "Alerts": {
        "type": "object",
        "description": "the spend alerts; null is that alert off",
        "properties": {
          "days_left": {
            "type": [
              "integer",
              "null"
            ],
            "description": "write when fewer days than this are left at the current rate",
            "example": 3
          },
          "floor": {
            "type": "string",
            "description": "the floor as a person reads it; empty when it is off",
            "example": "€5.00"
          },
          "floor_micro": {
            "type": [
              "integer",
              "null"
            ],
            "description": "write when the balance falls under this many micro-euros",
            "example": 5000000
          }
        },
        "required": [
          "days_left",
          "floor_micro",
          "floor"
        ]
      },
      "AutoTopUp": {
        "type": "object",
        "description": "a workspace's automatic top-ups",
        "properties": {
          "amount_kind": {
            "type": "string",
            "example": "euros",
            "enum": [
              "euros",
              "days"
            ]
          },
          "amount_now": {
            "type": "string",
            "example": "€25.00"
          },
          "amount_value": {
            "type": "integer",
            "example": 25
          },
          "banner": {
            "type": "string",
            "description": "a problem's first line: what went wrong; empty when nothing did",
            "example": "The €25.00 top-up on 1 Oct was declined: not enough money on the card."
          },
          "card": {
            "type": "string",
            "description": "the saved card; empty when there is none",
            "example": "Visa ending 4242"
          },
          "changed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "changed_by": {
            "type": "string",
            "example": "sara@acme.dev"
          },
          "enabled": {
            "type": "boolean",
            "example": true
          },
          "fail_limit": {
            "type": "integer",
            "description": "declines in a row after which they stop until another card is saved",
            "example": 3
          },
          "fail_streak": {
            "type": "integer",
            "description": "automatic top-ups declined in a row",
            "example": 1
          },
          "hourly_now": {
            "type": "string",
            "example": "€0.058"
          },
          "hourly_now_micro": {
            "type": "integer",
            "description": "today's spend an hour in micro-euros; 0 when it cannot be read",
            "example": 58000
          },
          "last": {
            "type": [
              "object",
              "null"
            ],
            "description": "the most recent automatic top-up, whatever came of it",
            "properties": {
              "amount": {
                "type": "string",
                "example": "€25.00"
              },
              "amount_micro": {
                "type": "integer",
                "example": 25000000
              },
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "reason": {
                "type": "string",
                "description": "why it failed, in plain words"
              },
              "receipt_url": {
                "type": "string",
                "description": "Stripe's receipt, on the last paid one only"
              },
              "status": {
                "type": "string",
                "example": "paid",
                "enum": [
                  "pending",
                  "paid",
                  "failed",
                  "needs_action"
                ]
              }
            }
          },
          "last_paid": {
            "type": [
              "object",
              "null"
            ],
            "description": "the most recent paid automatic top-up, with its receipt",
            "properties": {
              "amount": {
                "type": "string",
                "example": "€25.00"
              },
              "amount_micro": {
                "type": "integer",
                "example": 25000000
              },
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "reason": {
                "type": "string",
                "description": "why it failed, in plain words"
              },
              "receipt_url": {
                "type": "string",
                "description": "Stripe's receipt, on the last paid one only"
              },
              "status": {
                "type": "string",
                "example": "paid",
                "enum": [
                  "pending",
                  "paid",
                  "failed",
                  "needs_action"
                ]
              }
            }
          },
          "limit_in_force": {
            "type": "string",
            "example": "€100.00"
          },
          "limit_in_force_micro": {
            "type": "integer",
            "description": "today's limit: lower than monthly_limit_micro while a raise waits",
            "example": 100000000
          },
          "mandate": {
            "type": "string",
            "description": "what the person agreed to when the card was saved"
          },
          "month": {
            "type": "string",
            "example": "September"
          },
          "month_full": {
            "type": "boolean",
            "description": "the next top-up would pass the limit in force: none is charged until the month turns or a higher limit starts",
            "example": false
          },
          "month_used": {
            "type": "string",
            "example": "€50.00"
          },
          "month_used_micro": {
            "type": "integer",
            "description": "automatic top-ups this month, in flight and paid",
            "example": 50000000
          },
          "monthly_limit": {
            "type": "string",
            "example": "€100.00"
          },
          "monthly_limit_micro": {
            "type": "integer",
            "description": "the limit set, a waiting raise included",
            "example": 100000000
          },
          "next_try_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when a declined top-up is tried once more; null when no retry is coming"
          },
          "pill": {
            "type": "string",
            "example": "On"
          },
          "raise_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when a higher limit takes effect; null when none waits"
          },
          "standing": {
            "type": "string",
            "description": "the balance and how long it lasts, for a problem's second line",
            "example": "Balance €1.90, about 5 days."
          },
          "state": {
            "type": "string",
            "example": "on",
            "enum": [
              "off",
              "on",
              "declined",
              "not_charged",
              "card",
              "needs_you",
              "stopped"
            ]
          },
          "threshold_kind": {
            "type": "string",
            "example": "days",
            "enum": [
              "euros",
              "days"
            ]
          },
          "threshold_now": {
            "type": "string",
            "description": "the line at today's spend; empty when the spend cannot be read",
            "example": "€4.18"
          },
          "threshold_value": {
            "type": "integer",
            "example": 3
          }
        },
        "required": [
          "enabled",
          "threshold_kind",
          "threshold_value",
          "amount_kind",
          "amount_value",
          "monthly_limit_micro",
          "monthly_limit",
          "limit_in_force_micro",
          "limit_in_force",
          "raise_at",
          "card",
          "threshold_now",
          "amount_now",
          "hourly_now",
          "hourly_now_micro",
          "month",
          "month_used_micro",
          "month_used",
          "month_full",
          "state",
          "pill",
          "banner",
          "standing",
          "fail_streak",
          "fail_limit",
          "next_try_at",
          "last",
          "last_paid",
          "changed_by",
          "changed_at",
          "mandate"
        ]
      },
      "BypassToken": {
        "type": "object",
        "description": "a token that opens the project's protected addresses; never the token itself",
        "properties": {
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-27T10:00:00Z"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when it stops working; null for never"
          },
          "id": {
            "type": "integer",
            "example": 41
          },
          "last_opened": {
            "type": [
              "object",
              "null"
            ],
            "description": "the address it opened last, from the edge's access log; null when it opened none in 90 days",
            "properties": {
              "at": {
                "type": "string",
                "format": "date-time",
                "description": "the first pass of that hour",
                "example": "2026-10-27T11:00:00Z"
              },
              "host": {
                "type": "string",
                "example": "jaeger-tracing.nuved.app"
              }
            },
            "required": [
              "host",
              "at"
            ]
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "its last use, at most a minute old; null for never"
          },
          "minted_by": {
            "type": "string",
            "description": "the e-mail of the person who made it; empty for one made before makers were recorded",
            "example": "owner@example.com"
          },
          "name": {
            "type": "string",
            "description": "what uses it",
            "example": "playwright"
          },
          "scopes": {
            "type": "array",
            "description": "open, and anything else it may do",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "code",
                "deploy",
                "logs",
                "artifacts",
                "secrets",
                "open"
              ]
            }
          }
        },
        "required": [
          "id",
          "name",
          "minted_by",
          "created_at",
          "expires_at",
          "last_used_at",
          "last_opened",
          "scopes"
        ]
      },
      "BypassTokenList": {
        "type": "object",
        "description": "a project's bypass tokens, newest first",
        "properties": {
          "tokens": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BypassToken"
            }
          }
        },
        "required": [
          "tokens"
        ]
      },
      "Caps": {
        "type": "object",
        "description": "what a project may use: cpu as a Kubernetes quantity like 500m or 2, and mem like 1Gi",
        "properties": {
          "cpu": {
            "type": "string",
            "description": "vCPU cap, 0.25 to 8",
            "example": "500m"
          },
          "mem": {
            "type": "string",
            "description": "memory cap, 0.25Gi to 32Gi",
            "example": "1Gi"
          }
        }
      },
      "CommitDomains": {
        "type": "object",
        "description": "the domains a workspace accepts commit addresses under; empty for any verified address",
        "properties": {
          "domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "acme.com"
            ]
          }
        },
        "required": [
          "domains"
        ]
      },
      "CommitIdentity": {
        "type": "object",
        "description": "a name and an address a commit is authored as",
        "properties": {
          "email": {
            "type": "string",
            "example": "dev@acme.com"
          },
          "name": {
            "type": "string",
            "example": "Dev Example"
          }
        },
        "required": [
          "name",
          "email"
        ]
      },
      "Domain": {
        "type": "object",
        "description": "one of a service's own hostnames (a custom domain)",
        "properties": {
          "cname_target": {
            "type": "string",
            "description": "where the hostname's CNAME points",
            "example": "customers.nuved.app"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-17T12:00:00Z"
          },
          "hostname": {
            "type": "string",
            "example": "shop.example.com"
          },
          "message": {
            "type": "string",
            "description": "what the state means for the customer, and what to do",
            "example": "Add a CNAME record for shop.example.com pointing to customers.nuved.app at your DNS provider."
          },
          "public": {
            "type": "boolean",
            "description": "true: anybody may open it; false: the workspace's people, after the sign-in",
            "example": false
          },
          "status": {
            "type": "string",
            "description": "pending_dns: the CNAME is not there yet; issuing: it is, the certificate is on its way; active: served; failed: see message",
            "example": "pending_dns",
            "enum": [
              "pending_dns",
              "issuing",
              "active",
              "failed"
            ]
          },
          "verify": {
            "$ref": "#/components/schemas/DomainVerify"
          }
        },
        "required": [
          "hostname",
          "public",
          "status",
          "message",
          "cname_target",
          "created_at",
          "verify"
        ]
      },
      "DomainConflict": {
        "type": "object",
        "description": "a hostname that cannot be added as it stands",
        "properties": {
          "code": {
            "type": "string",
            "description": "taken: another workspace holds the hostname; it moves here once the record in verify matches. too_many_claims: prove or remove a waiting hostname first",
            "example": "taken",
            "enum": [
              "taken",
              "too_many_claims"
            ]
          },
          "error": {
            "type": "string",
            "example": "that hostname is already connected to a service"
          },
          "verify": {
            "$ref": "#/components/schemas/DomainVerify"
          }
        },
        "required": [
          "error"
        ]
      },
      "DomainVerify": {
        "type": "object",
        "description": "the DNS record that proves a hostname the customer's; nothing is served until it matches",
        "properties": {
          "name": {
            "type": "string",
            "example": "_nuved-verify.shop.example.com"
          },
          "type": {
            "type": "string",
            "example": "TXT",
            "enum": [
              "TXT"
            ]
          },
          "value": {
            "type": "string",
            "example": "nuved-verify=q3k6v2x7m4c5b8n9d2f4g6h7j8k9l2m3"
          }
        },
        "required": [
          "type",
          "name",
          "value"
        ]
      },
      "Domains": {
        "type": "object",
        "description": "a service's own hostnames",
        "properties": {
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Domain"
            }
          }
        },
        "required": [
          "domains"
        ]
      },
      "EdgeAccess": {
        "type": "object",
        "description": "somebody who passed the edge to one of the service's addresses",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-22T21:00:00Z"
          },
          "client": {
            "type": "string",
            "description": "the client's address",
            "example": "203.0.113.7"
          },
          "host": {
            "type": "string",
            "example": "forge-riegel.nuved.app"
          },
          "how": {
            "type": "string",
            "example": "token",
            "enum": [
              "session",
              "token",
              "guest",
              "open"
            ]
          },
          "path": {
            "type": "string",
            "description": "the first path asked, without its query",
            "example": "/riegel/firmware.git/info/refs"
          },
          "who": {
            "type": "string",
            "description": "an e-mail (a person signed in), `token:\u003cname\u003e` (an agent token), `guest:\u003ce-mail\u003e`, or open (an open path)",
            "example": "token:ci"
          }
        },
        "required": [
          "at",
          "host",
          "who",
          "how",
          "client",
          "path"
        ]
      },
      "EdgeAccessList": {
        "type": "object",
        "description": "who opened a service's addresses, newest first",
        "properties": {
          "access": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EdgeAccess"
            }
          }
        },
        "required": [
          "access"
        ]
      },
      "Entitlements": {
        "type": "object",
        "description": "the workspace's plan, where it stands, the numbers the relay enforces, and every reserved name; the same answer the relay reads (internal/plans testdata/entitlement.golden.json), except for a workspace that runs the platform, which the relay does not hold to its plan (GET /v1/plan, relay_limits_applied)",
        "properties": {
          "guests_per_share": {
            "type": "integer",
            "description": "invited addresses one share lets in",
            "example": 25
          },
          "hold_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the held reserved names are released; null when none are held",
            "example": "2026-11-20T14:30:00Z"
          },
          "live_shares": {
            "type": "integer",
            "description": "shares connected at once",
            "example": 10
          },
          "org": {
            "type": "string",
            "example": "novid"
          },
          "own_domain": {
            "type": "boolean",
            "description": "whether a share may answer on your own domain",
            "example": true
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro"
            ]
          },
          "reserved": {
            "type": "array",
            "description": "every reserved name, by host; empty when there is none",
            "items": {
              "type": "object",
              "properties": {
                "held_until": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "when a held name is released; null while active",
                  "example": "2026-11-20T14:30:00Z"
                },
                "host": {
                  "type": "string",
                  "description": "the whole name, as the relay wrote it",
                  "example": "novid-k3j5h2m4pq2x.nuved.dev"
                },
                "state": {
                  "type": "string",
                  "description": "held: kept, not answering, not released",
                  "enum": [
                    "active",
                    "held"
                  ]
                }
              },
              "required": [
                "host",
                "state",
                "held_until"
              ]
            }
          },
          "reserved_names": {
            "type": "integer",
            "description": "names kept whether or not a tunnel is connected",
            "example": 3
          },
          "status": {
            "type": "string",
            "description": "lapsed: on Free because the balance could not pay for Pro",
            "enum": [
              "active",
              "lapsed"
            ]
          },
          "traffic_bytes": {
            "type": "integer",
            "description": "fair-use traffic a month, in decimal bytes; over it shares pause until the next month or an upgrade, never deleted",
            "example": 50000000000
          }
        },
        "required": [
          "org",
          "plan",
          "status",
          "hold_until",
          "live_shares",
          "reserved_names",
          "guests_per_share",
          "traffic_bytes",
          "own_domain",
          "reserved"
        ]
      },
      "Error": {
        "type": "object",
        "description": "every refusal on this platform, whatever went wrong",
        "properties": {
          "code": {
            "type": "string",
            "description": "a stable name for the refusal, on the endpoints that give one (the copy to GitHub): the sentence is for people, this is for programs",
            "example": "agent_token"
          },
          "error": {
            "type": "string",
            "example": "no such project"
          }
        },
        "required": [
          "error"
        ]
      },
      "Event": {
        "type": "object",
        "description": "one thing that happened to a workspace",
        "properties": {
          "action": {
            "type": "string",
            "description": "what was done, in the past tense: the activity vocabulary (internal/activity)",
            "example": "applied",
            "enum": [
              "accepted",
              "adopted",
              "applied",
              "blocked",
              "cancelled",
              "changed",
              "checked",
              "claimed",
              "closed",
              "collected",
              "confirmed",
              "connected",
              "copied",
              "created",
              "credited",
              "declined",
              "deleted",
              "deployed",
              "disconnected",
              "drained",
              "exposed",
              "failed",
              "handed in",
              "imported",
              "invited",
              "limited",
              "made",
              "minted",
              "moved",
              "passed",
              "paused",
              "pinned",
              "published",
              "queued",
              "ran",
              "read",
              "received",
              "rejected",
              "released",
              "removed",
              "renamed",
              "reopened",
              "resumed",
              "retried",
              "reversed",
              "revoked",
              "rotated",
              "sent back",
              "set",
              "started",
              "stopped",
              "summarised",
              "topped up",
              "unadopted",
              "unblocked",
              "undrained",
              "unexposed",
              "unlimited",
              "unpinned",
              "unsealed"
            ]
          },
          "actor": {
            "type": "string",
            "description": "a person's address, an agent token's name, or billing or meter for the platform itself",
            "example": "claude-code"
          },
          "actor_kind": {
            "type": "string",
            "example": "agent",
            "enum": [
              "human",
              "agent",
              "system"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-19T14:07:02Z"
          },
          "detail": {
            "type": "string",
            "description": "the project, an amount, or why it was refused",
            "example": "novid-shop"
          },
          "infinitive": {
            "type": "string",
            "description": "the action after \"tried to\", which is how a refused row reads (\"Tried to disconnect repository/shop\"); empty for an action written before the vocabulary had it",
            "example": "apply"
          },
          "result": {
            "type": "string",
            "description": "refused: the kube proxy did not let it through, or the copy to GitHub was refused",
            "example": "ok",
            "enum": [
              "ok",
              "refused"
            ]
          },
          "target": {
            "type": "string",
            "description": "what it was done to, as kind/name",
            "example": "deployment/web"
          }
        },
        "required": [
          "at",
          "actor",
          "actor_kind",
          "action",
          "target",
          "result",
          "detail",
          "infinitive"
        ]
      },
      "Exposure": {
        "type": "object",
        "description": "where a service answers from outside, once the queued change is carried out",
        "properties": {
          "project": {
            "type": "string",
            "example": "testcorp-default"
          },
          "public": {
            "type": "boolean",
            "description": "anybody may open it, with no sign-in: a cluster's route always; an address of the platform's edge once the workspace's owner made it public (PATCH .../expose); false while the address is going",
            "example": false
          },
          "service": {
            "type": "string",
            "example": "hello"
          },
          "status": {
            "type": "string",
            "example": "exposing",
            "enum": [
              "exposing",
              "unexposing"
            ]
          },
          "url": {
            "type": "string",
            "description": "open this in a browser; empty when the address is going",
            "example": "https://hello-testcorp.nuved.io"
          }
        },
        "required": [
          "service",
          "project",
          "url",
          "status",
          "public"
        ]
      },
      "ExposureOnEdge": {
        "type": "object",
        "description": "a service's address at the platform's edge, and who may open it",
        "properties": {
          "open_paths": {
            "type": "array",
            "description": "what passes with no sign-in: git (git's smart-HTTP paths) and path prefixes; empty for none",
            "items": {
              "type": "string"
            },
            "example": [
              "git",
              "/api/"
            ]
          },
          "project": {
            "type": "string",
            "example": "riegel"
          },
          "public": {
            "type": "boolean",
            "description": "anybody may open it, with no sign-in",
            "example": false
          },
          "service": {
            "type": "string",
            "example": "forge"
          },
          "status": {
            "type": "string",
            "example": "exposing",
            "enum": [
              "exposing"
            ]
          },
          "url": {
            "type": "string",
            "example": "https://forge-riegel.nuved.app"
          }
        },
        "required": [
          "service",
          "project",
          "url",
          "status",
          "public",
          "open_paths"
        ]
      },
      "Flow": {
        "type": "object",
        "description": "a project's flow, as git-router enforces it; every sentence is written there",
        "properties": {
          "agent": {
            "type": "object",
            "description": "what an agent needs to work under the flow",
            "properties": {
              "branch": {
                "type": "string",
                "description": "the branch to push to; `\u003ctask\u003e` stands for the work",
                "example": "`agent/\u003ctask\u003e`"
              },
              "checks": {
                "type": "object",
                "properties": {
                  "enforced": {
                    "type": "boolean"
                  },
                  "path": {
                    "type": "string"
                  },
                  "ref": {
                    "type": "string"
                  },
                  "why": {
                    "type": "string"
                  }
                }
              },
              "merge": {
                "type": "object",
                "properties": {
                  "available": {
                    "type": "boolean"
                  },
                  "command": {
                    "type": "string"
                  },
                  "into": {
                    "type": "string"
                  },
                  "today": {
                    "type": "string"
                  }
                }
              },
              "next": {
                "type": "array",
                "description": "what to do, in order",
                "items": {
                  "type": "string"
                }
              },
              "push": {
                "type": "object",
                "properties": {
                  "allowed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "never": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "signed": {
                "type": "boolean"
              },
              "signing": {
                "type": "string"
              },
              "told": {
                "type": "string",
                "description": "what a refused agent push ends with, word for word: the flow, the exact command, the version"
              }
            }
          },
          "can_edit": {
            "type": "boolean"
          },
          "closest": {
            "type": "object",
            "description": "the named preset nearest to the rules in force, and what differs today, one sentence each",
            "properties": {
              "differences": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "example": [
                  "Tags aren't protected today."
                ]
              },
              "preset": {
                "type": "string",
                "example": "branch-gate",
                "enum": [
                  "direct",
                  "branch-gate",
                  "branch-dev-main"
                ]
              }
            }
          },
          "effective": {
            "type": "array",
            "description": "one sentence per rule; default marks the operator's default, which nobody set",
            "items": {
              "type": "object",
              "properties": {
                "default": {
                  "type": "boolean"
                },
                "rule": {
                  "type": "string",
                  "enum": [
                    "preset",
                    "agent_refs",
                    "route",
                    "protected_refs",
                    "person_https",
                    "signed_commits",
                    "agent_signing"
                  ]
                },
                "sentence": {
                  "type": "string",
                  "example": "Agents push to feature/* and agent/*; they may not write main, dev, release/* and tags."
                }
              }
            }
          },
          "person": {
            "type": "object",
            "description": "what a person is told when a push is refused, word for word",
            "properties": {
              "https_told": {
                "type": "string",
                "description": "ends a refused https push to main or dev where the project keeps those for ssh; empty where it does not"
              },
              "told": {
                "type": "string",
                "description": "ends a refused push to a protected ref"
              }
            }
          },
          "preset": {
            "type": "string",
            "enum": [
              "direct",
              "branch-gate",
              "branch-dev-main",
              "advanced",
              "unset"
            ]
          },
          "presets": {
            "type": "array",
            "description": "what an owner picks from, in order",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "sentence": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                }
              }
            }
          },
          "project": {
            "type": "string",
            "example": "novid/blog"
          },
          "routes": {
            "type": "array",
            "description": "how work moves towards main through a merge, and who moves it: a person until the merge gate is built, then the gate",
            "items": {
              "type": "object",
              "properties": {
                "by": {
                  "type": "string",
                  "enum": [
                    "person",
                    "gate"
                  ]
                },
                "from": {
                  "type": "string",
                  "description": "agents (an agent's branches) or a branch",
                  "example": "dev"
                },
                "into": {
                  "type": "string",
                  "example": "main"
                },
                "sentence": {
                  "type": "string"
                }
              }
            }
          },
          "rules": {
            "type": "object",
            "properties": {
              "agent_refs": {
                "type": "array",
                "description": "what an agent's credential may write; feature/* is refs/heads/feature/*",
                "items": {
                  "type": "string"
                },
                "example": [
                  "refs/heads/feature/*",
                  "refs/heads/agent/*"
                ]
              },
              "agents_unrestricted": {
                "type": "boolean",
                "description": "the explicit yes for a list that covers every branch, or an empty one"
              },
              "merge_into": {
                "type": "string",
                "description": "where agents' branches merge; empty when agents push main",
                "enum": [
                  "main",
                  "dev",
                  ""
                ]
              },
              "person_https": {
                "type": "boolean",
                "description": "a person's https credential may write main and dev"
              },
              "protected_refs": {
                "type": "array",
                "description": "refs that only move forward",
                "items": {
                  "type": "string"
                }
              },
              "signed_commits": {
                "type": "boolean",
                "description": "a person's commits must be signed by their own key"
              }
            }
          },
          "version": {
            "type": "string",
            "description": "moves with every change; also the ETag",
            "example": "3"
          },
          "why_not": {
            "type": "string",
            "description": "why the caller may not change it; empty when they may"
          },
          "widenings": {
            "type": "array",
            "description": "on a preview only: each widening, as the sentence a confirm shows",
            "items": {
              "type": "string"
            }
          },
          "widens": {
            "type": "boolean",
            "description": "on a preview only: the change gives somebody more, or removes a protection"
          }
        },
        "required": [
          "project",
          "preset",
          "presets",
          "rules",
          "effective",
          "agent",
          "version",
          "can_edit",
          "why_not"
        ]
      },
      "FlowRefusal": {
        "type": "object",
        "description": "a flow change that was not made",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "invalid",
              "flow_changed",
              "no_project"
            ]
          },
          "error": {
            "type": "string",
            "description": "the sentence a person reads, with each field's sentence after it"
          },
          "fields": {
            "type": "object",
            "description": "one sentence per field that is wrong",
            "example": {
              "agent_refs": "Add at least one pattern, for example feature/*."
            }
          },
          "flow": {
            "type": "object",
            "description": "on a conflict: the flow as it is now"
          }
        },
        "required": [
          "error",
          "code"
        ]
      },
      "GitCredential": {
        "type": "object",
        "description": "what git's credential protocol needs, for one push over https",
        "properties": {
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-07T18:15:00Z"
          },
          "password": {
            "type": "string",
            "description": "the credential itself, shown once",
            "example": "nvd_person_…"
          },
          "username": {
            "type": "string",
            "description": "the person; git-router reads the password",
            "example": "support@nimanou.com"
          }
        },
        "required": [
          "username",
          "password",
          "expires_at"
        ]
      },
      "HistoryRow": {
        "type": "object",
        "description": "one line of the billing history",
        "properties": {
          "amount": {
            "type": "string",
            "example": "−€1.39"
          },
          "amount_micro": {
            "type": "integer",
            "description": "negative for money out, positive for money in",
            "example": -1390000
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-09-18"
          },
          "kind": {
            "type": "string",
            "description": "usage for a day of running services, topup for a payment, or another kind of credit",
            "example": "usage"
          },
          "receipt_url": {
            "type": "string",
            "description": "Stripe's receipt, for a top-up; empty otherwise",
            "example": ""
          },
          "what": {
            "type": "string",
            "example": "Running services"
          }
        },
        "required": [
          "date",
          "kind",
          "what",
          "amount_micro",
          "amount",
          "receipt_url"
        ]
      },
      "Invitation": {
        "type": "object",
        "description": "an invitation in flight, and when it stops working",
        "properties": {
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-16T08:00:00Z"
          },
          "email": {
            "type": "string",
            "example": "teammate@example.com"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "seven days after it was sent",
            "example": "2026-09-23T08:00:00Z"
          },
          "id": {
            "type": "integer",
            "description": "what DELETE /v1/invitations/{id} takes",
            "example": 3
          },
          "invited_by": {
            "type": "string",
            "example": "owner@example.com"
          },
          "kind": {
            "type": "string",
            "example": "member",
            "enum": [
              "member",
              "platform"
            ]
          },
          "project": {
            "type": "string",
            "description": "an invitation to exactly one project: that project; absent otherwise",
            "example": "novid-shop"
          },
          "projects": {
            "type": "array",
            "description": "the projects an invitation to some projects opens, and the role on each; empty for the whole workspace and for the platform",
            "items": {
              "$ref": "#/components/schemas/ProjectRole"
            }
          },
          "referral": {
            "type": "object",
            "description": "platform invitations only: how far the person has come, and whether it earned each side €5",
            "properties": {
              "credited": {
                "type": "boolean",
                "description": "€5 went to each side",
                "example": false
              },
              "signed_up": {
                "type": "boolean",
                "description": "they named a workspace of their own",
                "example": true
              },
              "topped_up": {
                "type": "boolean",
                "description": "that workspace has paid at least once",
                "example": true
              }
            },
            "required": [
              "signed_up",
              "topped_up",
              "credited"
            ]
          },
          "role": {
            "type": "string",
            "description": "with project: the role they will have there",
            "example": "viewer",
            "enum": [
              "member",
              "viewer"
            ]
          },
          "tenant": {
            "type": "string",
            "description": "the workspace they are invited to; empty for a platform invitation",
            "example": "acme"
          },
          "used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when somebody used it, in UTC; null while it is unused. Only an unused invitation can be taken back",
            "example": "2026-09-17T10:00:00Z"
          }
        },
        "required": [
          "id",
          "email",
          "kind",
          "expires_at",
          "created_at",
          "used_at",
          "projects"
        ]
      },
      "Key": {
        "type": "object",
        "description": "a registered SSH public key. It opens the ssh door for this workspace's repositories, and it is what a commit signature is checked against",
        "properties": {
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-22T09:10:00Z"
          },
          "fingerprint": {
            "type": "string",
            "example": "SHA256:B4haf4k6qHdTSy/bx3rbUJNxJrvJianmZYQdrKqUrik"
          },
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "mac"
          },
          "principal": {
            "type": "string",
            "description": "the address git matches a signature's signer identity against",
            "example": "support@nimanou.com"
          },
          "public_key": {
            "type": "string",
            "example": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA…"
          }
        },
        "required": [
          "id",
          "name",
          "principal",
          "fingerprint",
          "public_key",
          "created_at"
        ]
      },
      "LogLine": {
        "type": "object",
        "description": "one line a container wrote, as each data: event of the logs stream carries it",
        "properties": {
          "container": {
            "type": "string",
            "example": "api"
          },
          "level": {
            "type": "string",
            "description": "the first of these that appears as a word in the first 40 characters; INFO when none does",
            "example": "ERROR",
            "enum": [
              "ERROR",
              "WARN",
              "INFO",
              "DEBUG"
            ]
          },
          "line": {
            "type": "string",
            "description": "the text, cut at 16 KiB",
            "example": "POST /orders 500"
          },
          "pod": {
            "type": "string",
            "example": "api-5d4b9-k2p"
          },
          "t": {
            "type": "string",
            "format": "date-time",
            "description": "when the kubelet read it, to the nanosecond",
            "example": "2026-09-19T14:07:02Z"
          }
        },
        "required": [
          "t",
          "pod",
          "container",
          "level",
          "line"
        ]
      },
      "MailAddress": {
        "type": "object",
        "description": "one of a person's addresses",
        "properties": {
          "added_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when it was asked for; for the sign-in address, when the person first had a place in a workspace, null when no record says"
          },
          "email": {
            "type": "string",
            "description": "lower case",
            "example": "dev+nuved@example.com"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "while it waits: when the link in its letter stops working, 24 hours after it was sent; absent once verified"
          },
          "sign_in": {
            "type": "boolean",
            "description": "the address the person signs in with: listed first, and never removed"
          },
          "verified": {
            "type": "boolean",
            "description": "confirmed on the page its letter's link opens; always true for the sign-in address"
          }
        },
        "required": [
          "email",
          "verified",
          "sign_in",
          "added_at"
        ]
      },
      "Me": {
        "type": "object",
        "description": "who a credential belongs to, and what is left to spend",
        "properties": {
          "access": {
            "$ref": "#/components/schemas/Access",
            "description": "a session's only: what it reaches. For scope projects, balance is empty and balance_micro 0: the bill is the workspace's. Absent for an agent token"
          },
          "balance": {
            "type": "string",
            "description": "the balance, as a person reads it",
            "example": "€49.99"
          },
          "balance_micro": {
            "type": "integer",
            "description": "the same balance in micro-euros; money is never a float",
            "example": 49990000
          },
          "commit_addresses": {
            "type": "array",
            "description": "a session's only: the person's verified addresses this workspace accepts, lower case, the sign-in address first; a commit here carries one of these or git-plane refuses the push. Absent for an agent token",
            "items": {
              "type": "string"
            },
            "example": [
              "owner@example.com"
            ]
          },
          "commit_domains": {
            "type": "array",
            "description": "a session's only: the domains this workspace accepts commit addresses under; empty for any verified address. Absent for an agent token",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "commit_identity": {
            "type": [
              "object",
              "null"
            ],
            "description": "a session's only: the name and address the person's commits in this workspace carry: the identity their place chose (PUT /v1/me/commit-identity) while the workspace accepts it, or else the sign-in address with its part before the @ for a name, where the workspace accepts that; null when neither. Absent for an agent token",
            "properties": {
              "email": {
                "type": "string",
                "example": "owner@example.com"
              },
              "name": {
                "type": "string",
                "example": "owner"
              }
            },
            "required": [
              "name",
              "email"
            ]
          },
          "email": {
            "type": "string",
            "description": "who signed in: present for a session that carries an address, never for an agent token",
            "example": "owner@example.com"
          },
          "has_account": {
            "type": "boolean",
            "description": "the workspace has an account row; false before there is one, when balance is €0.00 for want of an account rather than of money",
            "example": true
          },
          "invites_on": {
            "type": "boolean",
            "description": "a session's only: invitations work on this installation (an identity provider and a mail server are configured). Absent for an agent token, which never learns about the people",
            "example": true
          },
          "kind": {
            "type": "string",
            "description": "a person's session, or one project's agent token",
            "example": "session",
            "enum": [
              "session",
              "agent"
            ]
          },
          "may_invite_platform": {
            "type": "boolean",
            "description": "a session's only: the caller may invite someone new to the platform, because they are an owner and the workspace is Nuved's own or has paid at least once; an invite credit does not count. Absent for an agent token",
            "example": false
          },
          "operator": {
            "type": "boolean",
            "description": "a session's only, and only when true: the workspace runs the platform, so the console shows it the operator's pages (Machines) in its sidebar. It decides nothing: each of those routes checks it again. Absent otherwise",
            "example": false
          },
          "org": {
            "type": "string",
            "description": "the workspace this credential belongs to",
            "example": "novid"
          },
          "org_name": {
            "type": "string",
            "description": "what the workspace is called, as GET /v1/orgs names it; its tenant when it was given none",
            "example": "GrowGoddess"
          },
          "paused": {
            "type": "boolean",
            "description": "services are scaled to zero because the balance ran out",
            "example": false
          },
          "project": {
            "type": "string",
            "description": "an agent token's project; empty for a session",
            "example": ""
          },
          "two_step_required": {
            "type": "boolean",
            "description": "present and true when this workspace requires two-step login and this session signed in without it: balance is empty, access and the rest are absent, and every route but the person's own answers 403 with code two_step_required. Absent otherwise",
            "example": true
          }
        },
        "required": [
          "org",
          "org_name",
          "kind",
          "project",
          "balance",
          "balance_micro",
          "paused",
          "has_account"
        ]
      },
      "Member": {
        "type": "object",
        "description": "one person who can sign in to a workspace",
        "properties": {
          "all_projects": {
            "type": "boolean",
            "description": "true for an owner or a member, who reach every project; false when projects is what they reach",
            "example": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-16T08:00:00Z"
          },
          "email": {
            "type": "string",
            "example": "teammate@example.com"
          },
          "has_key": {
            "type": "boolean",
            "description": "they published at least one device's keys (PUT /v1/me/keys), so a vault share can be encrypted to them; the vault's holder picker offers only them",
            "example": true
          },
          "invited_by": {
            "type": "string",
            "description": "who invited them; empty for the first owner",
            "example": "owner@example.com"
          },
          "projects": {
            "type": "array",
            "description": "the projects a person with project roles reaches, and the role on each; empty when all_projects",
            "items": {
              "$ref": "#/components/schemas/ProjectRole"
            }
          },
          "role": {
            "type": "string",
            "description": "an owner can invite and remove people; owner and member reach every project; project reaches only the projects listed",
            "example": "member",
            "enum": [
              "owner",
              "member",
              "project"
            ]
          },
          "status": {
            "type": "string",
            "description": "invited until they sign in for the first time",
            "example": "active",
            "enum": [
              "invited",
              "active"
            ]
          },
          "you": {
            "type": "boolean",
            "description": "this is the person holding the session that asked",
            "example": false
          }
        },
        "required": [
          "email",
          "role",
          "status",
          "created_at",
          "you"
        ]
      },
      "Plan": {
        "type": "object",
        "description": "the workspace's plan, what happens next and what it costs, the balance it is paid from, and every plan",
        "properties": {
          "balance": {
            "type": "string",
            "example": "€24.31"
          },
          "balance_micro": {
            "type": "integer",
            "example": 24310000
          },
          "covered": {
            "type": "boolean",
            "description": "whether the balance pays the next renewal (on Pro) or a subscription now (on Free)",
            "example": true
          },
          "downgrade_at_period_end": {
            "type": "boolean",
            "description": "a switch to Free asked for, carried out when the period ends",
            "example": false
          },
          "held_names": {
            "type": "array",
            "description": "reserved names held since Pro ended: not answering and not released",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "hold_days": {
            "type": "integer",
            "description": "how long reserved names are held after Pro ends, unpaid or switched off",
            "example": 14
          },
          "hold_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the held names are released; null when none are held",
            "example": "2026-11-20T14:30:00Z"
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits"
          },
          "org": {
            "type": "string",
            "example": "novid"
          },
          "period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when Pro renews, or ends if downgrade_at_period_end; null on Free",
            "example": "2026-11-06T14:30:00Z"
          },
          "period_start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the period paid for started; null on Free",
            "example": "2026-10-06T14:30:00Z"
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro"
            ]
          },
          "plans": {
            "type": "array",
            "description": "every plan, least first, with what it includes",
            "items": {
              "type": "object",
              "properties": {
                "limits": {
                  "$ref": "#/components/schemas/PlanLimits"
                },
                "name": {
                  "type": "string",
                  "enum": [
                    "free",
                    "pro"
                  ]
                },
                "title": {
                  "type": "string",
                  "example": "Pro"
                }
              },
              "required": [
                "name",
                "title",
                "limits"
              ]
            }
          },
          "prices_set": {
            "type": "string",
            "description": "the day these numbers were confirmed",
            "example": "2026-10-06"
          },
          "relay_limits_applied": {
            "type": "boolean",
            "description": "whether the tunnel relay holds the workspace to this plan's numbers: false only for an workspace that runs the platform, for which the relay is answered plan operator, which limits nothing, so linking its relay tenant never cuts its live shares. The plan is billed the same either way",
            "example": true
          },
          "remind_days": {
            "type": "integer",
            "description": "how long before a renewal the balance does not cover the owner is written to",
            "example": 3
          },
          "renewal": {
            "type": "string",
            "example": "€4.00"
          },
          "renewal_micro": {
            "type": "integer",
            "description": "what that renewal costs, in micro-euros; 0 when there is none",
            "example": 4000000
          },
          "renews_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when Pro is next paid for; null on Free and when it ends at the period's end",
            "example": "2026-11-06T14:30:00Z"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "lapsed"
            ]
          }
        },
        "required": [
          "org",
          "plan",
          "status",
          "period_start",
          "period_end",
          "downgrade_at_period_end",
          "hold_until",
          "held_names",
          "limits",
          "prices_set",
          "renews_on",
          "renewal_micro",
          "renewal",
          "balance_micro",
          "balance",
          "covered",
          "plans",
          "hold_days",
          "remind_days",
          "relay_limits_applied"
        ]
      },
      "PlanLimits": {
        "type": "object",
        "description": "what one plan allows: internal/plans' table, the numbers confirmed on 2026-10-06",
        "properties": {
          "guests_per_share": {
            "type": "integer",
            "description": "invited addresses one share lets in",
            "example": 25
          },
          "live_shares": {
            "type": "integer",
            "description": "shares connected at once",
            "example": 10
          },
          "own_domain": {
            "type": "boolean",
            "description": "whether a share may answer on your own domain",
            "example": true
          },
          "price": {
            "type": "string",
            "example": "€4.00"
          },
          "price_micro": {
            "type": "integer",
            "description": "a month, in micro-euros, debited from the balance",
            "example": 4000000
          },
          "reserved_names": {
            "type": "integer",
            "description": "addresses kept whether or not a tunnel is connected; they never lapse while the plan is paid",
            "example": 3
          },
          "session_limit": {
            "type": "boolean",
            "description": "a cut-off on how long a visit lasts: never, on either plan",
            "example": false
          },
          "traffic_gb_month": {
            "type": "integer",
            "description": "fair-use traffic a month, in GB; over it shares pause until the next month or an upgrade, never deleted",
            "example": 50
          },
          "visit_list": {
            "type": "boolean",
            "description": "whether the list of who opened a share is kept",
            "example": true
          },
          "visit_list_days": {
            "type": "integer",
            "description": "how long that list is kept; 0 when there is none",
            "example": 90
          },
          "warning_page": {
            "type": "boolean",
            "description": "an interstitial before a share: never, on either plan",
            "example": false
          }
        },
        "required": [
          "price_micro",
          "price",
          "live_shares",
          "reserved_names",
          "own_domain",
          "guests_per_share",
          "visit_list",
          "visit_list_days",
          "traffic_gb_month",
          "warning_page",
          "session_limit"
        ]
      },
      "Preview": {
        "type": "object",
        "description": "a branch of the project running beside production at its own address, with its own new database",
        "properties": {
          "address": {
            "type": "string",
            "description": "where the preview answers, behind the workspace's sign-in; empty until it is live and published",
            "example": "https://novid-3kq9x0c2m1ab.nuved.dev"
          },
          "branch": {
            "type": "string",
            "example": "feature/login"
          },
          "commit": {
            "type": "string",
            "description": "the commit it runs or is deploying; empty until the machine resolved the branch"
          },
          "deploy": {
            "type": [
              "object",
              "null"
            ],
            "description": "the newest deploy or removal of the preview",
            "properties": {
              "branch": {
                "type": "string",
                "example": "main"
              },
              "commit": {
                "type": "string",
                "description": "the full commit, or empty when the agent resolves the branch's tip",
                "example": "4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d4a3c2b1d"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "end_reason": {
                "type": "string",
                "description": "why the machine ended a build before it finished, apart from the message so a retry can read it: machine_hours (it ran out of its hours), machine_silent, machine_gone, machine_stopped (the machine was taken away); empty for every other end",
                "enum": [
                  "",
                  "machine_hours",
                  "machine_silent",
                  "machine_gone",
                  "machine_stopped"
                ]
              },
              "environment": {
                "type": "string",
                "description": "production, preview, or a branch's preview: preview-\u003cthe branch folded\u003e-\u003c6 hex of the branch\u003e (nuved preview)",
                "example": "production",
                "pattern": "^(production|preview|preview(-[a-z0-9]+)+)$"
              },
              "finished_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "when it went live, failed or was cancelled"
              },
              "id": {
                "type": "integer",
                "example": 12
              },
              "kind": {
                "type": "string",
                "description": "deploy; remove: the machine told to stop a deleted project's containers and forget it, which ends removed or failed; build: a build-only project's, which keeps files and starts nothing",
                "enum": [
                  "deploy",
                  "remove",
                  "build"
                ]
              },
              "message": {
                "type": "string",
                "description": "the agent's one sentence on progress or failure; empty until it says"
              },
              "project": {
                "type": "string",
                "example": "novid-pol"
              },
              "requested_by": {
                "type": "string",
                "description": "the person's address; git-plane for a push the hook could not name a person for",
                "example": "owner@example.com"
              },
              "started_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "when the agent took it"
              },
              "status": {
                "type": "string",
                "description": "waiting is a build-only project's build that has no machine yet; its message says why, and its id is the id of the deploy it becomes",
                "enum": [
                  "waiting",
                  "queued",
                  "running",
                  "live",
                  "failed",
                  "cancelled",
                  "removed"
                ]
              },
              "target": {
                "type": "object",
                "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
                "properties": {
                  "arch": {
                    "type": "string",
                    "description": "the machine's CPU architecture",
                    "example": "aarch64"
                  },
                  "country": {
                    "type": "string",
                    "example": "DE"
                  },
                  "display_name": {
                    "type": "string",
                    "description": "told to an owner of the workspace only; empty for everyone else",
                    "example": "Hetzner"
                  },
                  "id": {
                    "type": "string",
                    "example": "tg_1"
                  },
                  "kernel": {
                    "type": "string",
                    "description": "the machine's kernel release, as its gate read it",
                    "example": "6.8.0-146-generic"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "compose",
                      "kubernetes"
                    ]
                  },
                  "name": {
                    "type": "string",
                    "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                    "example": "nuved-box"
                  },
                  "provider": {
                    "type": "string",
                    "example": "Hetzner"
                  },
                  "region": {
                    "type": "string",
                    "example": "Falkenstein"
                  },
                  "runtime": {
                    "type": "string",
                    "description": "the container engine on a compose machine",
                    "example": "podman version 4.9.3"
                  }
                },
                "required": [
                  "id",
                  "name",
                  "display_name",
                  "kind",
                  "provider",
                  "region",
                  "country"
                ]
              }
            }
          },
          "environment": {
            "type": "string",
            "description": "the preview's own environment: its containers, network, volumes and address are its own",
            "example": "preview-feature-login-3fa9c1"
          },
          "message": {
            "type": "string",
            "description": "the machine's last sentence on the newest deploy or removal"
          },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "health": {
                  "type": "string",
                  "example": "healthy"
                },
                "public": {
                  "type": "boolean"
                },
                "service": {
                  "type": "string",
                  "example": "web"
                },
                "status": {
                  "type": "string",
                  "example": "live"
                }
              },
              "required": [
                "service",
                "status",
                "health",
                "public"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "deploying",
              "live",
              "failed",
              "removing",
              "stopped"
            ]
          },
          "target": {
            "type": "object",
            "description": "the machine it runs on, as a listing shows it: \"Hetzner · Falkenstein (DE) · compose\"",
            "properties": {
              "arch": {
                "type": "string",
                "description": "the machine's CPU architecture",
                "example": "aarch64"
              },
              "country": {
                "type": "string",
                "example": "DE"
              },
              "display_name": {
                "type": "string",
                "description": "told to an owner of the workspace only; empty for everyone else",
                "example": "Hetzner"
              },
              "id": {
                "type": "string",
                "example": "tg_1"
              },
              "kernel": {
                "type": "string",
                "description": "the machine's kernel release, as its gate read it",
                "example": "6.8.0-146-generic"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "compose",
                  "kubernetes"
                ]
              },
              "name": {
                "type": "string",
                "description": "the machine's own name, told to an owner of the workspace only; empty for everyone else",
                "example": "nuved-box"
              },
              "provider": {
                "type": "string",
                "example": "Hetzner"
              },
              "region": {
                "type": "string",
                "example": "Falkenstein"
              },
              "runtime": {
                "type": "string",
                "description": "the container engine on a compose machine",
                "example": "podman version 4.9.3"
              }
            },
            "required": [
              "id",
              "name",
              "display_name",
              "kind",
              "provider",
              "region",
              "country"
            ]
          }
        },
        "required": [
          "environment",
          "branch",
          "commit",
          "address",
          "status",
          "message",
          "target",
          "services",
          "deploy"
        ]
      },
      "Project": {
        "type": "object",
        "description": "one isolated project: its own namespace, its own caps, its own network rules, and whatever is running in it",
        "properties": {
          "attempts": {
            "type": "integer",
            "description": "while waiting or failed: how many times the job has tried",
            "example": 3
          },
          "caps": {
            "$ref": "#/components/schemas/Caps"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when it was made; null when nobody knows",
            "example": "2026-10-21T09:00:00Z"
          },
          "created_by": {
            "type": "string",
            "description": "who made it: a person's address, or `agent \u003cproject\u003e/\u003ctoken\u003e`; empty when nobody knows",
            "example": "owner@example.com"
          },
          "display_name": {
            "type": "string",
            "description": "the name as it was typed (2026-10-21); for a project from before, its name less the workspace's prefix",
            "example": "shop"
          },
          "git": {
            "type": "object",
            "description": "where the project's repository is cloned from and pushed to; both shown whether or not anything has been pushed",
            "properties": {
              "https": {
                "type": "string",
                "example": "https://git.nuved.io/novid/novid-shop"
              },
              "ssh": {
                "type": "string",
                "example": "ssh://git@ssh.nuved.io/novid/novid-shop"
              }
            },
            "required": [
              "https",
              "ssh"
            ]
          },
          "last_error": {
            "type": "string",
            "description": "while waiting or failed: what went wrong the last time",
            "example": "the cluster did not answer"
          },
          "name": {
            "type": "string",
            "description": "the project's name: the workspace's, a dash and the name as typed, made DNS-safe; its namespace, repository and addresses",
            "example": "novid-shop"
          },
          "next_attempt": {
            "type": "string",
            "format": "date-time",
            "description": "while waiting: when it tries next",
            "example": "2026-09-30T14:04:00Z"
          },
          "phase": {
            "type": "string",
            "description": "what the platform has done with it so far",
            "example": "Active"
          },
          "rate": {
            "type": "string",
            "description": "what that reservation costs an hour",
            "example": "€0.005"
          },
          "reserved": {
            "type": "object",
            "description": "what the project reserves right now, which is what it is charged for",
            "properties": {
              "cpu_milli": {
                "type": "integer",
                "example": 100
              },
              "mem_bytes": {
                "type": "integer",
                "example": 134217728
              }
            }
          },
          "self_hosted": {
            "type": "boolean",
            "description": "present and true for a project that runs on a machine of the workspace's own (POST /v1/projects/self-hosted): no caps, nothing reserved or charged, its services where they were registered",
            "example": true
          },
          "services": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Service"
            }
          },
          "status": {
            "type": "string",
            "description": "creating: a job is writing it and it is not up yet; waiting: that job is retrying because the cluster did not answer; ready: up; failed: the job stopped, and POST /v1/projects/{name}/retry tries again; deleting: a delete is queued or retrying",
            "example": "ready",
            "enum": [
              "creating",
              "waiting",
              "ready",
              "failed",
              "deleting"
            ]
          },
          "unplaced": {
            "type": "boolean",
            "description": "present and true for a project that runs nowhere yet: created on a console with no cluster behind it, it is its repository until a placement puts it somewhere. No caps, nothing reserved or charged, no services; caps, addresses, kubeconfig, logs and runs are refused for it",
            "example": true
          }
        },
        "required": [
          "name",
          "display_name",
          "created_by",
          "created_at",
          "caps",
          "reserved",
          "rate",
          "phase",
          "services",
          "git",
          "status"
        ]
      },
      "ProjectRole": {
        "type": "object",
        "description": "one person's role on one project",
        "properties": {
          "name": {
            "type": "string",
            "description": "the project's full name, as GET /v1/projects lists it",
            "example": "novid-shop"
          },
          "role": {
            "type": "string",
            "description": "member changes the project (services, caps, tokens, addresses); viewer reads it (logs, code, runs)",
            "example": "viewer",
            "enum": [
              "member",
              "viewer"
            ]
          }
        },
        "required": [
          "name",
          "role"
        ]
      },
      "ProjectState": {
        "type": "object",
        "description": "a project and where it stands",
        "properties": {
          "name": {
            "type": "string",
            "example": "novid-shop"
          },
          "status": {
            "type": "string",
            "example": "deleting",
            "enum": [
              "creating",
              "waiting",
              "ready",
              "failed",
              "deleting"
            ]
          }
        },
        "required": [
          "name",
          "status"
        ]
      },
      "Rates": {
        "type": "object",
        "description": "the rate card in force: three prices, and the measurements they came from",
        "properties": {
          "card_id": {
            "type": "integer",
            "description": "which rate card this is; 0 is the built-in default, before any was derived",
            "example": 1
          },
          "cpu_hour": {
            "type": "string",
            "description": "per vCPU-hour, as a person reads it",
            "example": "€0.11"
          },
          "cpu_micro_per_milli_hour": {
            "type": "integer",
            "description": "the same cpu price in micro-euros per millicore-hour; money is never a float",
            "example": 111
          },
          "derived_from": {
            "type": "object",
            "description": "the measurements behind the card; absent for the built-in default",
            "properties": {
              "bill_day": {
                "type": "string",
                "description": "which days the card was priced from: a range and a count, or one named day",
                "example": "2026-09-10 to 2026-09-16, the mean of 6 complete days of 7"
              },
              "bill_day_usd": {
                "type": "number",
                "description": "what AWS charged that day, every service, in USD",
                "example": 2.7445
              },
              "days": {
                "type": "array",
                "description": "every day the window looked at, with its instance-hours and whether it counted",
                "items": {
                  "type": "object",
                  "properties": {
                    "complete": {
                      "type": "boolean",
                      "example": true
                    },
                    "cost_usd": {
                      "type": "number",
                      "example": 2.7445
                    },
                    "day": {
                      "type": "string",
                      "example": "2026-09-16"
                    },
                    "instance_hours": {
                      "type": "number",
                      "description": "EC2 instance-hours on that day's bill; a day short of 98% of the cluster's hours is still being published and does not count",
                      "example": 24
                    }
                  }
                }
              },
              "fx": {
                "type": "number",
                "description": "USD to EUR, the ECB rate of the bill day",
                "example": 0.86
              },
              "margin": {
                "type": "number",
                "description": "what is added on top of cost",
                "example": 0
              },
              "sellable_cpu": {
                "type": "number",
                "description": "vCPU left after the platform's own pods",
                "example": 0.86
              },
              "sellable_mem_gib": {
                "type": "number",
                "description": "GiB left, the same",
                "example": 5.6
              },
              "utilization": {
                "type": "number",
                "description": "the fraction of sellable capacity the price is set against",
                "example": 0.7
              }
            }
          },
          "disk_micro_per_gb_month": {
            "type": "integer",
            "example": 81870
          },
          "disk_month": {
            "type": "string",
            "description": "per GB of disk a month",
            "example": "€0.08"
          },
          "example": {
            "type": "string",
            "description": "the worked example the sign-in page shows",
            "example": "0.25 vCPU, 512 MiB"
          },
          "example_month": {
            "type": "string",
            "description": "what that example costs a month",
            "example": "€21"
          },
          "mem_hour": {
            "type": "string",
            "description": "per GiB-hour",
            "example": "€0.008"
          },
          "mem_micro_per_gib_hour": {
            "type": "integer",
            "example": 7966
          },
          "shared_max": {
            "type": "object",
            "description": "the most one container may reserve on a shared machine; admission refuses a container whose cpu or memory request is above it. Absent until the rate-card job has worked one out",
            "properties": {
              "computed": {
                "type": "string",
                "format": "date-time",
                "description": "when it was worked out; it moves when the machines do",
                "example": "2026-09-19T15:04:05Z"
              },
              "cpu": {
                "type": "string",
                "description": "as a manifest writes it",
                "example": "203m"
              },
              "cpu_milli": {
                "type": "integer",
                "description": "the same cpu in millicores",
                "example": 203
              },
              "mem_bytes": {
                "type": "integer",
                "description": "the same memory in bytes",
                "example": 1536163840
              },
              "memory": {
                "type": "string",
                "description": "as a manifest writes it",
                "example": "1465Mi"
              },
              "rule": {
                "type": "string",
                "description": "what the number is",
                "example": "a quarter of the smallest shared machine's sellable capacity"
              }
            },
            "required": [
              "cpu",
              "memory",
              "cpu_milli",
              "mem_bytes",
              "rule"
            ]
          },
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "when the card was derived; absent for card 0",
            "example": "2026-09-17T06:00:00Z"
          }
        },
        "required": [
          "card_id",
          "cpu_hour",
          "mem_hour",
          "disk_month",
          "cpu_micro_per_milli_hour",
          "mem_micro_per_gib_hour",
          "disk_micro_per_gb_month",
          "example",
          "example_month"
        ]
      },
      "Service": {
        "type": "object",
        "description": "one thing running in a project, as the console and the CLI show it",
        "properties": {
          "desired": {
            "type": "integer",
            "description": "how many were asked for",
            "example": 1
          },
          "image": {
            "type": "string",
            "example": "hashicorp/http-echo:1.0"
          },
          "name": {
            "type": "string",
            "example": "hello"
          },
          "rate": {
            "type": "string",
            "description": "what this service costs an hour",
            "example": "€0.005"
          },
          "ready": {
            "type": "integer",
            "description": "how many copies are answering",
            "example": 1
          },
          "status": {
            "type": "string",
            "description": "what the platform sees it doing; empty for a self-hosted project's service, which the platform does not watch",
            "example": "Healthy",
            "enum": [
              "Healthy",
              "Progressing",
              "Degraded",
              "Idle",
              ""
            ]
          },
          "url": {
            "type": "string",
            "description": "where it answers from outside, empty when nothing routes to it",
            "example": "https://hello-novid.nuved.io"
          }
        },
        "required": [
          "name",
          "image",
          "status",
          "rate",
          "url",
          "ready",
          "desired"
        ]
      },
      "SignIn": {
        "type": "object",
        "description": "what a program needs to sign a person in",
        "properties": {
          "client_id": {
            "type": "string",
            "description": "the public client the device flow runs as"
          },
          "device_authorization_endpoint": {
            "type": "string",
            "description": "where the device flow starts, discovered from the issuer; absent when the provider does not offer it"
          },
          "device_sign_in": {
            "type": "string",
            "description": "the console's own device sign-in (POST it, then poll the same address with /token after it), allowed on the console's /device page; an app that knows it uses it instead",
            "example": "https://console.nuved.io/auth/device"
          },
          "issuer": {
            "type": "string",
            "description": "the OpenID Connect issuer, for discovery",
            "example": "https://novid-auth.fromnovid.com/application/o/nuved/"
          }
        },
        "required": [
          "issuer",
          "client_id"
        ]
      },
      "Token": {
        "type": "object",
        "description": "a credential, as it is listed. The token itself appears once, in the answer that minted it, and never again: what is stored is a SHA-256",
        "properties": {
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-15T10:04:30Z"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "when the token stops working; null for never"
          },
          "id": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "session",
              "agent"
            ]
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null until something uses it",
            "example": "2026-09-15T11:30:00Z"
          },
          "name": {
            "type": "string",
            "example": "claude-code"
          },
          "project": {
            "type": "string",
            "description": "the one project it reaches; empty for a session",
            "example": "novid-shop"
          },
          "scopes": {
            "type": "array",
            "description": "what an agent token may do; empty is everything (a token from before scopes)",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "code",
                "deploy",
                "logs",
                "artifacts",
                "secrets",
                "open"
              ]
            }
          },
          "token": {
            "type": "string",
            "description": "the credential itself, present only in the answer that minted it",
            "example": "nvd_agent_…"
          }
        },
        "required": [
          "id",
          "project",
          "name",
          "kind",
          "created_at"
        ]
      },
      "Usage": {
        "type": "object",
        "description": "what each project cost, day by day, and when the balance runs out",
        "properties": {
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageDay"
            }
          },
          "days_left": {
            "type": "integer",
            "description": "whole days the balance lasts at hourly_now, rounded down: the console's about N days. 0 when nothing is reserved, nothing is left, or the cluster could not be read. It can be one less than the days until runs_out_on, which counts the day itself",
            "example": 4
          },
          "hourly_now": {
            "type": "string",
            "description": "what is reserved right now costs this an hour",
            "example": "€0.058"
          },
          "month_to_date": {
            "type": "string",
            "description": "usage since the first of this month, UTC",
            "example": "€5.90"
          },
          "runs_out_on": {
            "type": "string",
            "description": "the UTC day the balance runs out at that rate; empty when nothing is reserved",
            "example": "2026-10-03"
          }
        },
        "required": [
          "days",
          "month_to_date",
          "hourly_now",
          "runs_out_on",
          "days_left"
        ]
      },
      "UsageDay": {
        "type": "object",
        "description": "one UTC day of usage",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-09-18"
          },
          "projects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "amount": {
                  "type": "string",
                  "example": "€1.12"
                },
                "amount_micro": {
                  "type": "integer",
                  "example": 1116000
                },
                "project": {
                  "type": "string",
                  "example": "novid-shop"
                }
              },
              "required": [
                "project",
                "amount_micro",
                "amount"
              ]
            }
          },
          "total": {
            "type": "string",
            "example": "€1.39"
          },
          "total_micro": {
            "type": "integer",
            "description": "the day's usage in micro-euros; money is never a float",
            "example": 1390000
          }
        },
        "required": [
          "date",
          "projects",
          "total_micro",
          "total"
        ]
      }
    },
    "securitySchemes": {
      "agent": {
        "type": "http",
        "scheme": "bearer",
        "description": "an agent token: `Authorization: Bearer nvd_agent_…`. Scoped to one project, and refused wherever money, caps or credentials are involved."
      },
      "gitPlane": {
        "type": "http",
        "scheme": "bearer",
        "description": "git-plane's service token (GITPLANE_SERVICE_TOKEN), shared by git-plane and this backend: what the push hook presents. No person or agent holds it."
      },
      "machineService": {
        "type": "http",
        "scheme": "bearer",
        "description": "the machine service's credential (MACHINE_SERVICE_TOKEN), shared by the service that creates machines and this backend: what it presents to mint the join token of a machine it is about to create. No person, agent or machine holds it."
      },
      "session": {
        "type": "http",
        "scheme": "bearer",
        "description": "a sign-in session: `Authorization: Bearer nvd_sess_…`, or the `__Host-nuved_session` cookie the console is given. Speaks for the whole workspace."
      },
      "stripeSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "Stripe-Signature",
        "description": "Stripe's signature over the raw request body, verified against the endpoint's own signing secret. Not a credential anybody else can present."
      },
      "target": {
        "type": "http",
        "scheme": "bearer",
        "description": "a target's own credential: `Authorization: Bearer nvd_target_…`, answered once when the machine enrolled. Good for that target's heartbeat and report, and nothing else."
      }
    }
  }
}