openapi: 3.1.0
info:
  title: UatuCode public API
  version: 11.24.0-experimental
  license: { name: MIT, identifier: MIT }
  summary: Hub revision 11 and workspace revision 24
  description: |
    Canonical HTTP contract for the UatuCode Hub API, the only public API.
    Under `/s/{workspaceId}/` the Hub serves one family itself, personal
    state, and it is part of this contract. Everything else under that
    prefix is the internal protocol between the Hub, the workspace child,
    and the web client shipped in the same build, and `exclusions.yaml`
    lists it. Workspace payloads reach public clients
    as topics of the Hub's live stream, and the workspace revision versions
    those payloads. `streaming.yaml` defines streaming messages and lifecycle
    rules.

    `info.version` encodes the revision pair as `<hub>.<workspace>.0` with an
    `-experimental` tag while stability is experimental; `x-uatu-revisions`
    is the authoritative machine-readable pair, and the product version of a
    published snapshot is recorded in that snapshot's `contract.json`.
  x-uatu-revisions:
    hubApiRevision: 11
    workspaceApiRevision: 24
servers:
  - url: https://{hubHost}
    variables:
      hubHost: { default: uatu.example.test }
tags:
  - { name: Hub authentication, description: Native JSON session creation and revocation. }
  - { name: Hub, description: "Workspace registry, lifecycle, browsing, device sessions, and per-user workspace state." }
  - { name: Hub credentials, description: "Hub-managed credentials, workspace assignments, and credential-tool readiness." }
  - { name: Hub agent accounts, description: "The Hub machine's chat-agent logins (OpenCode providers and the Claude Code account): status, key and browser logins, logout, and credential switching. Logins are written to each agent's own store and apply to every workspace and every user of the Hub; the Hub keeps no agent secret." }
  - { name: Clone jobs, description: "Asynchronous clone creation, interaction, cancellation, and events." }
  - { name: Live stream, description: "One SSE connection per client for document state, conversation inventory and events, and cross-workspace activity." }
  - { name: Worktrees, description: "Git worktrees of a workspace's repository as independent workspaces: authoritative inventory, remote fetch, creation, opening, read-only deletion preflight, guarded deletion, and registering or forgetting a checkout. Every operation runs over the same service and the same safety rules as the Hub's own worktree presentation." }
security:
  - hubBearer: []
  - hubCookie: []
paths:
  /api/hub/notifications:
    get:
      operationId: hubGetNotifications
      tags: [Hub]
      summary: Read push configuration and this user's device enrollment
      parameters:
        - { name: device, in: query, schema: { type: string, maxLength: 128 } }
      responses:
        '200':
          description: Push support and the selected device, without endpoint or private keys
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationState' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '500': { $ref: '#/components/responses/NoStoreError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
    put:
      operationId: hubEnrollNotifications
      tags: [Hub]
      summary: Enroll or reconcile a browser push subscription and device preferences
      description: Requires an explicit user enrollment action. Cookie requests require same-origin CSRF validation. Enrollment is authorized by the current login session; expiry or revocation stops future sends until reconciliation. With `allWorkspaces` true the device covers every workspace the login can reach, including workspaces registered later; `workspaceIds` is still validated and stored so the explicit selection is restored when the rule is turned off. Enrollment waits for the feed position of every running covered workspace before saving.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NotificationEnrollment' }
      responses:
        '200':
          description: Device enrollment saved
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationState' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/NoStoreError' }
        '413': { $ref: '#/components/responses/NoStoreError' }
        '500': { $ref: '#/components/responses/NoStoreError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
    delete:
      operationId: hubRemoveNotifications
      tags: [Hub]
      summary: Disable notifications for an owned device
      parameters:
        - { name: device, in: query, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
      responses:
        '200':
          description: Device removed and unsent notifications cancelled
          content:
            application/json:
              schema: { type: object, required: [removed], additionalProperties: false, properties: { removed: { const: true } } }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '500': { $ref: '#/components/responses/NoStoreError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /login:
    post:
      operationId: hubLogin
      tags: [Hub authentication]
      summary: Create a native Hub session
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NativeLoginRequest' }
            example: { name: alice, password: secret, deviceLabel: Alice's Mac }
      responses:
        '200':
          description: Session issued. The session ID is also set as a cookie.
          headers:
            Set-Cookie: { schema: { type: string } }
            Cache-Control: { schema: { type: string, const: no-store } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NativeLoginResponse' }
              example: { sessionId: token-value, user: alice }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '429': { $ref: '#/components/responses/NoStoreError' }
  /logout:
    post:
      operationId: hubLogout
      tags: [Hub authentication]
      summary: Revoke the bearer session
      security: [{ hubBearer: [] }]
      responses:
        '200': { $ref: '#/components/responses/Revoked' }
        '403': { $ref: '#/components/responses/Error' }
  /api/hub/state:
    get:
      operationId: hubGetState
      tags: [Hub]
      summary: Read Hub state and registered workspaces
      responses:
        '200':
          description: Hub state
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HubState' }
              examples:
                ordinary:
                  summary: An ordinary workspace
                  value:
                    version: 0.5.1 (abcdef0)
                    hubApiRevision: 10
                    workspaceApiRevision: 20
                    workspaces: [{ id: uatu, displayName: Uatu Docs, path: /src/uatu, backend: local, running: true, credentialRestartRequired: false, credentialAssignments: { authentication: [Work GitHub], signing: [Work signing] }, workspaceApiRevision: 20, shells: [{ attached: false, label: zsh }] }]
                    workspaceDefaults: { configured: /srv/workspaces, configuredAvailable: true, effective: /srv/workspaces }
                worktrees:
                  summary: A main workspace and one linked worktree
                  value:
                    version: 0.5.1 (abcdef0)
                    hubApiRevision: 10
                    workspaceApiRevision: 20
                    worktreeApi: /api/hub/worktrees
                    workspaces:
                      - { id: atlas, displayName: Atlas, path: /src/atlas, backend: local, running: true, credentialRestartRequired: false, credentialAssignments: { authentication: [Work GitHub], signing: [] }, workspaceApiRevision: 20, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, branch: main, createWorktree: true }
                      - { id: feature-login, displayName: feature/login, path: /src/atlas.worktrees/feature-login, backend: local, running: false, credentialRestartRequired: false, credentialAssignments: { authentication: [], signing: [] }, workspaceApiRevision: 20, parentId: atlas, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, branch: feature/login, sourceRef: main, ownership: uatu }
        '401': { $ref: '#/components/responses/Error' }
  /api/hub/browse:
    get:
      operationId: hubBrowse
      tags: [Hub]
      summary: List child directories on the Hub host
      parameters:
        - { name: path, in: query, schema: { type: string }, description: Absolute directory path; defaults to the Hub user's home directory. }
      responses:
        '200':
          description: Directory listing
          content:
            application/json: { schema: { $ref: '#/components/schemas/BrowseResult' } }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/folders/create:
    post:
      operationId: hubCreateFolder
      tags: [Hub]
      summary: Create an empty child folder
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateFolderRequest' }
            example: { parent: /src, name: new-project }
      responses:
        '200':
          description: Folder created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateFolderResult' }
              example: { path: /src/new-project }
        '400': { $ref: '#/components/responses/FolderMutationError' }
        '401': { $ref: '#/components/responses/FolderMutationError' }
        '403': { $ref: '#/components/responses/FolderMutationError' }
        '404': { $ref: '#/components/responses/FolderMutationError' }
        '409': { $ref: '#/components/responses/FolderMutationError' }
        '500': { $ref: '#/components/responses/FolderMutationError' }
  /api/hub/folders/rename:
    post:
      operationId: hubRenameFolder
      tags: [Hub]
      summary: Rename a folder to an unused sibling name
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RenameFolderRequest' }
            example: { path: /src/old-project, name: new-project, stop: true }
      responses:
        '200':
          description: Folder renamed and affected workspace paths updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RenameFolderResult' }
              example: { path: /src/new-project, workspaceIds: [old-project] }
        '400': { $ref: '#/components/responses/FolderMutationError' }
        '401': { $ref: '#/components/responses/FolderMutationError' }
        '403': { $ref: '#/components/responses/FolderMutationError' }
        '404': { $ref: '#/components/responses/FolderMutationError' }
        '409':
          description: The destination or another operation conflicts, the folder is not empty, or affected sessions need explicit stop authorization.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: '#/components/schemas/FolderMutationError' }
                  - { $ref: '#/components/schemas/FolderStopConflict' }
              examples:
                destinationExists: { value: { error: destination already exists } }
                needsStop: { value: { error: affected workspace sessions must be stopped, needsStop: true, workspaceIds: [old-project] } }
        '500': { $ref: '#/components/responses/FolderMutationError' }
  /api/hub/folders/remove:
    post:
      operationId: hubRemoveFolder
      tags: [Hub]
      summary: Remove an empty folder
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RemoveFolderRequest' }
            example: { path: /src/old-project, stop: true }
      responses:
        '200':
          description: Empty folder removed, with its workspace registration when present
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RemoveFolderResult' }
              examples:
                unregistered: { value: { path: /src/empty-folder } }
                registered: { value: { path: /src/old-project, workspaceId: old-project } }
        '400': { $ref: '#/components/responses/FolderMutationError' }
        '401': { $ref: '#/components/responses/FolderMutationError' }
        '403': { $ref: '#/components/responses/FolderMutationError' }
        '404': { $ref: '#/components/responses/FolderMutationError' }
        '409':
          description: The folder is not empty, another operation conflicts, or an affected session needs explicit stop authorization.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: '#/components/schemas/FolderMutationError' }
                  - { $ref: '#/components/schemas/FolderStopConflict' }
              examples:
                notEmpty: { value: { error: folder is not empty } }
                needsStop: { value: { error: affected workspace sessions must be stopped, needsStop: true, workspaceIds: [old-project] } }
        '500': { $ref: '#/components/responses/FolderMutationError' }
  /api/hub/workspaces:
    post:
      operationId: hubCreateWorkspace
      tags: [Hub]
      summary: Register and optionally start a workspace
      description: |
        A registration that commits but whose recovery journal cannot be
        cleared answers 200 with `recoveryRequired` instead of an error: the
        workspace is registered and stopped, a requested start was not
        attempted, and every further workspace change is refused until the
        Hub restarts and recovers.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateWorkspaceRequest' }
            example: { path: /src/uatu, start: true }
      responses:
        '200': { $ref: '#/components/responses/WorkspaceAction' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409':
          description: Initialization confirmation required, target reserved, or a pending folder mutation awaits recovery
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceConflict' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/workspaces/configure:
    post:
      operationId: hubConfigureWorkspace
      tags: [Hub]
      summary: Register an existing folder as a configured stopped workspace
      description: |
        Atomically commits the registration, display name, and selected
        credential assignments before any optional start. The default start
        intent is false; the normal result is a configured stopped workspace.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConfigureWorkspaceRequest' }
            example: { path: /src/payments-service, displayName: Payments API, authentication: [{ credentialId: cred-1, host: github.com }], signing: cred-2, start: false }
      responses:
        '200':
          description: The committed workspace and explicit start outcome.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingResult' }
              example: { workspace: { id: payments-service, displayName: Payments API, path: /src/payments-service, backend: local, running: false }, started: false, startError: null }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '409':
          description: Already registered, path reserved, init confirmation required, credential selection conflict, or a pending recovery journal.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingError' }
        '500':
          description: Git initialization, persistence, or recovery failure. No partial registration or assignment is retained.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/workspaces/create:
    post:
      operationId: hubCreateConfiguredWorkspace
      tags: [Hub]
      summary: Create and register a new stopped Git workspace
      description: |
        Creates one visible child folder under an absolute parent without
        replacing anything, runs `git init`, and commits the registration and
        credential assignments as one result. Failures after initialization
        retain the new repository and report its path for retry through the
        configure operation.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateConfiguredWorkspaceRequest' }
            example: { parent: /srv/workspaces, folderName: payments-service, displayName: Payments API, start: false }
      responses:
        '200':
          description: The created, initialized, committed workspace.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingResult' }
              example: { workspace: { id: payments-service, displayName: Payments API, path: /srv/workspaces/payments-service, backend: local, running: false }, started: false, startError: null }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '409':
          description: Destination exists, is reserved, a credential selection conflicts, or a pending recovery journal awaits Hub recovery.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingError' }
        '500':
          description: Git initialization or persistence failure. `retainedPath` identifies an initialized repository that was kept.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceOnboardingError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/workspaces/{workspaceId}/display-name:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubUpdateWorkspaceDisplayName
      tags: [Hub]
      summary: Rename a workspace display name
      description: Changes only the mutable display name. Valid while the workspace is running or stopped; the session, folder, stable id, URL, personal state, and assignments are untouched. Refused with 409 while a recovery journal awaits Hub recovery; the check runs inside the workspace lifecycle operation, so a folder mutation cannot journal between it and the update.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateWorkspaceDisplayNameRequest' }
            example: { displayName: Payments API }
      responses:
        '200':
          description: The updated workspace.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceSummaryResponse' }
              example: { workspace: { id: payments-service, displayName: Payments API, path: /src/payments-service, backend: local, running: true } }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '409':
          description: A pending recovery journal awaits Hub recovery. The journal records whole registry entries, display name included, and recovery restores them verbatim — so a rename admitted now would be silently reverted at the next restart.
          headers: { Cache-Control: { schema: { type: string, const: no-store } } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: a pending recovery journal requires Hub recovery before further folder changes }
        '500':
          description: The validated name could not be persisted (a retryable registry save failure, not a request problem).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: workspace display name could not be persisted }
  /api/hub/settings/workspace-defaults:
    get:
      operationId: hubGetWorkspaceDefaults
      tags: [Hub]
      summary: Read the default workspace parent preference
      responses:
        '200':
          description: Configured and effective default workspace parent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceDefaults' }
              example: { configured: /srv/workspaces, configuredAvailable: true, effective: /srv/workspaces }
        '401': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/NoStoreError' }
    post:
      operationId: hubUpdateWorkspaceDefaults
      tags: [Hub]
      summary: Configure or clear the default workspace parent
      description: |
        Requires an existing direct non-symbolic-link directory; the canonical
        path is persisted Hub-wide. The default guides initial create, clone,
        and browse locations only — it never constrains where workspaces may
        be registered. Null clears the preference.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateWorkspaceDefaultsRequest' }
            example: { defaultWorkspaceParent: /srv/workspaces }
      responses:
        '200':
          description: The resulting configured and effective values.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkspaceDefaults' }
              example: { configured: /srv/workspaces, configuredAvailable: true, effective: /srv/workspaces }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/NoStoreError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/sessions/{workspaceId}/start:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubStartWorkspace
      tags: [Hub]
      summary: Start a registered workspace
      description: Refused with 409 while a pending onboarding journal or a pending folder mutation awaits Hub recovery; the folder check also runs inside the workspace lifecycle operation, so a folder mutation cannot journal between it and the start.
      responses:
        '200': { $ref: '#/components/responses/WorkspaceAction' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409':
          description: A pending onboarding journal or folder-mutation journal awaits Hub recovery; starts are refused so a partially configured workspace cannot run and so no session is spawned at a path recovery must still move or restore.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example: { error: a pending onboarding requires Hub recovery before session starts }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/sessions/{workspaceId}/stop:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubStopWorkspace
      tags: [Hub]
      summary: Stop a workspace
      responses:
        '200': { $ref: '#/components/responses/WorkspaceAction' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/workspaces/{workspaceId}/forget:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubForgetWorkspace
      tags: [Hub]
      summary: Unregister a stopped workspace without deleting files
      responses:
        '200': { $ref: '#/components/responses/WorkspaceAction' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/workspaces/{workspaceId}/credential-assignments:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubAssignWorkspaceCredentials
      tags: [Hub credentials]
      summary: Atomically replace selected workspace credential defaults
      description: Authentication and signing selections commit together in one workspace lifecycle operation and one metadata transaction.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AssignWorkspaceCredentialsRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/CredentialAssignments' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/NoStoreError' }
        '500': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/sessions:
    get:
      operationId: hubListDeviceSessions
      tags: [Hub]
      summary: List the current user's device sessions
      responses:
        '200':
          description: Device sessions
          content:
            application/json: { schema: { $ref: '#/components/schemas/DeviceSessionList' } }
        '401': { $ref: '#/components/responses/Error' }
  /api/hub/sessions/{sessionHandle}/revoke:
    parameters:
      - { name: sessionHandle, in: path, required: true, schema: { type: string, minLength: 1 }, description: Non-secret session handle returned by the list operation. }
    post:
      operationId: hubRevokeDeviceSession
      tags: [Hub]
      summary: Revoke one device session
      responses:
        '200': { $ref: '#/components/responses/Revoked' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/credentials:
    get:
      operationId: hubListCredentials
      tags: [Hub credentials]
      summary: List public credential metadata and assignments
      responses:
        '200': { $ref: '#/components/responses/CredentialList' }
        '401': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/credential-tools:
    get:
      operationId: hubListCredentialTools
      tags: [Hub credentials]
      summary: List credential-tool readiness
      responses:
        '200': { $ref: '#/components/responses/CredentialToolList' }
        '401': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/{credentialId}/public-key:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    get:
      operationId: hubGetCredentialPublicKey
      tags: [Hub credentials]
      summary: Export public key material
      responses:
        '200': { $ref: '#/components/responses/CredentialPublicKey' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/ssh/generate:
    post:
      operationId: hubGenerateSshCredential
      tags: [Hub credentials]
      summary: Generate a passphrase-protected SSH credential
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/GenerateSshCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/ssh/import:
    post:
      operationId: hubImportSshCredential
      tags: [Hub credentials]
      summary: Import an SSH private key without returning it
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ImportSshCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/openpgp/generate:
    post:
      operationId: hubGenerateOpenPgpCredential
      tags: [Hub credentials]
      summary: Generate a passphrase-protected OpenPGP credential
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/GenerateOpenPgpCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/openpgp/import:
    post:
      operationId: hubImportOpenPgpCredential
      tags: [Hub credentials]
      summary: Import an OpenPGP private key without returning it
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ImportOpenPgpCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /api/hub/agent-accounts:
    get:
      operationId: hubGetAgentAccounts
      tags: [Hub agent accounts]
      summary: Read every chat agent's login state and the logins in progress
      description: Starts the Hub's own short-lived agent runtimes when they are not running and waits a bounded time for them. An agent still starting is reported as `starting`; poll until it settles. Reads never start a login, change a credential, or spend model tokens.
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/key:
    post:
      operationId: hubConnectAgentAccountKey
      tags: [Hub agent accounts]
      summary: Log in to an agent provider with a key
      description: The key and answers are handed to the agent's own login interface and are never stored or returned by the Hub.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountKeyRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/login:
    post:
      operationId: hubStartAgentAccountLogin
      tags: [Hub agent accounts]
      summary: Start a browser or device-code login
      description: Replaces this agent's or provider's earlier unfinished login. The answer lists the new attempt with the URL to open and how to finish it.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountLoginRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/attempts/{attemptId}/code:
    parameters: [{ $ref: '#/components/parameters/AgentAccountAttemptId' }]
    post:
      operationId: hubSubmitAgentAccountCode
      tags: [Hub agent accounts]
      summary: Finish a code login with the code the sign-in page showed
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountCodeRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/attempts/{attemptId}/redirect:
    parameters: [{ $ref: '#/components/parameters/AgentAccountAttemptId' }]
    post:
      operationId: hubSubmitAgentAccountRedirect
      tags: [Hub agent accounts]
      summary: Finish a loopback-redirect login with the address the browser landed on
      description: Delivered only when the address is `http:` on a loopback host with the port and path of this attempt's own callback; anything else is refused without any request. The Hub requests it once on the Hub machine, follows no redirect, and discards the listener's answer.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountRedirectRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/attempts/{attemptId}/cancel:
    parameters: [{ $ref: '#/components/parameters/AgentAccountAttemptId' }]
    post:
      operationId: hubCancelAgentAccountLogin
      tags: [Hub agent accounts]
      summary: Cancel a login in progress
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/logout:
    post:
      operationId: hubLogoutAgentAccount
      tags: [Hub agent accounts]
      summary: Log out a saved agent login on the Hub machine
      description: Applies to every workspace and the agent's own tools. Logins the Hub cannot remove (an environment key, a configuration file, a third-party cloud provider) are refused.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountLogoutRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/agent-accounts/activate:
    post:
      operationId: hubActivateAgentAccountCredential
      tags: [Hub agent accounts]
      summary: Make a saved credential the active one for its provider
      description: Offered only where the agent supports several saved credentials per provider (`capabilities.activate`).
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountActivateRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/AgentAccounts' }
        '400': { $ref: '#/components/responses/AgentAccountError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/AgentAccountError' }
        '500': { $ref: '#/components/responses/AgentAccountError' }
        '503': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/credentials/token:
    post:
      operationId: hubCreateTokenCredential
      tags: [Hub credentials]
      summary: Store an HTTPS or provider CLI token without returning it
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CreateTokenCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/credential-tools/{tool}:
    parameters: [{ $ref: '#/components/parameters/CredentialTool' }]
    post:
      operationId: hubSetCredentialTool
      tags: [Hub credentials]
      summary: Set or clear an absolute credential-tool override
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SetCredentialToolRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/CredentialTool' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/credential-tools/{tool}/test:
    parameters: [{ $ref: '#/components/parameters/CredentialTool' }]
    post:
      operationId: hubTestCredentialTool
      tags: [Hub credentials]
      summary: Re-probe one credential tool
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses:
        '200': { $ref: '#/components/responses/CredentialTool' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/{credentialId}/unlock:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubUnlockCredential
      tags: [Hub credentials]
      summary: Unlock a key credential in its Hub-managed runtime
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/UnlockCredentialRequest' } } } }
      responses:
        '200': { $ref: '#/components/responses/Credential' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /api/hub/credentials/{credentialId}/lock:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubLockCredential
      tags: [Hub credentials]
      summary: Remove a key credential from its Hub-managed runtime
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses: { '200': { $ref: '#/components/responses/Credential' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' }, '503': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/enable:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubEnableCredential
      tags: [Hub credentials]
      summary: Enable a credential for new operations
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses: { '200': { $ref: '#/components/responses/Credential' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' }, '503': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/disable:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubDisableCredential
      tags: [Hub credentials]
      summary: Disable a credential for new operations
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses: { '200': { $ref: '#/components/responses/Credential' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' }, '503': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/assign:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubAssignCredential
      tags: [Hub credentials]
      summary: Assign a credential as a workspace default
      description: Assignments select normal tool configuration but are advisory for the local backend because workspaces share the Hub daemon's OS UID.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/AssignCredentialRequest' } } } }
      responses: { '200': { $ref: '#/components/responses/CredentialAssignment' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '409': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/unassign:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubUnassignCredential
      tags: [Hub credentials]
      summary: Remove a credential assignment from a workspace
      description: A running workspace session must be stopped first or the request must carry stop:true; a catalog-only removal would leave the session's projected credential configuration in effect.
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/UnassignCredentialRequest' } } } }
      responses: { '200': { $ref: '#/components/responses/CredentialUnassigned' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '409': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/test:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubTestCredential
      tags: [Hub credentials]
      summary: Test credential readiness with sanitized results
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EmptyObject' } } } }
      responses: { '200': { $ref: '#/components/responses/CredentialTest' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '409': { $ref: '#/components/responses/Error' }, '429': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' }, '503': { $ref: '#/components/responses/Error' } }
  /api/hub/credentials/{credentialId}/delete:
    parameters: [{ $ref: '#/components/parameters/CredentialId' }]
    post:
      operationId: hubDeleteCredential
      tags: [Hub credentials]
      summary: Delete a credential after explicit confirmation
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DeleteCredentialRequest' } } } }
      responses: { '200': { $ref: '#/components/responses/CredentialDeleted' }, '400': { $ref: '#/components/responses/Error' }, '401': { $ref: '#/components/responses/Error' }, '403': { $ref: '#/components/responses/Error' }, '404': { $ref: '#/components/responses/Error' }, '409': { $ref: '#/components/responses/Error' }, '500': { $ref: '#/components/responses/Error' }, '503': { $ref: '#/components/responses/Error' } }
  /api/hub/clone-jobs:
    post:
      operationId: hubCreateCloneJob
      tags: [Clone jobs]
      summary: Start an asynchronous git clone
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateCloneJobRequest' }
            example: { url: https://github.com/example/repo.git, dest: /src, folderName: repo }
      responses:
        '202':
          description: Clone job created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CloneJob' }
              example: { jobId: 550e8400-e29b-41d4-a716-446655440000 }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/clone-jobs/{jobId}/events:
    parameters: [{ $ref: '#/components/parameters/JobId' }]
    head:
      operationId: hubProbeCloneJobEvents
      tags: [Clone jobs]
      summary: Check whether an owned clone job exists
      responses:
        '204': { description: Job exists }
        '401': { description: Session missing or invalid. HEAD strips the JSON error body the GET variant carries; only the application/json content-type header survives. }
        '404': { description: Job absent or owned by another user. HEAD strips the JSON error body the GET variant carries; only the application/json content-type header survives. }
    get:
      operationId: hubStreamCloneJobEvents
      tags: [Clone jobs]
      summary: Replay and follow clone job events
      parameters:
        - { name: Last-Event-ID, in: header, schema: { type: integer, minimum: 0 }, description: Resume after this event identifier. }
      responses:
        '200':
          description: SSE stream described by streaming.yaml#channels.cloneJobEvents
          headers:
            Cache-Control: { schema: { type: string, const: 'no-cache, no-transform' } }
          content:
            text/event-stream: { schema: { type: string } }
        '401': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/clone-jobs/{jobId}/input:
    parameters: [{ $ref: '#/components/parameters/JobId' }]
    post:
      operationId: hubSendCloneJobInput
      tags: [Clone jobs]
      summary: Send a response to an interactive clone process
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/CloneJobInput' } }
      responses:
        '200': { $ref: '#/components/responses/NoStoreAccepted' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '409': { $ref: '#/components/responses/NoStoreError' }
        '413': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/clone-jobs/{jobId}/cancel:
    parameters: [{ $ref: '#/components/parameters/JobId' }]
    post:
      operationId: hubCancelCloneJob
      tags: [Clone jobs]
      summary: Cancel a clone job
      responses:
        '200':
          description: Cancellation outcome
          content:
            application/json: { schema: { $ref: '#/components/schemas/CloneJobCancelResult' } }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /api/hub/live:
    get:
      operationId: hubStreamLive
      tags: [Live stream]
      summary: Open the live stream for a workspace
      description: |
        One long-lived Server-Sent Events connection per client. It carries
        every pushed update for one workspace, meaning document state, the
        conversation inventory, conversation events, and worktree inventory
        invalidations. On request it also
        carries an activity summary of every workspace the caller may access.
        streaming.yaml#channels.live defines the frames, envelopes, topics,
        cursors, and signals.

        The response writes an `: open` comment frame at once, then one
        `hello` event. Its stream id addresses `hubUpdateLiveSubscriptions`,
        and only the Hub session that opened the stream may use it. Upstream
        failures arrive as signals for the affected subscription and never
        end the stream. To reconnect, open a new stream and present every
        retained cursor in `subs`. The stream does not use the SSE `id` field
        or `Last-Event-ID`.
      parameters:
        - name: ws
          in: query
          required: true
          schema: { $ref: '#/components/schemas/WorkspaceId' }
          description: "Workspace whose document, inventory, conversation, and worktrees topics the stream carries. Required. An absent or empty id, or one longer than 256 UTF-8 bytes, answers 400 (absent: `workspace id required`). An id that is unknown, or that the caller may not access, answers 404."
        - name: activity
          in: query
          schema: { type: string, enum: ['0', '1'], default: '0' }
          description: "`1` adds the activity topic. The Hub sends one summary per workspace the caller may access when the stream opens, then one whenever a workspace's facts change."
        - name: subs
          in: query
          schema: { type: string, contentMediaType: application/json, contentSchema: { $ref: '#/components/schemas/LiveSubscriptionList' } }
          description: "Initial subscriptions as a JSON array of LiveSubscription, each with the cursor to resume from. A reconnect presents every retained cursor here. Malformed JSON or an invalid entry answers 400. The set rides the query string, and proxies commonly refuse a request line past about 8 KB. A client whose set would exceed that presents part of it here and adds the rest with hubUpdateLiveSubscriptions after `hello`, from the same cursors."
        - name: reconnect
          in: query
          schema: { type: string, enum: ['0', '1'], default: '0' }
          description: "`1` when this stream replaces one the client lost. The Hub records it in diagnostics and behaves the same either way."
      responses:
        '200':
          description: SSE stream described by streaming.yaml#channels.live
          content:
            text/event-stream: { schema: { type: string } }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403':
          description: "A cookie-authenticated request failed the same-origin check, and nothing was subscribed. Its `Origin` names another origin, or its `Sec-Fetch-Site` is present and neither `same-origin` nor `none`, as on a link followed from another site or from another port on the same host. A bearer-authenticated request is not checked."
          content:
            application/json: { schema: { $ref: '#/components/schemas/Error' } }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/live/{streamId}/subscriptions:
    parameters: [{ $ref: '#/components/parameters/LiveStreamId' }]
    post:
      operationId: hubUpdateLiveSubscriptions
      tags: [Live stream]
      summary: Change an open live stream's subscriptions
      description: |
        Adds and removes subscriptions on an open stream without reconnecting.
        Removals apply before additions. Adding a topic and key that is
        already subscribed replaces the subscription and re-attaches it from
        the new cursor. A client answers a `resync` signal this way. It takes
        a fresh snapshot, then adds the subscription again with the
        snapshot's cursor. Once this operation answers, the stream carries no
        further envelope for a removed subscription.

        A 400, 403, or 404 answer means this stream cannot take the change,
        and repeating the request fails the same way. Reconnect and present
        the whole subscription set in `subs`. Retry a 5xx, or a request that
        got no answer, with backoff a bounded number of times, then
        reconnect. Validate subscriptions against the byte and count bounds
        before sending: one the Hub refuses here is refused in `subs` too, on
        every reconnect.
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/LiveSubscriptionChange' } }
      responses:
        '200':
          description: Subscriptions changed
          content:
            application/json: { schema: { $ref: '#/components/schemas/LiveSubscriptionsUpdated' } }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403':
          description: The stream belongs to another Hub session, or a cookie-authenticated request failed the same-origin check.
          content:
            application/json: { schema: { $ref: '#/components/schemas/Error' } }
        '404':
          description: The stream is unknown or has ended. Reconnect and present the whole subscription set in `subs`.
          content:
            application/json: { schema: { $ref: '#/components/schemas/Error' } }
  /s/{workspaceId}/api/personal-state:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    get:
      operationId: workspaceGetPersonalState
      tags: [Hub]
      summary: Read per-user workspace state
      description: The Hub stores personal state per Hub user and workspace and answers this request itself, without proxying it to the workspace child, so it answers whether or not the workspace is running. The operation ID predates the move to the Hub domain and is kept for generated clients.
      responses:
        '200':
          description: Personal state
          content:
            application/json: { schema: { $ref: '#/components/schemas/PersonalState' } }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
    patch:
      operationId: workspacePatchPersonalState
      tags: [Hub]
      summary: Patch per-user workspace state
      description: The Hub applies and stores the patch itself, without proxying it to the workspace child. The operation ID predates the move to the Hub domain and is kept for generated clients.
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/PersonalStatePatch' } }
      responses:
        '200':
          description: Updated personal state
          content:
            application/json: { schema: { $ref: '#/components/schemas/PersonalState' } }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
  /s/{workspaceId}/api/activity-viewed:
    parameters: [{ $ref: '#/components/parameters/WorkspaceId' }]
    post:
      operationId: hubAcknowledgeWorkspaceActivity
      tags: [Hub]
      summary: Report that the caller has the workspace's chat in view
      description: |
        Clears the `finished` fact the live stream's activity topic reports
        for this workspace, for the calling user on every device. The Hub
        answers it itself, without proxying to the workspace child, and
        re-emits the user's activity summary when the fact changes. Other
        users' summaries are unaffected. Idempotent: acknowledging a
        workspace that is not finished changes nothing. The web client sends
        it whenever the workspace's summary reads finished while its chat
        surface is visible.
      responses:
        '204': { description: Acknowledged. No body. }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /api/hub/worktrees:
    get:
      operationId: hubListWorktrees
      tags: [Worktrees]
      summary: List a repository's checkouts
      description: |
        The authoritative Git worktree inventory of one workspace's
        repository family, reconciled from Git at the time of the request. A
        workspace that is a linked worktree resolves to its main workspace,
        which owns the family and its credential policy.
      parameters:
        - name: source
          in: query
          required: true
          schema: { $ref: '#/components/schemas/WorkspaceId' }
          description: "The workspace whose repository family to list."
      responses:
        '200': { $ref: '#/components/responses/WorktreeInventory' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/fetch:
    post:
      operationId: hubFetchWorktreeRefs
      tags: [Worktrees]
      summary: Fetch remote branches
      description: |
        One explicit remote fetch, credential-aware through the repository's
        parent workspace policy exactly as the Hub's own worktree
        presentation performs it — never an ambient credential, never a
        silent fallback. Refs are reported whether or not the fetch itself
        succeeded: a partial fetch may still have updated some of them, and a
        failure must never look like an empty repository. The Hub carries no
        draft across this call; a selection that vanished from the refreshed
        listing is the caller's own to reconcile.

        A refusal is a completed request: it answers 200 with `ok: false`
        and a sanitized error, because the Hub processed the request and
        decided against it. HTTP statuses report transport-level problems
        only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FetchWorktreeRefsRequest' }
            example: { sourceWorkspaceId: atlas }
      responses:
        '200': { $ref: '#/components/responses/WorktreeRefs' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/create:
    post:
      operationId: hubCreateWorktree
      tags: [Worktrees]
      summary: Create a linked worktree
      description: |
        Adds an ordinary linked worktree and registers it as its own
        workspace, through the same service and the same safety rules the
        Hub's own interface uses. The destination is predetermined
        (`<main-folder>.worktrees/<safe-branch-folder>`) and is never an
        input. Nothing is ever forced: a branch checked out elsewhere, an
        occupied destination or a ref that is not available is a refusal
        carrying the existing checkout to open instead.

        A refusal is a completed request: it answers 200 with
        `ok: false` and a sanitized error, because the Hub processed the
        request and decided against it. HTTP statuses report transport-level
        problems only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateWorktreeRequest' }
            examples:
              newBranch:
                summary: A new branch from an unqualified ref the Hub resolves
                value: { sourceWorkspaceId: atlas, mode: new-branch, branch: feature/login, baseRef: main, start: false }
              remoteTracking:
                summary: A tracking branch from a remote-qualified ref
                value: { sourceWorkspaceId: atlas, mode: remote-tracking, base: { kind: remote, ref: origin/release } }
      responses:
        '200': { $ref: '#/components/responses/WorktreeOperation' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/open:
    post:
      operationId: hubOpenWorktree
      tags: [Worktrees]
      summary: Report, and optionally start, an existing checkout
      description: |
        Reports where a registered checkout of this repository family is and
        whether it runs. With `start: true` it also starts that workspace,
        through the Hub's ordinary start with all its recovery fences. A
        failed explicit start does not fail the operation: the workspace
        stays configured and stopped, and `startError` reports why. This
        operation never registers an unregistered checkout and never
        migrates a conversation.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OpenWorktreeRequest' }
            example: { sourceWorkspaceId: atlas, reference: feature/login, start: true }
      responses:
        '200': { $ref: '#/components/responses/WorktreeOperation' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/preflight-delete:
    post:
      operationId: hubPreflightDeleteWorktree
      tags: [Worktrees]
      summary: Check whether a worktree may be deleted
      description: |
        Read-only. Reports the ONE blocker that would refuse a deletion —
        ownership, identity, Git locks, nested worktrees, initialized
        submodules or nested Git repositories, a Git operation in progress or
        known activity — or, when none applies, whether proceeding needs the
        caller's explicit stop authorization (`requiresStop`) and which local
        data would be deleted with the checkout (`localData`). Tracked
        changes, untracked files and ignored files are not blockers here:
        they are disclosed per category with a count, a short sorted sample
        of checkout-relative paths and one `fingerprint` of the complete set,
        which a deletion must echo back as `localDataFingerprint`. Nothing
        here mutates, and no acknowledgement or client-supplied force gets
        past any blocker it finds.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PreflightDeleteWorktreeRequest' }
            example: { sourceWorkspaceId: atlas, reference: feature/login }
      responses:
        '200': { $ref: '#/components/responses/WorktreeDeletionPreflight' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/delete:
    post:
      operationId: hubDeleteWorktree
      tags: [Worktrees]
      summary: Delete a worktree Uatu created
      description: |
        Removes a verified Uatu-created linked checkout after every safety
        check — ownership, identity, Git locks, nested worktrees, initialized
        submodules or nested Git repositories, Git operations in progress,
        known activity and local data — and always keeps the branch.
        `confirm: true` is required; for a checkout without local data it is
        itself the authorization. When the checkout has local data (tracked
        changes, untracked files or ignored files), `localDataFingerprint`
        must also carry the preflight's `localData.fingerprint`: it
        acknowledges deleting exactly that data with the checkout, and is
        re-verified against the checkout at every recheck up to the moment
        before Git runs. Without it the request is refused as `local-data`;
        with a fingerprint that no longer matches, it is refused as
        `local-data` with `retry: refresh`: resending the same request cannot
        succeed, so run `preflight-delete` again and review the data it now
        reports. Removal itself runs under a long bound of its own (ten
        minutes), so a large checkout is not cut off part-way. The acknowledgement never
        overrides any other blocker. There is no client-facing force option;
        the Hub may pass Git a single force only to remove acknowledged
        tracked changes or untracked files, never the double force that
        overrides a Git lock. `stop: true` additionally authorizes stopping
        this worktree's own Uatu sessions; activity Uatu does not own remains
        a blocker. A blocked or failed deletion retains both the checkout and
        its registration, and no branch is ever deleted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeleteWorktreeRequest' }
            examples:
              clean:
                summary: A checkout without local data, running
                value: { sourceWorkspaceId: atlas, reference: feature/login, confirm: true, stop: true }
              acknowledged:
                summary: Acknowledging the local data preflight disclosed
                value: { sourceWorkspaceId: atlas, reference: feature/login, confirm: true, localDataFingerprint: 3f9a1c0e7b52d4e86a0f1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6 }
      responses:
        '200': { $ref: '#/components/responses/WorktreeOperation' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/register:
    post:
      operationId: hubRegisterWorktree
      tags: [Worktrees]
      summary: Register a checkout Git lists but the Hub does not
      description: |
        Covers two cases, told apart by the service against its own pending
        journal rather than by anything the caller states: retrying the
        registration of a Uatu-created checkout that outlived a failed
        registration (`registration-failed`, `retainedCheckoutId` on the
        earlier create's error), and registering an external tree for the
        first time. Either way, nothing is moved, copied or recreated — the
        checkout stays exactly where it is.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RegisterWorktreeRequest' }
            example: { sourceWorkspaceId: atlas, reference: feature/external, start: false }
      responses:
        '200': { $ref: '#/components/responses/WorktreeOperation' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
  /api/hub/worktrees/forget:
    post:
      operationId: hubForgetWorktree
      tags: [Worktrees]
      summary: Remove a workspace from Uatu, keeping the checkout
      description: |
        Unregisters a workspace and nothing else: its checkout, branch,
        files and creation provenance all stay, so registering the same
        verified tree again later recovers its Uatu ownership. Calling this
        operation is itself the authorization to stop the workspace's own
        Uatu sessions first, if it is running — there is no `stop: false`
        variant, and activity Uatu does not own is never claimed to have
        been stopped. A main workspace with registered linked worktrees is
        refused; remove those first.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ForgetWorktreeRequest' }
            example: { sourceWorkspaceId: atlas, reference: feature-login }
      responses:
        '200': { $ref: '#/components/responses/WorktreeOperation' }
        '400': { $ref: '#/components/responses/NoStoreError' }
        '401': { $ref: '#/components/responses/NoStoreError' }
        '403': { $ref: '#/components/responses/NoStoreError' }
        '404': { $ref: '#/components/responses/NoStoreError' }
        '405': { $ref: '#/components/responses/NoStoreError' }
components:
  securitySchemes:
    hubBearer: { type: http, scheme: bearer, description: Session ID returned by native JSON login. }
    hubCookie:
      type: apiKey
      in: cookie
      name: uatu_hub
      description: |
        The browser session cookie. `uatu_hub` is its name when the Hub is reached at the scheme's default port. At any other port the name carries that port as `uatu_hub_<port>`, so `uatu_hub_4701` for a Hub reached at `127.0.0.1:4701`. Browsers scope cookies by host, not port, and Hubs on different ports of one host would otherwise overwrite each other's session. Login sets, every request reads, and sign-out clears the cookie under that name. A bare `uatu_hub` presented at a non-default port is ignored.
  parameters:
    WorkspaceId: { name: workspaceId, in: path, required: true, schema: { $ref: '#/components/schemas/WorkspaceId' } }
    CredentialId: { name: credentialId, in: path, required: true, schema: { $ref: '#/components/schemas/CredentialId' } }
    CredentialTool: { name: tool, in: path, required: true, schema: { $ref: '#/components/schemas/CredentialToolName' } }
    AgentAccountAttemptId: { name: attemptId, in: path, required: true, schema: { type: string, minLength: 1, maxLength: 64 } }
    JobId: { name: jobId, in: path, required: true, schema: { type: string, minLength: 1 } }
    LiveStreamId: { name: streamId, in: path, required: true, schema: { type: string, minLength: 1 }, description: "Opaque id from the stream's hello event." }
  responses:
    Error:
      description: JSON error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: request rejected }
    FolderMutationError:
      description: Folder mutation rejected
      content:
        application/json:
          schema: { $ref: '#/components/schemas/FolderMutationError' }
          example: { error: folder operation failed }
    Accepted:
      description: Request accepted
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Accepted' }
    NoStoreAccepted:
      description: Secret input accepted
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Accepted' } } }
    NoStoreError:
      description: Request failed; the response must not be stored because the request may contain secret input.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    AgentAccounts:
      description: Every chat agent's login state and the logins in progress, without any secret
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountsSnapshot' } } }
    AgentAccountError:
      description: An Agent accounts request failed; `field` names the input it concerns, never its value
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/AgentAccountError' } } }
    Revoked:
      description: Session revoked
      content:
        application/json:
          schema: { $ref: '#/components/schemas/RevocationResult' }
    WorkspaceAction:
      description: Workspace lifecycle result
      content:
        application/json:
          schema: { $ref: '#/components/schemas/WorkspaceAction' }
    CredentialList:
      description: Public credential inventory. Stored private keys, passphrases, and token values are never returned.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialList' } } }
    CredentialToolList:
      description: Credential-tool inventory
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialToolList' } } }
    CredentialPublicKey:
      description: Public key material. No private key material is returned.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialPublicKey' } } }
    Credential:
      description: Public credential metadata. Submitted and stored secrets are never returned.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialResponse' } } }
    CredentialTool:
      description: Credential-tool readiness
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialToolResponse' } } }
    CredentialAssignment:
      description: Persisted advisory workspace assignment
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialAssignmentResponse' } } }
    CredentialAssignments:
      description: Atomically persisted advisory workspace assignments
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialAssignmentsResponse' } } }
    CredentialUnassigned:
      description: Assignment removal result
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialUnassignedResponse' } } }
    CredentialTest:
      description: Bounded, sanitized readiness results
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialTestResponse' } } }
    CredentialDeleted:
      description: Credential deletion result
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/CredentialDeletedResponse' } } }
    WorktreeInventory:
      description: Authoritative Git worktree inventory for one repository family
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/WorktreeInventoryResponse' }
          example:
            inventory:
              repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e
              sourceWorkspaceId: atlas
              status: ready
              checkouts:
                - { checkoutId: 9a1b, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, workspaceId: atlas, path: /src/atlas, branch: main, detached: false, main: true, ownership: main, availability: present, registered: true, running: true, locked: false }
                - { checkoutId: 7c2d, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, workspaceId: feature-login, parentWorkspaceId: atlas, path: /src/atlas.worktrees/feature-login, branch: feature/login, detached: false, main: false, ownership: uatu, availability: present, registered: true, running: false, locked: false, sourceRef: main }
              refs: { local: [main, release], remote: [origin/main], fetchedAt: null }
    WorktreeOperation:
      description: |
        The operation's own outcome. A refusal is a completed request and
        answers 200 with `ok: false` and a sanitized error; HTTP statuses
        report transport-level problems only.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/WorktreeOperationResult' }
          examples:
            created:
              summary: Created and left stopped
              value:
                ok: true
                operationId: 1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9
                kind: create
                phase: complete
                registered: true
                started: false
                checkout: { checkoutId: 7c2d, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, workspaceId: feature-login, parentWorkspaceId: atlas, path: /src/atlas.worktrees/feature-login, branch: feature/login, detached: false, main: false, ownership: uatu, availability: present, registered: true, running: false, locked: false, sourceRef: main }
            refused:
              summary: Refused without force, naming the existing checkout
              value:
                ok: false
                operationId: 2a3b4c5d-6e7f-4801-9234-5f6e7d8c9b0a
                kind: create
                phase: validating
                error: { code: branch-in-use, message: That branch is checked out in another worktree. Open that checkout instead., retry: open-existing, conflictCheckoutId: 7c2d, phase: validating }
    WorktreeRefs:
      description: |
        A fetch's own outcome. Refs are reported whether or not the fetch
        succeeded, so a failure never looks like an empty repository.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/WorktreeRefsResponse' }
          examples:
            fetched:
              summary: Fetched just now
              value: { ok: true, refs: { local: [main, release], remote: [origin/main, origin/release], fetchedAt: 1758150000000 } }
            refused:
              summary: Refused; the cached listing is reported unchanged
              value:
                ok: false
                error: { code: fetch-authentication, message: Remote fetch is unavailable on this Hub., retry: none }
                refs: { local: [main, release], remote: [origin/main], fetchedAt: null }
    WorktreeDeletionPreflight:
      description: Read-only deletion check. Nothing here mutates.
      headers: { Cache-Control: { schema: { type: string, const: no-store } } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/WorktreeDeletionPreflight' }
          examples:
            deletable:
              summary: Deletable; the checkout is not running
              value:
                ok: true
                requiresStop: false
                checkout: { checkoutId: 7c2d, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, workspaceId: feature-login, parentWorkspaceId: atlas, path: /src/atlas.worktrees/feature-login, branch: feature/login, detached: false, main: false, ownership: uatu, availability: present, registered: true, running: false, locked: false, sourceRef: main }
            localData:
              summary: Deletable once the listed local data is acknowledged
              value:
                ok: true
                requiresStop: false
                checkout: { checkoutId: 7c2d, repositoryId: 5f0c2a9d4e1b7a3c6d8e9f0a1b2c3d4e, workspaceId: feature-login, parentWorkspaceId: atlas, path: /src/atlas.worktrees/feature-login, branch: feature/login, detached: false, main: false, ownership: uatu, availability: present, registered: true, running: false, locked: false, sourceRef: main }
                localData:
                  tracked: { count: 2, sample: [README.md, src/app.ts] }
                  untracked: { count: 7, sample: [notes/a.md, notes/b.md, notes/c.md, notes/d.md, scratch.txt] }
                  ignored: { count: 2, sample: [.env, node_modules/] }
                  fingerprint: 3f9a1c0e7b52d4e86a0f1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6
            blocked:
              summary: Refused by a blocker; nothing was removed
              value:
                ok: false
                error: { code: git-lock, message: "This worktree is locked in Git. Unlock it outside Uatu if it is safe, then retry. Nothing was removed.", retry: retry-delete }
  schemas:
    NotificationEnrollment:
      type: object
      required: [subscription, workspaceIds, needsAnswer, completed]
      properties:
        id: { type: string, maxLength: 128 }
        subscription:
          type: object
          required: [endpoint, keys]
          properties:
            endpoint: { type: string, format: uri, maxLength: 4096 }
            expirationTime: { type: [number, 'null'] }
            keys:
              type: object
              required: [p256dh, auth]
              properties:
                p256dh: { type: string }
                auth: { type: string }
        workspaceIds: { type: array, maxItems: 256, items: { type: string } }
        allWorkspaces:
          type: boolean
          default: false
          description: Cover every accessible workspace, including ones registered later, instead of `workspaceIds`.
        needsAnswer: { type: boolean }
        completed: { type: boolean }
    NotificationState:
      type: object
      required: [configured, publicKey, device]
      additionalProperties: false
      properties:
        configured: { type: boolean }
        publicKey: { type: [string, 'null'] }
        device:
          oneOf:
            - { type: 'null' }
            - type: object
              required: [id, allWorkspaces, workspaceIds, needsAnswer, completed, active]
              additionalProperties: false
              properties:
                id: { type: string }
                allWorkspaces: { type: boolean, description: True when the device covers every accessible workspace as a standing rule; `workspaceIds` then holds the explicit selection it overrides. }
                workspaceIds: { type: array, items: { type: string } }
                needsAnswer: { type: boolean }
                completed: { type: boolean }
                active: { type: boolean }
    Error:
      type: object
      required: [error]
      properties: { error: { type: string }, needsInit: { type: boolean } }
      additionalProperties: false
    Accepted:
      type: object
      required: [accepted]
      properties: { accepted: { type: boolean, const: true } }
      additionalProperties: false
    RevocationResult:
      type: object
      required: [revoked]
      properties: { revoked: { type: boolean }, current: { type: boolean } }
      additionalProperties: false
    WorkspaceId: { type: string, pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' }
    ViewMode: { enum: [rendered, source, diff] }
    PersonalState:
      type: object
      required: [version]
      properties: { version: { const: 1 }, documentPath: { type: string }, follow: { type: boolean }, previewMode: { $ref: '#/components/schemas/ViewMode' }, compareTarget: { enum: [base, last-commit] }, filesFilter: { enum: [all, changed] }, lastPtyId: { type: string, format: uuid } }
      additionalProperties: false
    PersonalStatePatch:
      type: object
      properties: { version: { const: 1 }, documentPath: { type: [string, 'null'] }, follow: { type: [boolean, 'null'] }, previewMode: { oneOf: [{ $ref: '#/components/schemas/ViewMode' }, { type: 'null' }] }, compareTarget: { enum: [base, last-commit, null] }, filesFilter: { enum: [all, changed, null] }, lastPtyId: { oneOf: [{ type: string, format: uuid }, { type: 'null' }] } }
      additionalProperties: false
    CredentialId: { type: string, minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9_-]+$' }
    CredentialToolName: { enum: [ssh, ssh-agent, ssh-add, ssh-keygen, gpg, gpgconf, git, gh, glab] }
    NativeLoginRequest:
      type: object
      required: [name, password]
      properties: { name: { type: string }, password: { type: string }, deviceLabel: { type: string } }
      additionalProperties: false
    NativeLoginResponse:
      type: object
      required: [sessionId, user]
      properties: { sessionId: { type: string }, user: { type: string } }
      additionalProperties: false
    CompatibilityMetadata:
      type: object
      required: [hubApiRevision, workspaceApiRevision]
      properties: { hubApiRevision: { type: integer, minimum: 1 }, workspaceApiRevision: { type: integer, minimum: 1 } }
      additionalProperties: false
      description: Public Hub and proxied workspace compatibility identities.
    HubState:
      type: object
      required: [version, hubApiRevision, workspaceApiRevision, workspaces]
      properties:
        version: { type: string }
        hubApiRevision: { type: integer, minimum: 1 }
        workspaceApiRevision: { type: integer, minimum: 1 }
        workspaces:
          type: array
          items: { $ref: '#/components/schemas/HubWorkspace' }
        workspaceDefaults: { $ref: '#/components/schemas/WorkspaceDefaults' }
        worktreeApi: { type: string, minLength: 1, description: "Present when this Hub serves worktree operations: the origin-rooted path of the published JSON family (GET .../worktrees?source=, POST .../worktrees/{fetch,create,open,preflight-delete,delete,register,forget}). The workspace picker and dashboard mount their worktree affordances from this field's presence alone; no server-rendered worktree URL is published." }
      additionalProperties: false
    HubWorkspace:
      type: object
      required: [id, displayName, path, backend, running, credentialRestartRequired, credentialAssignments, workspaceApiRevision]
      properties:
        id: { $ref: '#/components/schemas/WorkspaceId' }
        displayName: { $ref: '#/components/schemas/WorkspaceDisplayName' }
        path: { type: string }
        backend: { type: string, const: local }
        running: { type: boolean }
        credentialRestartRequired: { type: boolean, description: "Whether changed credential assignments require this running workspace to restart before its generated credential configuration is current." }
        credentialAssignments: { $ref: '#/components/schemas/WorkspaceCredentialAssignmentSummary' }
        workspaceApiRevision: { type: integer, minimum: 1, description: "Workspace API revision spoken by this workspace's child. Matches the top-level workspaceApiRevision for the local backend; backends running children of other builds report the child's own revision." }
        shells: { type: array, items: { type: object, required: [attached, label], properties: { attached: { type: boolean }, label: { type: string } }, additionalProperties: false } }
        parentId: { $ref: '#/components/schemas/WorkspaceId', description: "Present for a registered linked worktree. The main workspace it belongs to, whose credentials and shared configuration it inherits live. Its own `credentialAssignments` stay its own rows, normally none; read the parent's for the inherited policy." }
        repositoryId: { type: string, minLength: 1, description: "Opaque canonical repository identity shared by a main workspace and its linked worktrees. Group by this and `parentId`, never by names or paths. Absent when unknown." }
        branch: { type: string, minLength: 1, description: "The checkout's current local branch, read from its HEAD. For a linked worktree it is also its name. Absent when detached or unknown." }
        detached: { type: boolean, const: true, description: "Present when the checkout's HEAD is detached." }
        sourceRef: { type: string, minLength: 1, description: "Linked worktrees only. The recorded ref the branch was created from, when Uatu created it. Never inferred from upstream or history; absent means origin unknown." }
        ownership: { type: string, enum: [uatu, external, uncertain], description: "Linked worktrees only. `uatu` for a checkout with verified Uatu creation provenance, which alone can be deleted through Uatu." }
        availability: { type: string, enum: [missing, replaced], description: "Linked worktrees only, present when the registered checkout is not at its path (`missing`) or the path holds a different checkout (`replaced`). Such a workspace cannot start until resolved outside Uatu." }
        createWorktree: { type: boolean, const: true, description: "Present and true for a main checkout that can host linked worktrees. Construct the fork's create request against worktreeApi (mode: new-branch or existing-local/remote-tracking) with sourceWorkspaceId set to this workspace's id; no URL is published here." }
      additionalProperties: false
    WorkspaceCredentialAssignmentSummary:
      type: object
      required: [authentication, signing]
      properties:
        authentication: { type: array, uniqueItems: true, items: { type: string, minLength: 1 }, description: "Deduplicated public names of credentials assigned for Git authentication, regardless of current usability." }
        signing: { type: array, uniqueItems: true, items: { type: string, minLength: 1 }, description: "Deduplicated public names of credentials assigned for commit signing, regardless of current usability." }
      additionalProperties: false
    BrowseResult:
      type: object
      required: [path, parent, dirs]
      properties:
        path: { type: string }
        parent: { type: [string, 'null'] }
        dirs:
          type: array
          items: { type: object, required: [name, git, registeredId, displayName, running], properties: { name: { type: string }, git: { type: boolean }, registeredId: { oneOf: [{ $ref: '#/components/schemas/WorkspaceId' }, { type: 'null' }] }, displayName: { oneOf: [{ $ref: '#/components/schemas/WorkspaceDisplayName' }, { type: 'null' }], description: "Registered workspace display name; null for unregistered directories." }, running: { type: boolean, description: "Whether the registered workspace currently has a live session. Always false for unregistered directories." } }, additionalProperties: false }
      additionalProperties: false
    FolderName:
      type: string
      minLength: 1
      pattern: '^(?!\.)(?![\s\S]*[\\/\p{Cc}\p{Cf}])[\s\S]*\S[\s\S]*$'
      description: >-
        A visible, non-hidden single path segment. Dot-prefixed names, path separators, control characters
        (Cc — the C0 range, DEL, and the C1 range U+0080–U+009F), Unicode format characters (Cf — the
        zero-width family, the bidi embedding and override controls, the BOM, and the tag characters), and
        names made only of whitespace or format characters are rejected: none of them survive a nonempty
        check, and a bidi control can make a folder display a name other than the path it occupies. The
        pattern is a Unicode-mode expression, as JSON Schema requires, so its `\p{Cc}` and `\p{Cf}` escapes
        cover the whole categories, format characters above the BMP included; it accepts exactly the names the
        server accepts.
    CreateFolderRequest:
      type: object
      required: [parent, name]
      properties:
        parent: { type: string, minLength: 1, description: Absolute path of the existing direct parent directory. }
        name: { $ref: '#/components/schemas/FolderName' }
      additionalProperties: false
    RenameFolderRequest:
      type: object
      required: [path, name]
      properties:
        path: { type: string, minLength: 1, description: Absolute path of the direct non-symbolic-link directory to rename. }
        name: { $ref: '#/components/schemas/FolderName' }
        stop: { type: boolean, default: false, description: Authorizes stopping all affected workspace sessions before mutation. }
      additionalProperties: false
    RemoveFolderRequest:
      type: object
      required: [path]
      properties:
        path: { type: string, minLength: 1, description: Absolute path of the empty direct non-symbolic-link directory to remove. }
        stop: { type: boolean, default: false, description: Authorizes stopping the affected workspace session before mutation. }
      additionalProperties: false
    CreateFolderResult:
      type: object
      required: [path]
      properties: { path: { type: string, minLength: 1, description: Absolute path of the created folder. } }
      additionalProperties: false
    RenameFolderResult:
      type: object
      required: [path, workspaceIds]
      properties:
        path: { type: string, minLength: 1, description: Absolute destination path. }
        workspaceIds: { type: array, uniqueItems: true, items: { $ref: '#/components/schemas/WorkspaceId' }, description: Stable ids of every registration moved with the folder. }
      additionalProperties: false
    RemoveFolderResult:
      type: object
      required: [path]
      properties:
        path: { type: string, minLength: 1, description: Absolute path removed by the operation. }
        workspaceId: { $ref: '#/components/schemas/WorkspaceId', description: Stable id of the registration removed with the folder, when one existed. }
      additionalProperties: false
    FolderMutationError:
      type: object
      required: [error]
      properties: { error: { type: string, minLength: 1 } }
      additionalProperties: false
    FolderStopConflict:
      type: object
      required: [error, needsStop, workspaceIds]
      properties:
        error: { type: string, minLength: 1 }
        needsStop: { type: boolean, const: true }
        workspaceIds: { type: array, minItems: 1, uniqueItems: true, items: { $ref: '#/components/schemas/WorkspaceId' }, description: Complete list of running or starting affected workspaces. }
      additionalProperties: false
    CreateWorkspaceRequest:
      type: object
      required: [path]
      properties: { path: { type: string }, init: { type: boolean }, start: { type: boolean, default: true } }
      additionalProperties: false
    WorkspaceConflict:
      allOf: [{ $ref: '#/components/schemas/Error' }]
    WorkspaceAction:
      type: object
      required: [id]
      properties:
        id: { $ref: '#/components/schemas/WorkspaceId' }
        running: { type: boolean }
        wasRunning: { type: boolean }
        forgotten: { type: boolean }
        recoveryRequired: { type: string, description: "Registration only: the workspace committed but its recovery journal could not be cleared. The workspace is registered and stopped — any requested start was not attempted — and the Hub needs a restart before starts, assignment changes, folder mutations, and further registration resume." }
      additionalProperties: false
    WorkspaceDisplayName:
      type: string
      minLength: 1
      maxLength: 64
      description: Mutable human-facing workspace label. Trimmed, 1-64 visible characters, no control characters. Never unique and never part of URLs; the stable workspace id is the routing identity.
    AuthenticationSelection:
      type: object
      required: [credentialId, host]
      properties:
        credentialId: { $ref: '#/components/schemas/CredentialId' }
        host: { type: string, minLength: 1, description: Normalized provider host this credential becomes the authentication default for. }
      additionalProperties: false
    ConfigureWorkspaceRequest:
      type: object
      required: [path, displayName]
      properties:
        path: { type: string, minLength: 1, description: Absolute path of the existing direct non-symbolic-link folder to register. }
        displayName: { $ref: '#/components/schemas/WorkspaceDisplayName' }
        authentication: { type: array, items: { $ref: '#/components/schemas/AuthenticationSelection' }, description: At most one default per provider host. }
        signing: { oneOf: [{ $ref: '#/components/schemas/CredentialId' }, { type: 'null' }], description: Default commit-signing credential. }
        init: { type: boolean, default: false, description: Explicit confirmation to run `git init` on a non-Git folder. }
        start: { type: boolean, default: false, description: "Explicit start intent. Omitted or false, the workspace finishes registered and stopped." }
      additionalProperties: false
    CreateConfiguredWorkspaceRequest:
      type: object
      required: [parent, folderName, displayName]
      properties:
        parent: { type: string, minLength: 1, description: Absolute path of the existing direct parent directory. }
        folderName: { $ref: '#/components/schemas/FolderName' }
        displayName: { $ref: '#/components/schemas/WorkspaceDisplayName' }
        authentication: { type: array, items: { $ref: '#/components/schemas/AuthenticationSelection' } }
        signing: { oneOf: [{ $ref: '#/components/schemas/CredentialId' }, { type: 'null' }] }
        start: { type: boolean, default: false }
      additionalProperties: false
    OnboardedWorkspace:
      type: object
      required: [id, displayName, path, backend, running]
      properties:
        id: { $ref: '#/components/schemas/WorkspaceId' }
        displayName: { $ref: '#/components/schemas/WorkspaceDisplayName' }
        path: { type: string }
        backend: { type: string, const: local }
        running: { type: boolean }
      additionalProperties: false
    WorkspaceOnboardingResult:
      type: object
      required: [workspace, started, startError]
      properties:
        workspace: { $ref: '#/components/schemas/OnboardedWorkspace' }
        started: { type: boolean }
        startError: { type: [string, 'null'], description: "Non-null exactly when an explicitly requested start failed after the configuration committed; the workspace is preserved stopped for retry." }
        recoveryRequired: { type: string, description: "Present when the configuration committed but its recovery journal could not be cleared: the workspace is preserved stopped, and the Hub needs a restart before starts, assignment changes, folder mutations, and further onboarding resume." }
      additionalProperties: false
    WorkspaceOnboardingError:
      type: object
      required: [error]
      properties:
        error: { type: string, minLength: 1 }
        needsInit: { type: boolean, const: true, description: The folder is not a Git repository; re-submit with init true after user confirmation. }
        retainedPath: { type: string, description: "A created repository was initialized before the failure and is retained at this path; retry through the Existing folder flow." }
      additionalProperties: false
    UpdateWorkspaceDisplayNameRequest:
      type: object
      required: [displayName]
      properties: { displayName: { $ref: '#/components/schemas/WorkspaceDisplayName' } }
      additionalProperties: false
    WorkspaceSummaryResponse:
      type: object
      required: [workspace]
      properties: { workspace: { $ref: '#/components/schemas/OnboardedWorkspace' } }
      additionalProperties: false
    WorkspaceDefaults:
      type: object
      required: [configured, configuredAvailable, effective]
      properties:
        configured: { type: [string, 'null'], description: "The saved canonical default workspace parent, retained for diagnosis even while unavailable." }
        configuredAvailable: { type: boolean, description: Whether the configured directory is currently a readable direct directory. }
        effective: { type: string, description: "Where pathless browse/create/clone flows start: the configured parent while usable, the daemon user's home otherwise." }
      additionalProperties: false
    UpdateWorkspaceDefaultsRequest:
      type: object
      required: [defaultWorkspaceParent]
      properties:
        defaultWorkspaceParent: { type: [string, 'null'], description: "Absolute path of an existing direct non-symbolic-link directory, or null to clear the preference." }
      additionalProperties: false
    DeviceSessionList:
      type: object
      required: [sessions]
      properties:
        sessions: { type: array, items: { $ref: '#/components/schemas/DeviceSession' } }
      additionalProperties: false
    DeviceSession:
      type: object
      required: [handle, deviceLabel, issuedAt, current]
      properties: { handle: { type: string }, deviceLabel: { type: string }, issuedAt: { type: integer }, current: { type: boolean } }
      additionalProperties: false
    ReadinessResult:
      type: object
      required: [layer, status, message]
      properties:
        layer: { enum: [binary, version, runtime, credential, capability] }
        status: { enum: [ready, unavailable, not-applicable] }
        message: { type: string, minLength: 1 }
      additionalProperties: false
    AuthenticationCredentialAssignment:
      type: object
      required: [workspaceId, credentialId, role, host]
      properties:
        workspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        credentialId: { $ref: '#/components/schemas/CredentialId' }
        role: { const: authentication }
        host: { type: string, minLength: 1, description: "Normalized HTTPS provider host, optionally including a port." }
      additionalProperties: false
    SigningCredentialAssignment:
      type: object
      required: [workspaceId, credentialId, role]
      properties:
        workspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        credentialId: { $ref: '#/components/schemas/CredentialId' }
        role: { const: signing }
      additionalProperties: false
    CredentialAssignment:
      oneOf:
        - { $ref: '#/components/schemas/AuthenticationCredentialAssignment' }
        - { $ref: '#/components/schemas/SigningCredentialAssignment' }
      description: Advisory normal-tool selection for the local backend; same-UID processes are not isolated by this assignment.
    PublicSshCredential:
      type: object
      required: [id, name, type, capabilities, enabled, createdAt, metadata, assignments, readiness]
      properties:
        id: { $ref: '#/components/schemas/CredentialId' }
        name: { type: string, minLength: 1 }
        type: { const: ssh }
        capabilities: { type: array, minItems: 1, uniqueItems: true, items: { enum: [ssh-authentication, ssh-signing] } }
        enabled: { type: boolean }
        createdAt: { type: string, format: date-time }
        metadata:
          type: object
          required: [publicKey, fingerprint]
          properties: { publicKey: { type: string, minLength: 1 }, fingerprint: { type: string, minLength: 1 } }
          additionalProperties: false
        assignments: { type: array, items: { $ref: '#/components/schemas/CredentialAssignment' } }
        readiness: { type: array, items: { $ref: '#/components/schemas/ReadinessResult' } }
      additionalProperties: false
    PublicOpenPgpCredential:
      type: object
      required: [id, name, type, capabilities, enabled, createdAt, metadata, assignments, readiness]
      properties:
        id: { $ref: '#/components/schemas/CredentialId' }
        name: { type: string, minLength: 1 }
        type: { const: openpgp }
        capabilities: { type: array, minItems: 1, maxItems: 1, uniqueItems: true, items: { const: openpgp-signing } }
        enabled: { type: boolean }
        createdAt: { type: string, format: date-time }
        metadata:
          type: object
          required: [publicKey, fingerprint]
          properties: { publicKey: { type: string, minLength: 1 }, fingerprint: { type: string, minLength: 1 } }
          additionalProperties: false
        assignments: { type: array, items: { $ref: '#/components/schemas/CredentialAssignment' } }
        readiness: { type: array, items: { $ref: '#/components/schemas/ReadinessResult' } }
      additionalProperties: false
    PublicTokenCredential:
      type: object
      required: [id, name, type, capabilities, enabled, createdAt, metadata, assignments, readiness]
      properties:
        id: { $ref: '#/components/schemas/CredentialId' }
        name: { type: string, minLength: 1 }
        type: { const: token }
        capabilities: { type: array, minItems: 1, uniqueItems: true, items: { enum: [https-git, github-cli, gitlab-cli] } }
        enabled: { type: boolean }
        createdAt: { type: string, format: date-time }
        metadata:
          type: object
          required: [host]
          properties: { host: { type: string, minLength: 1 }, username: { type: string, minLength: 1 } }
          additionalProperties: false
        assignments: { type: array, items: { $ref: '#/components/schemas/CredentialAssignment' } }
        readiness: { type: array, items: { $ref: '#/components/schemas/ReadinessResult' } }
      additionalProperties: false
    PublicCredential:
      oneOf:
        - { $ref: '#/components/schemas/PublicSshCredential' }
        - { $ref: '#/components/schemas/PublicOpenPgpCredential' }
        - { $ref: '#/components/schemas/PublicTokenCredential' }
      description: Explicit public DTO. It never contains a private key, passphrase, token value, secret path, or reusable agent credential.
    PublicCredentialTool:
      type: object
      required: [tool, path, version, results, guidance]
      properties:
        tool: { $ref: '#/components/schemas/CredentialToolName' }
        path: { type: [string, 'null'] }
        version: { type: [string, 'null'] }
        results: { type: array, items: { $ref: '#/components/schemas/ReadinessResult' } }
        guidance: { type: [string, 'null'] }
      additionalProperties: false
    CredentialList: { type: object, required: [credentials], properties: { credentials: { type: array, items: { $ref: '#/components/schemas/PublicCredential' } } }, additionalProperties: false }
    CredentialToolList: { type: object, required: [tools], properties: { tools: { type: array, items: { $ref: '#/components/schemas/PublicCredentialTool' } } }, additionalProperties: false }
    CredentialPublicKey:
      type: object
      required: [id, type, publicKey, fingerprint]
      properties: { id: { $ref: '#/components/schemas/CredentialId' }, type: { enum: [ssh, openpgp] }, publicKey: { type: string, minLength: 1 }, fingerprint: { type: string, minLength: 1 } }
      additionalProperties: false
    CredentialResponse: { type: object, required: [credential], properties: { credential: { $ref: '#/components/schemas/PublicCredential' } }, additionalProperties: false }
    CredentialToolResponse: { type: object, required: [tool], properties: { tool: { $ref: '#/components/schemas/PublicCredentialTool' } }, additionalProperties: false }
    CredentialAssignmentResponse: { type: object, required: [assignment], properties: { assignment: { $ref: '#/components/schemas/CredentialAssignment' } }, additionalProperties: false }
    CredentialAssignmentsResponse: { type: object, required: [assignments], properties: { assignments: { type: array, minItems: 1, maxItems: 2, items: { $ref: '#/components/schemas/CredentialAssignment' } } }, additionalProperties: false }
    CredentialUnassignedResponse: { type: object, required: [removed], properties: { removed: { type: boolean } }, additionalProperties: false }
    CredentialTestResponse: { type: object, required: [results], properties: { results: { type: array, items: { $ref: '#/components/schemas/ReadinessResult' } } }, additionalProperties: false }
    CredentialDeletedResponse: { type: object, required: [deleted], properties: { deleted: { type: boolean } }, additionalProperties: false }
    Passphrase: { type: string, minLength: 1, maxLength: 4096, x-uatu-maxUtf8Bytes: 4096, description: One-operation secret; never persisted or returned. }
    GenerateSshCredentialRequest:
      type: object
      required: [name, capabilities, passphrase]
      properties: { name: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, capabilities: { type: array, minItems: 1, uniqueItems: true, items: { enum: [ssh-authentication, ssh-signing] } }, passphrase: { $ref: '#/components/schemas/Passphrase' } }
      additionalProperties: false
    ImportSshCredentialRequest:
      type: object
      required: [name, capabilities, privateKey, passphrase]
      properties: { name: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, capabilities: { type: array, minItems: 1, uniqueItems: true, items: { enum: [ssh-authentication, ssh-signing] } }, privateKey: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 1048576 }, passphrase: { type: string, maxLength: 4096, x-uatu-maxUtf8Bytes: 4096, description: "Existing key passphrase, or empty when the imported key is not encrypted. Never persisted or returned." } }
      additionalProperties: false
    GenerateOpenPgpCredentialRequest:
      type: object
      required: [name, userId, passphrase]
      properties: { name: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, userId: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, passphrase: { $ref: '#/components/schemas/Passphrase' } }
      additionalProperties: false
    ImportOpenPgpCredentialRequest:
      type: object
      required: [name, privateKey]
      properties: { name: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, privateKey: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 1048576 } }
      additionalProperties: false
    CreateTokenCredentialRequest:
      type: object
      required: [name, host, token, capabilities]
      properties: { name: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, host: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, username: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, token: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 65536 }, capabilities: { type: array, minItems: 1, uniqueItems: true, items: { enum: [https-git, github-cli, gitlab-cli] } } }
      additionalProperties: false
    SetCredentialToolRequest: { type: object, required: [path], properties: { path: { type: [string, 'null'], x-uatu-maxUtf8Bytes: 32768 } }, additionalProperties: false }
    UnlockCredentialRequest: { type: object, required: [passphrase], properties: { passphrase: { type: string, maxLength: 4096, x-uatu-maxUtf8Bytes: 4096, description: "One-operation secret. Empty is valid for an unencrypted SSH key; OpenPGP unlock still requires a nonempty passphrase. Never persisted or returned." } }, additionalProperties: false }
    AuthenticationAssignmentRequest:
      type: object
      required: [workspaceId, role, host]
      properties: { workspaceId: { $ref: '#/components/schemas/WorkspaceId' }, role: { const: authentication }, host: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, replace: { type: boolean } }
      additionalProperties: false
    SigningAssignmentRequest:
      type: object
      required: [workspaceId, role]
      properties: { workspaceId: { $ref: '#/components/schemas/WorkspaceId' }, role: { const: signing }, replace: { type: boolean } }
      additionalProperties: false
    AssignCredentialRequest: { oneOf: [{ $ref: '#/components/schemas/AuthenticationAssignmentRequest' }, { $ref: '#/components/schemas/SigningAssignmentRequest' }] }
    AssignWorkspaceCredentialsRequest:
      type: object
      minProperties: 1
      properties:
        authentication: { type: object, required: [credentialId, host], properties: { credentialId: { $ref: '#/components/schemas/CredentialId' }, host: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 } }, additionalProperties: false }
        signing: { type: object, required: [credentialId], properties: { credentialId: { $ref: '#/components/schemas/CredentialId' } }, additionalProperties: false }
      additionalProperties: false
    UnassignCredentialRequest:
      oneOf:
        - { type: object, required: [workspaceId, role, host], properties: { workspaceId: { $ref: '#/components/schemas/WorkspaceId' }, role: { const: authentication }, host: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }, stop: { type: boolean } }, additionalProperties: false }
        - { type: object, required: [workspaceId, role], properties: { workspaceId: { $ref: '#/components/schemas/WorkspaceId' }, role: { const: signing }, stop: { type: boolean } }, additionalProperties: false }
        - { type: object, required: [workspaceId], properties: { workspaceId: { $ref: '#/components/schemas/WorkspaceId' }, stop: { type: boolean } }, additionalProperties: false }
    DeleteCredentialRequest: { type: object, required: [confirm], properties: { confirm: { type: boolean, const: true }, unassign: { type: boolean } }, additionalProperties: false }
    CreateCloneJobRequest:
      type: object
      required: [url, dest]
      properties:
        url: { type: string }
        dest: { type: string }
        folderName: { type: string, description: "Checkout folder name, independent of the workspace display name, trimmed before use. Absent, blank, or whitespace-only derives it from the clone URL. Any other value must be one visible path segment — `.`, `..`, path separators, control characters, and Unicode format characters (Cf) answer 400 — but, unlike FolderName, a dot-prefixed checkout is accepted." }
        credentialId: { $ref: '#/components/schemas/CredentialId', description: Clone authentication identity. Never retained as a workspace assignment unless explicitly requested. }
        retainAssignment: { type: boolean, default: false, description: Explicitly retain the clone credential as the workspace authentication default for its host. }
        displayName: { $ref: '#/components/schemas/WorkspaceDisplayName', description: Workspace display name; defaults from the checkout folder name. }
        retainedAuthentication: { type: array, items: { $ref: '#/components/schemas/AuthenticationSelection' }, description: "Workspace authentication defaults committed with the registration, independent of the clone identity." }
        signing: { oneOf: [{ $ref: '#/components/schemas/CredentialId' }, { type: 'null' }], description: Default commit-signing credential committed with the registration. }
        start: { type: boolean, default: false, description: "Explicit start-after-clone intent. Omitted or false, a successful clone finishes as a registered stopped workspace." }
      dependentSchemas:
        retainAssignment:
          if: { properties: { retainAssignment: { const: true } } }
          then: { properties: { credentialId: { $ref: '#/components/schemas/CredentialId' } }, required: [credentialId] }
      additionalProperties: false
    CloneJob:
      type: object
      required: [jobId]
      properties: { jobId: { type: string } }
      additionalProperties: false
    CloneJobInput:
      type: object
      required: [input]
      properties: { input: { type: string, description: "At most 8192 bytes UTF-8 encoded (a byte limit, not a character count); larger payloads are rejected with 413." } }
      additionalProperties: false
    CloneJobCancelResult:
      type: object
      required: [status]
      properties: { status: { enum: [cancelled, terminal] } }
      additionalProperties: false
    EmptyObject: { type: object, additionalProperties: false }
    AgentAccountError:
      type: object
      required: [error]
      properties:
        error: { type: string, description: "What went wrong, safe to show. Never contains a submitted key, code, or address." }
        field: { type: string, maxLength: 256, description: "The submitted field the error concerns (`key`, `code`, `address`, or a method field's key)." }
      additionalProperties: false
    AgentAccountId: { enum: [opencode, claude] }
    AgentAccountAnswers:
      type: object
      description: Answers to a login method's extra fields, as strings; the Hub converts each to the field's `valueType`.
      maxProperties: 32
      additionalProperties: { type: string, x-uatu-maxUtf8Bytes: 4096 }
    AgentAccountKeyRequest:
      type: object
      required: [agent, target, method, key]
      properties:
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        target: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
        method: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
        key: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 65536, description: "One-operation secret. Handed to the agent; never persisted or returned by the Hub." }
        answers: { $ref: '#/components/schemas/AgentAccountAnswers' }
      additionalProperties: false
    AgentAccountLoginRequest:
      type: object
      required: [agent, target, method]
      properties:
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        target: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256, description: "The OpenCode provider or integration id; `claude` for Claude Code." }
        method: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
        answers: { $ref: '#/components/schemas/AgentAccountAnswers' }
      additionalProperties: false
    AgentAccountCodeRequest:
      type: object
      required: [code]
      properties: { code: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 8192, description: "One-operation secret: the code the sign-in page showed. Never persisted or returned." } }
      additionalProperties: false
    AgentAccountRedirectRequest:
      type: object
      required: [address]
      properties: { address: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 8192, description: "The full address the browser landed on. One-operation secret: never persisted or returned." } }
      additionalProperties: false
    AgentAccountLogoutRequest:
      type: object
      required: [agent, target, credential]
      properties:
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        target: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
        credential: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
      additionalProperties: false
    AgentAccountActivateRequest:
      type: object
      required: [agent, credential]
      properties:
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        credential: { type: string, minLength: 1, x-uatu-maxUtf8Bytes: 256 }
      additionalProperties: false
    AgentAccountFieldCondition:
      type: object
      required: [key, op, value]
      properties: { key: { type: string }, op: { enum: [eq, neq] }, value: { type: string } }
      additionalProperties: false
    AgentAccountField:
      type: object
      required: [key, label, kind, valueType, required]
      description: One extra field a login method asks for. Shown only while every `when` condition holds against the earlier answers.
      properties:
        key: { type: string }
        label: { type: string }
        kind: { enum: [text, select] }
        valueType: { enum: [string, number, boolean, list], description: "How the Hub converts the submitted string; `list` is comma-separated." }
        required: { type: boolean }
        secret: { type: boolean, description: "The agent marked the answer as sensitive. Clients mask it and do not keep it after submitting." }
        placeholder: { type: string }
        description: { type: string }
        options:
          type: array
          items: { type: object, required: [value, label], properties: { value: { type: string }, label: { type: string }, hint: { type: string } }, additionalProperties: false }
        when: { type: array, items: { $ref: '#/components/schemas/AgentAccountFieldCondition' } }
      additionalProperties: false
    AgentAccountCompletion: { enum: [device, code, redirect], description: "`device`: approve on the provider's site, nothing to paste. `code`: paste the code the sign-in page shows. `redirect`: the sign-in redirects to a loopback address on the Hub machine; from another device, paste the address the browser landed on." }
    AgentAccountKeyMethod:
      type: object
      required: [id, kind, label, fields]
      properties: { id: { type: string }, kind: { const: key }, label: { type: string }, fields: { type: array, items: { $ref: '#/components/schemas/AgentAccountField' } } }
      additionalProperties: false
    AgentAccountOAuthMethod:
      type: object
      required: [id, kind, label, fields]
      properties: { id: { type: string }, kind: { const: oauth }, label: { type: string }, fields: { type: array, items: { $ref: '#/components/schemas/AgentAccountField' } }, completion: { $ref: '#/components/schemas/AgentAccountCompletion' } }
      additionalProperties: false
    AgentAccountEnvMethod:
      type: object
      required: [id, kind, label, variables]
      description: The agent reads a variable from its environment; set it in the Hub's environment.
      properties: { id: { type: string }, kind: { const: env }, label: { type: string }, variables: { type: array, items: { type: string } } }
      additionalProperties: false
    AgentAccountCommandMethod:
      type: object
      required: [id, kind, label, command]
      description: The agent runs a local helper the Hub does not run; log in with `command` on the Hub machine.
      properties: { id: { type: string }, kind: { const: command }, label: { type: string }, command: { type: string } }
      additionalProperties: false
    AgentAccountMethod:
      oneOf:
        - { $ref: '#/components/schemas/AgentAccountKeyMethod' }
        - { $ref: '#/components/schemas/AgentAccountOAuthMethod' }
        - { $ref: '#/components/schemas/AgentAccountEnvMethod' }
        - { $ref: '#/components/schemas/AgentAccountCommandMethod' }
    AgentAccountCredential:
      type: object
      required: [id, label, kind, active, removable]
      properties:
        id: { type: string }
        label: { type: string }
        kind: { enum: [key, oauth, saved, env, config, other], description: "`key`/`oauth`: saved by a key or browser login. `saved`: saved by a login whose kind the agent does not report. `env`: read from the agent's environment. `config`: set in the agent's configuration. `other`: anything else, such as a built-in provider." }
        active: { type: boolean }
        removable: { type: boolean }
        variables: { type: array, items: { type: string } }
        status: { type: string, description: "The agent's own message about the credential, such as needing a new login." }
      additionalProperties: false
    AgentAccountTarget:
      type: object
      required: [id, name, connected, credentials, methods]
      description: One OpenCode provider (1.x) or integration (2.x).
      properties:
        id: { type: string }
        name: { type: string }
        connected: { type: boolean }
        credentials: { type: array, items: { $ref: '#/components/schemas/AgentAccountCredential' } }
        methods: { type: array, items: { $ref: '#/components/schemas/AgentAccountMethod' } }
      additionalProperties: false
    ClaudeAccountState:
      type: object
      required: [source]
      properties:
        source: { enum: [subscription, console, env-api-key, env-token, api-key-helper, third-party, none, unknown] }
        email: { type: string }
        organization: { type: string }
        plan: { type: string }
        provider: { type: string, description: "The API backend when it is not Anthropic's own." }
      additionalProperties: false
    AgentAccountStatus:
      type: object
      required: [agent, name, state, capabilities, targets, methods, loginCommand]
      properties:
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        name: { type: string }
        state: { enum: [idle, starting, ready, not-installed, unavailable] }
        version: { type: string }
        generation: { enum: [1, 2] }
        message: { type: string }
        capabilities:
          type: object
          required: [login, logout, activate]
          properties: { login: { type: boolean }, logout: { type: boolean }, activate: { type: boolean } }
          additionalProperties: false
        targets: { type: array, items: { $ref: '#/components/schemas/AgentAccountTarget' } }
        claude: { $ref: '#/components/schemas/ClaudeAccountState' }
        methods: { type: array, items: { $ref: '#/components/schemas/AgentAccountMethod' } }
        loginCommand: { type: string }
      additionalProperties: false
    AgentAccountAttempt:
      type: object
      required: [id, agent, target, methodId, url, instructions, completion, state, startedAt, expiresAt]
      properties:
        id: { type: string }
        agent: { $ref: '#/components/schemas/AgentAccountId' }
        target: { type: string }
        methodId: { type: string }
        url: { type: string }
        instructions: { type: string }
        completion: { $ref: '#/components/schemas/AgentAccountCompletion' }
        state: { enum: [pending, complete, failed, expired, cancelled] }
        message: { type: string }
        startedAt: { type: number, description: Unix epoch milliseconds. }
        expiresAt: { type: number, description: Unix epoch milliseconds. }
      additionalProperties: false
    AgentAccountsSnapshot:
      type: object
      required: [agents, attempts]
      properties:
        agents: { type: array, items: { $ref: '#/components/schemas/AgentAccountStatus' } }
        attempts: { type: array, items: { $ref: '#/components/schemas/AgentAccountAttempt' } }
      additionalProperties: false
    ChatAgentDescriptor: { type: object, required: [id, name], description: "The identity of one offered agent. Capabilities ride the agent's availability, which only exists once its runtime has spoken.", properties: { id: { type: string, minLength: 1, maxLength: 64, pattern: '^[^:]+$' }, name: { type: string, minLength: 1, maxLength: 512 } }, additionalProperties: false }
    ConversationStatus: { enum: [idle, sending, running, completed, interrupted, failed, background, retrying, compacting, scheduled], description: "`background`: no turn is running but the agent still holds live background work; prompting is possible. Presented only for agents declaring `background-tasks`. `scheduled`: nothing runs, but the agent's session holds one or more future turns it scheduled for itself (pending `scheduled_wakeup` items); prompting is possible, and the session can be released. Presented only for agents declaring `scheduled-wakeups`; `background` wins when both hold. `retrying` and `compacting` are live-turn states with a name: the agent is waiting to retry a failed API request, or compacting its context; both return to `running` when the turn resumes." }
    ModelSelection: { type: object, required: [providerId, modelId], properties: { providerId: { type: string, minLength: 1, maxLength: 512 }, modelId: { type: string, minLength: 1, maxLength: 512 } }, additionalProperties: false }
    ConversationSummary:
      type: object
      required: [id, title, createdAt, updatedAt, status, agent]
      description: "`id` is agent-qualified (`<agentId>:<providerConversationId>`); ids from different agents never collide. `agent` names the owner, fixed at creation for the conversation's lifetime."
      properties: { id: { type: string, minLength: 1, maxLength: 512 }, title: { type: string }, createdAt: { type: number, minimum: 0 }, updatedAt: { type: number, minimum: 0 }, status: { $ref: '#/components/schemas/ConversationStatus' }, agent: { $ref: '#/components/schemas/ChatAgentDescriptor' } }
      additionalProperties: false
    ConversationInventoryEvent: { type: object, required: [type], properties: { type: { const: conversation.inventory }, catalogs: { type: object, description: "Agent id to opaque catalog revision. Re-read an agent's models when its revision changes; reconnect frames carry current revisions to recover missed updates.", additionalProperties: { type: string, minLength: 1, maxLength: 512 } } }, additionalProperties: false }
    ContextWindowMetadata:
      type: object
      required: [source, freshness]
      properties:
        source: { enum: [session, catalog, estimate] }
        freshness: { enum: [current, cached] }
        observedAt: { type: number, minimum: 0 }
        kind: { enum: [effective, compaction] }
        capacity: { type: integer, minimum: 1, description: "Model capacity only when separately reported by the agent, never inferred from the compaction window." }
        compactionThreshold: { type: integer, minimum: 1 }
      additionalProperties: false
    ContextWindowItem:
      type: object
      description: "Limit-only metadata. Never render as a message or use as a token-occupancy sample. Prefer an applicable session observation to a catalog value or estimate, and update the meter even when the usage item is unchanged. A newer observation can increase or decrease the limit. A matching contextKey scopes the observation to the execution that produced usage."
      required: [id, type, createdAt, model, limit, window]
      properties:
        id: { type: string }
        type: { const: context_window }
        createdAt: { type: number, minimum: 0 }
        model: { $ref: '#/components/schemas/ModelSelection' }
        contextKey: { type: string, minLength: 1, maxLength: 512 }
        limit: { type: integer, minimum: 1 }
        window: { $ref: '#/components/schemas/ContextWindowMetadata' }
      additionalProperties: false
    ActivityCompletionTime:
      type: number
      minimum: 0
      description: "Provider-reported terminal timestamp in Unix epoch milliseconds. Omitted when unknown; never inferred from creation time, duration, receive time, or silence. Display only for a terminal item status, not pending or running."
    TimelineBase: { type: object, required: [id, type, createdAt], properties: { id: { type: string }, type: { type: string }, createdAt: { type: number, minimum: 0 } } }
    ConversationItem:
      oneOf:
        - { type: object, required: [id, type, createdAt, text], properties: { id: { type: string }, type: { const: user_message }, createdAt: { type: number }, text: { type: string }, requestId: { type: string }, attachments: { type: array, maxItems: 8, items: { $ref: '#/components/schemas/MessageAttachment' } }, origin: { const: wakeup, description: "Present when the user did not type this prompt: `wakeup` is a scheduled wakeup firing, and the item opens that wakeup's turn. Clients present it as a wakeup, never as the user's message. Absent means the user typed it." }, wakeupId: { type: string, minLength: 1, maxLength: 512, description: "With `origin: wakeup`: the `scheduled_wakeup` item that fired, when known." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, markdown], properties: { id: { type: string }, type: { const: assistant_message }, createdAt: { type: number }, markdown: { type: string }, completedAt: { type: number }, contextKey: { type: string, minLength: 1, maxLength: 512, description: "Execution identity joining usage to its window observation, including the selected window variant." }, usage: { allOf: [{ $ref: '#/components/schemas/TokenUsage' }], description: "Message-level tokens. Normalization emits exactly one dedicated carrier per reported provider message: its id is `usage:<provider-message-id>` and its markdown is empty, so clients MUST treat it as data rather than render it as a message bubble. Present only where the agent declares the context capability and reported usage." }, model: { allOf: [{ $ref: '#/components/schemas/ModelSelection' }], description: "The model that reported this usage carrier, when known. Context percentages MUST use this model's context limit rather than a newly selected model that has not produced usage yet." }, agent: { type: string, minLength: 1, description: "The agent that produced the message, as the provider names it (OpenCode: `build`, `plan`, `compaction`, or a subagent's kind). Rides the usage carrier so a cost receipt can name the main agent's lines. Absent where the provider names none." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, text, status], properties: { id: { type: string }, type: { const: reasoning }, createdAt: { type: number }, text: { type: string }, status: { enum: [pending, running, completed, failed, cancelled] }, durationMs: { type: number }, label: { type: string, minLength: 1, description: "The row's label when it is recalled context rather than the model's own thinking (\"Recalled from memory\"). Absent for ordinary reasoning." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, name, status], properties: { id: { type: string }, type: { const: tool }, createdAt: { type: number }, name: { type: string }, status: { enum: [pending, running, completed, failed, cancelled] }, completedAt: { $ref: '#/components/schemas/ActivityCompletionTime' }, input: { type: string }, output: { type: string }, error: { type: string }, childConversationId: { type: string }, model: { type: string, description: "For a task tool: the model the subagent's own session ran, mirrored from that child session. A subagent may run a different model from its parent." }, usage: { allOf: [{ $ref: '#/components/schemas/TokenUsage' }], description: "For a task tool: what the subagent spent on THE TASK THIS ROW REPRESENTS — the messages it produced answering that task's prompt. A subagent given several tasks is one `childConversationId` on several rows, each stating its own task; together they are what the subagent's session spent. Subagents the subagent launched are NOT included: they are listed in `descendants`. Absent until the task has reported any." }, descendants: { type: array, items: { $ref: '#/components/schemas/SubagentLine' }, description: "For a task tool: every subagent launched beneath this row, at any depth, flat and ordered so that each line follows the row or line named by its `parentId`. The row's own `usage` plus its lines' is what the whole branch spent, each message counted once. Absent when the subagent launched none." }, elapsedMs: { type: number, minimum: 0, description: "How long the tool has been running, from the agent's own progress heartbeat, for a tool still producing no output." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, command, status], properties: { id: { type: string }, type: { const: command }, createdAt: { type: number }, command: { type: string }, status: { enum: [pending, running, completed, failed, cancelled] }, completedAt: { $ref: '#/components/schemas/ActivityCompletionTime' }, output: { type: string }, exitCode: { type: integer } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, path, operation], properties: { id: { type: string }, type: { const: file_change }, createdAt: { type: number }, path: { type: string }, operation: { enum: [create, update, delete] }, additions: { type: integer, minimum: 0 }, deletions: { type: integer, minimum: 0 } }, additionalProperties: false }
        - { $ref: '#/components/schemas/PermissionItem' }
        - { $ref: '#/components/schemas/QuestionItem' }
        - { type: object, required: [id, type, createdAt, entries], description: "The agent's live task list: one presentation per conversation, upserted in place as items progress — never an entry per update. Present only for agents that keep a task list.", properties: { id: { type: string }, type: { const: task_progress }, createdAt: { type: number }, entries: { type: array, items: { $ref: '#/components/schemas/TaskProgressEntry' } } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, status], properties: { id: { type: string }, type: { const: turn_status }, createdAt: { type: number }, status: { $ref: '#/components/schemas/ConversationStatus' }, message: { type: string } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, level, message], properties: { id: { type: string }, type: { const: notice }, createdAt: { type: number }, level: { enum: [info, warning, error] }, message: { type: string }, code: { type: string, minLength: 1, maxLength: 512, description: "A machine-readable kind for notices the surface reacts to: `rate-limit-warning` and `rate-limit-rejected` carry the current rate-limit standing and are data for the composer, never a timeline row — a client MUST treat them the way it treats `context_report`. The standing occupies one item id for the conversation's life, upserted as it changes and removed when requests are allowed again. `refusal-fallback` names a model swap. `login-failed` marks a turn that failed because the agent's login is missing, expired, or refused; `message` is the agent's own words, and a client offers a way to log in. `reauthenticating` is the one item an agent occupies while it signs in again mid-session, removed when that finishes. Absent otherwise." }, resetsAt: { type: number, minimum: 0, description: "When the rate-limit standing's window resets." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, total], description: "The agent's own statement of how full the context window is, at the point in the timeline where it was reported (after a turn, after a compaction). Data for the context readout, never rendered as a row: a client MUST treat it like the empty-markdown usage carrier. The readout uses whichever of a report or a usage carrier is newest in the timeline.", properties: { id: { type: string }, type: { const: context_report }, createdAt: { type: number }, contextKey: { type: string, minLength: 1, maxLength: 512 }, window: { $ref: '#/components/schemas/ContextWindowMetadata' }, total: { type: integer, minimum: 0, description: "Tokens occupying the window." }, max: { type: integer, minimum: 1, description: "The window the total was measured against. Absent when the agent did not state one; the reporting model's known limit then applies." }, model: { $ref: '#/components/schemas/ModelSelection' }, categories: { type: array, items: { type: object, required: [name, tokens, kind], properties: { name: { type: string, minLength: 1 }, tokens: { type: integer, minimum: 0 }, kind: { enum: [used, free, buffer, deferred], description: "`used` rows occupy the window and sum to `total`; `free` is the remaining window; `buffer` a compaction reserve; `deferred` rows are out-of-window tool schemas listed for awareness only." } }, additionalProperties: false } }, plan: { $ref: '#/components/schemas/PlanUtilization' } }, additionalProperties: false }
        - { $ref: '#/components/schemas/ContextWindowItem' }
        - { type: object, required: [id, type, createdAt, taskId, description, status], description: "One background task the agent runs (a backgrounded command, a backgrounded subagent, a monitor), updated in place as it progresses and settles. A running task is presented in the composer's live list, a settled one as a timeline row with its outcome and summary; `toolUseId` links it to the tool row that launched it. Only for agents declaring `background-tasks`; ambient housekeeping tasks never appear.", properties: { id: { type: string }, type: { const: background_task }, createdAt: { type: number }, taskId: { type: string, minLength: 1, maxLength: 512 }, description: { type: string, minLength: 1 }, taskType: { type: string }, toolUseId: { type: string, minLength: 1, maxLength: 512 }, status: { enum: [running, completed, failed, stopped] }, progress: { type: string, description: "The agent's latest progress note, while running." }, summary: { type: string, description: "The agent's summary, once settled." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt, wakeupId, prompt, recurring, schedule, status], description: "One future turn the agent scheduled for itself (a self-paced wakeup, a cron, a loop), as its session reports it, updated in place through its life. `pending` rows are listed with the conversation's scheduled state and shown in the timeline; a one-shot that fired reads `fired` with `firedTurnId` naming the `user_message` (origin `wakeup`) that opened its turn; a recurring one stays `pending` across fires with `nextFireAt` moving on. `cancelled`: the agent removed it, or the user cancelled it or released the session; the workspace blocks a cancelled wakeup from firing again even if the agent's tooling later rebuilds it. `paused`: the session ended, but the agent's tooling rebuilds this wakeup (a cron) when the conversation runs again; `message` says so, and a conversation opened without a live session lists its paused wakeups. `lost`: the session ended some other way and the schedule ended with it; `message` says so. A reopened conversation whose session is gone replays no pending row. Only for agents declaring `scheduled-wakeups`.", properties: { id: { type: string }, type: { const: scheduled_wakeup }, createdAt: { type: number }, wakeupId: { type: string, minLength: 1, maxLength: 512 }, prompt: { type: string, description: "The prompt the wakeup submits when it fires, as the agent reports it (possibly clipped by the agent)." }, recurring: { type: boolean }, schedule: { type: string, minLength: 1, description: "Five-field cron expression in the workspace host's local time; a one-shot encodes a single fire time." }, nextFireAt: { type: number, minimum: 0, description: "The workspace's reading of the next fire time (epoch ms), for display only: the agent fires on its own clock. Only on a pending row, and absent when the expression could not be read." }, status: { enum: [pending, fired, cancelled, paused, lost] }, firedTurnId: { type: string, minLength: 1, maxLength: 512 }, message: { type: string, description: "What happened to a wakeup that did not fire, when that needs saying (a lost or paused schedule)." } }, additionalProperties: false }
        - { type: object, required: [id, type, createdAt], description: "The agent compacted the conversation at this point. Content before the marker stays in the timeline; the figures are the agent's own pre/post token counts when it reported them.", properties: { id: { type: string }, type: { const: compaction }, createdAt: { type: number }, trigger: { enum: [manual, auto] }, preTokens: { type: integer, minimum: 0 }, postTokens: { type: integer, minimum: 0 } }, additionalProperties: false }
    PlanUtilizationWindow: { type: object, properties: { utilization: { type: number, minimum: 0, description: "Percent of the window used, 0-100." }, resetsAt: { type: number, minimum: 0, description: "When the window resets." } }, additionalProperties: false }
    PlanUtilization: { type: object, description: "Plan utilization the login reports (claude.ai plans only). Empty when the login reports no windows (an API-key session). Absent from a report that does not speak to the plan (a compaction's post-count, an agent that never reports it): the previous report's plan then still stands.", properties: { fiveHour: { $ref: '#/components/schemas/PlanUtilizationWindow' }, sevenDay: { $ref: '#/components/schemas/PlanUtilizationWindow' } }, additionalProperties: false }
    SubagentLine: { type: object, description: "A subagent launched by a subagent, as a line beneath the task row that launched its branch. Lines sharing a `conversationId` are one subagent given several tasks.", required: [id, parentId, description, conversationId], properties: { id: { type: string, minLength: 1, description: "The launching row's item id in its own conversation. Unique within the list." }, parentId: { type: string, minLength: 1, description: "The task row carrying the list, or an earlier line in it: what launched this subagent." }, description: { type: string, minLength: 1 }, subagent: { type: string, description: "The kind of subagent, where the launcher named one." }, conversationId: { type: string, minLength: 1, description: "The subagent's own session." }, model: { type: string }, usage: { allOf: [{ $ref: '#/components/schemas/TokenUsage' }], description: "What the subagent spent on this task alone. Absent until it has reported any." } }, additionalProperties: false }
    TokenUsage: { type: object, description: "What a message spent, as the agent reports it. Every component is optional: an absent one means the agent did not report it, which is not the same statement as zero. Window occupancy is input + cacheRead + cacheWrite; output is what came back rather than what occupies the window.", properties: { input: { type: integer, minimum: 0 }, output: { type: integer, minimum: 0 }, reasoning: { type: integer, minimum: 0 }, cacheRead: { type: integer, minimum: 0 }, cacheWrite: { type: integer, minimum: 0 }, costUsd: { type: number, minimum: 0, description: "What the message cost in USD, where the agent prices its messages (OpenCode reports one per assistant message). Not a token component. Absent means the agent did not price it, which is not the same statement as zero; zero is what OpenCode reports for a model it has no price for." } }, additionalProperties: false }
    PermissionItem: { type: object, required: [id, type, createdAt, requestId, action, resources, status], properties: { id: { type: string }, type: { const: permission }, createdAt: { type: number }, requestId: { type: string }, conversationId: { type: string, minLength: 1, maxLength: 512, description: "Conversation that owns this request. Differs from the containing conversation when a subagent's request is shown in the conversation that launched it; answers are addressed here." }, action: { type: string }, resources: { type: array, items: { type: string } }, alwaysPatterns: { type: array, items: { type: string, minLength: 1 }, description: "What the persistent approval (`approved-session`) will authorize, in the owning agent's own syntax (OpenCode `git status *`, Claude Code `Bash(git status:*)`). Distinct from `resources`: the agent derives a reusable pattern from the request, and that pattern is what it installs. Empty means the agent supplied nothing reusable; absent means it did not say. Render verbatim; never derive scope from a resource." }, status: { enum: [pending, resolved] }, outcome: { enum: [approved-once, approved-session, rejected] }, diff: { type: string, description: "Unified diff a file-edit permission would apply, when the agent attaches one. Absent for a permission with nothing to show." }, plan: { type: string, description: "The plan this approval would put into effect, as markdown (a Claude Code plan approval)." }, choices: { type: array, minItems: 1, items: { $ref: '#/components/schemas/PermissionChoice' }, description: "Agent-provided approval intents. When present they replace the generic approve pair; rejecting stays universal." }, choiceId: { type: string, description: "On resolution: the choice that approved it, when choices were offered." }, sourceToolId: { type: string, minLength: 1, maxLength: 512, description: "The `tool` item this permission belongs to (`tool:<call id>`), when the agent names the call it is asking about (OpenCode 2.x `source`). What lets a request naming no resource of its own (an MCP tool's `*`) show the arguments of the call it would allow. Absent when the agent did not say; a client then falls back to the running tool row bearing the request's `action`." }, mcp: { $ref: '#/components/schemas/PermissionMcpTool' } }, additionalProperties: false }
    PermissionMcpTool: { type: object, description: "The MCP server and tool a permission's `action` names, when the action is an MCP tool's registry name (`<server>_<tool>`, each half sanitized by OpenCode) and the agent resolved it against the MCP servers it reports. `server` is the name the user registered; `tool` the remainder. Absent for a built-in tool or an action no reported server matches. The `action` remains what a persistent approval installs.", required: [server, tool], properties: { server: { type: string, minLength: 1 }, tool: { type: string, minLength: 1 } }, additionalProperties: false }
    TaskProgressEntry: { type: object, required: [text, status], properties: { text: { type: string, minLength: 1 }, status: { enum: [pending, in_progress, completed] }, activeText: { type: string, description: "Present-continuous label shown while in progress, when the agent provides one." } }, additionalProperties: false }
    PermissionChoice: { type: object, required: [id, label], properties: { id: { type: string, minLength: 1, maxLength: 128 }, label: { type: string, minLength: 1 }, description: { type: string } }, additionalProperties: false }
    StructuredQuestion: { type: object, required: [prompt, header, options, multiple, allowFreeForm], properties: { prompt: { type: string }, header: { type: string }, options: { type: array, items: { type: object, required: [label, description], properties: { label: { type: string }, description: { type: string } }, additionalProperties: false } }, multiple: { type: boolean }, allowFreeForm: { type: boolean }, optional: { type: boolean, description: "The question may be answered with an empty answer array (an optional field of an elicitation form). Absent means required." } }, additionalProperties: false }
    QuestionOutcome:
      oneOf:
        - { type: object, required: [kind], properties: { kind: { const: rejected } }, additionalProperties: false }
        - { type: object, required: [kind, answers], properties: { kind: { const: answered }, answers: { type: array, maxItems: 32, description: "One answer array per question, in the same order as QuestionItem.questions. Offered option labels and, when allowFreeForm is true, ordinary custom strings are sent directly in each array; an array is empty only for a question marked optional.", items: { type: array, minItems: 0, maxItems: 32, items: { type: string, minLength: 1, maxLength: 4096, pattern: '\S' } } } }, additionalProperties: false }
    QuestionItem: { type: object, required: [id, type, createdAt, requestId, questions, status], properties: { id: { type: string }, type: { const: question }, createdAt: { type: number }, requestId: { type: string }, conversationId: { type: string, minLength: 1, maxLength: 512, description: "Conversation that owns this request. See PermissionItem.conversationId." }, questions: { type: array, items: { $ref: '#/components/schemas/StructuredQuestion' } }, status: { enum: [pending, resolved] }, outcome: { $ref: '#/components/schemas/QuestionOutcome' }, source: { enum: [dialog, elicitation], description: "Who is asking, beyond the agent's own question tool: a tool-driven blocking dialog the agent asked the host to render, or an MCP server's elicitation. Answered and rejected through the same question operations." }, intro: { type: string, minLength: 1, description: "A sentence of context shown above the form: the server and message behind an elicitation, the refusal a dialog is about." }, link: { type: string, minLength: 1, pattern: '^https?://', description: "A link the request asks the user to open (a URL-mode elicitation)." }, schema: { type: object, description: "The request as received (an elicitation's JSON schema, a dialog's kind and payload), for a reader auditing what was asked. Opaque." } }, additionalProperties: false }
    ConversationConfiguration:
      type: object
      description: Effective configuration reported for this conversation. An absent field is unknown or agent-controlled, not an invitation to select the first offered value.
      properties:
        model: { $ref: '#/components/schemas/ModelSelection' }
        mode: { type: string, minLength: 1, maxLength: 512 }
        variant: { type: string, minLength: 1, maxLength: 512, description: "Reasoning variant of `model`. Invalid without `model` in the same configuration record." }
      dependentRequired: { variant: [model] }
      additionalProperties: false
    MessageAttachment: { type: object, required: [name, mimeType], description: "An image riding a user message, referenced by workspace attachment id — never by bytes. `id` is absent on a replayed attachment whose reference could not be recovered; clients render those as labeled placeholders.", properties: { id: { type: string, minLength: 1, maxLength: 512 }, name: { type: string, minLength: 1, maxLength: 200 }, mimeType: { type: string, enum: [image/png, image/jpeg, image/gif, image/webp] } }, additionalProperties: false }
    QueuedMessage: { type: object, required: [id, text, queuedAt], description: "A submission the workspace holds until the running turn ends. Not a timeline item; it becomes one when delivered. `requestId` echoes the accepting mutation's client request id so the submitting client can reconcile.", properties: { id: { type: string }, text: { type: string, description: "Empty for an image-only message." }, queuedAt: { type: number }, requestId: { type: string }, attachments: { type: array, maxItems: 8, items: { $ref: '#/components/schemas/MessageAttachment' } } }, additionalProperties: false }
    ChatEvent:
      oneOf:
        - { type: object, required: [generation, sequence, conversationId, type, item], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: item.upsert }, item: { $ref: '#/components/schemas/ConversationItem' } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, itemId], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: item.remove }, itemId: { type: string } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, itemId, delta], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: item.text_delta }, itemId: { type: string }, delta: { type: string, minLength: 1 } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, status], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: conversation.status }, status: { $ref: '#/components/schemas/ConversationStatus' }, message: { type: string } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, configuration], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: conversation.configuration }, configuration: { $ref: '#/components/schemas/ConversationConfiguration' } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, conversation], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: conversation.updated }, conversation: { allOf: [{ $ref: '#/components/schemas/ConversationSummary' }], description: "Updated summary for this conversation. Its id is the event conversationId." } }, additionalProperties: false }
        - { type: object, required: [generation, sequence, conversationId, type, queued, change], description: "Restates the whole held queue after a change, so replayed or duplicated frames converge on the same state.", properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: conversation.queue }, queued: { type: array, items: { $ref: '#/components/schemas/QueuedMessage' } }, change: { type: object, required: [kind, messageId], properties: { kind: { enum: [held, removed, delivered] }, messageId: { type: string } }, additionalProperties: false } }, additionalProperties: false }
        - { $ref: '#/components/schemas/ChatResyncEvent' }
    ChatResyncEvent: { type: object, required: [generation, sequence, conversationId, type, reason], properties: { generation: { type: string }, sequence: { type: integer, minimum: 0 }, conversationId: { type: string }, type: { const: resync }, reason: { enum: [generation-changed, retention-gap, invalid-cursor, conversation-rewritten] } }, additionalProperties: false }
    DocumentMeta:
      type: object
      required: [id, name, relativePath, mtimeMs, rootId, kind, revision]
      properties: { id: { type: string }, name: { type: string }, relativePath: { type: string }, mtimeMs: { type: number }, rootId: { type: string }, kind: { enum: [markdown, asciidoc, text, binary] }, revision: { type: integer, minimum: 0 } }
      additionalProperties: false
    RootGroup:
      type: object
      required: [id, label, path, docs, hiddenCount]
      properties: { id: { type: string }, label: { type: string }, path: { type: string }, docs: { type: array, items: { $ref: '#/components/schemas/DocumentMeta' } }, hiddenCount: { type: integer, minimum: 0 } }
      additionalProperties: false
    WorkspaceState:
      type: object
      required: [kind, epoch, revision, discovery, repositoryState, workspaceApiRevision, roots, repositories, compareTarget, initialFollow, defaultDocumentId, changedId, generatedAt, build, scope]
      properties:
        kind: { const: snapshot }
        epoch: { type: string }
        revision: { type: integer, minimum: 0 }
        discovery: { $ref: '#/components/schemas/DocumentDiscovery' }
        repositoryState: { $ref: '#/components/schemas/RepositoryFreshness' }
        latestChange: { type: object, required: [id, revision], properties: { id: { type: string }, revision: { type: integer, minimum: 0 } }, additionalProperties: false }
        workspaceApiRevision: { type: integer, minimum: 1 }
        roots: { type: array, items: { $ref: '#/components/schemas/RootGroup' } }
        repositories: { type: array, items: { $ref: '#/components/schemas/RepositorySnapshot' } }
        compareTarget: { enum: [base, last-commit] }
        initialFollow: { type: boolean }
        defaultDocumentId: { type: [string, 'null'] }
        changedId: { type: [string, 'null'] }
        generatedAt: { type: number }
        build: { $ref: '#/components/schemas/BuildSummary' }
        scope: { oneOf: [{ type: object, required: [kind], properties: { kind: { const: folder } }, additionalProperties: false }, { type: object, required: [kind, documentId], properties: { kind: { const: file }, documentId: { type: string } }, additionalProperties: false }] }
        unscopedFingerprint: { type: string }
        terminal: { enum: [enabled, disabled] }
      additionalProperties: false
    DocumentUpdate:
      oneOf:
        - { $ref: '#/components/schemas/WorkspaceState' }
        - { $ref: '#/components/schemas/DocumentPatch' }
    DocumentDiscovery:
      type: object
      required: [status, discovered]
      properties: { status: { enum: [indexing, ready, recovering, error] }, discovered: { type: integer, minimum: 0 }, message: { type: string } }
      additionalProperties: false
    RepositoryFreshness:
      type: object
      required: [generation, status]
      properties: { generation: { type: integer, minimum: 0 }, status: { enum: [pending, ready, stale, error] }, message: { type: string } }
      additionalProperties: false
    DocumentRoot:
      type: object
      required: [id, label, path, hiddenCount]
      properties: { id: { type: string }, label: { type: string }, path: { type: string }, hiddenCount: { type: integer, minimum: 0 } }
      additionalProperties: false
    DocumentPatch:
      type: object
      required: [kind, epoch, previousRevision, revision, generatedAt, scope, compareTarget, unscopedFingerprint, upserts, removals, changedId]
      properties:
        kind: { const: patch }
        epoch: { type: string }
        previousRevision: { type: integer, minimum: 0 }
        revision: { type: integer, minimum: 0 }
        generatedAt: { type: number }
        scope: { oneOf: [{ type: object, required: [kind], properties: { kind: { const: folder } }, additionalProperties: false }, { type: object, required: [kind, documentId], properties: { kind: { const: file }, documentId: { type: string } }, additionalProperties: false }] }
        compareTarget: { enum: [base, last-commit] }
        unscopedFingerprint: { type: string }
        upserts: { type: array, items: { $ref: '#/components/schemas/DocumentMeta' } }
        removals: { type: array, items: { type: object, required: [rootId, id], properties: { rootId: { type: string }, id: { type: string } }, additionalProperties: false } }
        roots: { type: array, items: { $ref: '#/components/schemas/DocumentRoot' } }
        discovery: { $ref: '#/components/schemas/DocumentDiscovery' }
        repositories: { type: array, items: { $ref: '#/components/schemas/RepositorySnapshot' } }
        repositoryState: { $ref: '#/components/schemas/RepositoryFreshness' }
        defaultDocumentId: { type: [string, 'null'] }
        changedId: { type: [string, 'null'] }
      additionalProperties: false
    BuildSummary:
      type: object
      required: [version, branch, commitSha, commitShort, release, identifier, bundledWebRevision]
      properties: { version: { type: string }, branch: { type: string }, commitSha: { type: string }, commitShort: { type: string }, release: { type: boolean }, identifier: { type: string }, bundledWebRevision: { type: integer } }
      additionalProperties: false
    RepositorySnapshot:
      type: object
      required: [id, rootPath, label, watchedRootIds, metadata, status, base, changedFiles, gitIgnoredFiles, configWarnings, message, commitLog]
      properties:
        id: { type: string }
        rootPath: { type: string }
        label: { type: string }
        watchedRootIds: { type: array, items: { type: string } }
        metadata: { type: object, additionalProperties: true }
        status: { enum: [available, non-git, unavailable] }
        base: { type: object, additionalProperties: true }
        changedFiles: { type: array, items: { type: object, additionalProperties: true } }
        gitIgnoredFiles: { type: array, items: { type: string } }
        configWarnings: { type: array, items: { type: string } }
        message: { type: [string, 'null'] }
        commitLog: { type: array, items: { $ref: '#/components/schemas/CommitLogEntry' } }
      additionalProperties: false
    CommitLogEntry:
      type: object
      description: "One recent commit. Entries are open objects: consumers must ignore fields they don't know."
      properties:
        sha: { type: string, description: "Short commit SHA." }
        subject: { type: string }
        message: { type: string, description: "Full commit message." }
        author: { type: [string, 'null'] }
        relativeTime: { type: [string, 'null'], description: "Git's relative age text as of collection (\"3 hours ago\"). It does not advance between repository changes; render ages from committedAtMs instead." }
        committedAtMs: { type: [number, 'null'], description: "Commit time in epoch milliseconds. Clients render the commit's age from this so it stays current." }
      additionalProperties: true
    LiveCursor:
      type: string
      x-uatu-maxUtf8Bytes: 1024
      description: "Opaque cursor scoped to one subscription, at most 1024 UTF-8 bytes. Conversation cursors are the workspace's own replay ids. The Hub assigns document, inventory, and activity cursors. An empty or absent cursor starts from the current position."
    LiveDocumentKey:
      type: string
      x-uatu-maxUtf8Bytes: 4096
      description: "The watch context as a form-urlencoded query string of `compareTarget`, `scope`, and `documentId`, in that order, each only when set. For example `compareTarget=last-commit&scope=file&documentId=README.md`. `compareTarget` is `base` or `last-commit`, `scope` is `folder` or `file`, and `scope=file` also needs `documentId`. Empty or absent means the default context. Streams that present the same key share one upstream subscription. At most 4096 UTF-8 bytes, which leaves room for a file pinned deep in the tree, since `documentId` is a URL-encoded absolute path."
    LiveConversationKey:
      type: string
      minLength: 1
      x-uatu-maxUtf8Bytes: 4096
      description: "Agent-qualified conversation id, `<agentId>:<providerConversationId>`, at most 4096 UTF-8 bytes."
    LiveSubscriptionKey:
      description: "One subscription on a stream, meaning a topic and its key where the topic has one. The activity topic is not subscribed here. Request it with the stream's `activity` parameter."
      oneOf:
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: document }
            key: { $ref: '#/components/schemas/LiveDocumentKey' }
          additionalProperties: false
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: inventory }
          additionalProperties: false
        - type: object
          required: [topic, key]
          properties:
            topic: { type: string, const: conversation }
            key: { $ref: '#/components/schemas/LiveConversationKey' }
          additionalProperties: false
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: worktrees }
          additionalProperties: false
    LiveSubscription:
      description: A subscription and the cursor to resume it from.
      oneOf:
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: document }
            key: { $ref: '#/components/schemas/LiveDocumentKey' }
            cursor: { $ref: '#/components/schemas/LiveCursor' }
          additionalProperties: false
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: inventory }
            cursor: { $ref: '#/components/schemas/LiveCursor' }
          additionalProperties: false
        - type: object
          required: [topic, key]
          properties:
            topic: { type: string, const: conversation }
            key: { $ref: '#/components/schemas/LiveConversationKey' }
            cursor: { $ref: '#/components/schemas/LiveCursor' }
          additionalProperties: false
        - type: object
          required: [topic]
          properties:
            topic: { type: string, const: worktrees }
            cursor: { $ref: '#/components/schemas/LiveCursor', description: "Ignored. The worktrees topic has no replay cursor; every attach receives a fresh invalidation." }
          additionalProperties: false
    LiveSubscriptionList:
      type: array
      maxItems: 16
      items: { $ref: '#/components/schemas/LiveSubscription' }
      description: At most 16 entries. A later entry for the same topic and key replaces an earlier one.
    LiveSubscriptionKeyList:
      type: array
      maxItems: 16
      items: { $ref: '#/components/schemas/LiveSubscriptionKey' }
    LiveSubscriptionChange:
      type: object
      properties:
        add: { $ref: '#/components/schemas/LiveSubscriptionList' }
        remove: { $ref: '#/components/schemas/LiveSubscriptionKeyList' }
      additionalProperties: false
      description: Removals apply before additions. Adding a topic and key that is already subscribed replaces the subscription and re-attaches it from the new cursor.
    LiveSubscriptionsUpdated:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean, const: true }
      additionalProperties: false
    WorktreeOwnership:
      type: string
      enum: [main, uatu, external, uncertain]
      description: "How this checkout relates to Uatu. `main` is the repository's main checkout. `uatu` means verified Uatu creation provenance, which alone may be deleted through Uatu. `external` is somebody else's tree, whether or not it is registered. `uncertain` means the recorded identity could not be confirmed; its content is retained and destructive actions are refused. Never inferred from a name, a path, or the presence of an agent's configuration directory."
    WorktreeAvailability:
      type: string
      enum: [present, missing, replaced]
      description: "`present` is the ordinary case. `missing` is a registered path that is not there; `replaced` is a path occupied by a different checkout. Neither is ever silently repaired, and neither can start."
    WorktreeCheckout:
      type: object
      required: [checkoutId, repositoryId, path, branch, detached, main, ownership, availability, registered, running, locked]
      properties:
        checkoutId: { type: string, minLength: 1, description: "Canonical identity of this checkout. Ownership and every destructive action are decided on this, never on a path: a path reused by another tree must not inherit an older record's ownership." }
        repositoryId: { type: string, minLength: 1, description: "Canonical repository identity shared by a main checkout and its linked worktrees. Group by this, never by names or paths." }
        workspaceId: { $ref: '#/components/schemas/WorkspaceId', description: "The Hub registration, present only once registered. Present exactly when `registered` is true." }
        parentWorkspaceId: { $ref: '#/components/schemas/WorkspaceId', description: "The registered main workspace this checkout belongs to, when known. It owns the credential policy this checkout inherits live." }
        path: { type: string, minLength: 1 }
        branch: { type: [string, 'null'], minLength: 1, description: "The exact local branch, slashes included; for a linked worktree it is also its display name. Null for a detached or unreadable HEAD, which is stated rather than guessed." }
        detached: { type: boolean }
        main: { type: boolean, description: "Whether this is the repository's main checkout. A main checkout is never deletable through Uatu." }
        ownership: { $ref: '#/components/schemas/WorktreeOwnership' }
        availability: { $ref: '#/components/schemas/WorktreeAvailability' }
        registered: { type: boolean }
        running: { type: boolean }
        locked: { type: boolean, description: "Whether Git reports the checkout locked. A locked checkout is never removed." }
        sourceRef: { type: string, minLength: 1, description: "Immutable branch-creation snapshot: the ref explicitly selected when Uatu created this branch. Not the upstream and not the starting revision, and never inferred from either. Absent means the origin is unknown." }
        upstream: { type: string, minLength: 1 }
        head: { type: string, minLength: 1, description: "Short revision, for diagnostics. Never provenance." }
      additionalProperties: false
    WorktreeRefs:
      type: object
      required: [local, remote, fetchedAt]
      properties:
        local: { type: array, items: { type: string, minLength: 1 } }
        remote: { type: array, items: { type: string, minLength: 1 }, description: "Remote-qualified, such as origin/release." }
        fetchedAt: { type: [integer, 'null'], description: "Unix epoch milliseconds of the last successful fetch in this Hub run, or null when the listing comes from repository state alone. A cached listing is never network-fresh." }
      additionalProperties: false
    WorktreeErrorCode:
      type: string
      enum: [invalid-input, git-unsupported, not-found, branch-exists, branch-in-use, destination-occupied, ref-unavailable, fetch-authentication, fetch-network, registration-failed, start-failed, local-data, git-lock, external-activity, nested-dependency, stop-failed, ownership-required, identity-uncertain, inventory-unavailable, folder-dependency, folder-dependency-unknown, conflict, permission-denied, timeout, cancelled, internal]
      description: "The closed set of refusals. A client that cannot map a code refuses rather than inventing a recovery."
    WorktreeRetryAction:
      type: string
      enum: [none, retry-registration, retry-start, retry-fetch, retry-delete, refresh, open-existing]
      description: "The one follow-up the originating flow offers. `none` means there is none; it is never a licence to force."
    WorktreePhase:
      type: string
      enum: [validating, reserving, creating, verifying, registering, assigning, starting, preflight, fencing, stopping, rechecking, removing, unregistering, complete]
      description: "The last boundary the operation provably passed. Not a progress percentage: it is what restart recovery reconciles against actual Git state. Create phases run validating to complete; delete phases preflight to complete; unphased kinds report only complete."
    WorktreeError:
      type: object
      required: [code, message, retry]
      properties:
        code: { $ref: '#/components/schemas/WorktreeErrorCode' }
        message: { type: string, minLength: 1, maxLength: 300, description: "Already sanitized: no absolute path, no credential, no control character, bounded length. Safe to show a user verbatim." }
        retry: { $ref: '#/components/schemas/WorktreeRetryAction' }
        conflictCheckoutId: { type: string, minLength: 1, description: "The existing checkout an occupancy conflict points at, to open instead of forcing." }
        retainedCheckoutId: { type: string, minLength: 1, description: "A checkout that outlived a failed operation and must not be recreated." }
        phase: { $ref: '#/components/schemas/WorktreePhase' }
      additionalProperties: false
    WorktreeInventoryResponse:
      type: object
      required: [inventory]
      properties:
        inventory:
          type: object
          required: [repositoryId, sourceWorkspaceId, status, checkouts, refs]
          properties:
            repositoryId: { type: string, minLength: 1, description: "The reserved word `unknown` when the repository could not be identified at all, which is legal only on an explicitly stale error listing with no checkouts." }
            sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
            status: { type: string, enum: [ready, loading, error] }
            checkouts: { type: array, items: { $ref: '#/components/schemas/WorktreeCheckout' } }
            refs: { $ref: '#/components/schemas/WorktreeRefs' }
            error: { $ref: '#/components/schemas/WorktreeError', description: "Present when status is error: the listing shown is explicitly stale." }
          additionalProperties: false
      additionalProperties: false
    WorktreeOperationResult:
      type: object
      required: [ok, operationId, kind]
      properties:
        ok: { type: boolean }
        operationId: { type: string, minLength: 1 }
        kind: { type: string, enum: [create, register, start, fetch, refresh, delete, forget] }
        phase: { $ref: '#/components/schemas/WorktreePhase' }
        checkout: { $ref: '#/components/schemas/WorktreeCheckout' }
        registered: { type: boolean }
        started: { type: boolean }
        startError: { $ref: '#/components/schemas/WorktreeError', description: "An explicitly requested start that failed. It does not fail the operation: the workspace remains configured and stopped." }
        error: { $ref: '#/components/schemas/WorktreeError' }
        retainedCheckout: { $ref: '#/components/schemas/WorktreeCheckout', description: "Present when Git succeeded and a later step did not. Retry acts on this checkout; a second one is never created." }
      additionalProperties: false
      description: "A successful result carries phase, registered and started; a refusal carries error, and retainedCheckout when a checkout outlived it."
    CreateWorktreeRequest:
      type: object
      required: [mode]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId', description: "Required." }
        mode: { type: string, enum: [new-branch, existing-local, remote-tracking] }
        branch: { type: string, minLength: 1, description: "The target local branch. Required for new-branch. For the other modes the Hub derives it — the existing local ref, or the remote ref minus its remote prefix — and rejects a mismatching guess. Held to Git's branch grammar before any repository is touched, so an option-like name or a revision expression never reaches a Git argument list." }
        base:
          type: object
          required: [kind, ref]
          properties:
            kind: { type: string, enum: [local, remote] }
            ref: { type: string, minLength: 1 }
          additionalProperties: false
          description: "An exactly qualified starting ref. Mutually exclusive with baseRef."
        baseRef: { type: string, minLength: 1, description: "A starting ref left for the Hub to qualify against the repository's own listing, for callers that have one name rather than a committed selection — feature/login is a valid local branch and origin/main a remote-qualified one, and only the listing tells them apart. A ref that is not in the listing is refused; one that is both is refused as ambiguous. Nothing is substituted for a near match. Ignored when base is present." }
        start: { type: boolean, description: "Post-commit intent. Creation leaves the workspace stopped by default, and a failed start does not fail the creation." }
      additionalProperties: false
    OpenWorktreeRequest:
      type: object
      required: [reference]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        reference: { $ref: '#/components/schemas/WorktreeReference' }
        start: { type: boolean }
      additionalProperties: false
    DeleteWorktreeRequest:
      type: object
      required: [reference, confirm]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        reference: { $ref: '#/components/schemas/WorktreeReference' }
        confirm: { type: boolean, const: true, description: "Required — the same role the destructive button plays in the Hub's dialog, and for a checkout without local data the whole authorization. It authorizes the stated operation and overrides no blocker. There is no client-facing force option in this family." }
        stop: { type: boolean, description: "Additionally authorizes stopping this worktree's own Uatu sessions before removal. Activity Uatu does not own stays a blocker: Uatu never claims it can stop an external application." }
        localDataFingerprint: { type: string, pattern: '^[0-9a-f]{64}$', description: "The acknowledgement of local data: the `localData.fingerprint` the deletion preflight reported, echoed back. It authorizes deleting exactly the disclosed tracked changes, untracked files and ignored files with the checkout, and is re-verified at every recheck — a checkout whose local data changed since is refused as `local-data`. It never overrides a lock, a nested worktree, a submodule or nested repository, a Git operation in progress, external activity, uncertain identity or ownership. Only for acknowledged tracked or untracked data may the Hub pass Git a single force, never the double force that overrides a lock. Omitted for a checkout without local data." }
      additionalProperties: false
    WorktreeReference:
      type: string
      minLength: 1
      description: "A registered workspace id, a canonical checkout id, or the exact branch of exactly one linked checkout in this repository family. A branch is resolved against the authoritative inventory and yields a canonical identity, which is what every safety check then runs on — so a stale name can never address a path's new occupant. A branch matching more than one checkout is refused as ambiguous."
    FetchWorktreeRefsRequest:
      type: object
      required: [sourceWorkspaceId]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
      additionalProperties: false
    WorktreeRefsResponse:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean }
        refs: { $ref: '#/components/schemas/WorktreeRefs', description: "Reported whether or not the fetch succeeded: absent only when a refusal is decided before any repository is read." }
        error: { $ref: '#/components/schemas/WorktreeError' }
      additionalProperties: false
      description: "ok: true carries refs alone; ok: false carries error, and usually refs — the cached listing, unchanged."
    PreflightDeleteWorktreeRequest:
      type: object
      required: [reference]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        reference: { $ref: '#/components/schemas/WorktreeReference' }
      additionalProperties: false
    WorktreeDeletionPreflight:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean }
        checkout: { $ref: '#/components/schemas/WorktreeCheckout' }
        requiresStop: { type: boolean, description: "Present when ok is true: whether proceeding to delete needs the caller's explicit stop authorization." }
        localData: { $ref: '#/components/schemas/WorktreeLocalData', description: "Present only when ok is true and the checkout has local data: what deletion would permanently discard, and the fingerprint a deletion must echo back to acknowledge it." }
        error: { $ref: '#/components/schemas/WorktreeError', description: "The ONE blocker that replaces deletion's normal consequences. No acknowledgement and no client-supplied force gets past it." }
      additionalProperties: false
      description: "ok: true carries checkout and requiresStop, and localData when the checkout has local data; ok: false carries error and, when the checkout itself was identified before the blocker was found, checkout too."
    WorktreeLocalData:
      type: object
      required: [fingerprint]
      minProperties: 2
      properties:
        tracked: { $ref: '#/components/schemas/WorktreeLocalDataCategory', description: "Staged or unstaged changes to tracked files." }
        untracked: { $ref: '#/components/schemas/WorktreeLocalDataCategory', description: "Untracked files." }
        ignored: { $ref: '#/components/schemas/WorktreeLocalDataCategory', description: "Ignored entries; a wholly ignored directory is one entry, listed with its trailing `/`, and stands for that folder and everything inside it." }
        fingerprint: { type: string, pattern: '^[0-9a-f]{64}$', description: "Opaque: identifies the complete set of local-data status entries across all three categories, bound to this checkout. Echo it back as the deletion's `localDataFingerprint`." }
      additionalProperties: false
      description: "Local data a deletion would permanently discard. A category is present only when it has at least one entry, and at least one is present."
    WorktreeLocalDataCategory:
      type: object
      required: [count, sample]
      properties:
        count: { type: integer, minimum: 1, description: "Status entries in this category." }
        sample:
          type: array
          maxItems: 5
          items: { type: string, minLength: 1, pattern: '^[^/]', description: "A checkout-relative path, never absolute." }
          description: "Up to five of this category's entries, sorted; never more than count."
      additionalProperties: false
    RegisterWorktreeRequest:
      type: object
      required: [reference]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        reference: { $ref: '#/components/schemas/WorktreeReference' }
        start: { type: boolean }
      additionalProperties: false
    ForgetWorktreeRequest:
      type: object
      required: [reference]
      properties:
        sourceWorkspaceId: { $ref: '#/components/schemas/WorkspaceId' }
        reference: { $ref: '#/components/schemas/WorktreeReference' }
      additionalProperties: false
      description: "Calling this operation is itself the authorization to stop the workspace's own Uatu sessions first, if it is running — there is no stop: false variant."
