openapi: 3.1.0
# ─────────────────────────────────────────────────────────────────────────────
# THE VERAGENT MANAGEMENT API CONTRACT (Arc B Slice 4 — management-api.md §9).
#
# This file is NORMATIVE and machine-pinned: a CI check
# (src/lib/auth/v1OpenApiContract.test.ts) diffs every implemented /api/v1
# route + method against these paths IN BOTH DIRECTIONS, and verifies each
# operation's `x-required-scope` and `x-ratelimit-bucket` against the actual
# requireV1({ scope, limit }) call in the route file — a mismatch is a failing
# build, never a docs bug.
#
# CLAIMS DISCIPLINE applies to machine-readable claims: response schemas are
# documented TO THE DEPTH THAT'S TRUE. Where a field list below is closed it
# mirrors the one exported column constant the implementation reads; where a
# shape is genuinely dynamic (full agent rows, artifact internals) the schema
# says so honestly instead of inventing structure. No schema here describes a
# field the route does not return.
#
# Extensions used (all verified by the CI check):
#   x-required-scope   the exact scope the endpoint enforces (null = key
#                      authentication only, no scope check)
#   x-ratelimit-bucket v1:read (600/min/key) | v1:write (120/min/key) |
#                      v1:export (10/min/key) | null (not rate limited)
# ─────────────────────────────────────────────────────────────────────────────
info:
  title: Veragent Management API
  version: 1.0.0
  description: |
    The key-authenticated management plane for a Veragent organisation:
    governance rules, agents, budgets, channels, permissions, governance
    events and exports, break-glass grants, and the signed compliance
    artifacts.

    **Authentication.** Every endpoint (except this document) takes
    `Authorization: Bearer vmk_…` — a management key minted in the dashboard
    by an administrator under a fresh two-factor challenge. Scopes are chosen
    at mint and immutable for the key's lifetime; rotation preserves them.

    **Stability posture** (the customer-facing statement lives at
    https://www.veragent.io/docs/api): within v1, changes are additive-only —
    new endpoints, new response fields, new optional parameters. The error
    envelope, the auth header, the scope names, and the `truncated` flag
    semantics are stable. Anything breaking means `/api/v2`.

    **Error envelope.** Failures are `{ "error": string }`, optionally with a
    machine `code`; validation failures on write endpoints are
    `{ "errors": string[] }` with every reason at once. The codes in use:
    `insufficient_scope` (403, names the `required_scope`), `rate_limited`
    (429, with a `Retry-After` header in seconds), `ip_not_allowed` (401 — an
    authentication failure when the key carries an IP allowlist; it is never a
    governance event and leaves no decision row), and `plan_required` (403,
    names the `feature`, `required_tier` and `current_tier`).
  contact:
    name: Veragent
    url: https://www.veragent.io
servers:
  - url: https://www.veragent.io
security:
  - managementKey: []

components:
  securitySchemes:
    managementKey:
      type: http
      scheme: bearer
      bearerFormat: vmk_ management key
      description: |
        A Veragent management key (`vmk_…`), minted show-once in the dashboard
        (Settings → API keys) by an administrator under elevation. The key is
        checked on every call: type, liveness (active / not deleted / not
        expired) and, when configured, its IP allowlist. Scopes are fixed at
        mint. **The scope names below are the contract** — pinned in code
        (src/lib/managementScopes.ts) and verified against this document by
        CI; renaming one is a breaking change with ceremony, not a refactor.
      # The 17-name scope vocabulary — CI asserts this list equals
      # MANAGEMENT_SCOPES exactly (order included).
      x-scopes:
        compliance:read: Fetch signed receipts, incident timelines and seal exports
        exports:read: Poll export-job status and download completed exports
        breakglass:read: List break-glass grants
        breakglass:revoke: Revoke break-glass grants
        rules:read: List governance rules
        agents:read: List agents
        budgets:read: List agent budgets
        channels:read: List notification channels
        permissions:read: List the permission catalog
        events:read: List and count governance events
        events:export: Export the governance-event record — sync download or async job
        rules:write: Create, edit, delete, enforce/demote, install packs and backtest governance rules
        budgets:write: Create, edit and delete agent budgets
        agents:write: Register, edit, pause/resume and soft-delete individual agents
        agents:kill: Flip the org-wide kill switch — pause-all and resume-all
        channels:write: Create, edit and delete notification channels
        permissions:write: Create, edit and delete catalog permissions

  headers:
    RetryAfter:
      description: Seconds until the rate-limit window resets.
      schema:
        type: integer

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string
          description: Machine-readable code, present on classified failures.
      additionalProperties: true
    ValidationErrors:
      type: object
      required: [errors]
      properties:
        errors:
          type: array
          items:
            type: string
      description: Every validation reason at once — the write endpoints' shape.
    InsufficientScope:
      type: object
      required: [error, code, required_scope]
      properties:
        error: { type: string }
        code: { const: insufficient_scope }
        required_scope: { type: string }
    RateLimited:
      type: object
      required: [error, code]
      properties:
        error: { type: string }
        code: { const: rate_limited }
    PlanRequired:
      type: object
      required: [error, code, feature, required_tier, current_tier]
      properties:
        error: { type: string }
        code: { const: plan_required }
        feature: { type: string }
        required_tier: { type: string }
        current_tier: { type: string }
      description: |
        A tier gate refused the operation. Existing configuration keeps
        working (gates cover create-or-widen, never evaluation); the body
        names the feature and both tiers.

    Rule:
      type: object
      description: >
        A governance rule row — the exact read shape both transports return
        (rulesService RULE_COLUMNS). `condition` and `effects` are the rule
        grammar (GRAMMAR_VERSION 2), documented in the dashboard; they are
        validated jsonb, not free-form.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        is_enabled: { type: boolean }
        scope: { type: string, enum: [agent, org] }
        priority: { type: integer }
        condition: { type: object, additionalProperties: true }
        effects:
          type: array
          items: { type: object, additionalProperties: true }
        enforcement_mode: { type: string, enum: [shadow, enforce] }
        canary_percent:
          type: [integer, "null"]
          description: 1–99, or null (= 100%, full enforcement).
        canary_started_at: { type: [string, "null"] }
        source_kind: { type: string }
        source_id: { type: [string, "null"] }
        created_at: { type: string }
        updated_at: { type: string }

    PlanDiffResult:
      description: |
        THE ARC B CONTRACT (docs/architecture/rule-backtest.md §7b, verbatim):
        the old-vs-new verdict diff a rule edit would produce over recent
        history. Never a blocker — every no-preview path is the honest
        `{ available: false, reason }` arm.
      oneOf:
        - type: object
          required: [available, reason]
          properties:
            available: { const: false }
            reason: { type: string }
          additionalProperties: false
        - type: object
          required: [available, horizon_days, anchor, evaluated, decisions, events, samples, disclosures]
          properties:
            available: { const: true }
            horizon_days: { type: integer, description: Window length in days (default 7). }
            anchor: { type: string, description: ISO timestamp — end of the window. }
            evaluated:
              type: object
              required: [decisions, events]
              properties:
                decisions: { type: integer }
                events: { type: integer }
            decisions:
              type: object
              required: [newly_denied, newly_escalated, newly_auto_approved, released, transitions]
              properties:
                newly_denied: { type: integer }
                newly_escalated: { type: integer }
                newly_auto_approved: { type: integer }
                released: { type: integer }
                transitions:
                  type: object
                  description: The full "<old>-><new>" transition matrix.
                  additionalProperties: { type: integer }
            events:
              type: object
              required: [newly_matched, no_longer_matched, still_matched]
              properties:
                newly_matched: { type: integer }
                no_longer_matched: { type: integer }
                still_matched: { type: integer }
            samples:
              type: object
              required: [newly_denied, newly_escalated, newly_auto_approved, released]
              properties:
                newly_denied: { type: array, items: { type: string }, maxItems: 5 }
                newly_escalated: { type: array, items: { type: string }, maxItems: 5 }
                newly_auto_approved: { type: array, items: { type: string }, maxItems: 5 }
                released: { type: array, items: { type: string }, maxItems: 5 }
            disclosures:
              type: array
              items: { type: string }
              description: The replay engine's honesty ledger, verbatim.
          additionalProperties: false

    BacktestReport:
      type: object
      description: >
        A historical replay report ("would have matched N times"). Top-level
        fields listed; sample rows carry per-condition resolved values and are
        row-shaped (dynamic by design).
      properties:
        horizonDays: { type: integer }
        evaluated:
          type: object
          properties:
            decisions: { type: integer }
            events: { type: integer }
        skipped:
          type: object
          properties:
            missingAgent: { type: integer }
        notSimulated: { type: array, items: { type: string } }
        totals:
          type: object
          properties:
            matched: { type: integer }
            byPhase: { type: object, additionalProperties: { type: integer } }
        outcomes:
          type: object
          description: would-have counters (denied/escalated/allowed/notified/flagged/paused/revoked).
          additionalProperties: { type: integer }
        series:
          type: array
          items:
            type: object
            properties:
              bucketStart: { type: string }
              matched: { type: integer }
        samples:
          type: array
          items: { type: object, additionalProperties: true }
        disclosures: { type: array, items: { type: string } }
      additionalProperties: true

    CanaryReport:
      type: object
      required: [canary]
      description: >
        Live canary evidence for one rule. `canary: false` when the rule is
        not an enforcing canary; every other field appears only on a live
        canary. Would-have counts are lower bounds (shadow-slot competition).
      properties:
        canary: { type: boolean }
        percent: { type: integer }
        since: { type: string }
        cohort:
          type: object
          properties:
            enforced: { type: integer }
            total: { type: integer }
        enforced:
          type: object
          properties:
            denied: { type: integer }
            pending: { type: integer }
            resolved: { type: integer }
        would_have:
          type: object
          properties:
            denied: { type: integer }
            escalated: { type: integer }

    Budget:
      type: object
      description: An agent budget row (budgetsService BUDGET_COLUMNS).
      properties:
        id: { type: string, format: uuid }
        scope: { type: string, enum: [agent, org] }
        agent_name: { type: [string, "null"] }
        kind: { type: string }
        meter_key: { type: [string, "null"] }
        window_seconds: { type: [integer, "null"] }
        window_unit: { type: [string, "null"], description: Calendar window (today/week/month) — mutually exclusive with window_seconds. }
        cap: { type: number }
        warn_fraction: { type: [number, "null"] }
        act_effect: { type: [string, "null"] }
        channel_ids: { type: [array, "null"], items: { type: string } }
        unit_label: { type: [string, "null"] }
        is_enabled: { type: boolean }
        warn_rule_id: { type: [string, "null"] }
        act_rule_id: { type: [string, "null"] }
        created_at: { type: string }
        updated_at: { type: string }

    Agent:
      type: object
      description: >
        An agent row. The list endpoint returns the full row (select *), so
        this schema documents the stable fields and admits the rest honestly.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        owner: { type: string }
        status: { type: string, enum: [active, paused, flagged, revoked] }
        score: { type: number, description: Trust score, 0–100. }
        permissions: { type: [array, "null"], items: { type: string } }
        last_action: { type: [string, "null"] }
        registered_at: { type: [string, "null"] }
      additionalProperties: true

    Channel:
      type: object
      description: >
        A notification channel (channelsService CHANNEL_SELECT). The webhook
        signing secret is NEVER in any read — it is returned exactly once, at
        creation, as a sibling `signing_secret` field.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { type: string }
        config: { type: object, additionalProperties: true }
        enabled: { type: boolean }
        organisation_id: { type: string, format: uuid }
        created_at: { type: string }
        secret_revealed_at: { type: [string, "null"] }

    Permission:
      type: object
      description: A catalog permission (permissionsService PERMISSION_SELECT).
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: [string, "null"] }
        group: { type: [string, "null"] }
        requires_elevation: { type: boolean }

    GovernanceEvent:
      type: object
      required: [kind]
      description: >
        One governance event — a violation, an escalation, or an enforcement
        action (the read-time classifier's taxonomy; an unannotated allowed
        decision is not an event). Fields vary by kind; the shared and
        per-kind fields are listed.
      properties:
        kind: { type: string, enum: [violation, escalation, enforcement_action] }
        agent: { type: string }
        rule: { type: [string, "null"] }
        at: { type: string }
        enforced:
          type: boolean
          description: violations/escalations — false marks the standalone shadow ("would have") event.
        action: { type: string, description: violations/escalations — the attempted action. }
        reason: { type: [string, "null"], description: violations/enforcement actions. }
        decisionId: { type: string, description: violations/escalations. }
        requestId: { type: string, description: escalations. }
        resolution: { type: string, description: escalations — pending/approved/denied/timed_out/auto_approved/not_raised. }
        action_taken: { type: string, description: enforcement actions — flag/pause/revoke. }
        shadowNote:
          type: object
          description: Present only when an enforced event's row also carries a shadow would-have.
          properties:
            status: { type: string }
            rule: { type: [string, "null"] }
            reason: { type: [string, "null"] }

    ExportJob:
      type: object
      description: >
        An async export job (exportJobsService EXPORT_JOB_COLUMNS). Status
        vocabulary is FROZEN — queued / running / complete / refused / failed;
        complete-or-refused honesty, never a silently partial artifact (a
        refused job's reason is in `detail`).
      properties:
        id: { type: string, format: uuid }
        kind: { const: governance_events }
        format: { type: string, enum: [csv, json] }
        params: { type: object, additionalProperties: true }
        status: { type: string, enum: [queued, running, complete, refused, failed] }
        row_count: { type: integer }
        chunk_count: { type: integer }
        detail: { type: [string, "null"] }
        created_at: { type: string }
        completed_at: { type: [string, "null"] }
        expires_at: { type: [string, "null"], description: Jobs expire after 7 days — an export is a download, not an archive. }

    BreakGlassGrant:
      type: object
      description: A break-glass grant row (BREAK_GLASS_GRANT_COLUMNS) — active and lapsed rows both list; the paper trail is the product.
      properties:
        id: { type: string, format: uuid }
        agent_name: { type: string }
        permission: { type: string }
        justification: { type: string }
        granted_by: { type: [string, "null"] }
        created_at: { type: string }
        expires_at: { type: string }
        revoked_at: { type: [string, "null"] }
        revoked_by: { type: [string, "null"] }

    SignedArtifact:
      type: object
      description: >
        A signed compliance artifact (oversight receipt or incident timeline).
        The canonical form, signature scheme and boundary statement are
        versioned INSIDE the artifact (`format`, and `algo` within the signed
        core) and verified offline with scripts/verify-audit-seals.mjs plus
        the published keys at /.well-known/veragent-audit-signing-keys — the
        artifact's internals are governed by that contract, not re-specified
        here.
      required: [format, signature, key_id]
      properties:
        format: { type: string }
        signature: { type: string }
        key_id: { type: string }
        signing_keys_well_known: { type: string }
      additionalProperties: true

  responses:
    AuthScopeAndLimitFailures:
      description: |
        The standard failure envelope for authenticated operations:
        400 `{error}` / `{errors: []}` on invalid input; 401 `{error}` on a
        missing/invalid/expired key, or `{error, code: "ip_not_allowed"}` when
        the key's IP allowlist refuses this network (an authentication
        failure — never a governance event); 403
        `{error, code: "insufficient_scope", required_scope}` when the key
        lacks the operation's scope; 429 `{error, code: "rate_limited"}` with
        a `Retry-After` header when the key's bucket is exhausted.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/Error'
              - $ref: '#/components/schemas/ValidationErrors'
              - $ref: '#/components/schemas/InsufficientScope'
              - $ref: '#/components/schemas/RateLimited'

paths:
  /api/v1/openapi.yaml:
    get:
      operationId: getOpenApiSpec
      summary: This contract document, byte-for-byte
      description: >
        Serves the repository's docs/api/openapi.yaml verbatim — one source;
        a test asserts the served bytes are the repo file's bytes. Public by
        design (you need the contract before you have a key) — the ONE
        unauthenticated /api/v1 route, allowlisted by name in the v1 prologue
        pin.
      security: []
      x-required-scope: null
      x-ratelimit-bucket: null
      responses:
        '200':
          description: The OpenAPI document (YAML).
          content:
            application/yaml:
              schema: { type: string }

  /api/v1/me:
    get:
      operationId: getMe
      summary: Key introspection
      description: Who am I — organisation, key name/prefix, and the scopes this key carries. Authentication only; no scope required.
      x-required-scope: null
      x-ratelimit-bucket: null
      responses:
        '200':
          description: The key's identity.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      organisation_id: { type: string, format: uuid }
                      organisation_name: { type: [string, "null"] }
                      key_name: { type: [string, "null"] }
                      key_prefix: { type: [string, "null"] }
                      key_type: { const: management }
                      scopes: { type: array, items: { type: string } }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Governance rules ────────────────────────────────────────────────────────
  /api/v1/rules:
    get:
      operationId: listRules
      summary: List governance rules
      x-required-scope: rules:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The org's rules, priority order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Rule' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    post:
      operationId: createRule
      summary: Create a governance rule
      description: >
        The full authoring path — grammar validation, tier gates (a gated
        aspect refuses with `plan_required`; existing config always keeps
        evaluating), channel-reference and aggregate-cap checks, then the
        write and its sealed, key-attributed audit record.
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: A rule in the grammar the dashboard authors (name, condition, effects, enforcement_mode, …) — validated server-side; every violation is returned at once.
              additionalProperties: true
      responses:
        '201':
          description: The created rule.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Rule' }
        '403':
          description: Tier gate (`plan_required`) or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PlanRequired'
                  - $ref: '#/components/schemas/InsufficientScope'
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      operationId: updateRule
      summary: Edit a rule (enforce/demote ride the same PATCH)
      description: >
        Field-level overrides — omitted fields keep the stored value.
        Promotion/demotion is `enforcement_mode` (+ optional `canary_percent`)
        on this PATCH; no separate ceremony exists. The response carries the
        plan diff the save computed and stored with its audit record.
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  plan_diff: { $ref: '#/components/schemas/PlanDiffResult' }
        '404': { description: 'No such rule in this organisation.' }
        '409':
          description: The rule is owned by an agent budget — edit it through the budget.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  budget_id: { type: string }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    delete:
      operationId: deleteRule
      summary: Delete a rule
      description: The scoped key substitutes for the dashboard's elevation ceremony (a management key is minted under admin + elevation).
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '404': { description: 'No such rule in this organisation.' }
        '409': { description: Budget-owned rule — delete it through the budget. }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/install-template:
    post:
      operationId: installTemplatePack
      summary: Install a curated rule pack (shadow mode)
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [pack_id]
              properties:
                pack_id: { type: string }
      responses:
        '201':
          description: The installed rules (always shadow mode).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Rule' }
        '409': { description: Some of the pack's rule names already exist. }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/backtest:
    post:
      operationId: backtestRule
      summary: Replay a candidate rule over history
      description: >
        "Would have fired N times over the last N days." Nothing is stored;
        the run leaves one audit record. Over-cap windows REFUSE with a
        message rather than silently truncating — a truncated backtest is a
        wrong number.
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [rule, horizon_days]
              properties:
                rule: { type: object, additionalProperties: true }
                horizon_days: { type: integer, minimum: 1, maximum: 90 }
      responses:
        '200':
          description: The backtest report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BacktestReport' }
        '403':
          description: Tier gate (`plan_required`) or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PlanRequired'
                  - $ref: '#/components/schemas/InsufficientScope'
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/{id}/plan:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      operationId: planRuleEdit
      summary: Plan-before-apply — preview an edit's verdict diff
      description: >
        The same field-level overrides the PATCH accepts (+ optional
        `horizon_days`); preview and save evaluate the identical merged
        candidate. Read-only — no audit record (the witness belongs to the
        save). The response is PlanDiffResult, the Arc B contract, verbatim.
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: The plan.
          content:
            application/json:
              schema:
                type: object
                required: [plan]
                properties:
                  plan: { $ref: '#/components/schemas/PlanDiffResult' }
        '404': { description: 'No such rule in this organisation.' }
        '403':
          description: Tier gate (`plan_required`) or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PlanRequired'
                  - $ref: '#/components/schemas/InsufficientScope'
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/{id}/canary-report:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: getCanaryReport
      summary: Live canary evidence for a rule
      description: Read-only evidence fronting the promote decision — what the canary has actually done at N%, versus the plan endpoint's "what would it do at 100%".
      x-required-scope: rules:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema:
                type: object
                required: [report]
                properties:
                  report: { $ref: '#/components/schemas/CanaryReport' }
        '404': { description: 'No such rule in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/rules/synthesize:
    post:
      operationId: synthesizeRules
      summary: Synthesize candidate rules from an incident
      description: >
        Deterministic template candidates at 2–3 breadths from one decision
        row, each with a blast-radius backtest over one shared window.
        Nothing is saved — candidates save through POST /api/v1/rules (shadow
        like any create). One audit record witnesses the run.
      x-required-scope: rules:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [decision_id]
              properties:
                decision_id: { type: string, format: uuid }
                horizon_days: { type: integer, minimum: 1, maximum: 90 }
      responses:
        '200':
          description: The candidates, narrowest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  incident:
                    type: object
                    properties:
                      id: { type: string }
                      agent_name: { type: string }
                      action: { type: string }
                      status: { type: string }
                      created_at: { type: string }
                  horizon_days: { type: integer }
                  candidates:
                    type: array
                    items:
                      type: object
                      properties:
                        strategy: { type: string }
                        rationale: { type: string }
                        rule: { type: object, additionalProperties: true }
                        backtest:
                          oneOf:
                            - $ref: '#/components/schemas/BacktestReport'
                            - type: 'null'
                        backtest_unavailable_reason: { type: string }
        '404': { description: 'No such decision in this organisation.' }
        '422': { description: No valid candidates could be synthesized. }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Agent budgets ───────────────────────────────────────────────────────────
  /api/v1/budgets:
    get:
      operationId: listBudgets
      summary: List agent budgets
      x-required-scope: budgets:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The org's budgets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Budget' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    post:
      operationId: createBudget
      summary: Create an agent budget
      x-required-scope: budgets:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: A budget definition (scope, kind, cap, window — window_unit XOR window_seconds); validated server-side.
              additionalProperties: true
      responses:
        '201':
          description: The created budget.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Budget' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/budgets/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      operationId: updateBudget
      summary: Edit a budget
      x-required-scope: budgets:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '404': { description: 'No such budget in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    delete:
      operationId: deleteBudget
      summary: Delete a budget (and its compiled rules)
      x-required-scope: budgets:write
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '404': { description: 'No such budget in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Agents ─────────────────────────────────────────────────────────────────
  /api/v1/agents:
    get:
      operationId: listAgents
      summary: List agents
      parameters:
        - { name: limit, in: query, required: false, schema: { type: integer } }
      x-required-scope: agents:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The org's agents (soft-deleted rows excluded).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Agent' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    post:
      operationId: createAgent
      summary: Register an agent
      description: The plan's agent-count cap applies (403 when reached — the same refusal the dashboard gets).
      x-required-scope: agents:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name, owner]
              properties:
                name: { type: string }
                owner: { type: string }
                score: { type: number, minimum: 0, maximum: 100 }
                permissions: { type: array, items: { type: string } }
                last_action: { type: string }
                status: { type: string, enum: [active, paused, flagged, revoked] }
                registered_at: { type: string }
      responses:
        '200':
          description: The created agent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Agent' }
        '403':
          description: Agent limit reached for the plan, or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Error'
                  - $ref: '#/components/schemas/InsufficientScope'
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/agents/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      operationId: updateAgent
      summary: Edit an agent (status, score, permissions, owner, name, soft-delete)
      description: >
        Per-agent pause/resume is `status`; soft-delete is `is_deleted`.
        Renaming an agent with budgets attached 409s (budgets pin the name).
        A manual score change writes its trust-ledger row.
      x-required-scope: agents:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                status: { type: string, enum: [active, paused, flagged, revoked] }
                score: { type: number, minimum: 0, maximum: 100 }
                permissions: { type: array, items: { type: string } }
                last_action: { type: string }
                owner: { type: string }
                name: { type: string }
                is_deleted: { type: boolean }
                paused_at: { type: [string, "null"] }
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '409':
          description: Rename blocked — budgets reference this agent by name.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  budget_ids: { type: array, items: { type: string } }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/agents/pause-all:
    post:
      operationId: pauseAllAgents
      summary: The org kill switch — pause every active agent
      description: >
        One scope (`agents:kill`) owns the switch in both directions; it
        substitutes for the dashboard's elevation ceremony. Never tier-gated
        on any plan — pause is the protective direction. The halt is durable
        and binds on the very next authorization.
      x-required-scope: agents:kill
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: What was paused.
          content:
            application/json:
              schema:
                type: object
                properties:
                  paused: { type: integer }
                  agent_ids: { type: array, items: { type: string } }
                  recorded: { type: boolean, description: Whether the resume-set audit record landed (resume-all reads it). }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    get:
      operationId: getKillSwitchStatus
      summary: Kill-switch status
      description: Reading the switch's state needs only agents:read; flipping it needs agents:kill.
      x-required-scope: agents:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: Whether a kill-switch set is active and how many agents it would resume.
          content:
            application/json:
              schema:
                type: object
                properties:
                  active: { type: boolean }
                  resumable: { type: integer }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/agents/resume-all:
    post:
      operationId: resumeAllAgents
      summary: Resume the agents the kill switch paused
      description: Resumes exactly the set the last pause-all recorded — never agents paused individually for other reasons.
      x-required-scope: agents:kill
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: What was resumed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  resumed: { type: integer }
                  agent_ids: { type: array, items: { type: string } }
                  recorded: { type: boolean }
        '400':
          description: No active kill-switch set to resume.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Notification channels ──────────────────────────────────────────────────
  /api/v1/channels:
    get:
      operationId: listChannels
      summary: List notification channels
      description: Configs are returned in the admin posture (a management key is minted under admin + elevation). The webhook signing secret is never in any read.
      x-required-scope: channels:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The org's channels.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Channel' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    post:
      operationId: createChannel
      summary: Create a channel
      description: For `webhook` channels the HMAC `signing_secret` is returned ONCE, here, and never again.
      x-required-scope: channels:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name, type]
              properties:
                name: { type: string }
                type: { type: string }
                config: { type: object, additionalProperties: true }
                enabled: { type: boolean }
      responses:
        '200':
          description: The created channel (+ show-once signing_secret for webhooks).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Channel' }
                  signing_secret: { type: string }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/channels/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      operationId: updateChannel
      summary: Edit a channel
      x-required-scope: channels:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '404': { description: 'No such channel in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    delete:
      operationId: deleteChannel
      summary: Delete a channel
      x-required-scope: channels:write
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '404': { description: 'No such channel in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Permission catalog ─────────────────────────────────────────────────────
  /api/v1/permissions:
    get:
      operationId: listPermissions
      summary: List the permission catalog
      x-required-scope: permissions:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Permission' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    post:
      operationId: createPermission
      summary: Create a catalog permission
      x-required-scope: permissions:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                group: { type: string }
                requires_elevation: { type: boolean }
      responses:
        '200':
          description: The created permission.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Permission' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    patch:
      operationId: updatePermission
      summary: Edit a catalog permission (id in the body)
      description: This surface mirrors the dashboard's single-file shape — the target `id` travels in the body, not the path.
      x-required-scope: permissions:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: { type: string, format: uuid }
              additionalProperties: true
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
    delete:
      operationId: deletePermission
      summary: Delete a catalog permission (id in the body)
      x-required-scope: permissions:write
      x-ratelimit-bucket: v1:write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: { type: string, format: uuid }
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Governance events ──────────────────────────────────────────────────────
  /api/v1/events:
    get:
      operationId: listEvents
      summary: The governance-event feed
      parameters:
        - { name: kind, in: query, required: false, schema: { type: string, enum: [violation, escalation, enforcement_action] }, description: Repeatable. Omit for all kinds. }
        - { name: since, in: query, required: false, schema: { type: string, format: date } }
      x-required-scope: events:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: Events, newest window first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/GovernanceEvent' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/events/counts:
    get:
      operationId: countEvents
      summary: Exact aggregate event counts
      parameters:
        - { name: since, in: query, required: false, schema: { type: string, format: date } }
      x-required-scope: events:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: >
            Counts keyed by the taxonomy: enforced vs shadow violations,
            escalations broken out by resolution (band auto-approvals are
            their own line, never folded into human approvals), enforcement
            actions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    additionalProperties: true
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/events/export:
    get:
      operationId: exportEvents
      summary: The sync governance-event export (≤100k rows)
      description: >
        Byte-identical to the dashboard export — same formats, same cap, same
        honesty: a capped walk sets `truncated: true` (JSON) / appends a
        TRUNCATED marker row (CSV) rather than pretending completeness. Every
        key-authed export writes an attribution record (key, scope, window,
        row count, truncated flag). Past the cap, use the async job.
      parameters:
        - { name: format, in: query, required: false, schema: { type: string, enum: [csv, json], default: csv } }
        - { name: scope, in: query, required: false, schema: { type: string, enum: [view, all], default: view } }
        - { name: kind, in: query, required: false, schema: { type: string, enum: [violation, escalation, enforcement_action] }, description: Repeatable; scope=view only. }
        - { name: since, in: query, required: false, schema: { type: string, format: date }, description: scope=view only. }
      x-required-scope: events:export
      x-ratelimit-bucket: v1:export
      responses:
        '200':
          description: The export file (attachment).
          content:
            text/csv:
              schema: { type: string }
            application/json:
              schema:
                type: object
                properties:
                  generatedAt: { type: string }
                  scope: { type: string }
                  filters: { type: [object, "null"] }
                  count: { type: integer }
                  truncated: { type: boolean }
                  limit: { type: integer, description: Present only when truncated. }
                  events:
                    type: array
                    items: { $ref: '#/components/schemas/GovernanceEvent' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/events/export/async:
    post:
      operationId: queueAsyncExport
      summary: Queue an async export (past the sync cap)
      description: >
        The same egress grant as the sync export — sync vs async is a size
        decision, not a different kind of act. Same query-param vocabulary.
        One active job per organisation. Complete-or-refused, never partial:
        poll GET /api/v1/export-jobs/{id}, then download.
      parameters:
        - { name: format, in: query, required: false, schema: { type: string, enum: [csv, json], default: csv } }
        - { name: scope, in: query, required: false, schema: { type: string, enum: [view, all], default: all } }
        - { name: kind, in: query, required: false, schema: { type: string, enum: [violation, escalation, enforcement_action] }, description: Repeatable; scope=view only. }
        - { name: since, in: query, required: false, schema: { type: string, format: date }, description: scope=view only. }
      x-required-scope: events:export
      x-ratelimit-bucket: v1:export
      responses:
        '202':
          description: The queued job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/ExportJob' }
        '409':
          description: An export job is already queued or running for this organisation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  job_id: { type: string }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Export jobs ────────────────────────────────────────────────────────────
  /api/v1/export-jobs:
    get:
      operationId: listExportJobs
      summary: List export jobs (newest first)
      x-required-scope: exports:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The org's jobs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ExportJob' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/export-jobs/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: getExportJob
      summary: Poll one job's status
      x-required-scope: exports:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/ExportJob' }
        '404': { description: 'No such export job in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/export-jobs/{id}/download:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: downloadExportJob
      summary: Download a COMPLETE export as one file
      description: >
        Refusal over silent partial, here too: anything but status `complete`
        is a 409 naming the status (a refused job's reason travels in the
        body). A downloadable job is by construction the whole record and the
        JSON header says so (`complete: true, truncated: false`). Key-authed
        downloads write an attribution record — this is the egress moment.
      x-required-scope: exports:read
      x-ratelimit-bucket: v1:export
      responses:
        '200':
          description: The export file (attachment; streamed).
          content:
            text/csv:
              schema: { type: string }
            application/json:
              schema: { type: object, additionalProperties: true }
        '404': { description: 'No such export job in this organisation.' }
        '409':
          description: The job is not complete — its status (and reason, if refused) is in the body.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  status: { type: string }
                  detail: { type: string }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Compliance artifacts ───────────────────────────────────────────────────
  /api/v1/compliance/receipts/{decision_id}:
    parameters:
      - { name: decision_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: getOversightReceipt
      summary: A signed oversight receipt for one decision
      description: >
        Ed25519-signed, netstring-canonical, the boundary statement inside
        the signed bytes; verify offline with scripts/verify-audit-seals.mjs
        and the published keys. Enterprise-gated (`plan_required` otherwise) —
        the gate covers the signed presentation, never the underlying sealed
        record. Honest refusals: a pending decision has no receipt (409); an
        unsealed record has none yet (409, retry within ~70 minutes); a
        signing-key-less environment issues nothing (503).
      x-required-scope: compliance:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The receipt artifact (format veragent-oversight-receipt-v1).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SignedArtifact' }
        '403':
          description: Enterprise gate (`plan_required`) or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PlanRequired'
                  - $ref: '#/components/schemas/InsufficientScope'
        '404': { description: 'No such decision in this organisation.' }
        '409': { description: Pending decision, or record not yet sealed — the body says which and when to retry. }
        '503': { description: No signing key in this environment — an unsigned receipt is not issued. }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/compliance/incident-timeline:
    get:
      operationId: getIncidentTimeline
      summary: A signed incident timeline for a window
      description: >
        The window's governance events in order, each entry citing its
        covering seal; the document is Ed25519-signed with the boundary
        inside the signed bytes (algo timeline-v3 — including the statement
        that seals attest integrity of what was recorded, never completeness
        of recording). Enterprise-gated. Refusal over silent partial: an
        over-cap window 400s with the count and the fix, never a quietly
        truncated narrative.
      parameters:
        - { name: from, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: to, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: agent, in: query, required: false, schema: { type: string } }
      x-required-scope: compliance:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The timeline artifact (format veragent-incident-timeline-v1).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SignedArtifact' }
        '400':
          description: 'Invalid window, or the over-cap refusal (`refused: true` with the count and the fix).'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  refused: { type: boolean }
        '403':
          description: Enterprise gate (`plan_required`) or missing scope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PlanRequired'
                  - $ref: '#/components/schemas/InsufficientScope'
        '503': { description: No signing key in this environment — an unsigned timeline is not issued. }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/compliance/seal-export:
    get:
      operationId: getSealExport
      summary: The auditor handoff — the whole verification chain, one file
      description: >
        Everything a third party needs to verify this organisation's audit
        chain offline — seals, Ed25519 signatures, Rekor anchors, erasure
        records, and the hashed columns byte-exact as the sealer consumed
        them. DELIBERATELY UN-TIERED (no plan gate, either transport): the
        export exists so outsiders can verify without trusting Veragent, and
        gating the raw verification material behind a tier would contradict
        that. Key-authed pulls write an attribution record (this is the
        organisation's entire audit trail leaving through a credential).
      x-required-scope: compliance:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The seal export (format veragent-seal-export-v1).
          content:
            application/json:
              schema:
                type: object
                required: [format, organisation_id, seals, signatures, anchors, erasures, rows]
                properties:
                  format: { const: veragent-seal-export-v1 }
                  exported_at: { type: string }
                  organisation_id: { type: string, format: uuid }
                  signing_keys_well_known: { type: string }
                  signing_keys:
                    type: array
                    items: { type: object, additionalProperties: true }
                    description: Convenience copy — cross-check against the well-known URL independently.
                  seals: { type: array, items: { type: object, additionalProperties: true } }
                  signatures: { type: array, items: { type: object, additionalProperties: true } }
                  anchors: { type: array, items: { type: object, additionalProperties: true } }
                  erasures: { type: array, items: { type: object, additionalProperties: true } }
                  rows: { type: array, items: { type: object, additionalProperties: true } }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  # ── Break-glass grants ─────────────────────────────────────────────────────
  /api/v1/break-glass-grants:
    get:
      operationId: listBreakGlassGrants
      summary: List break-glass grants (active and lapsed)
      description: >
        `active=1` filters with the authorization gate's own read-time
        predicate (not revoked, not expired), so "active" can never diverge
        from what the gate would honor. There is deliberately no POST here:
        ISSUING a grant creates new capability at runtime and stays
        human-only (dashboard, admin + fresh two-factor challenge).
      parameters:
        - { name: agent, in: query, required: false, schema: { type: string } }
        - { name: active, in: query, required: false, schema: { type: string, enum: ["1"] } }
      x-required-scope: breakglass:read
      x-ratelimit-bucket: v1:read
      responses:
        '200':
          description: The grants, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/BreakGlassGrant' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'

  /api/v1/break-glass-grants/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      operationId: revokeBreakGlassGrant
      summary: Revoke a grant early
      description: >
        The safety direction — privilege-reducing, so it is never tier-gated
        and never takes an elevation ceremony, on either transport. The row
        is never deleted: revocation stamps it, is its own sealed audit
        event, and the authorization gate stops honoring the grant on the
        next decision.
      x-required-scope: breakglass:revoke
      x-ratelimit-bucket: v1:write
      responses:
        '200':
          description: Revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
        '400': { description: Already revoked, or already expired — nothing to revoke. }
        '404': { description: 'No such grant in this organisation.' }
        4XX:
          $ref: '#/components/responses/AuthScopeAndLimitFailures'
