openapi: 3.1.1
info:
  title: Deedglass API
  version: "2026-10-01"
  summary: The execution and accountability layer for AI agents.
  description: >-
    Deedglass sits between your AI agents and the tools they use, deciding what each agent may do,
    running it under control, and recording what actually happened as a signed deed.


    Every response carries `Deedglass-Request-Id` and the resolved `Deedglass-Version`. Errors use
    one envelope with a `doc_url` for every code. This document is written by hand; tests in
    `internal/httpx` fail when it and the mounted routes drift apart.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
servers:
  - url: http://127.0.0.1:8080
    description: A local server (make dev)
  - url: https://api.deedglass.com
    description: The cloud beta
tags:
  - name: People
    description: >-
      Signing in with an emailed link or code (no passwords), sessions, your org and its members
      (docs/IDENTITY.md).
  - name: Agents
    description: >-
      Agents, their API keys, Stop and Stop all (docs/AGENTS.md). Control-plane routes accept a
      Console session (by role) or an org admin key (`admin` scope). Agent keys get 403
      `insufficient_scope` on all of them: no agent key changes any agent, including its own. A 401
      carries `WWW-Authenticate: Bearer realm="deedglass", error="invalid_token",
      error_description="…"`; a 403 `insufficient_scope` carries `WWW-Authenticate: Bearer
      realm="deedglass", error="insufficient_scope", scope="…"`. Every change seals an admin deed.
  - name: Pages
    description: The server-rendered sign-in pages and the Console.
  - name: System
    description: Health, version and this document.
  - name: Ledger
    description: >-
      The searchable history of every action an agent took and every state change, and the deed
      chain check (docs/LEDGER.md).
  - name: Events
    description: >-
      The live event stream (Server-Sent Events) the Console and `deedglass actions list --follow`
      read (docs/EVENTS.md).
  - name: Overview
    description: >-
      The Console's first screen as numbers: actions, blocks, verdicts, approvals, the most active
      agents and what needs a person (docs/OVERVIEW_METRICS.md).
  - name: Gate
    description: >-
      Sending actions through the Gate, cancelling one that waits for approval, and reporting the
      result of a proxy-mode action (docs/GATE.md).
  - name: Observe
    description: >-
      Observe mode (S50): watched sessions over a Postgres connector's change log, their seal, the
      free undo SQL and the one-step session undo (Pro) (docs/OBSERVE.md).
  - name: Deeds
    description: >-
      Signed records of what agents did, and the public seal keys that check them offline
      (docs/DEED_SPEC.md).
  - name: Intents
    description: "Signed intents: the conformance check SDK authors run against Deedglass."
  - name: Policies
    description: >-
      Policies and their immutable versions, activation as enforce or shadow (each sealed as an
      admin deed), and the lint, dry-run and test tools (docs/POLICY_LANGUAGE.md). Every route
      accepts a Console session (by role) or an org admin key (`admin` scope). Agent keys get 403
      `insufficient_scope` on all of them, reads included.
  - name: Connectors
    description: >-
      Connectors turn an API into governed tools, and hold its credential (docs/CONNECTORS.md).
      Credentials are write-only: no route returns a value, only the kind, the field names and a
      short hint. Every route accepts a Console session (viewers read, admins change) or an org
      admin key (`admin` scope); agent keys get 403 `insufficient_scope` on all of them, reads
      included. Every change seals an admin deed.
  - name: Insights
    description: >-
      What your history says (docs/INSIGHTS.md): replaying a policy version over past decisions,
      where the shadow policy disagrees with the enforced one, decision and verdict stats, and
      anomaly episodes for a person to review. Insights reads and reports; its only input into
      decisions is the opt-in anomaly facts. Every route accepts a Console session (viewers read,
      admins change) or an org admin key (`admin` scope); agent keys get 403 `insufficient_scope`
      on all of them, reads included.
  - name: MCP
    description: "MCP authorization readiness: OAuth protected-resource metadata (RFC 9728) and the /mcp endpoints."
paths:
  /v1/health:
    get:
      operationId: getHealth
      summary: Check the server and its database
      description: >-
        Public. The checks are cached for 1 s, so frequent probes don't load the database. The body
        shows statuses only; details go to the server log. Returns 503 when the database is
        unreachable or the server is draining.
      tags: [System]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The server can serve requests (`status` is `ok` or `degraded`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
        "503":
          description: The database is unreachable (`down`) or the server is shutting down (`draining`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
        default:
          $ref: "#/components/responses/Error"
  /v1/version:
    get:
      operationId: getVersion
      summary: Show the server's version and API versions
      tags: [System]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: This server's build and the API versions it serves.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Version"
        default:
          $ref: "#/components/responses/Error"
  /api/openapi.yaml:
    get:
      operationId: getOpenAPISpec
      summary: Download this document
      tags: [System]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: This OpenAPI document, exactly as embedded in the server.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/yaml:
              schema:
                type: string
        default:
          $ref: "#/components/responses/Error"
  /.well-known/deedglass/keys.json:
    get:
      operationId: getSealKeys
      summary: List the public seal keys
      description: >-
        Public and not versioned. Every seal key this deployment ever registered, oldest first, so
        any deed it sealed can be checked offline with `deedglass verify`. Retired keys stay listed;
        a deed is valid under a key when `activated_at ≤ issued_at < retired_at` and the key is not
        revoked. Readable cross-origin (`Access-Control-Allow-Origin: *`). Cached for 60 s on the
        server and 300 s by clients; send `If-None-Match` with the `ETag` to get 304. The server
        never returns a partial list: when it can't read the keys it answers 503
        `service_unavailable`.
      tags: [Deeds]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - name: If-None-Match
          in: header
          required: false
          description: The `ETag` of a copy you hold.
          schema:
            type: string
      responses:
        "200":
          description: The seal key set.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            ETag:
              description: '`"sha256:<hex>"` of the body.'
              schema:
                type: string
            Cache-Control:
              schema:
                type: string
                const: public, max-age=300
            Access-Control-Allow-Origin:
              schema:
                type: string
                const: "*"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SealKeySet"
        "304":
          description: The copy named in `If-None-Match` is current.
          headers:
            ETag:
              description: '`"sha256:<hex>"` of the body.'
              schema:
                type: string
        default:
          $ref: "#/components/responses/Error"
  /v1/events:
    get:
      operationId: streamEvents
      summary: Stream live events (Server-Sent Events)
      description: >-
        What Postgres committed in your org, in commit order, as `text/event-stream`
        (docs/EVENTS.md). A Console session (viewer and up) or an org admin key sees the whole org;
        an agent key needs `actions:read` and sees only its own agent's actions, approvals and
        deeds. The stream starts with `retry: 3000`, then `event: hello`, then one message per
        event: `id: <instance>.<seq>` (the stream cursor, not the `evt_` id), `event: <type>` and
        `data:` the event object, which is exactly the webhook envelope (`object`, `id`, `type`,
        `api_version`, `created_at`, `livemode`, `org`, `data.object`) plus `hydrated`. When
        `hydrated` is true, `data.object` is the object in the shape its REST list returns
        (`deed.sealed` carries a `deed_summary`); otherwise it is the thin webhook object. Events
        never carry request or result payloads, approval context or email addresses. A `: ping`
        comment comes every 15 s when nothing else does; the server closes the stream after 30
        minutes (reconnect with the last cursor). The credential is checked again every 60 s; when
        it is no longer valid, `event: session_ended` carries the error code and message, and the
        stream closes. Whenever you may have missed events (a cursor this instance can't serve,
        the server's connection to Postgres reconnected, or your client fell behind), `event:
        resync` names the reason: refetch your lists. Resume is best effort, within about 5
        minutes on the same instance. Filters and cursors are checked before the first byte; a bad
        one is a `400` envelope. More than 8 open streams for one person or key is `429`
        `rate_limited`.
      tags: [Events]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: types, in: query, required: false, description: 'A comma list of event types or family wildcards: `action.*,deed.sealed`. Default every type.', schema: {type: string, maxLength: 1200}, example: "action.*,deed.sealed"}
        - {name: agent, in: query, required: false, description: Only this agent's events. An agent key may name only its own agent., schema: {type: string, pattern: "^agt_[0-7][0-9a-hjkmnp-tv-z]{25}$"}}
        - {name: last_event_id, in: query, required: false, description: 'The last `id:` you received, to resume (for clients that can''t set `Last-Event-ID`; the header wins).', schema: {type: string, pattern: "^[0-9a-z]{1,32}\\.[0-9]{1,19}$"}, example: "7f3k2m.1842"}
        - {name: Last-Event-ID, in: header, required: false, description: The last `id:` you received; EventSource sends it on reconnect., schema: {type: string}}
      responses:
        "200":
          description: >-
            The stream. Messages: `hello` (StreamHello), one per event (Event), `resync`
            (StreamResync), `session_ended` (StreamSessionEnded), and `: ping` comments.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events; each data line is one JSON object of the schemas named above.
              examples:
                live:
                  summary: A hello, then an action event
                  value: |+
                    retry: 3000

                    id: 7f3k2m.1841
                    event: hello
                    data: {"object":"stream","instance":"7f3k2m","resumed":false,"heartbeat_s":15,"server_time":"2026-10-08T14:02:09.100Z"}

                    id: 7f3k2m.1842
                    event: action.witnessed
                    data: {"object":"event","id":"evt_01k6h7mz3c9d8e7f6g5h4j3k2m","type":"action.witnessed","api_version":"2026-10-01","created_at":"2026-10-08T14:02:09.512Z","livemode":false,"org":"org_01k6h7kp6msmzrhdzwtcr3jnkp","hydrated":true,"data":{"object":{"object":"action","id":"act_01k6h7mz3c9d8e7f6g5h4j3k2m","state":"witnessing"}}}

                    : ping

                resync:
                  summary: The client may have missed events
                  value: |+
                    id: 7f3k2m.1900
                    event: resync
                    data: {"object":"resync","reason":"cursor_expired"}

        default:
          $ref: "#/components/responses/Error"
  /v1/overview:
    get:
      operationId: getOverview
      summary: Get the overview
      description: >-
        The Overview screen's numbers for your org (docs/OVERVIEW_METRICS.md has every
        definition). `window` is `24h` (24 hourly buckets, the default) or `7d` (28 six-hour
        buckets). `to` is the first bucket boundary at or after now, in UTC, so the current partial
        bucket is the last one; `from` is `to` minus the window; an action counts when `from <=
        created_at < to`. `previous` is the window before. Blocked means the decision was `deny`
        (policy, budget and Stop). `not_witnessed` is the window's actions with no verdict. Approvals
        count every pending approval, whatever the window. `agents` is the 8 most recently active
        agents with today's usage in the org's time zone. `attention` holds at most 8 items in
        priority order (`chain_broken`, `org_stopped`, `slot_invalidated`, `slot_lagging`,
        `mismatch`, `approval_expiring`, `budget_near`): kinds and data, never sentences, except a
        mismatch's witness summary. Answers are cached for 10 s per org and window. Any other
        parameter, or another window, is 400 `validation_failed`. A Console session (viewer and up)
        or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Overview]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: window, in: query, required: false, description: '`24h` (default) or `7d`, sent once.', schema: {type: string, enum: [24h, 7d], default: 24h}}
      responses:
        "200":
          description: The overview.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Overview"
        default:
          $ref: "#/components/responses/Error"
  /v1/actions:
    get:
      operationId: listActions
      summary: List actions
      description: >-
        Newest first, ordered by `(created_at, id)` descending, so rows added while you page only
        appear on a fresh first page. Filters combine with AND; list filters take a comma list
        (`verdict=MISMATCH,UNVERIFIABLE`). Unknown parameters are refused. A cursor is bound to the
        filters it was issued with: reuse it with different filters and the answer is 400
        `validation_failed` on `cursor`. Requires `actions:read` (viewer and up).
      tags: [Ledger]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: agent, in: query, required: false, description: An agent id (`agt_…`)., schema: {type: string, pattern: "^agt_[0-7][0-9a-hjkmnp-tv-z]{25}$"}}
        - {name: tool, in: query, required: false, description: 'A tool name, exact or a glob with `*` (`stripe.*`, `*.execute`); at most 200 bytes and three `*`.', schema: {type: string, maxLength: 200}}
        - {name: state, in: query, required: false, description: A comma list of lifecycle states (contract §5)., schema: {type: string}}
        - {name: decision, in: query, required: false, description: 'A comma list of `allow`, `deny`, `require_approval`.', schema: {type: string}}
        - {name: verdict, in: query, required: false, description: 'A comma list of `VERIFIED`, `MISMATCH`, `UNVERIFIABLE`.', schema: {type: string}}
        - {name: level, in: query, required: false, description: 'A comma list of `L0`, `L1`, `L2`.', schema: {type: string}}
        - {name: on_behalf_of, in: query, required: false, description: The exact on-behalf-of reference., schema: {type: string, maxLength: 256}}
        - {name: "created_at[gte]", in: query, required: false, description: RFC 3339; inclusive., schema: {type: string, format: date-time}}
        - {name: "created_at[lt]", in: query, required: false, description: RFC 3339; exclusive., schema: {type: string, format: date-time}}
        - {name: q, in: query, required: false, description: Text to find in the tool name or the error code (2 to 64 characters)., schema: {type: string, minLength: 2, maxLength: 64}}
        - {name: shadow_differs, in: query, required: false, description: 'Only `true` (S26): actions whose shadow policy decided, and decided differently from the enforced decision. Any other value is 400 `validation_failed`.', schema: {type: string, enum: ["true"]}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of actions.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createAction
      summary: Send an action through the Gate
      description: >-
        Every tool call an agent makes passes through here (docs/GATE.md). The Gate checks the
        signed intent (when the agent's intent mode asks for one), reads the agent's Stop state,
        asks the active policy, and then refuses the action, waits for a person, runs the tool
        (gateway mode) or authorizes the edge to run it (proxy mode). Every action it records ends
        as a sealed deed. `status` is the one field an agent needs: `executed` (200: the tool ran
        and the action is sealed, whatever the verdict), `failed` (200: the tool certainly didn't
        apply the call), `authorized` (200, proxy mode: run the tool at the edge now, then report
        it to `next_action.report_url`), `processing` (202: still running when `wait` ended; poll
        `GET /v1/actions/{id}?wait=25`) and `pending_approval` (202: waiting for a person;
        `next_action.poll_url` waits for the decision). A refusal is an error envelope that carries
        the sealed action in `error.action`: `policy_denied` and `policy_unavailable` (403
        `policy_error`), `agent_stopped`, `agent_suspended` and `org_stopped` (403
        `permission_error`), `budget_exceeded` (429 `rate_limit_error`, with `Retry-After`) and
        `tool_unknown` (404). The message is the decision's own sentence, verbatim. Malformed
        requests (400 `validation_failed`) and identity failures (`intent_required`,
        `intent_invalid`, `intent_replay`) are not recorded. A gateway action keeps running when
        the HTTP request ends: it is recorded and sealed either way. **The Gate owns idempotency
        on this route**, not S03's middleware: a key names one action per agent for 24 hours, the
        same key with the same request answers with the original action (and
        `Deedglass-Idempotent-Replayed: true`), a duplicate that arrives while the original runs
        waits for it, and the same key with a different request is 422 `idempotency_conflict`. An
        agent key with `actions:execute`; a stopped or suspended agent's attempt is recorded and
        sealed, then refused.
      tags: [Gate]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Recommended (the SDKs always send one). 1 to 255 printable ASCII characters. It names
            one action of this agent for 24 hours. The comparison is the request digest plus
            `Deedglass-On-Behalf-Of` and `Deedglass-Session`; a signed intent isn't part of it, so
            a retry signed again still replays.
          schema:
            type: string
            minLength: 1
            maxLength: 255
        - $ref: "#/components/parameters/DeedglassIntent"
        - $ref: "#/components/parameters/DeedglassOnBehalfOf"
        - $ref: "#/components/parameters/DeedglassSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionRequest"
            examples:
              gateway:
                summary: Deedglass runs the tool
                value:
                  tool: fake.echo
                  args: {message: hello}
                  mode: gateway
                  wait: 10
                  context: {client: {name: deedglass-cli, version: 0.1.0}, transport: rest}
              proxy:
                summary: The edge runs the tool and reports back
                value:
                  tool: filesystem.write_file
                  args: {path: /tmp/deedglass-demo/allowed/notes.txt, content: hello}
                  mode: proxy
                  connector_type: mcp
                  tool_digest: sha256:7c675408077d30b6ef05421be9370eb9c75c661f7cd6966c1b73454db1cc8018
                  context: {client: {name: deedglass-mcp, version: 0.1.0}, transport: mcp_stdio}
      responses:
        "200":
          description: >-
            `executed` or `failed` (sealed), or `authorized` (proxy mode). `execution.result` holds
            the tool's result on this response only.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
              examples:
                executed:
                  summary: Gateway mode, executed and sealed
                  value:
                    object: action
                    id: act_01k6w3p8r2t4v6x8z0b2d4f6h8
                    created_at: "2026-10-05T04:12:09.114Z"
                    livemode: false
                    agent: {id: agt_01k633rrmh3bcz2p39gg4vp46q, name: support-bot}
                    tool: fake.echo
                    connector: null
                    mode: gateway
                    state: sealed
                    status: executed
                    on_behalf_of: null
                    session: null
                    decision:
                      outcome: allow
                      code: null
                      source: rule
                      rule_id: allow-fake
                      reason: Fake tools are safe to run.
                      message: null
                      policy: pol_01k6r2m8q0s2v4x6z8b0d2f4h6
                      policy_version: pv_01k6r2m9a1c3e5g7j9m1q3s5v7
                      shadow: null
                    policy: {id: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, version_id: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, mode: enforce, decision: allow, rule_id: allow-fake}
                    approval: null
                    execution:
                      status: succeeded
                      started_at: "2026-10-05T04:12:09.116Z"
                      finished_at: "2026-10-05T04:12:09.117Z"
                      latency_ms: 1
                      error_code: null
                      error_message: null
                      result: {echo: {message: hello}}
                    witness: {object: witness, level: L0, verdict: VERIFIED, method: reported, required_level: L0, summary: The tool reported success., reasons: [], witnessed_at: "2026-10-05T04:12:09.130Z", detail_digest: "sha256:0b5f2e9a7c4d1e8f3a6b9c2d5e8f1a4b7c0d3e6f9a2b5c8d1e4f7a0b3c6d9e2f"}
                    cost: {amount: 0, currency: usd}
                    request_digest: sha256:c2cb9a8c51c029f97049fed02f985cc49cee22836423d2c9a8e1b74e333a3c4c
                    result_digest: sha256:fca08626754877b4929151a6f5a278d4189da0bdff4e71b4f523d6f97bb8f0d0
                    payload_pruned_at: null
                    deed: deed_01k6w3p9a1c3e5g7j9m1q3s5v7
                    next_action: null
                authorized:
                  summary: Proxy mode, the edge may run the tool now
                  value:
                    object: action
                    id: act_01k6w5b3d5f7h9k1n3q5s7w9y1
                    created_at: "2026-10-05T04:20:10.902Z"
                    livemode: false
                    agent: {id: agt_01k633rrmh3bcz2p39gg4vp46q, name: support-bot}
                    tool: filesystem.write_file
                    connector: null
                    mode: proxy
                    state: executing
                    status: authorized
                    on_behalf_of: null
                    session: null
                    decision: {outcome: allow, code: null, source: rule, rule_id: allow-demo-dir, reason: Writes inside the demo folder are fine., message: null, policy: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, policy_version: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, shadow: null}
                    policy: {id: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, version_id: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, mode: enforce, decision: allow, rule_id: allow-demo-dir}
                    approval: null
                    execution: null
                    witness: null
                    cost: null
                    request_digest: sha256:30f3b488ca53be83e50a9adf9c649ec4d2c1ec5087de8ebfc473bc1b834c01de
                    result_digest: null
                    payload_pruned_at: null
                    deed: null
                    next_action:
                      type: execute_at_edge
                      report_url: /v1/actions/act_01k6w5b3d5f7h9k1n3q5s7w9y1/result
                      deadline: "2026-10-05T04:30:10.903Z"
        "202":
          description: >-
            `processing` (still executing or witnessing when `wait` ended) or `pending_approval`
            (waiting for a person). Poll `GET /v1/actions/{id}?wait=25`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
              example:
                object: action
                id: act_01k6w6c4e6g8j0m2p4r6t8w0y2
                created_at: "2026-10-05T04:25:31.208Z"
                livemode: false
                agent: {id: agt_01k633rrmh3bcz2p39gg4vp46q, name: support-bot}
                tool: fake.write
                connector: null
                mode: gateway
                state: pending_approval
                status: pending_approval
                on_behalf_of: u_4821
                session: null
                decision: {outcome: require_approval, code: approval_required, source: rule, rule_id: writes-need-approval, reason: Writes wait for a human., message: "Waiting for approval under policy \"writes-need-approval\": Writes wait for a human.", policy: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, policy_version: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, shadow: null}
                policy: {id: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, version_id: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, mode: enforce, decision: require_approval, rule_id: writes-need-approval}
                approval: {id: apr_01k6w6c5f7h9k1n3q5s7v9x1z3, status: pending, expires_at: "2026-10-05T04:40:31.210Z"}
                execution: null
                witness: null
                cost: null
                request_digest: sha256:f7ee87ae04b701c55919513f07a30d1c1d738b0373f17df44ed99607f3580749
                result_digest: null
                payload_pruned_at: null
                deed: null
                next_action:
                  type: await_approval
                  approval: apr_01k6w6c5f7h9k1n3q5s7v9x1z3
                  expires_at: "2026-10-05T04:40:31.210Z"
                  poll_url: /v1/actions/act_01k6w6c4e6g8j0m2p4r6t8w0y2?wait=25
        "400":
          description: >-
            `validation_failed`: a malformed request, not recorded. `param` names the field (`tool`,
            `args`, `mode`, `wait`, `connector_type`, `tool_digest`, `context.transport`,
            `context.client`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: validation_failed
                  message: args is 312 KiB; the limit is 256 KiB. Send a reference to the data instead of the data itself.
                  param: args
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/validation_failed
        "401":
          description: The API key is missing, not valid, revoked or expired. Nothing is recorded.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: >-
            Refused. `policy_denied` and `policy_unavailable` (`policy_error`), and
            `agent_stopped`, `agent_suspended` and `org_stopped` (`permission_error`) are recorded
            and sealed, and carry the action in `error.action`. `intent_required`,
            `intent_invalid` and `intent_replay` (`permission_error`) and `insufficient_scope` are
            identity failures: nothing is recorded and there is no `error.action`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                policy_denied:
                  summary: A rule denied it; the refusal is sealed
                  value:
                    error:
                      type: policy_error
                      code: policy_denied
                      message: "Blocked by policy \"no-bulk-writes\": writes over 100 rows need a person, not an agent."
                      param: null
                      request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                      doc_url: https://docs.deedglass.com/errors/policy_denied
                      action:
                        object: action
                        id: act_01k6w4a2c4e6g8j0m2p4r6t8v0
                        created_at: "2026-10-05T04:14:52.630Z"
                        livemode: false
                        agent: {id: agt_01k633rrmh3bcz2p39gg4vp46q, name: support-bot}
                        tool: fake.write
                        connector: null
                        mode: gateway
                        state: sealed
                        status: denied
                        on_behalf_of: null
                        session: null
                        decision: {outcome: deny, code: policy_denied, source: rule, rule_id: no-bulk-writes, reason: "writes over 100 rows need a person, not an agent.", message: "Blocked by policy \"no-bulk-writes\": writes over 100 rows need a person, not an agent.", policy: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, policy_version: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, shadow: null}
                        policy: {id: pol_01k6r2m8q0s2v4x6z8b0d2f4h6, version_id: pv_01k6r2m9a1c3e5g7j9m1q3s5v7, mode: enforce, decision: deny, rule_id: no-bulk-writes}
                        approval: null
                        execution: null
                        witness: null
                        cost: null
                        request_digest: sha256:be1b8c0dafafa9da4de5a1acf33834b5d7306ea41eec46ea1f8befb7985332dc
                        result_digest: null
                        payload_pruned_at: null
                        deed: deed_01k6w4a4e6g8j0m2p4r6t8w0y2
                        next_action: null
                agent_stopped:
                  summary: The agent is stopped; its attempt is sealed
                  value:
                    error:
                      type: permission_error
                      code: agent_stopped
                      message: Agent "support-bot" is stopped. Deedglass refuses its calls until an admin starts it in Console › Agents.
                      param: null
                      request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                      doc_url: https://docs.deedglass.com/errors/agent_stopped
                      action: {object: action, id: act_01k6w4a2c4e6g8j0m2p4r6t8v0, state: sealed, status: denied, deed: deed_01k6w4a4e6g8j0m2p4r6t8w0y2}
                intent_required:
                  summary: An MCP call for an agent that must sign; nothing is recorded
                  value:
                    error:
                      type: permission_error
                      code: intent_required
                      message: Agent "support-bot" requires a signed intent for every call, and MCP clients can't sign intents yet. Call it through the SDK or the REST API, or an admin can set its intent mode to optional in Console › Agents.
                      param: Deedglass-Intent
                      request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                      doc_url: https://docs.deedglass.com/errors/intent_required
        "404":
          description: >-
            `tool_unknown`: no connector serves the tool (gateway mode), or the name isn't a tool
            name (a look-alike, say). The attempt is recorded and sealed (`decision.source:
            builtin`), and `error.action` carries it.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: tool_unknown
                  message: No tool named "fake.ecko". Tools look like "connector.tool", for example "fake.echo".
                  param: null
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/tool_unknown
                  action: {object: action, id: act_01k6w4a2c4e6g8j0m2p4r6t8v0, tool: fake.ecko, state: sealed, status: denied, deed: deed_01k6w4a4e6g8j0m2p4r6t8w0y2}
        "409":
          description: >-
            `idempotency_key_in_use`, rarely: two requests with the same key raced, and the other
            one's action can't be read yet. Retry in a moment with the same key.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: >-
            `idempotency_conflict`: this agent used the Idempotency-Key in the last 24 hours with a
            different request (another tool, other arguments, another on-behalf-of or session).
            Also `validation_failed` for a malformed `Deedglass-On-Behalf-Of` or
            `Deedglass-Session` header.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: idempotency_conflict
                  message: This Idempotency-Key was used with a different request in the last 24 hours. Use a new key for a new request.
                  param: Idempotency-Key
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/idempotency_conflict
        "429":
          description: >-
            `budget_exceeded`: a budget refused it. The refusal is recorded and sealed, and
            `error.action` carries it. `Retry-After` says when the budget's window resets.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Retry-After:
              description: Seconds until the budget's window resets, at least 1.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: rate_limit_error
                  code: budget_exceeded
                  message: "Budget \"agent-rate\" reached: 60 of 60 calls this minute. Try again in 14 s."
                  param: null
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/budget_exceeded
                  action: {object: action, id: act_01k6w4a2c4e6g8j0m2p4r6t8v0, state: sealed, status: denied, deed: deed_01k6w4a4e6g8j0m2p4r6t8w0y2}
        default:
          $ref: "#/components/responses/Error"
  /v1/actions/{id}:
    get:
      operationId: getAction
      summary: Show one action, its events and deeds
      description: >-
        The action plus its payloads (`request`, `result`, and with `?expand[]=witness.detail` the
        witness detail; `null` once retention removed them, see `payload_pruned_at`), up to 200 events in order, and the deeds about it
        (the action deed and any amendments). An id from another org is 404 `not_found`, exactly
        like an id that doesn't exist. Requires `actions:read`. With `?wait=` (0 to 25 seconds)
        it long-polls: it answers when the action is settled (sealed, or waiting for a person),
        when its state changes, or when the wait ends. An action already `pending_approval` when
        the wait starts waits for its next change, which is how `next_action.poll_url` waits for a
        person's decision. For an agent key a Stop ends the wait at once with the stop error, and
        each agent may hold 20 open waits per instance (the 21st is 429 `rate_limited`, with
        `Retry-After: 1`).
      tags: [Ledger]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The action id (`act_…`)., schema: {type: string}}
        - {name: wait, in: query, required: false, description: 'Seconds to long-poll, 0 to 25, sent once. `0` (the default) answers at once.', schema: {type: number, minimum: 0, maximum: 25}}
        - name: "expand[]"
          in: query
          required: false
          description: >-
            `request`, `result`, `events` or `witness.detail`, repeatable. The first three are
            accepted for the SDKs (this response always carries them); `witness.detail` adds the
            full witness_detail v1 as `witness.detail`, with passes and notes (S19). Any other
            value, or any other query parameter, is 400 `validation_failed`.
          schema:
            type: array
            items: {type: string, enum: [request, result, events, witness.detail]}
          style: form
          explode: true
      responses:
        "200":
          description: The action.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionDetail"
        default:
          $ref: "#/components/responses/Error"
  /v1/actions/{id}/cancel:
    post:
      operationId: cancelAction
      summary: Cancel an action waiting for approval
      description: >-
        Only from `pending_approval`: the action goes `cancelled`, then sealed, its approval is
        cancelled and its budget reservation goes back. From any other state the answer is 409
        `conflict`: "This action is already executing, so it can't be cancelled." while it runs,
        and "This action has already finished, so there is nothing to cancel." once it ended. The
        agent's own key (with `actions:execute`), an admin+ Console session (with the
        `Deedglass-CSRF` header) or an org admin key. Another agent's action is 404 `not_found`,
        exactly like an id that doesn't exist.
      tags: [Gate]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The action id (`act_…`)., schema: {type: string}}
      responses:
        "200":
          description: "The action, `status: cancelled`, sealed."
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        "404":
          description: '`not_found`: no such action in this org, or (for an agent key) another agent''s.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: '`conflict`: the action isn''t waiting for approval.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: conflict
                  message: This action is already executing, so it can't be cancelled.
                  param: null
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/conflict
        default:
          $ref: "#/components/responses/Error"
  /v1/actions/{id}/undo/preview:
    post:
      operationId: previewUndo
      summary: Preview an undo
      description: >-
        Plans the undo of a Postgres write from what Postgres committed (the decoder's row
        images, never the agent's report), and runs the twelve safety checks without taking a
        lock. It writes nothing. `available` is true when no check failed; `steps.data` holds the
        first 20 steps (`?steps_limit=` up to 1,000); `policy` is a dry run for the `console`
        system agent. A signed-in admin or owner (with `Deedglass-CSRF`) or an org admin key;
        agent keys get 403 `insufficient_scope`. Another org's action is 404 `not_found`.
      tags: [Gate]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - {name: id, in: path, required: true, description: The action to undo (`act_…`)., schema: {type: string}}
        - {name: steps_limit, in: query, required: false, description: Steps to list, 1 to 1000 (default undo.preview_steps, 20)., schema: {type: integer, minimum: 1, maximum: 1000}}
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                allow_triggers: {type: boolean, description: Let triggers fire again (the triggers check is then a warning).}
      responses:
        "200":
          description: The preview.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UndoPreview"
        default:
          $ref: "#/components/responses/Error"
  /v1/actions/{id}/undo:
    post:
      operationId: createUndo
      summary: Undo an action
      description: >-
        Previews again, refuses `undo_unavailable` (409, with `error.checks`), `undo_plan_stale`
        (409, the plan changed since the preview) or `already_undone` (409), then sends the undo
        through the Gate as an action of the org's `console` system agent on behalf of the person:
        tool `<connector>.undo`, policy (the starter rule asks for a countersign), approval, one
        transaction in Postgres with the ledger row and the marker, an L2 witness against the
        plan's exact footprint, and its own sealed deed. The answer is the action object (202
        `pending_approval` with `next_action`, or 200 once sealed), with `undo.undoes`. A row
        that changed after the plan was made fails the undo with `undo_conflict` as its
        `execution.error_code`: Postgres rolled it back and nothing was changed.
      tags: [Gate]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The action to undo (`act_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [plan_digest, reason]
              properties:
                plan_digest: {type: string, pattern: "^sha256:[0-9a-f]{64}$", description: The preview's plan_digest.}
                reason: {type: string, minLength: 1, maxLength: 280, description: Why; the approver and the Ledger read it.}
                allow_triggers: {type: boolean}
                wait: {type: number, minimum: 0, maximum: 25, description: Seconds to wait for the outcome (gate.wait_timeout by default).}
      responses:
        "200":
          description: The undo action, sealed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        "202":
          description: The undo action, waiting for a person's countersign (or still running).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        "409":
          description: '`undo_unavailable`, `undo_plan_stale` or `already_undone`.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: undo_plan_stale
                  message: The rows changed since you previewed this undo. Preview it again and use the new plan.
                  param: plan_digest
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/undo_plan_stale
        default:
          $ref: "#/components/responses/Error"
  /v1/actions/{id}/result:
    post:
      operationId: reportActionResult
      summary: Report the result of a proxy-mode action
      description: >-
        Proxy mode only: after `POST /v1/actions` answered `status: authorized`, the edge runs the
        tool and reports what it returned here, once, before `next_action.deadline`
        (`gate.edge_result_timeout`, 10 minutes by default). Deedglass records the result as the
        edge's word (witness method `edge_reported`, level L0), then seals. Send `result`, or
        `result_digest` alone for a result too large to send; when both are sent the digest must
        match (sha256 of the result's RFC 8785 form), else 400 `validation_failed`. Errors: 404
        `not_found` (also for another agent's action, so ids can't be probed); 409 `conflict` for a
        gateway action, an action not yet authorized, a second report ("A result was already
        reported for this action."), or a report after the deadline ("This action was already
        sealed as UNVERIFIABLE because its result arrived after the deadline."). The same agent's
        key, with `actions:execute`.
      tags: [Gate]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The action id (`act_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EdgeResult"
            example:
              status: succeeded
              started_at: "2026-10-05T04:20:11.020Z"
              finished_at: "2026-10-05T04:20:11.061Z"
              result: {content: [{type: text, text: Wrote 5 bytes to /tmp/deedglass-demo/allowed/notes.txt}], isError: false}
              error_code: null
              error_message: null
      responses:
        "200":
          description: The action, witnessed and sealed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        "400":
          description: >-
            `validation_failed`: `status`, the times, `error_code`, a result over
            `gate.max_result_bytes`, or a `result_digest` that doesn't match `result`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: '`not_found`: no such action in this org, or another agent''s.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: '`conflict`: not a proxy action, not authorized yet, already reported, or past its deadline.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: conflict
                  message: This action was already sealed as UNVERIFIABLE because its result arrived after the deadline.
                  param: null
                  request_id: req_01k6w4a3d5f7h9k1n3q5s7v9x1
                  doc_url: https://docs.deedglass.com/errors/conflict
        default:
          $ref: "#/components/responses/Error"
  /v1/tools:
    get:
      operationId: listTools
      summary: The native tools this agent may see
      description: >-
        The tools of the Gate's native connectors (the ones `/mcp/native` serves), named
        `<connector>.<tool>`, for SDKs and `deedglass mcp serve`: the built-ins and, since S18, the
        caller's org's own connectors of native types (`postgres.*`, REST connectors; up to 100;
        `mcp_http` connectors are served at `/mcp/{connector}` instead). Tools the active policy always
        denies to this agent are left out; `include_hidden=true` lists them too, with `visibility:
        hidden` (the same answer `POST /v1/tools/resolve` gives, so nothing secret). Every call is
        still decided by `POST /v1/actions`. An agent key with `actions:execute`.
      tags: [Gate]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - name: include_hidden
          in: query
          required: false
          description: Also list the tools the policy always denies, with `visibility` hidden.
          schema: {type: boolean, default: false}
      responses:
        "200":
          description: The tools, sorted by name. Not paginated (`has_more` is false).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ToolList"
              example:
                object: list
                data:
                  - object: tool
                    name: fake.echo
                    connector: fake
                    title: Echo
                    description: Returns its arguments.
                    input_schema: {type: object, properties: {message: {type: string}}, additionalProperties: false}
                    side_effect: read
                    visibility: visible
                has_more: false
                next_cursor: null
                policy_version: pv_01k6w4a3d5f7h9k1n3q5s7v9x1
        default:
          $ref: "#/components/responses/Error"
  /v1/tools/resolve:
    post:
      operationId: resolveTools
      summary: Which of an edge's tools the policy always denies
      description: >-
        For MCP wrappers (`deedglass mcp wrap`, docs/MCP.md): send the tools an MCP server lists,
        and Deedglass answers, for each, whether the enforced policy denies every call to it from
        this agent (`hidden`) or might allow it (`visible`). It reads the policy's static structure
        only: the first rule whose `match` covers `<connector>.<name>` decides, a rule with a `when`
        can't be decided ahead of time and counts as visible, and with no covering rule the
        policy's default decides. Budgets and Stop are ignored, because they change from minute to
        minute. MCP `annotations` are never read. Hiding is a convenience for the model: every call
        still goes through `POST /v1/actions` and is decided there. Nothing is recorded. At most
        500 tools. An agent key with `actions:execute`.
      tags: [Gate]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ToolResolveRequest"
            example:
              connector: scratch
              tools:
                - {name: read_text_file, digest: "sha256:1b0e5c1f8a7d6e4b3c2a19f0e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9"}
                - {name: move_file, digest: "sha256:a41c5c1f8a7d6e4b3c2a19f0e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9"}
      responses:
        "200":
          description: One `tool_visibility` per tool, in request order.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ToolVisibilityList"
              example:
                object: list
                data:
                  - {object: tool_visibility, name: read_text_file, tool: scratch.read_text_file, visibility: visible, rule_id: allow-scratch-reads, reason: null}
                  - {object: tool_visibility, name: move_file, tool: scratch.move_file, visibility: hidden, rule_id: null, reason: "No rule allows this tool, and the policy default is deny."}
                has_more: false
                next_cursor: null
                policy_version: pv_01k6w4a3d5f7h9k1n3q5s7v9x1
        "400":
          description: '`validation_failed`: the connector name, a tool name or digest, or more than 500 tools.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        default:
          $ref: "#/components/responses/Error"
  /v1/deeds:
    get:
      operationId: listDeeds
      summary: List deeds
      description: >-
        Deed envelopes by seq, oldest first (`order=desc` for newest first). Each item is a stored
        deed; `GET /v1/deeds/{id}` returns its exact canonical bytes. Requires `deeds:read`.
      tags: [Deeds]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/FromSeq"
        - $ref: "#/components/parameters/ToSeq"
        - {name: kind, in: query, required: false, description: 'A comma list of `action`, `admin`, `amendment`.', schema: {type: string}}
        - {name: action, in: query, required: false, description: 'An action id: its deed and any amendments.', schema: {type: string}}
        - {name: order, in: query, required: false, description: '`asc` (default) or `desc`.', schema: {type: string, enum: [asc, desc]}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of deeds.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeedList"
        default:
          $ref: "#/components/responses/Error"
  /v1/deeds/{id}:
    get:
      operationId: getDeed
      summary: Get one deed's exact bytes
      description: >-
        The stored canonical JSON of the deed envelope, byte for byte, so its hash and seal check
        offline. Deeds never change: the response is cacheable forever and its `ETag` is the
        `body_hash`. With `Accept: application/cose` the deed comes as a COSE_Sign1 signed
        statement (RFC 9052; not an RFC 9942 transparency-service object). Requires `deeds:read`.
      tags: [Deeds]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The deed id (`deed_…`)., schema: {type: string}}
        - {name: If-None-Match, in: header, required: false, description: The `ETag` of a copy you hold., schema: {type: string}}
      responses:
        "200":
          description: The deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            ETag:
              description: '`"sha256:<hex>"`: the deed''s `body_hash`.'
              schema:
                type: string
            Cache-Control:
              schema:
                type: string
                const: private, max-age=31536000, immutable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Deed"
            application/cose:
              schema:
                type: string
                contentMediaType: application/cose
        "304":
          description: The copy named in `If-None-Match` is current.
        default:
          $ref: "#/components/responses/Error"
  /v1/deeds/files:
    post:
      operationId: sealFile
      summary: Seal a file you hold
      description: >-
        What `deedglass deeds seal-file` calls (S38): seals the admin deed `file.sealed` from a
        file's sha256, size, media type and label. The file itself never reaches the server; the
        CLI hashes it locally. The deed carries the label as `admin.reason` and `data_digest` over
        these four fields, so `deedglass verify <deed.json> --file <path>` checks the file against
        the deed offline. It seals the file's hash, size, type and label. It says nothing about
        whether the file's contents are right. An admin+ Console session or an org admin key;
        agent keys get 403 `insufficient_scope`.
      tags: [Deeds]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [sha256, bytes, media_type, label]
              properties:
                sha256: {type: string, pattern: "^sha256:[0-9a-f]{64}$"}
                bytes: {type: integer, minimum: 0, maximum: 9007199254740991}
                media_type: {type: string, enum: [application/pdf, text/markdown, application/json, text/plain, text/html]}
                label: {type: string, minLength: 1, maxLength: 120, description: No line breaks or control characters.}
      responses:
        "201":
          description: The sealed file and its deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SealedFile"
        default:
          $ref: "#/components/responses/Error"
  /v1/deeds/export:
    get:
      operationId: exportDeeds
      summary: Export deeds as a bundle
      description: >-
        A streamed export bundle v1 (docs/BUNDLE_FORMAT.md): a deterministic tar.gz holding
        README.txt, keys.json, deeds.jsonl, manifest.json and manifest.seal.json, read from one
        database snapshot. Choose deeds by seq or by issued_at, at most 1,000,000 at a time; an
        empty range is 404 with where the chain ends. Check the file with `deedglass verify
        <file> --keys <this server>/.well-known/deedglass/keys.json`. Requires `deeds:read`.
      tags: [Deeds]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/FromSeq"
        - $ref: "#/components/parameters/ToSeq"
        - {name: "issued_at[gte]", in: query, required: false, description: RFC 3339; inclusive. Not with from_seq or to_seq., schema: {type: string, format: date-time}}
        - {name: "issued_at[lt]", in: query, required: false, description: RFC 3339; exclusive., schema: {type: string, format: date-time}}
      responses:
        "200":
          description: The bundle, streamed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Content-Disposition:
              description: '`attachment; filename="deedglass-<org>-<first>-<last>.tar.gz"`.'
              schema:
                type: string
          content:
            application/gzip:
              schema:
                type: string
                contentMediaType: application/gzip
        default:
          $ref: "#/components/responses/Error"
  /v1/ledger/check:
    get:
      operationId: checkLedger
      summary: Check the deed chain
      description: >-
        Checks every deed in the range in seq order against this server's own seal keys: hashes,
        seals, links and seq continuity. A broken chain is a 200 result with `status: broken` and
        the first break, not an error. The check stops after 30 s or 2,000,000 deeds with `status:
        incomplete` and `next_from_seq`. Keep `head` somewhere outside Deedglass: it is how a
        deletion from the end of the chain becomes visible. Requires `deeds:read`.
      tags: [Ledger]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/FromSeq"
        - $ref: "#/components/parameters/ToSeq"
        - {name: "issued_at[gte]", in: query, required: false, description: RFC 3339; inclusive. Not with from_seq or to_seq., schema: {type: string, format: date-time}}
        - {name: "issued_at[lt]", in: query, required: false, description: RFC 3339; exclusive., schema: {type: string, format: date-time}}
      responses:
        "200":
          description: The result.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LedgerCheck"
        default:
          $ref: "#/components/responses/Error"
  /v1/auth/login:
    post:
      operationId: requestLogin
      summary: Ask for a sign-in link and code
      description: >-
        Public. Always 202 with the same body for every well-formed address, whether or not it has
        an account: the server never reads the database here, and a worker sends the email. The
        email carries one link (`/auth/confirm#t=…`, valid once for 15 minutes) and a 6-digit code
        for another device. Limits: 20 requests per IP and 5 per address per hour (429
        `rate_limited` with `Retry-After`).
      tags: [People]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [email]
              properties:
                email: {type: string, maxLength: 254}
            example: {email: Priya@Acme.com}
      responses:
        "202":
          description: Accepted. The same for known and unknown addresses.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginRequest"
        default:
          $ref: "#/components/responses/Error"
  /v1/auth/preview:
    post:
      operationId: previewLogin
      summary: See what a sign-in link would do, without using it
      description: >-
        Public. The confirm page calls this first and shows whose account and which org the link
        signs into, so nobody is signed in to an account they didn't expect (login CSRF). Nothing is
        consumed. An unknown, used, replaced or expired token is 400 `login_link_invalid`; 30
        failures per IP per hour are allowed.
      tags: [People]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [token]
              properties:
                token: {type: string, minLength: 43, maxLength: 43}
            example: {token: OEVkbr-q18qimgaDkbcE5mnZqQ2a1zGG7_TgYq8PKFc}
      responses:
        "200":
          description: What the link does.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginPreview"
        default:
          $ref: "#/components/responses/Error"
  /v1/auth/callback:
    post:
      operationId: completeLogin
      summary: Sign in with a link's token, or with an email address and code
      description: >-
        Public. Uses the token once (only while unused, not replaced by a newer email and not
        expired), accepts the invite when the link is one, and sets the session and CSRF cookies.
        With `email` and `code`, the code must belong to the newest email; each code allows 5
        attempts. Every miss is 400 `login_code_invalid` with the same message.
      tags: [People]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  additionalProperties: false
                  required: [token]
                  properties:
                    token: {type: string}
                - type: object
                  additionalProperties: false
                  required: [email, code]
                  properties:
                    email: {type: string}
                    code: {type: string, description: Six digits; spaces are ignored.}
            examples:
              link: {value: {token: OEVkbr-q18qimgaDkbcE5mnZqQ2a1zGG7_TgYq8PKFc}}
              code: {value: {email: priya@acme.com, code: "482913"}}
      responses:
        "200":
          description: Signed in. `Set-Cookie` carries `__Host-deedglass_session` and `__Host-deedglass_csrf`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Set-Cookie:
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        default:
          $ref: "#/components/responses/Error"
  /v1/auth/logout:
    post:
      operationId: logout
      summary: Sign out
      description: >-
        Ends this session, or with `everywhere` every session of yours in every org, and expires the
        cookies. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                everywhere: {type: boolean, default: false}
      responses:
        "204":
          description: Signed out.
        default:
          $ref: "#/components/responses/Error"
  /v1/auth/switch-org:
    post:
      operationId: switchOrg
      summary: Switch to another org you belong to
      description: >-
        Issues a new session in the org (new cookies) and ends the current one. Needs the
        `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [org]
              properties:
                org: {type: string}
      responses:
        "200":
          description: The new session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        default:
          $ref: "#/components/responses/Error"
  /v1/me:
    get:
      operationId: getMe
      summary: Who is signed in
      description: The person, the session's org, their role there, the orgs they can switch to, and the CSRF token.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: You.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        default:
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateMe
      summary: Change your name
      description: The name other members see. An empty string clears it. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: {type: string, maxLength: 80}
      responses:
        "200":
          description: You, updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        default:
          $ref: "#/components/responses/Error"
  /v1/org:
    get:
      operationId: getOrg
      summary: Get your org
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The org.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Org"
        default:
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateOrg
      summary: Change the org's name, time zone, payload retention or approval settings
      description: >-
        Admins and owners change `name` and `timezone`; only an owner may send `retention_days` or
        `settings` (an admin gets 403 `insufficient_role`). Only the fields and keys sent change;
        `settings.approvals` is merged into the stored settings. Seals one `org.updated` admin deed
        when something changed (data: `fields`, `timezone`, and the new `retention_days` and
        `approvals` when sent), none otherwise. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: {type: string, minLength: 1, maxLength: 100}
                timezone: {type: string, description: An IANA time zone name such as Asia/Kolkata.}
                retention_days:
                  type: [integer, "null"]
                  minimum: 1
                  maximum: 3650
                  description: Owner only (S26). Days to keep action payloads; null goes back to the default of 90.
                settings:
                  type: object
                  additionalProperties: false
                  description: Owner only (S26). Unknown keys are 400 `validation_failed` with their path as `param`.
                  properties:
                    approvals:
                      type: object
                      additionalProperties: false
                      properties:
                        email_links: {type: string, enum: [decide, review]}
                        email_detail: {type: string, enum: [summary, full]}
                        self_countersign: {type: string, enum: [deny, allow]}
            example: {timezone: Asia/Kolkata}
      responses:
        "200":
          description: The org, with the deed (null when nothing changed).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Org"
                  - type: object
                    required: [deed]
                    properties:
                      deed: {type: [string, "null"]}
        default:
          $ref: "#/components/responses/Error"
  /v1/org/members:
    get:
      operationId: listMembers
      summary: List the org's members
      description: Oldest first. Without `status`, invited and active members; `status=removed` lists people who were removed.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: status, in: query, required: false, schema: {type: string, enum: [invited, active, removed]}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of members.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MembershipList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: inviteMember
      summary: Invite someone
      description: >-
        Admins invite viewers and admins; only owners invite owners. Sends a one-time invite link,
        valid for 7 days, and seals `member.invited`. 201 for a new invite; 200 when the person was
        already invited (the old link stops working and a new one is sent); 409 `conflict` for an
        active member. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [email, role]
              properties:
                email: {type: string}
                role: {type: string, enum: [viewer, admin, owner]}
            example: {email: sam@acme.com, role: viewer}
      responses:
        "201":
          description: Invited.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        "200":
          description: Invited again, with a fresh link.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        default:
          $ref: "#/components/responses/Error"
  /v1/org/members/{id}:
    patch:
      operationId: updateMember
      summary: Change a member's role
      description: >-
        Owners only. Seals `member.role_changed`. Demoting the last owner is 409 `last_owner`. The
        change applies on the member's next request. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
        - {name: id, in: path, required: true, description: The membership id (`mem_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [role]
              properties:
                role: {type: string, enum: [viewer, admin, owner]}
      responses:
        "200":
          description: The membership.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        default:
          $ref: "#/components/responses/Error"
    delete:
      operationId: removeMember
      summary: Remove a member, or leave
      description: >-
        Admins remove viewers and admins; owners remove anyone; removing yourself is leaving. The
        member's sessions in the org end at once. Seals `member.removed`. Removing the last owner is
        409 `last_owner`. Needs the `Deedglass-CSRF` header.
      tags: [People]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
        - {name: id, in: path, required: true, description: The membership id (`mem_…`)., schema: {type: string}}
      responses:
        "200":
          description: The membership, now removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents:
    get:
      operationId: listAgents
      summary: List agents
      description: >-
        Oldest first. Filters combine with AND. Unknown parameters are refused. A Console session
        (viewer and up) or an org admin key (`admin` scope); agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: status, in: query, required: false, schema: {type: string, enum: [active, suspended, stopped]}}
        - {name: label, in: query, required: false, description: 'One label as `key:value`, like `env:prod`.', schema: {type: string}}
        - {name: name, in: query, required: false, description: The exact agent name (the CLI resolves names this way)., schema: {type: string, pattern: "^[a-z0-9][a-z0-9-]{1,62}$"}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: "expand[]"
          in: query
          required: false
          description: >-
            `usage` (also `expand=usage`) adds `usage` (S11's counters: calls this minute, hour and
            org-local day, spend today) and `last_action_at` to each agent, read in one batch for a
            page. Never included by default; any other value is 400 `validation_failed`.
          schema:
            type: array
            items: {type: string, enum: [usage]}
          style: form
          explode: true
      responses:
        "200":
          description: A page of agents.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createAgent
      summary: Create an agent
      description: >-
        Seals `agent.created`. The name (`^[a-z0-9][a-z0-9-]{1,62}$`, unique in the org) can't
        change later, because policies match on it; `console`, `system` and `deedglass` are
        reserved. At most 32 labels. Budgets are in minor units (cents for usd). `owner` defaults
        to the signed-in person. A taken name is 409 `conflict`. An admin+ Console session (with the
        `Deedglass-CSRF` header) or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: {type: string, pattern: "^[a-z0-9][a-z0-9-]{1,62}$", description: Immutable. Not console, system or deedglass.}
                description: {type: string, maxLength: 500}
                labels:
                  $ref: "#/components/schemas/Labels"
                budgets:
                  $ref: "#/components/schemas/Budgets"
                owner: {type: string, description: An active member's id (`usr_…`).}
            example:
              name: support-bot
              description: Answers refund tickets in Zendesk
              labels: {env: prod, team: support}
              budgets: {calls_per_minute: 60, calls_per_day: 5000, spend_per_day: {amount: 5000, currency: usd}}
      responses:
        "201":
          description: The agent, with the deed that sealed `agent.created`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}:
    get:
      operationId: getAgent
      summary: Get an agent
      description: >-
        An id from another org is 404 `not_found`, like an id that doesn't exist. A Console session
        (viewer and up) or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/AgentId"
        - name: "expand[]"
          in: query
          required: false
          description: >-
            `usage` (also `expand=usage`) adds `usage` (S11's counters: calls this minute, hour and
            org-local day, spend today) and `last_action_at` to each agent, read in one batch for a
            page. Never included by default; any other value is 400 `validation_failed`.
          schema:
            type: array
            items: {type: string, enum: [usage]}
          style: form
          explode: true
      responses:
        "200":
          description: The agent.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        default:
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateAgent
      summary: Change an agent's description, labels, budgets, owner or intent mode
      description: >-
        Send only what changes. `labels` and `budgets` replace the whole map; `owner: ""` clears the
        owner. Names are immutable, so sending `name` is 422 `validation_failed`: to rename, create
        a new agent and move its keys. Seals `agent.updated` unless nothing changed (then `deed` is
        null). Changing `intent_mode` seals its own deed, `agent.intent_mode_changed` with `from`
        and `to`; setting it to `required` while the agent has no intent keys is 409 `conflict`
        (add a key first). An admin+ Console session (with the `Deedglass-CSRF` header) or an org
        admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                description: {type: string, maxLength: 500}
                labels:
                  $ref: "#/components/schemas/Labels"
                budgets:
                  $ref: "#/components/schemas/Budgets"
                owner: {type: string, description: An active member's id (`usr_…`), or "" to clear it.}
                intent_mode:
                  type: string
                  enum: ["off", optional, required]
                  description: >-
                    Signed intents (S09): off (headers ignored), optional (checked when sent) or
                    required (every call needs one).
            example: {labels: {env: prod, team: support, tier: "1"}}
      responses:
        "200":
          description: The agent, with the deed (null when nothing changed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/suspend:
    post:
      operationId: suspendAgent
      summary: Suspend an agent
      description: >-
        A planned pause. New calls are refused with 403 `agent_suspended`; resume undoes it. Seals
        `agent.suspended`. Idempotent: suspending a suspended agent returns 200 unchanged with
        `deed: null`. Suspending a stopped agent is 409 `conflict`. An admin+ Console session (with
        the `Deedglass-CSRF` header) or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        $ref: "#/components/requestBodies/OptionalReason"
      responses:
        "200":
          description: The agent, with the deed (null when it was already suspended).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/resume:
    post:
      operationId: resumeAgent
      summary: Resume a suspended agent
      description: >-
        Seals `agent.resumed`. Idempotent: resuming an active agent returns 200 unchanged with
        `deed: null`. Resuming a stopped agent is 409 `conflict` (start it instead). An admin+
        Console session (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        $ref: "#/components/requestBodies/OptionalReason"
      responses:
        "200":
          description: The agent, with the deed (null when it was already active).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/stop:
    post:
      operationId: stopAgent
      summary: Stop an agent
      description: >-
        The emergency brake. Seals `agent.stopped`; every Deedglass instance refuses the agent's
        calls within 2 seconds, with 403 `agent_stopped`. Actions already running at a tool finish:
        Stop can't un-run them. The reason is optional and never shown to the agent. Idempotent:
        stopping a stopped agent returns 200 unchanged with `deed: null`. If the stop can't be
        recorded the answer is 500 `internal_error` and nothing changed; the API never says stopped
        unless it is. An admin+ Console session (with the `Deedglass-CSRF` header) or an org admin
        key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        $ref: "#/components/requestBodies/OptionalReason"
      responses:
        "200":
          description: The agent, now stopped, with the deed (null when it was already stopped).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
              example:
                object: agent
                id: agt_01k633rrmh3bcz2p39gg4vp46q
                name: support-bot
                description: Answers refund tickets in Zendesk
                labels: {env: prod, team: support}
                budgets: {calls_per_minute: 60, calls_per_day: 5000, spend_per_day: {amount: 5000, currency: usd}}
                status: stopped
                status_reason: Refund loop in staging
                status_changed_at: "2026-10-03T14:02:09.000Z"
                status_changed_by: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
                owner: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
                active_keys: 1
                last_used_at: "2026-10-03T14:01:58.512Z"
                created_at: "2026-10-03T10:02:11.000Z"
                updated_at: "2026-10-03T14:02:09.000Z"
                intent_mode: "off"
                deed: deed_01k6vrddq2atgb2023qvhhct3g
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/start:
    post:
      operationId: startAgent
      summary: Start a stopped agent
      description: >-
        Seals `agent.started`. The reason is required (3 to 280 characters, saying why it's safe now)
        and goes into the deed. Idempotent: starting an active agent returns 200 unchanged with
        `deed: null`. Starting a suspended agent is 409 `conflict` (resume it instead). While the
        org is stopped (Stop all), the agent stays refused with `org_stopped`. An admin+ Console
        session (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope`, so no agent can start itself.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        $ref: "#/components/requestBodies/RequiredReason"
      responses:
        "200":
          description: The agent, with the deed (null when it was already active).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/keys:
    get:
      operationId: listAgentKeys
      summary: List an agent's API keys
      description: >-
        The agent's keys, newest first, revoked and expired ones included (at most 100). The secret
        is never shown. A Console session (viewer and up) or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/AgentId"
      responses:
        "200":
          description: The keys, in one page.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createAgentKey
      summary: Create an API key for an agent
      description: >-
        201. The `secret` is shown once, in this response; Deedglass stores only its hash. Replaying
        the same `Idempotency-Key` returns the same key with `"secret": null` and
        `Deedglass-Idempotent-Replayed: true`; if the first response was lost, roll or revoke that
        key. Scopes default to `actions:execute`, `actions:read` and `deeds:read`; agent keys can't
        hold `admin`. An agent has at most 10 active keys; the 11th is 409 `conflict`. Seals
        `api_key.created`. An admin+ Console session (with the `Deedglass-CSRF` header) or an org
        admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: {type: string, maxLength: 64, description: A label for people, without line breaks.}
                mode: {type: string, enum: [live, test], default: live}
                scopes:
                  type: array
                  uniqueItems: true
                  items: {type: string, enum: ["actions:execute", "actions:read", "deeds:read", "actions:undo"]}
                  default: ["actions:execute", "actions:read", "deeds:read"]
            example: {name: prod runner, mode: live}
      responses:
        "201":
          description: The key, with its secret (null in a replay).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/intent-keys:
    get:
      operationId: listIntentKeys
      summary: List an agent's intent keys
      description: >-
        The Ed25519 public keys the agent signs intents with (at most two). A Console session
        (viewer and up) or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/AgentId"
      responses:
        "200":
          description: The intent keys, in one page.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntentKeyList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: addIntentKey
      summary: Register an intent key for an agent
      description: >-
        201. Registers an Ed25519 public key the agent signs intents with. The key must pass the
        registration rules: 32 bytes, canonical encoding, on the curve and not of small order;
        otherwise the answer is 422 `validation_failed` with `param` set to `public_key`. A key
        whose `kid` is already registered, or a third key, is 409 `conflict`: an agent has at most
        two intent keys; two lets you rotate without downtime. Seals `agent.intent_key_added`. An
        admin+ Console session (with the `Deedglass-CSRF` header) or an org admin key; agent keys
        get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/AgentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [public_key]
              properties:
                public_key:
                  type: string
                  pattern: "^ed25519:[A-Za-z0-9_-]{43}$"
                  description: '`ed25519:` and the 32-byte public key in base64url without padding.'
            example: {public_key: "ed25519:11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}
      responses:
        "201":
          description: The intent key, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntentKey"
        default:
          $ref: "#/components/responses/Error"
  /v1/agents/{id}/intent-keys/{kid}:
    delete:
      operationId: removeIntentKey
      summary: Remove an intent key
      description: >-
        Intents signed with the key are refused from now on. Removing the last key while the
        agent's `intent_mode` is `required` is 409 `conflict` (change the mode first); a `kid` the
        agent doesn't have is 404 `not_found`. Seals `agent.intent_key_removed`. An admin+ Console
        session (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/AgentId"
        - $ref: "#/components/parameters/IntentKid"
      responses:
        "200":
          description: The key is removed, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntentKeyDeleted"
        default:
          $ref: "#/components/responses/Error"
  /v1/keys/{id}/roll:
    post:
      operationId: rollKey
      summary: Roll an API key
      description: >-
        201. Creates a new key with the same agent, name, mode and scopes; its `secret` is shown
        once, and `previous` says when the old key stops working (`expires_in` seconds from now,
        0–604800, default 86400). Rolling a revoked or expired key is 409 `conflict`. Admin keys
        can't roll admin keys (403 `insufficient_role`), because that creates one. Replaying the
        same `Idempotency-Key` returns `"secret": null`. Seals `api_key.rolled`. An admin+ Console
        session (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/KeyId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                expires_in: {type: integer, minimum: 0, maximum: 604800, default: 86400, description: Seconds the old key keeps working.}
            example: {expires_in: 86400}
      responses:
        "201":
          description: The new key, with its secret and the old key's expiry.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyResult"
              example:
                object: api_key
                id: key_01k6jt6tgx72gjr876976xak5e
                agent: agt_01k633rrmh3bcz2p39gg4vp46q
                name: prod runner
                mode: live
                prefix: dg_live_0IDw6L5O
                scopes: ["actions:execute", "actions:read", "deeds:read"]
                secret: dg_live_0IDw6L5O_9hxevwY7nqdKIZZi5A2xlI2O2qn8znik
                expires_at: null
                last_used_at: null
                revoked_at: null
                rolled_from: key_01k6jz12ptrvg2zn5vts5xc1ay
                created_at: "2026-10-03T10:03:40.000Z"
                deed: deed_01k6vrddq2atgb2023qvhhct3g
                previous: {id: key_01k6jz12ptrvg2zn5vts5xc1ay, expires_at: "2026-10-04T10:03:40.000Z"}
        default:
          $ref: "#/components/responses/Error"
  /v1/keys/{id}/revoke:
    post:
      operationId: revokeKey
      summary: Revoke an API key
      description: >-
        The key stops working at once (401 `api_key_revoked`). Seals `api_key.revoked`. Idempotent:
        revoking a revoked key returns 200 with `deed: null`. An admin+ Console session (with the
        `Deedglass-CSRF` header) or an org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/KeyId"
      requestBody:
        $ref: "#/components/requestBodies/OptionalReason"
      responses:
        "200":
          description: The key, now revoked, with the deed (null when it was already revoked).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/org/stop-all:
    post:
      operationId: stopAllAgents
      summary: Stop every agent in the org
      description: >-
        Turns on the org's Stop all: every agent's calls are refused with 403 `org_stopped`,
        whatever its own status, on every instance within 2 seconds. Seals `org.stopped_all`. The reason
        is optional. Idempotent: repeating it returns 200 unchanged with `deed: null`. If it can't be
        recorded the answer is 500 `internal_error` and nothing changed. An admin+ Console session
        (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Agents]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        $ref: "#/components/requestBodies/OptionalReason"
      responses:
        "200":
          description: The org's Stop all state, with the deed (null when it was already on).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgStop"
        default:
          $ref: "#/components/responses/Error"
  /v1/org/start-all:
    post:
      operationId: startAllAgents
      summary: Turn off Stop all
      description: >-
        Owners only, in the Console: admin keys can't release the brake. The reason is required
        (3 to 280 characters) and goes into the deed. Agents stopped one by one stay stopped. Seals
        `org.started_all`. Idempotent: when Stop all is off, returns 200 unchanged with `deed: null`.
        Needs the `Deedglass-CSRF` header.
      tags: [Agents]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        $ref: "#/components/requestBodies/RequiredReason"
      responses:
        "200":
          description: The org's Stop all state, now off, with the deed (null when it was already off).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgStop"
        default:
          $ref: "#/components/responses/Error"
  /v1/org/keys:
    get:
      operationId: listAdminKeys
      summary: List the org's admin keys
      description: Newest first, revoked ones included (at most 100). The secret is never shown. Admins and owners, in the Console only.
      tags: [Agents]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The admin keys, in one page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createAdminKey
      summary: Create an org admin key
      description: >-
        201. An admin key acts as the admin role (never owner) on the control plane: it can't call
        start-all, change roles or create admin keys, so admin keys can't keep access alive on their
        own. Its scopes are exactly `[admin]`. Admins and owners, in the Console only (or `deedglass
        org keys create` on the server). The `secret` is shown once; a replay returns `"secret":
        null`. Seals `api_key.created`. Needs the `Deedglass-CSRF` header.
      tags: [Agents]
      security: [{sessionCookie: []}]
      x-deedglass-auth: session
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRF"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: {type: string, maxLength: 64}
                mode: {type: string, enum: [live, test], default: live}
                scopes:
                  type: array
                  description: Leave it out; admin keys always have exactly `[admin]`.
                  minItems: 1
                  maxItems: 1
                  items: {type: string, const: admin}
            example: {name: terraform}
      responses:
        "201":
          description: The admin key, with its secret (null in a replay).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/whoami:
    get:
      operationId: whoami
      summary: Which key, agent and org this is
      description: >-
        Any API key. It skips the stop checks, so a stopped or suspended agent (or one in a stopped
        org) can learn its status here. It never includes a stop reason or who stopped the agent.
        For an org admin key, `agent` and `budgets` are null. An invalid, revoked or expired key is
        401 with `WWW-Authenticate: Bearer realm="deedglass", error="invalid_token"`.
      tags: [Agents]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The key's identity and statuses.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Whoami"
        default:
          $ref: "#/components/responses/Error"
  /v1/intents/verify:
    post:
      operationId: verifyIntent
      summary: Check a signed intent without acting
      description: >-
        A conformance check for SDK authors. It checks the `Deedglass-Intent` header in every intent
        mode (`off` included) exactly as the Gate will, consumes the nonce, and never acts: no tool
        is called. Without the header the answer is 422 `validation_failed` ("Send a
        Deedglass-Intent header to check."), except for an agent in `required` mode, which gets 403
        `intent_required`. Refusals are 403 `permission_error` with code `intent_invalid` (`param`
        names the field), `intent_replay` or `intent_required`; never 401. `Idempotency-Key` is
        ignored. An agent key with `actions:execute`.
      tags: [Intents]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/DeedglassIntent"
        - $ref: "#/components/parameters/DeedglassOnBehalfOf"
        - $ref: "#/components/parameters/DeedglassSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [tool, args]
              properties:
                tool:
                  type: string
                  pattern: '^[a-z][a-z0-9-]{1,31}\.[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$'
                  description: The tool the intent names, as `<connector>.<tool>`.
                args:
                  description: The arguments, any JSON value (use {} for none; at most 256 KiB).
            example: {tool: stripe.refund, args: {charge: ch_3QxZ8f2L, amount: 1200}}
      responses:
        "200":
          description: The intent passed every check, and its nonce is consumed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntentCheck"
        "403":
          description: >-
            The intent was refused: `intent_invalid` (`param` names the field), `intent_replay` or
            `intent_required`.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                intent_replay:
                  summary: The nonce was already used
                  value:
                    error:
                      type: permission_error
                      code: intent_replay
                      message: This signed intent was already used at 09:30:02 UTC. Sign a new intent, with a fresh nonce, for every call.
                      param: nonce
                      request_id: req_01j9zr3m5k8q2w7x4t6v0b1c9d
                      doc_url: https://docs.deedglass.com/errors/intent_replay
                intent_invalid:
                  summary: The arguments differ from the signed ones
                  value:
                    error:
                      type: permission_error
                      code: intent_invalid
                      message: The signed intent's args_digest doesn't match the arguments sent. Sign the exact arguments you send, canonicalized with RFC 8785.
                      param: args_digest
                      request_id: req_01j9zr3m5k8q2w7x4t6v0b1c9d
                      doc_url: https://docs.deedglass.com/errors/intent_invalid
        default:
          $ref: "#/components/responses/Error"
  /v1/approvals:
    get:
      operationId: listApprovals
      summary: List approvals
      description: >-
        Approvals in this org (S22). `?status=pending` lists the inbox, oldest expiry first; any
        other filter lists newest first. Filters: `status` (comma-separated: pending, approved,
        denied, expired, cancelled), `agent` (id or name), `tool`, with `limit` (1 to 100) and
        `cursor`. A viewer+ Console session or an org admin key sees the full object; an agent key
        sees only its own agent's approvals, in the agent view (no context, no approvers, no
        names, `countersigned` as a count).
      tags: [Approvals]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: status, in: query, required: false, schema: {type: string}}
        - {name: agent, in: query, required: false, schema: {type: string}}
        - {name: tool, in: query, required: false, schema: {type: string}}
        - {name: limit, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
        - {name: cursor, in: query, required: false, schema: {type: string}}
      responses:
        "200":
          description: A list of approvals.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApprovalList"
        default:
          $ref: "#/components/responses/Error"
  /v1/approvals/{id}:
    get:
      operationId: getApproval
      summary: Retrieve an approval
      description: >-
        One approval (S22). `?wait=0..25` is a long poll: it returns as soon as the status is no
        longer `pending`, or when the wait ends. An agent key reads only its own agent's approvals
        (the agent view); another agent's is 404 `not_found`.
      tags: [Approvals]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The approval id (`apr_…`)., schema: {type: string}}
        - {name: wait, in: query, required: false, schema: {type: number, minimum: 0, maximum: 25}}
      responses:
        "200":
          description: The approval.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Approval"
        default:
          $ref: "#/components/responses/Error"
  /v1/approvals/{id}/approve:
    post:
      operationId: approveApproval
      summary: Countersign an approval
      description: >-
        A person countersigns (S22): a Console session with the `Deedglass-CSRF` header. Any role may
        call it; the rule's approvers decide who is eligible, read live. The body names the context
        the person reviewed (`context_digest`; a different one is 409 `context_changed`). The first
        final decision wins (`approval_decided`); four-eyes counts distinct people
        (`already_countersigned`); the person the action runs on behalf of can't countersign
        (`self_approval`). Agent and admin keys are 403 `approval_needs_person`. The answer never
        waits for the tool: an approved action answers `action_status: processing`.
      tags: [Approvals]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The approval id (`apr_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalVote"
      responses:
        "200":
          description: "The approval after the vote, with `action_status`."
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Approval"
        default:
          $ref: "#/components/responses/Error"
  /v1/approvals/{id}/deny:
    post:
      operationId: denyApproval
      summary: Deny an approval
      description: >-
        A person denies (S22): a veto, final at once. `reason` is required (at most 280 characters;
        the agent sees it). The action is denied and sealed in the same transaction. Same rules as
        approve, except the person the action runs on behalf of may deny.
      tags: [Approvals]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The approval id (`apr_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalVote"
      responses:
        "200":
          description: "The denied approval, with `action_status: denied`."
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Approval"
        default:
          $ref: "#/components/responses/Error"
  /v1/approval-links/preview:
    post:
      operationId: previewApprovalLink
      summary: Show the approval behind an emailed link
      description: >-
        Public: the one-time token (from the link's `#t=` fragment) in the body is the credential
        (S22). Previewing never consumes the link. Unknown, used, voided and expired tokens all
        answer the same 401 `approval_link_invalid`. Limited to 30 a minute per client and 10 per
        token.
      tags: [Approvals]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalLinkToken"
      responses:
        "200":
          description: The approval as the link's person sees it.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApprovalLinkPreview"
        default:
          $ref: "#/components/responses/Error"
  /v1/approval-links/decide:
    post:
      operationId: decideApprovalLink
      summary: Decide with an emailed link
      description: >-
        The link's person approves or denies (`via: email_link`). The token is consumed in the
        decision's own transaction, so a refused decision never burns it, and it works exactly
        once. An org with `settings.approvals.email_links: review` answers 403
        `approval_link_review_only`.
      tags: [Approvals]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalLinkDecision"
      responses:
        "200":
          description: The approval after the vote.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Approval"
        default:
          $ref: "#/components/responses/Error"
  /v1/approval-links/status:
    post:
      operationId: approvalLinkStatus
      summary: Follow a decided action from its link
      description: >-
        After deciding, the page follows the action until it is sealed: a token used within
        `approvals.link_status_window` (15 minutes), or one still usable. The action carries its
        state, witness and deed, never the tool's result.
      tags: [Approvals]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalLinkToken"
      responses:
        "200":
          description: The approval and its action.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApprovalLinkStatus"
        default:
          $ref: "#/components/responses/Error"
  /v1/slack/interact:
    post:
      operationId: slackInteract
      summary: Slack interactivity (approve and deny from an approval card)
      description: >-
        Slack's Interactivity Request URL (S29). Not part of the JSON API: Slack posts a form body
        with one field, `payload` (Slack's JSON), signed with the app's signing secret. Deedglass
        routes by the payload's team and app ids, checks `X-Slack-Signature`
        (`v0=` + hex HMAC-SHA256 of `v0:{timestamp}:{body}`) with that org's secret, refuses a
        timestamp more than 300 seconds from now, and acts on one signature once. Every refusal
        before the signature verifies is a bare 401 with no body. A verified click answers 200 within
        2.5 seconds; refusals of the decision (not eligible, self, already decided, expired) reach
        the person as a message only they can see. A deny opens a form whose submission answers 200,
        or 200 with `{"response_action":"errors",…}` when the reason is missing.
      tags: [Approvals]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - in: header
          name: X-Slack-Request-Timestamp
          required: true
          schema: {type: string, pattern: "^[0-9]{1,12}$"}
        - in: header
          name: X-Slack-Signature
          required: true
          schema: {type: string, pattern: "^v0=[0-9a-f]{64}$"}
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [payload]
              properties:
                payload:
                  type: string
                  description: Slack's interaction payload (block_actions or view_submission) as JSON, at most 256 KiB.
      responses:
        "200":
          description: Received. The body is empty, or the deny form's field errors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  response_action: {type: string, enum: [errors]}
                  errors: {type: object, additionalProperties: {type: string}}
        "401":
          description: Refused (bad or missing signature, stale timestamp, unknown team or app, malformed body). No body.
        "429":
          description: Too many requests from this address. No body.
        default:
          $ref: "#/components/responses/Error"
  /approve:
    get:
      operationId: approvePage
      summary: The page an approval link opens
      description: >-
        The token is in the URL fragment (`#t=…`), which browsers never send to servers. The page
        reads no cookie and decides nothing on load: only its Approve and Deny buttons post a
        decision. `Cache-Control: no-store`, `Referrer-Policy: no-referrer` and a strict
        Content-Security-Policy.
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: HTML, with a strict Content-Security-Policy.
          content:
            text/html:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /approve/assets/{file}:
    get:
      operationId: approveAsset
      summary: The approval page's stylesheet and script
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - {name: file, in: path, required: true, schema: {type: string, enum: [approve.css, approve.js]}}
      responses:
        "200":
          description: CSS or JavaScript.
          content:
            text/css:
              schema: {type: string}
            text/javascript:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /v1/webhooks:
    get:
      operationId: listWebhooks
      summary: List webhook endpoints
      description: Every endpoint of the org (at most 16). An admin+ Console session or an org admin key.
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: A list of webhooks.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createWebhook
      summary: Create a webhook endpoint
      description: >-
        The URL passes the egress rules (https outside dev; never metadata, private or loopback
        addresses) now and at every delivery. The signing secret (`whsec_…`) is in this response
        only; Deedglass keeps it in the vault. Seals `webhook.created`. At most 16 per org (409).
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookCreate"
      responses:
        "201":
          description: The webhook, with its secret (shown once).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Webhook"
        default:
          $ref: "#/components/responses/Error"
  /v1/webhooks/{id}:
    get:
      operationId: getWebhook
      summary: Retrieve a webhook endpoint
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The webhook id (`whk_…`)., schema: {type: string}}
      responses:
        "200":
          description: The webhook (never its secret).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Webhook"
        default:
          $ref: "#/components/responses/Error"
    delete:
      operationId: deleteWebhook
      summary: Delete a webhook endpoint
      description: Removes the endpoint and its queued deliveries, and seals `webhook.deleted`.
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - {name: id, in: path, required: true, description: The webhook id (`whk_…`)., schema: {type: string}}
      responses:
        "200":
          description: "`{object: webhook, id, deleted: true, deed}`."
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema: {type: object}
        default:
          $ref: "#/components/responses/Error"
  /v1/webhooks/{id}/test:
    post:
      operationId: testWebhook
      summary: Send a test event
      description: >-
        Sends `webhook.test` to this endpoint now, signed like every delivery, and answers with the
        outcome (`delivered`, `response_status`, `response_ms`). A failure is retried on the usual
        schedule.
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - {name: id, in: path, required: true, description: The webhook id (`whk_…`)., schema: {type: string}}
      responses:
        "200":
          description: The test delivery's outcome.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema: {type: object}
        default:
          $ref: "#/components/responses/Error"
  /v1/webhooks/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: List an endpoint's deliveries
      description: The endpoint's last deliveries, newest first (`limit` 1 to 100, default 20). Never a payload.
      tags: [Webhooks]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The webhook id (`whk_…`)., schema: {type: string}}
        - {name: limit, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
      responses:
        "200":
          description: A list of deliveries.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema: {type: object}
        default:
          $ref: "#/components/responses/Error"
  /v1/policies:
    get:
      operationId: listPolicies
      summary: List policies
      description: >-
        The org's policies, oldest first, each with its status and the versions that are enforced
        or in shadow. Unknown parameters are refused. A Console session (viewer and up) or an org
        admin key (`admin` scope); agent keys get 403 `insufficient_scope` ("Agents cannot read or
        test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of policies.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createPolicy
      summary: Create a policy from its YAML
      description: >-
        201. Compiles the source and saves the policy with its first version, which is not active
        until you activate it. The policy's name comes from the YAML. Nothing is sealed: the
        immutable version (author, time, `source_hash`, `version_hash`) is the record, and deeds
        are for changes that take effect. A source that doesn't compile is 400 `policy_invalid`
        with `diagnostics`, each with its line, code and hint. A name the org already uses is 409
        `conflict` (push a new version to it instead). Bodies up to 1 MiB; the source itself up to
        256 KiB. An admin+ Console session (with the `Deedglass-CSRF` header) or an org admin key;
        agent keys get 403 `insufficient_scope` ("Agents cannot read or test policies. Use the
        Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        $ref: "#/components/requestBodies/PolicySource"
      responses:
        "201":
          description: The policy, with its first version.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyCreated"
        "400":
          description: >-
            The source wasn't accepted (`policy_invalid`, with `diagnostics`), or a field isn't
            valid (`validation_failed`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                policy_invalid:
                  summary: A rule has no reason
                  value:
                    error:
                      type: invalid_request_error
                      code: policy_invalid
                      message: The policy wasn't accepted. The diagnostics list each problem with its line and a hint.
                      param: source
                      request_id: req_01j9zr3m5k8q2w7x4t6v0b1c9d
                      doc_url: https://docs.deedglass.com/errors/policy_invalid
                      diagnostics:
                        - object: policy_diagnostic
                          severity: error
                          code: reason_missing
                          message: Rule "block-destructive-sql" denies but has no reason.
                          hint: Write the one sentence the agent will see, like "Destructive SQL never runs for agents."
                          line: 43
                          column: 15
                          rule_id: block-destructive-sql
                          path: rules[2]
                          doc_url: https://docs.deedglass.com/policies/errors/reason_missing
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/active:
    get:
      operationId: getActivePolicies
      summary: Show the enforced and shadow policies
      description: >-
        The org's live activations: the version that decides (`enforce`) and the version recorded
        alongside it (`shadow`), each null when there is none. A Console session (viewer and up) or
        an org admin key (`admin` scope); agent keys get 403 `insufficient_scope` ("Agents cannot
        read or test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The active set.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyActiveSet"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}:
    get:
      operationId: getPolicy
      summary: Get a policy
      description: >-
        By `pol_` id or by name. A policy from another org is 404 `not_found`, like one that
        doesn't exist. A Console session (viewer and up) or an org admin key (`admin` scope); agent
        keys get 403 `insufficient_scope` ("Agents cannot read or test policies. Use the Console or
        an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/PolicyRef"
      responses:
        "200":
          description: The policy.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Policy"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}/versions:
    get:
      operationId: listPolicyVersions
      summary: List a policy's versions
      description: >-
        Newest first, without `source` (get one version to read it). Each version's `status` says
        whether it is enforced, in shadow or inactive. A Console session (viewer and up) or an org
        admin key (`admin` scope); agent keys get 403 `insufficient_scope` ("Agents cannot read or
        test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/PolicyRef"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of versions.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyVersionList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createPolicyVersion
      summary: Push a new version of a policy
      description: >-
        201 with the new version, which is not active until you activate it. Versions never
        change. Content identical to an existing version (the same `version_hash`) saves nothing:
        the answer is 200 with that version. The YAML's name must be the policy's name; another
        name is 400 `validation_failed` on `source` (push it as a new policy). A source that
        doesn't compile is 400 `policy_invalid` with `diagnostics`. Bodies up to 1 MiB. An admin+
        Console session (with the `Deedglass-CSRF` header) or an org admin key; agent keys get 403
        `insufficient_scope` ("Agents cannot read or test policies. Use the Console or an admin
        key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PolicyRef"
      requestBody:
        $ref: "#/components/requestBodies/PolicySource"
      responses:
        "201":
          description: The new version.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyVersion"
        "200":
          description: The content is identical to this existing version; nothing was saved.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyVersion"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}/versions/{v}:
    get:
      operationId: getPolicyVersion
      summary: Get one version of a policy, with its source
      description: >-
        By version number or `pv_` id; the body includes `source`, the YAML exactly as pushed. A
        malformed `{v}` is 400 `validation_failed` on `v`. A Console session (viewer and up) or an
        org admin key (`admin` scope); agent keys get 403 `insufficient_scope` ("Agents cannot read
        or test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/PolicyRef"
        - $ref: "#/components/parameters/PolicyVersionRef"
      responses:
        "200":
          description: The version, with its source.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyVersion"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}/versions/{v}/activate:
    post:
      operationId: activatePolicyVersion
      summary: Enforce a version, or run it in shadow
      description: >-
        `enforce` makes the version the org's deciding policy and replaces the one enforced before
        (`previous`); `shadow` records its decisions alongside without deciding. Every instance
        picks up the change within 2 s. Every activation seals an admin deed (`deeds`); promoting
        the shadow version to enforce seals two, the shadow ending and then the version enforced.
        Enforcing the version that is already enforced, or shadowing the enforced version, is 409
        `conflict`. Enforcing a policy with the `default_allow` warning needs
        `acknowledge_warnings: ["default_allow"]`; without it the answer is 400
        `validation_failed` on `acknowledge_warnings`. An admin+ Console session (with the
        `Deedglass-CSRF` header) or an org admin key; agent keys get 403 `insufficient_scope`
        ("Agents cannot read or test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PolicyRef"
        - $ref: "#/components/parameters/PolicyVersionRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [mode, reason]
              properties:
                mode: {type: string, enum: [enforce, shadow]}
                reason: {type: string, minLength: 3, maxLength: 280, description: One line, 3 to 280 characters. It goes into the deed.}
                acknowledge_warnings:
                  type: array
                  description: Warning codes the version carries that you accept, like `default_allow`.
                  items: {type: string}
            example: {mode: enforce, reason: Guard the orders database before the pilot.}
      responses:
        "200":
          description: The activation, with the deeds that sealed it.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyActivation"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}/versions/{v}/deactivate:
    post:
      operationId: deactivatePolicyVersion
      summary: Switch off the shadow policy
      description: >-
        Ends the version's shadow activation and seals an admin deed. Only the shadow version can
        be switched off: the enforced one is 409 `conflict` (activate another version instead), and
        a version that isn't the shadow policy is 409 `conflict`. The reason is required (3 to 280
        characters). An admin+ Console session (with the `Deedglass-CSRF` header) or an org admin
        key; agent keys get 403 `insufficient_scope` ("Agents cannot read or test policies. Use the
        Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PolicyRef"
        - $ref: "#/components/parameters/PolicyVersionRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [reason]
              properties:
                reason: {type: string, minLength: 3, maxLength: 280, description: One line, 3 to 280 characters. It goes into the deed.}
            example: {reason: Shadow run finished; v3 matched the review.}
      responses:
        "200":
          description: The activation, now ended, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyActivation"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/{id}/versions/{v}/replay:
    post:
      operationId: createReplay
      summary: Replay a policy version over past actions
      description: >-
        Queues a replay (S27): every decision recorded in the window is evaluated again under this
        version, in shadow mode, and compared with a baseline. A replay never runs a tool, never
        reserves a budget or touches usage, never seals a deed and never emits an action event.
        The baseline is the decisions recorded at the time (`recorded`, the default) or another
        version of the same policy evaluated over the same inputs (`version`), which separates your
        change from everything else that differed at the time. The answer is 202 with the replay
        object (`status: queued`); a runner on any instance picks it up (one running replay per
        org), and `GET /v1/replays/{id}` shows its progress and, once it `succeeded`, its result.
        The same inputs, version and sqlguard version give the same `result_digest`. The window
        defaults to the last 7 days (`to` defaults to now, `from` to 7 days before `to`); `from`
        must be before `to` and the window at most 90 days (400 `replay_window_invalid`). More
        than 1,000,000 actions in the window is 400 `replay_too_large`, and nothing is queued. An
        unknown policy or version is 404 `not_found`; a `{v}` that isn't a number is 400
        `validation_failed`. Each org starts at most 10 replays an hour (429 `rate_limited`, with
        `Retry-After`). Bodies up to 16 KiB. An admin+ Console session (with the `Deedglass-CSRF`
        header) or an org admin key (`admin` scope); agent keys get 403 `insufficient_scope`
        ("Agent keys can't read Insights. Use the Console or an org admin key.").
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PolicyRef"
        - {name: v, in: path, required: true, description: The version number to replay, like 3., schema: {type: integer, minimum: 1}}
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReplayRequest"
            examples:
              recorded:
                summary: The last 8 days, against the decisions recorded at the time
                value: {"from": "2026-09-26T07:03:15Z", "to": "2026-10-04T07:03:15Z"}
              version:
                summary: Against version 2 of the same policy, over the same inputs
                value: {"from": "2026-09-26T07:03:15Z", "to": "2026-10-04T07:03:15Z", "baseline": {"type": "version", "version": 2}}
      responses:
        "202":
          description: The replay, queued.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Replay"
              examples:
                queued:
                  summary: acme-sim v3 queued over 5,890 actions
                  value:
                    {"object": "replay", "id": "rpl_01m42vjp4z65td5hkdkcefczms", "status": "queued", "purpose": "manual",
                     "candidate": {"policy": "pol_01m42v9q4bjwfqmk8bd58vzkdh", "name": "acme-sim", "version": 3, "version_id": "pv_01m42vcmkk193cy009pdzt4c6s",
                                   "version_hash": "sha256:3d09f641a6d5f6962ef719d05a59c6cea0362d950ea6f920418b01125b6bbb7b"},
                     "baseline": {"type": "recorded", "version": 0},
                     "window": {"from": "2026-09-26T07:03:15Z", "to": "2026-10-04T07:03:15Z"},
                     "progress": {"processed": 0, "total": 5890, "percent": 0},
                     "requested_by": {"type": "api_key", "id": "key_01m42vbv2s105agpkrzjpnhzpa"},
                     "attempts": 0, "created_at": "2026-10-04T07:03:15.359342Z", "started_at": null, "finished_at": null, "error": null}
        "400":
          description: '`replay_window_invalid`, `replay_too_large` or `validation_failed`.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                window:
                  summary: from is after to
                  value:
                    error:
                      type: invalid_request_error
                      code: replay_window_invalid
                      message: The window must start before it ends, and it can be at most 90 days long. Send from before to.
                      param: to
                      request_id: req_01m42vk215p3bfyp3xasewttq4
                      doc_url: https://docs.deedglass.com/errors/replay_window_invalid
                too_large:
                  summary: More than 1,000,000 actions in the window
                  value:
                    error:
                      type: invalid_request_error
                      code: replay_too_large
                      message: This replay covers 1,240,512 actions; the limit is 1,000,000. Narrow the window with from and to.
                      param: null
                      request_id: req_01m42vk3a7c9e1g3j5m7p9s1v3
                      doc_url: https://docs.deedglass.com/errors/replay_too_large
        "403":
          description: An agent key (`insufficient_scope`), or a viewer session (`insufficient_role`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: permission_error
                  code: insufficient_scope
                  message: Agent keys can't read Insights. Use the Console or an org admin key.
                  param: null
                  request_id: req_01m42vk4b8d0f2h4k6n8q0t2w4
                  doc_url: https://docs.deedglass.com/errors/insufficient_scope
        "404":
          description: No such policy, or no such version of it.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: not_found
                  message: "No version 7 of acme-sim. Versions 1 to 3 exist. List them: deedglass policy versions acme-sim"
                  param: version
                  request_id: req_01m42vk5c9e1g3j5m7p9s1v3x5
                  doc_url: https://docs.deedglass.com/errors/not_found
        "429":
          description: This org started 10 replays in the last hour.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Retry-After:
              description: Seconds to wait (300).
              schema: {type: integer}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: rate_limit_error
                  code: rate_limited
                  message: This org started 10 replays in the last hour. Try again later, or narrow the window of the ones running.
                  param: null
                  request_id: req_01m42vk6d0f2h4k6n8q0t2w4y6
                  doc_url: https://docs.deedglass.com/errors/rate_limited
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/lint:
    post:
      operationId: lintPolicy
      summary: Check a policy's YAML without saving it
      description: >-
        Compiles the source and reports every error and warning. The answer is 200 even when the
        policy is invalid: `valid` says so, and `errors` lists why. Nothing is saved and nothing
        is sealed, so `Idempotency-Key` is ignored. Limited to 60 requests a minute per person or
        key (429 `rate_limited`), with a 2 s timeout and bodies up to 1 MiB. A Console session
        (viewer and up, with the `Deedglass-CSRF` header) or an org admin key (`admin` scope);
        agent keys get 403 `insufficient_scope` ("Agents cannot read or test policies. Use the
        Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [source]
              properties:
                source: {type: string, description: The policy YAML.}
                voice: {type: boolean, default: false, description: Also check reasons against the brand voice (`reason_style` warnings).}
            example: {source: "deedglass: policy/v1\nname: postgres-safe\n…"}
      responses:
        "200":
          description: The lint result, valid or not.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyLintResult"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/evaluate:
    post:
      operationId: evaluatePolicy
      summary: Dry-run one action against a policy
      description: >-
        Evaluates one case input against the org's active policies (the default), a stored version
        or a source, and returns the decision with its facts and, with `explain`, the trace. When a
        shadow policy is active, `shadow` holds its decision too. It never reserves usage and
        writes nothing: usage counts as zero unless the input gives it. An `agent.id` from another
        org is 404 `not_found`; leave it out to use a local agent. Limited to 60 requests a minute
        per person or key (429 `rate_limited`), with a 2 s timeout and bodies up to 1 MiB.
        `Idempotency-Key` is ignored. A Console session (viewer and up, with the `Deedglass-CSRF`
        header) or an org admin key (`admin` scope); agent keys get 403 `insufficient_scope`
        ("Agents cannot read or test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [input]
              properties:
                policy:
                  description: Which policy decides. Leave it out for the active policies.
                  oneOf:
                    - {type: string, const: active}
                    - type: object
                      additionalProperties: false
                      required: [id, version]
                      properties:
                        id: {type: string, description: A `pol_` id or the policy's name.}
                        version: {type: integer, minimum: 1}
                    - type: object
                      additionalProperties: false
                      required: [source]
                      properties:
                        source: {type: string, description: A policy YAML, compiled for this call only.}
                input:
                  type: object
                  description: >-
                    The action, in the case-input format (docs/POLICY_LANGUAGE.md §6.2): `tool`,
                    `args` (or the `sql` shorthand), and optionally `agent`, `org`, `user`,
                    `session`, `request`, `time`, `usage` and `anomaly`.
                explain: {type: boolean, default: false, description: Include the trace.}
            example:
              input: {tool: postgres.execute, sql: "DELETE FROM public.orders"}
              explain: true
      responses:
        "200":
          description: The decision.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyDecision"
        default:
          $ref: "#/components/responses/Error"
  /v1/policies/test:
    post:
      operationId: testPolicy
      summary: Run test cases against a policy
      description: >-
        Runs a cases file against a source, or against a stored version (`policy` and `version`),
        and reports each case and the rule coverage. Send `source` or `policy` with `version`, not
        both. It never reserves usage and writes nothing. Limited to 60 requests a minute per
        person or key (429 `rate_limited`), with a 2 s timeout and bodies up to 2 MiB.
        `Idempotency-Key` is ignored. A Console session (viewer and up, with the `Deedglass-CSRF`
        header) or an org admin key (`admin` scope); agent keys get 403 `insufficient_scope`
        ("Agents cannot read or test policies. Use the Console or an admin key.").
      tags: [Policies]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [cases]
              properties:
                source: {type: string, description: A policy YAML. Not with policy.}
                policy: {type: string, description: A stored policy, by `pol_` id or name. Needs version.}
                version: {type: integer, minimum: 1, description: The stored version's number, with policy.}
                cases:
                  description: >-
                    A cases file as a YAML string, or a list of cases (each with `name`, `input`
                    and `expect`), or an object with `cases`.
                  oneOf:
                    - type: string
                    - type: array
                      items: {type: object}
                    - type: object
            example:
              policy: postgres-safe
              version: 3
              cases:
                - name: deletes never run
                  input: {tool: postgres.execute, sql: "DELETE FROM public.orders"}
                  expect: {decision: deny, rule: block-destructive-sql}
      responses:
        "200":
          description: The test result.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolicyTestResult"
        default:
          $ref: "#/components/responses/Error"
  /.well-known/oauth-protected-resource:
    get:
      operationId: getProtectedResourceMetadata
      summary: Show the protected-resource metadata for the API
      description: >-
        Public and not versioned. The RFC 9728 protected-resource metadata for the server as a
        whole. `authorization_servers` is absent on purpose: Deedglass v1 takes pre-issued API keys
        as bearer tokens and has no authorization server to list; v2 adds the org's issuer.
        Readable cross-origin and cached for an hour. It never reads the database.
      tags: [MCP]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: The metadata document.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Cache-Control:
              schema:
                type: string
                const: public, max-age=3600
            Access-Control-Allow-Origin:
              schema:
                type: string
                const: "*"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProtectedResourceMetadata"
              example:
                resource: https://api.deedglass.com
                resource_name: Deedglass
                bearer_methods_supported: [header]
                scopes_supported: ["actions:execute", "actions:read", "deeds:read"]
                resource_documentation: https://docs.deedglass.com/mcp/authorization
        default:
          $ref: "#/components/responses/Error"
  /.well-known/oauth-protected-resource/mcp/{connector}:
    get:
      operationId: getMCPResourceMetadata
      summary: Show the protected-resource metadata for a connector's MCP endpoint
      description: >-
        Public and not versioned. The RFC 9728 metadata for `/mcp/{connector}`, at the
        path-inserted URL that the endpoint's bearer challenges name in `resource_metadata`. The
        document is the same for every well-formed connector id and never reads the database; a
        malformed id is 404 `not_found`. `authorization_servers` is absent on purpose, as in the
        root document. Readable cross-origin and cached for an hour.
      tags: [MCP]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - name: connector
          in: path
          required: true
          description: The upstream's connector name (`mcp.upstreams.<name>`), or a connector id (`con_…`) from S16.
          schema:
            type: string
            pattern: "^([a-z][a-z0-9-]{1,31}|con_[0-7][0-9a-hjkmnp-tv-z]{25})$"
      responses:
        "200":
          description: The metadata document.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Cache-Control:
              schema:
                type: string
                const: public, max-age=3600
            Access-Control-Allow-Origin:
              schema:
                type: string
                const: "*"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProtectedResourceMetadata"
              example:
                resource: https://api.deedglass.com/mcp/con_01k67c04kxevyex3pjyqe0t45d
                resource_name: Deedglass Gate
                bearer_methods_supported: [header]
                scopes_supported: ["actions:execute"]
                resource_documentation: https://docs.deedglass.com/mcp/authorization
        default:
          $ref: "#/components/responses/Error"
  /.well-known/oauth-protected-resource/mcp/native:
    get:
      operationId: getNativeMCPResourceMetadata
      summary: Show the protected-resource metadata for the native MCP endpoint
      description: >-
        Public and not versioned. The RFC 9728 metadata for `/mcp/native`, at the path-inserted URL
        that the endpoint's bearer challenges name in `resource_metadata`. It never reads the
        database. `authorization_servers` is absent on purpose, as in the root document. Readable
        cross-origin and cached for an hour.
      tags: [MCP]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: The metadata document.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Cache-Control:
              schema:
                type: string
                const: public, max-age=3600
            Access-Control-Allow-Origin:
              schema:
                type: string
                const: "*"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProtectedResourceMetadata"
              example:
                resource: https://api.deedglass.com/mcp/native
                resource_name: Deedglass Gate
                bearer_methods_supported: [header]
                scopes_supported: ["actions:execute"]
                resource_documentation: https://docs.deedglass.com/mcp/authorization
        default:
          $ref: "#/components/responses/Error"
  /mcp/{connector}:
    post:
      operationId: mcpConnector
      summary: Send an MCP message to an upstream MCP server through the Gate
      description: >-
        The MCP Streamable HTTP gateway (S15) for one upstream MCP server configured under
        `mcp.upstreams.<connector>` in deedglass.yaml. Deedglass runs every allowed `tools/call`
        itself (gateway mode) and holds the upstream's credentials. A POST-only JSON-RPC endpoint,
        sessionless and dual-era: 2026-07-28 requests carry `_meta`
        `io.modelcontextprotocol/protocolVersion` and the `MCP-Protocol-Version`, `Mcp-Method` and
        (for `tools/call`, `resources/read`, `prompts/get`) `Mcp-Name` headers, and any
        `Mcp-Param-*` a tool's `x-mcp-header` declares; a missing or different header is 400 with
        JSON-RPC `-32020`, an unsupported version 400 with `-32022` and `data.supported`. Legacy
        clients (2025-11-25, 2025-06-18, 2025-03-26) open with `initialize`; no `Mcp-Session-Id` is
        minted, and one sent is ignored. Notifications get 202 with no body. An unknown method is
        404 (modern) or 200 (legacy) with `-32601`. Responses are `application/json` with
        `Cache-Control: no-store`; an `Accept` without `application/json` is 406. Errors before the
        message is read are JSON-RPC errors without an id whose `data` names the Deedglass error
        `code`, `doc_url` and `request_id`: 401 with `WWW-Authenticate: Bearer realm="deedglass",
        resource_metadata="<base>/.well-known/oauth-protected-resource/mcp/<connector>",
        scope="actions:execute"`, 413 over 4 MiB, 415 for another `Content-Type`, 429 beyond 32
        requests in flight per agent. A browser `Origin` other than the server's own or one in
        `mcp.allowed_origins` gets 403. A stopped or suspended agent's `tools/call` is recorded and
        sealed, and answered as an `isError` tool result. GET and DELETE get 405 with `Allow:
        POST`. `Idempotency-Key` is ignored. An agent key with `actions:execute`. JSON-RPC is not
        modelled here; see docs/MCP.md.
      tags: [MCP]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - name: connector
          in: path
          required: true
          description: The upstream's connector name (`mcp.upstreams.<name>`), or a connector id (`con_…`) from S16.
          schema:
            type: string
            pattern: "^([a-z][a-z0-9-]{1,31}|con_[0-7][0-9a-hjkmnp-tv-z]{25})$"
      requestBody:
        $ref: "#/components/requestBodies/MCPMessage"
      responses:
        "403":
          $ref: "#/components/responses/MCPOriginRefused"
        "200":
          $ref: "#/components/responses/MCPResponse"
        "202":
          description: A notification was accepted. No body.
        "400":
          $ref: "#/components/responses/MCPRequestRefused"
        default:
          $ref: "#/components/responses/Error"
  /mcp/native:
    post:
      operationId: mcpNative
      summary: Send an MCP message to the Gate's native tools
      description: >-
        Deedglass as an MCP server (S15): the org's native connectors' tools, named
        `<connector>.<tool>` (`fake.echo` in dev; `postgres.*` and others as connectors arrive),
        filtered per agent by the active policy, plus `deedglass.check_approval`. The same
        sessionless, dual-era protocol rules as `/mcp/{connector}`; `deedglass mcp serve` bridges
        stdio clients to it. An agent key with `actions:execute`.
      tags: [MCP]
      security: [{bearerAuth: []}]
      x-deedglass-auth: agent_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      requestBody:
        $ref: "#/components/requestBodies/MCPMessage"
      responses:
        "403":
          $ref: "#/components/responses/MCPOriginRefused"
        "200":
          $ref: "#/components/responses/MCPResponse"
        "202":
          description: A notification was accepted. No body.
        "400":
          $ref: "#/components/responses/MCPRequestRefused"
        default:
          $ref: "#/components/responses/Error"
  /login:
    get:
      operationId: loginPage
      summary: The sign-in page
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: HTML, with a strict Content-Security-Policy.
          content:
            text/html:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /auth/confirm:
    get:
      operationId: confirmPage
      summary: The page a sign-in link opens
      description: >-
        The token is in the URL fragment (`#t=…`), which browsers never send to servers; the page
        shows what the link does and signs in only when the person clicks. Fetching this page uses
        nothing, so mail scanners that open links can't sign anyone in.
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: HTML, with a strict Content-Security-Policy.
          content:
            text/html:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /app:
    get:
      operationId: consoleIndex
      summary: The Console's Overview page
      description: >-
        The Console is static HTML embedded in the binary; pages hold no data (they read the API).
        Without a valid session the answer is `302` to `/login?next=/app`. Served with
        `Cache-Control: no-cache`, a content-hash `ETag` and the Console's Content-Security-Policy
        (docs/CONSOLE.md).
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      responses:
        "200":
          description: HTML, with the Console's Content-Security-Policy.
          content:
            text/html:
              schema: {type: string}
        "302":
          description: No valid session; `Location` is `/login?next=` with this path.
          headers:
            Location:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /app/{path...}:
    get:
      operationId: consolePage
      summary: A Console page or asset
      description: >-
        Pages (`/app/actions`, `/app/actions/act_…`, `/app/settings/…`) redirect to
        `/login?next=<path and query>` (`302`) without a valid session; `next` is kept only when it
        is a plain path under `/app`. An unknown page or an id with the wrong prefix serves the
        Console's 404 page, which never echoes the path. Assets (`/app/_astro/…`, fonts, icons)
        are public and immutable, sent precompressed by `Accept-Encoding`. `/app/_kit` is 404 in
        prod.
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - {name: "path...", in: path, required: true, description: The page or asset path under /app., schema: {type: string}}
      responses:
        "200":
          description: HTML, CSS, JavaScript, a font or an image.
          content:
            text/html:
              schema: {type: string}
        "302":
          description: No valid session; `Location` is `/login?next=` with this path and query.
          headers:
            Location:
              schema: {type: string}
        "404":
          description: The Console's 404 page.
          content:
            text/html:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /auth/assets/{file}:
    get:
      operationId: authAsset
      summary: The sign-in pages' stylesheet and script
      tags: [Pages]
      security: []
      x-deedglass-auth: public
      x-deedglass-since: "2026-10-01"
      parameters:
        - {name: file, in: path, required: true, schema: {type: string, enum: [tokens.css, auth.css, auth.js]}}
      responses:
        "200":
          description: CSS or JavaScript.
          content:
            text/css:
              schema: {type: string}
            text/javascript:
              schema: {type: string}
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors:
    get:
      operationId: listConnectors
      summary: List connectors
      description: >-
        Live connectors, oldest id first, each with its tools, the credential's metadata (never a
        value) and the last test result. A viewer+ Console session or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of connectors.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorList"
        default:
          $ref: "#/components/responses/Error"
    post:
      operationId: createConnector
      summary: Add a connector
      description: >-
        Validates and normalizes the config, checks `base_url` through the egress rules (400
        `destination_blocked` for metadata, reserved, loopback or private addresses that aren't
        allowed), and seals `connector.created` with the type, name and config hash. Names are
        `^[a-z][a-z0-9-]{1,31}$`, unique among live connectors; `deedglass`, `mcp`, `native` (and
        `fake` outside dev) are reserved. A taken name is 409 `conflict`. Credentials are set
        separately. An admin+ Console session (with `Deedglass-CSRF`) or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [type, name, config]
              properties:
                type: {type: string, description: "A connector type from GET /v1/connector-types (rest in S16)."}
                name: {type: string, pattern: "^[a-z][a-z0-9-]{1,31}$"}
                description: {type: string, maxLength: 500}
                config: {type: object, description: "The type's config (docs/CONNECTORS.md). At most 64 KiB."}
      responses:
        "201":
          description: The connector, with the id of its admin deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connector"
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors/{id}:
    get:
      operationId: getConnector
      summary: Get a connector
      description: By `con_…` id or by name. A viewer+ Console session or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/ConnectorRef"
      responses:
        "200":
          description: The connector.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connector"
        default:
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateConnector
      summary: Update a connector
      description: >-
        Replaces `description`, `status` (`active` or `disabled`) or the whole `config`. With
        `expected_config_hash`, a config that changed meanwhile is 409 `conflict`. A change seals
        `connector.updated`; the next call uses the new config. An admin+ Console session or an org
        admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                description: {type: string, maxLength: 500}
                status: {type: string, enum: [active, disabled]}
                config: {type: object}
                expected_config_hash: {type: string, pattern: "^sha256:[0-9a-f]{64}$"}
      responses:
        "200":
          description: The connector after the change.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connector"
        default:
          $ref: "#/components/responses/Error"
    delete:
      operationId: deleteConnector
      summary: Delete a connector
      description: >-
        Soft-deletes the connector and crypto-shreds its credential (the data key is destroyed).
        409 `conflict` while an action on it is executing. Seals `connector.deleted` (and
        `connector.credentials_removed` when it had one). An admin+ Console session or an org admin
        key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      responses:
        "200":
          description: The deletion.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorDeleted"
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors/{id}/credentials:
    put:
      operationId: setConnectorCredentials
      summary: Store a connector's credentials
      description: >-
        Write-only. Encrypts the secret with a fresh data key (AES-256-GCM envelope,
        docs/VAULT.md) as version n+1, replacing (and crypto-shredding) the previous one, and seals
        `connector.credentials_updated` with the kind, version and key id only. The body is never
        logged or stored for replay; errors name the `param`, never a value. The kind must be one the
        type takes and match the config's `auth.kind`. An admin+ Console session or an org admin
        key. 503 `vault_unavailable` when the server has no key ring.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [kind, secret]
              properties:
                kind: {type: string, enum: [bearer, header, basic, query, dsn, fields]}
                secret:
                  type: object
                  description: "bearer {token} · header {value} · basic {username, password} · query {value} · dsn {dsn, witness_dsn?} · fields {any}. At most 16 KiB."
                  additionalProperties: {type: string}
      responses:
        "200":
          description: The credential's metadata.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Credential"
        default:
          $ref: "#/components/responses/Error"
    delete:
      operationId: deleteConnectorCredentials
      summary: Delete a connector's credentials
      description: >-
        Removes the credential, which crypto-shreds it, and seals `connector.credentials_removed`.
        Removing nothing answers `deleted: true` with no deed. An admin+ Console session or an org
        admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      responses:
        "200":
          description: The credential, now unset.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Credential"
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors/{id}/test:
    post:
      operationId: testConnector
      summary: Test a connector
      description: >-
        Answers "will this work?" without changing anything: the config, the credential, the
        destination (resolved and classified), TLS, and the config's test request (a GET or a HEAD)
        with the credential. Records the result as the connector's `health`. At most 10 runs per
        minute per connector (429 `rate_limited`). An admin+ Console session or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      responses:
        "200":
          description: The checks.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorTest"
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors/{id}/initialized:
    post:
      operationId: recordConnectorInitialized
      summary: Record a Postgres database setup
      description: >-
        What `deedglass connector postgres init` calls after it applied a setup plan in one
        transaction: seals the admin deed `connector.postgres_initialized` with the plan's sha256,
        its change count, the database, the server version, the provider and the roles it created.
        Never the plan's SQL, a password or a verifier. Postgres connectors only. An admin+ Console
        session or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/ConnectorRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [plan_sha256, changes, database, server_version, provider]
              properties:
                plan_sha256: {type: string, pattern: "^sha256:[0-9a-f]{64}$"}
                changes: {type: integer, minimum: 1, maximum: 10000}
                database: {type: string, maxLength: 63}
                server_version: {type: string, example: "18.6"}
                provider: {type: string, enum: [local, rds, aurora, supabase, neon, cloudsql, azure, postgres]}
                roles_created:
                  type: array
                  items: {type: string, enum: [deedglass_owner, deedglass_agent, deedglass_witness]}
      responses:
        "201":
          description: The sealed setup record.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorSetup"
        default:
          $ref: "#/components/responses/Error"
  /v1/connectors/{id}/decoder:
    get:
      operationId: getConnectorDecoder
      summary: Show a connector's decoder
      description: >-
        What the decoder reads from this connector's database (S20): its slot, status and health,
        the WAL positions and lag, the watched tables, the frame counters per class and the most
        recent frames (`?recent=N`, 0 to 50, default 10; no row images). A witnessed connector that
        no runner has picked up yet shows `pending`. A connector that isn't witnessed is 409
        `witness_not_configured`. A viewer+ Console session or an org admin key; agent keys get 403
        `insufficient_scope`.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/ConnectorRef"
        - name: recent
          in: query
          required: false
          schema: {type: integer, minimum: 0, maximum: 50, default: 10}
      responses:
        "200":
          description: The decoder.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Decoder"
        default:
          $ref: "#/components/responses/Error"
    delete:
      operationId: dropConnectorDecoder
      summary: Drop a connector's slot
      description: >-
        Stops the connector's decoder, drops its replication slot (as the witness role), seals the
        admin deed `decoder.slot_dropped` and marks the decoder `dropped`. Actions still waiting for
        L2 evidence on this connector become evidence gaps. The next sync creates a new slot: a new
        baseline, and reconcile records any action in the gap as `missing`. The body must confirm
        the connector's name. 409 `conflict` while another process reads the slot; 409
        `witness_not_configured` for a connector that isn't witnessed. An admin+ Console session or
        an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/ConnectorRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [confirm]
              properties:
                confirm: {type: string, description: "The connector's name, typed to confirm.", example: postgres}
      responses:
        "200":
          description: The slot is dropped, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DecoderDropped"
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions:
    post:
      operationId: openObserveSession
      summary: Open a watched session
      description: >-
        Observe mode (S50). Opens a watched session on a Postgres connector whose
        `witness.observe.enabled` is true: every transaction Postgres commits on the connector's
        watched tables while the session is open, and that no Deedglass action claimed, belongs to
        it. Nothing about the agent changes; only the database's change log is read. `agent` links
        an agent record for display only and attributes nothing. 409 `observe_not_enabled` when the
        connector's observe mode is off. A signed-in admin or owner (with `Deedglass-CSRF`) or an
        org admin key; agent keys get 403 `insufficient_scope`.
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [connector]
              properties:
                connector: {type: string, description: The connector's name or `con_` id.}
                label: {type: string, minLength: 1, maxLength: 80, description: A name for the session (one line).}
                agent: {type: string, description: An agent's name or `agt_` id, shown beside the session; it attributes nothing.}
      responses:
        "201":
          description: The open session.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveSession"
        default:
          $ref: "#/components/responses/Error"
    get:
      operationId: listObserveSessions
      summary: List watched sessions
      description: >-
        This org's watched sessions, newest first, without their transactions. Filter by
        `connector` (name or id), `state` and `caught`. A Console session (viewer and up) or an org
        admin key; agent keys get 403 `insufficient_scope`.
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: connector, in: query, required: false, description: The connector's name or `con_` id., schema: {type: string}}
        - {name: state, in: query, required: false, description: Only this state., schema: {type: string, enum: [open, closing, sealed, undoing, undone]}}
        - {name: caught, in: query, required: false, description: Only sessions with (true) or without (false) a caught item., schema: {type: boolean}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of sessions.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more, next_cursor]
                properties:
                  object: {type: string, const: list}
                  data: {type: array, items: {$ref: "#/components/schemas/ObserveSession"}}
                  has_more: {type: boolean}
                  next_cursor: {type: [string, "null"]}
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions/{id}:
    get:
      operationId: getObserveSession
      summary: Get a watched session
      description: >-
        One session with its transactions, newest first; the newest `diff_frames` (default 20,
        at most 100) carry their first 20 changed rows. A watched session has no level and no
        Matches or Doesn't match verdict: `observed` says so. Another org's session is 404.
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The session (`obs_…`)., schema: {type: string}}
        - {name: diff_frames, in: query, required: false, description: How many transactions carry a diff (0 to 100; default 20)., schema: {type: integer, minimum: 0, maximum: 100}}
      responses:
        "200":
          description: The session.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveSession"
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions/{id}/close:
    post:
      operationId: closeObserveSession
      summary: Close a watched session
      description: >-
        Moves an open session to `closing`. Deedglass seals it (the admin deed
        `observe.session_sealed`: ids, counts and digests only) once the decoder has passed the
        close time, at most 30 seconds later, or flags `decoder_behind` and keeps attaching late
        transactions for up to 10 minutes. Closing a session that is already closing or sealed
        answers it as it is.
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The session (`obs_…`)., schema: {type: string}}
      responses:
        "200":
          description: The session.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveSession"
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions/{id}/frames/{dtx}/exclude:
    post:
      operationId: excludeObserveFrame
      summary: Leave a transaction out of a session's undo
      description: >-
        `{"excluded": true}` leaves one transaction out of the session's undo (another writer's,
        for example); `false` puts it back. Only while the session is sealed and before an undo
        starts (409 `conflict` otherwise). Each change seals `observe.frame_excluded`, and the
        undo's record lists what was left out.
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The session (`obs_…`)., schema: {type: string}}
        - {name: dtx, in: path, required: true, description: The transaction (`dtx_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [excluded]
              properties:
                excluded: {type: boolean}
      responses:
        "200":
          description: The session.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveSession"
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions/{id}/undo_preview:
    get:
      operationId: previewObserveUndo
      summary: Preview a session's undo
      description: >-
        Free. Plans the undo of every included transaction, newest first, from the row images
        Postgres committed, and runs S23's checks without a lock; it never executes. `conflicts`
        counts rows that changed again after the session (any conflict blocks the run). `sql` is
        the same plan as SQL you review and run yourself, in one transaction, each statement
        guarded by the values the session left; `refusals` say why there is no SQL (a TRUNCATE,
        images past the cap, sensitive columns, expired frames).
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The session (`obs_…`)., schema: {type: string}}
        - {name: allow_triggers, in: query, required: false, description: Let triggers fire again (the triggers check is then a warning)., schema: {type: boolean}}
        - {name: steps_limit, in: query, required: false, description: Steps to list, 1 to 1000 (default undo.preview_steps, 20)., schema: {type: integer, minimum: 1, maximum: 1000}}
      responses:
        "200":
          description: The preview.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveUndoPreview"
        default:
          $ref: "#/components/responses/Error"
  /v1/observe/sessions/{id}/undo:
    post:
      operationId: createObserveUndo
      summary: Undo a watched session in one step (Pro)
      description: >-
        Pro (`undo_session`): 403 `pro_required` without it, naming the free path. Previews again,
        refuses `undo_conflict` (rows changed after the session), `undo_unavailable` (with
        `error.checks`), `undo_plan_stale` (the plan changed since the preview) or `already_undone`,
        then sends the undo through the Gate as an action of the org's `console` system agent:
        tool `<connector>.undo_session`, policy (the starter rule asks for a countersign), one
        transaction in Postgres, an L2 witness against the plan's exact footprint, and its own
        deed. The answer is the action object (202 `pending_approval`, or 200 once sealed).
      tags: [Observe]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The session (`obs_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [plan_digest, reason]
              properties:
                plan_digest: {type: string, pattern: "^sha256:[0-9a-f]{64}$", description: The preview's plan_digest.}
                reason: {type: string, minLength: 1, maxLength: 280}
                allow_triggers: {type: boolean}
                wait: {type: number, minimum: 0, maximum: 25, description: Seconds to wait for the outcome.}
      responses:
        "200":
          description: The undo action, sealed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        "202":
          description: The undo action, waiting (usually for a countersign).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Action"
        default:
          $ref: "#/components/responses/Error"
  /v1/connector-types:
    get:
      operationId: listConnectorTypes
      summary: List connector types
      description: >-
        The types this server runs, each with its config schema and the credential kinds it takes
        (each field marked secret or not). S26 builds the Console's forms from it. A viewer+ Console
        session or an org admin key.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The connector types.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorTypeList"
        default:
          $ref: "#/components/responses/Error"
  /v1/connector-types/{type}:
    get:
      operationId: getConnectorType
      summary: Get one connector type
      description: >-
        One catalog entry (S30): the type's tools with their levels and read-back sentences, its
        credentials, setup steps and limits. A viewer+ Console session or an org admin key. The body
        carries an ETag; If-None-Match answers 304. An unknown type is 404 not_found.
      tags: [Connectors]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: type, in: path, required: true, schema: {type: string}, description: 'The type, like stripe.'}
      responses:
        "200":
          description: The connector type.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            ETag:
              schema: {type: string}
              description: '"sha256:…" of the body.'
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorType"
        "304":
          description: Not modified (If-None-Match matched the ETag).
        default:
          $ref: "#/components/responses/Error"
  /v1/replays:
    get:
      operationId: listReplays
      summary: List replays
      description: >-
        This org's replays, newest first (S27), each in the full replay object (with its result
        once it succeeded). Filters: `policy` (a `pol_` id) and `status` (queued, running,
        succeeded, failed or cancelled), with `limit` (1 to 100, default 25) and `cursor`. Any
        other parameter, or one sent twice, is 400 `validation_failed`. A Console session (viewer
        and up) or an org admin key (`admin` scope); agent keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: policy, in: query, required: false, description: Only replays of this policy (`pol_…`)., schema: {type: string}, example: pol_01m42v9q4bjwfqmk8bd58vzkdh}
        - {name: status, in: query, required: false, schema: {type: string, enum: [queued, running, succeeded, failed, cancelled]}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of replays.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReplayList"
              examples:
                running:
                  summary: One replay running, one page of one
                  value:
                    {"object": "list", "has_more": true, "next_cursor": "1791097395359342000.rpl_01m42vjp4z65td5hkdkcefczms",
                     "data": [{"object": "replay", "id": "rpl_01m42vjp4z65td5hkdkcefczms", "status": "running", "purpose": "manual",
                               "candidate": {"policy": "pol_01m42v9q4bjwfqmk8bd58vzkdh", "name": "acme-sim", "version": 3, "version_id": "pv_01m42vcmkk193cy009pdzt4c6s",
                                             "version_hash": "sha256:3d09f641a6d5f6962ef719d05a59c6cea0362d950ea6f920418b01125b6bbb7b"},
                               "baseline": {"type": "recorded", "version": 0},
                               "window": {"from": "2026-09-26T07:03:15Z", "to": "2026-10-04T07:03:15Z"},
                               "progress": {"processed": 3000, "total": 5890, "percent": 50},
                               "requested_by": {"type": "api_key", "id": "key_01m42vbv2s105agpkrzjpnhzpa"},
                               "attempts": 1, "created_at": "2026-10-04T07:03:15.359342Z", "started_at": "2026-10-04T07:03:15.363544Z", "finished_at": null, "error": null}]}
        default:
          $ref: "#/components/responses/Error"
  /v1/replays/{id}:
    get:
      operationId: getReplay
      summary: Retrieve a replay
      description: >-
        One replay (S27): its status, its progress and, once it `succeeded`, its result: the
        summary (`actions = evaluated + unknown`, `evaluated = unchanged + changed +
        rule_changed`), every decision transition from the baseline to the candidate with its
        rules, tools, the first 20 action ids and, for a transition out of `require_approval`,
        what people did with those actions (`human_outcome`), the same decisions reached by a
        different rule (`rule_changes`), the actions that couldn't be evaluated and why
        (`unknown`), the sqlguard versions recorded next to the current one, and `result_digest`.
        A failed replay carries `error`. An id that isn't one of this org's replays is 404
        `not_found`. A Console session (viewer and up) or an org admin key (`admin` scope); agent
        keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The replay id (`rpl_…`)., schema: {type: string}}
      responses:
        "200":
          description: The replay.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Replay"
              examples:
                succeeded:
                  summary: "acme-sim v3 over 8 days: 453 decisions would change"
                  value:
                    {"object": "replay", "id": "rpl_01m42vjp4z65td5hkdkcefczms", "status": "succeeded", "purpose": "manual",
                     "candidate": {"policy": "pol_01m42v9q4bjwfqmk8bd58vzkdh", "name": "acme-sim", "version": 3, "version_id": "pv_01m42vcmkk193cy009pdzt4c6s",
                                   "version_hash": "sha256:3d09f641a6d5f6962ef719d05a59c6cea0362d950ea6f920418b01125b6bbb7b"},
                     "baseline": {"type": "recorded", "version": 0},
                     "window": {"from": "2026-09-26T07:03:15Z", "to": "2026-10-04T07:03:15Z"},
                     "progress": {"processed": 5890, "total": 5890, "percent": 100},
                     "requested_by": {"type": "api_key", "id": "key_01m42vbv2s105agpkrzjpnhzpa"},
                     "attempts": 1, "created_at": "2026-10-04T07:03:15.359342Z", "started_at": "2026-10-04T07:03:15.363544Z",
                     "finished_at": "2026-10-04T07:03:15.475336Z", "error": null,
                     "summary": {"actions": 5890, "evaluated": 5890, "unchanged": 5422, "changed": 453, "rule_changed": 15, "unknown": 0},
                     "transitions": [
                       {"from": "require_approval", "to": "allow", "count": 411,
                        "human_outcome": {"approved": 397, "denied": 1, "expired": 5, "cancelled": 0, "pending": 8, "not_requested": 0},
                        "by_rule": [{"rule_id": "allow-order-status-updates", "count": 411}],
                        "by_tool": [{"tool": "postgres.execute", "count": 411}],
                        "samples": ["act_01m42v9q4yxpvvk84fgd7t8shf", "act_01m42v9q5mppyn3mxr17xdk5nj", "act_01m42v9q6p44tzz1jw9pb0vy34"]},
                       {"from": "allow", "to": "deny", "count": 42,
                        "by_rule": [{"rule_id": "refund-limit-strict", "count": 42}],
                        "by_tool": [{"tool": "stripe.create_refund", "count": 42}],
                        "samples": ["act_01m42v9q8qcyh3wvjsc8b2hq80", "act_01m42v9qdh2cc0fxhn9b1w3ad5", "act_01m42v9qhhw604y6x7aqdvrccm"]}],
                     "rule_changes": [
                       {"decision": "deny", "from_rule": "refund-limit", "to_rule": "refund-limit-strict", "count": 15,
                        "samples": ["act_01m42v9qthv2k3qsyq2a7z2vk6", "act_01m42v9wgcnx5jpv5ffs4smtdv", "act_01m42v9xhg704254e8e3e6672c"]}],
                     "unknown": {"input_pruned": 0, "input_missing": 0, "input_version_unsupported": 0, "input_unreadable": 0, "evaluation_time_limit": 0},
                     "sqlguard": {"recorded": ["1"], "current": "1"},
                     "result_digest": "sha256:98488d32f41913dadd2a55ac845be76fc76dab5b66cc3c80bb1638001ad15b9f",
                     "elapsed_ms": 103}
        "404":
          description: No such replay in this org.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: not_found
                  message: "No replay rpl_01m42vjp4z65td5hkdkcefczmx in this org. List recent replays: GET /v1/replays."
                  param: null
                  request_id: req_01m42vkfg0s05h3mvvxbvbvfr8
                  doc_url: https://docs.deedglass.com/errors/not_found
        default:
          $ref: "#/components/responses/Error"
  /v1/replays/{id}/cancel:
    post:
      operationId: cancelReplay
      summary: Cancel a replay
      description: >-
        Cancels a queued or running replay (S27): its status is `cancelled` at once, and a runner
        working on it stops after its current batch. It keeps no partial result. A replay that
        already finished (succeeded, failed or cancelled) is 409 `conflict`. No body. An admin+
        Console session (with the `Deedglass-CSRF` header) or an org admin key (`admin` scope);
        agent keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The replay id (`rpl_…`)., schema: {type: string}}
      responses:
        "200":
          description: The replay, cancelled.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Replay"
              examples:
                cancelled:
                  summary: Cancelled while running
                  value:
                    {"object": "replay", "id": "rpl_01m42vm8k2r4t6w8y0b2d4f6h8", "status": "cancelled", "purpose": "manual",
                     "candidate": {"policy": "pol_01m42v9q4bjwfqmk8bd58vzkdh", "name": "acme-sim", "version": 3, "version_id": "pv_01m42vcmkk193cy009pdzt4c6s",
                                   "version_hash": "sha256:3d09f641a6d5f6962ef719d05a59c6cea0362d950ea6f920418b01125b6bbb7b"},
                     "baseline": {"type": "version", "version_id": "pv_01m42va0ryp515cvtem633vqng", "version": 2,
                                  "version_hash": "sha256:5b1c0e7a9d3f2468ace0b2d4f6a8c0e2b4d6f8a0c2e4a6b8d0f2a4c6e8b0d2f4"},
                     "window": {"from": "2026-09-04T00:00:00Z", "to": "2026-10-04T00:00:00Z"},
                     "progress": {"processed": 2000, "total": 6012, "percent": 33},
                     "requested_by": {"type": "user", "id": "usr_01m42v9p7c3e5g7j9m1q3s5v7x"},
                     "attempts": 1, "created_at": "2026-10-04T07:10:02.118204Z", "started_at": "2026-10-04T07:10:02.121930Z",
                     "finished_at": "2026-10-04T07:10:02.164518Z", "error": null}
        "409":
          description: The replay already finished.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: conflict
                  message: This replay already finished, so it can't be cancelled. Start a new one to replay again.
                  param: null
                  request_id: req_01m42vm9n4q6s8v0x2z4b6d8f0
                  doc_url: https://docs.deedglass.com/errors/conflict
        default:
          $ref: "#/components/responses/Error"
  /v1/suggestions:
    get:
      operationId: listSuggestions
      summary: List suggestions
      description: >-
        Policy changes Insights proposes from the last 14 days of decisions (S28), ordered by
        impact (the actions whose decision the suggestion's replay would change), then by creation.
        `status` defaults to open and accepting; `all` lists every status. Nothing in a suggestion
        is applied until a person accepts it, and accepting only runs it in shadow. A Console
        session (viewer and up) or an org admin key (`admin` scope); agent keys get 403
        `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: status, in: query, required: false, description: 'open (default: open and accepting), accepting, accepted, rejected, superseded or all.', schema: {type: string, enum: [open, accepting, accepted, rejected, superseded, all]}}
        - {name: kind, in: query, required: false, description: Only this kind., schema: {type: string, enum: [loosen, tighten, budget, explicit_rule, advisory]}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of suggestions.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more, next_cursor]
                properties:
                  object: {type: string, const: list}
                  data: {type: array, items: {$ref: "#/components/schemas/Suggestion"}}
                  has_more: {type: boolean}
                  next_cursor: {type: [string, "null"]}
        "400":
          description: A bad status, kind, limit or cursor (`validation_failed`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        default:
          $ref: "#/components/responses/Error"
  /v1/suggestions/generate:
    post:
      operationId: generateSuggestions
      summary: Generate suggestions now
      description: >-
        Runs a suggest pass now (the server also runs one a day after 03:00 org-local): it reads
        14 days of decisions against the enforced version, runs the six generators, patches the
        policy text, compiles it with every existing rule intact, replays the change against the
        same 14 days and drops anything its guard refuses (a loosening that would allow an action
        a person denied, or more actions than people approved). Open suggestions written for a
        version that is no longer enforced are superseded first. At most 10 are open at once.
        Answers a `learn_run`. No body. An admin+ Console session (with the `Deedglass-CSRF`
        header) or an org admin key; viewers get 403 `insufficient_role`, agent keys 403
        `insufficient_scope`; 409 `conflict` while another pass runs for the org.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The pass's result.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LearnRun"
        "409":
          description: Another pass is running for this org (`conflict`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        default:
          $ref: "#/components/responses/Error"
  /v1/suggestions/{id}:
    get:
      operationId: getSuggestion
      summary: Retrieve a suggestion
      description: >-
        One suggestion with its evidence, the policy change as a unified diff, the replay summary
        (the full replay is `GET /v1/replays/{replay.id}`), the risk note and its decision. An id
        that isn't one of this org's suggestions is 404 `not_found`. A Console session (viewer and
        up) or an org admin key; agent keys get 403.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The suggestion id (`sug_…`)., schema: {type: string}}
      responses:
        "200":
          description: The suggestion.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Suggestion"
        "404":
          description: No such suggestion in this org.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        default:
          $ref: "#/components/responses/Error"
  /v1/suggestions/{id}/accept:
    post:
      operationId: acceptSuggestion
      summary: Accept a suggestion into shadow
      description: >-
        Creates a policy version from the suggestion (origin `suggestion`, `suggestion_id` set; an
        identical version already stored is reused and `note` says so) and runs it in shadow next
        to the enforced version. Nothing is enforced: promote it later with
        `POST /v1/policies/{id}/versions/{v}/activate` and `mode: enforce`. Seals
        `policy.activated` (shadow) and `suggestion.accepted` (`deeds`, in that order). `reason` is
        optional (up to 280 characters). An org runs at most one version in shadow: 409
        `shadow_slot_taken` unless `replace_shadow` is true; 409 `suggestion_stale` when the
        enforced version changed since (the suggestion is superseded); 409 `suggestion_decided`
        when it was already decided; 409 `conflict` for advice. An admin+ Console session or an
        org admin key; viewers get 403 `insufficient_role`, agent keys 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The suggestion id (`sug_…`)., schema: {type: string}}
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                reason: {type: string, maxLength: 280, description: Optional. It goes into both deeds.}
                replace_shadow: {type: boolean, default: false, description: Replace the version running in shadow now.}
            example: {reason: Ops approves these every time, replace_shadow: true}
      responses:
        "200":
          description: The suggestion, accepted, with the version it created and the deeds.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Suggestion"
        "409":
          description: '`shadow_slot_taken`, `suggestion_stale`, `suggestion_decided`, or `conflict` for advice.'
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: shadow_slot_taken
                  message: "acme-sim v2 is already running in shadow. Promote or clear it first, or accept with replace_shadow: true to replace it."
                  param: replace_shadow
                  request_id: req_01m435q4ne4yj7meyd2bdz8kdm
                  doc_url: https://docs.deedglass.com/errors/shadow_slot_taken
        default:
          $ref: "#/components/responses/Error"
  /v1/suggestions/{id}/reject:
    post:
      operationId: rejectSuggestion
      summary: Reject a suggestion
      description: >-
        Closes an open suggestion and seals `suggestion.rejected` with the reason. `reason` is
        required, 3 to 280 characters (400 `validation_failed` on `reason`). Deedglass doesn't
        suggest the same change again for 30 days unless its main count doubles. 409
        `suggestion_decided` when it was already decided. An admin+ Console session or an org
        admin key.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The suggestion id (`sug_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [reason]
              properties:
                reason: {type: string, minLength: 3, maxLength: 280, description: One line. It goes into the deed.}
            example: {reason: We review inventory writes by hand}
      responses:
        "200":
          description: The suggestion, rejected, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Suggestion"
        "400":
          description: No reason, or one outside 3 to 280 characters.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/stats:
    get:
      operationId: getInsightsStats
      summary: Get decision and verdict stats
      description: >-
        Counts and percentiles over this org's actions by decision time (S27), the same numbers
        `deedglass insights stats` prints: decisions, approvals (requested, outcomes, the approval
        rate `approved / (approved + denied)` and the wait), the deny rate (Stop decisions left
        out), verdicts and witnessed levels, latency, cost per currency, each budget's daily use,
        and one row per org-local day. Rates are rounded to 4 decimals and always come with their
        counts. The window is `window=7d` (the default) or `30d` ending now, or `from` and `to` in
        RFC 3339 (both; not with `window`; `from` before `to`, at most 90 days, else 400
        `replay_window_invalid`). `by` groups by `rule` (the deciding rule, with the policy
        versions that used it), `tool` or `agent` (default `none`: only `totals`). `agent` (an
        `agt_` id) and `tool` (a name or a glob like `stripe.*`) filter. `compare=previous` adds
        `trend` against the window of the same length just before: `delta_pct` for counts and
        `delta_pp` (percentage points) for rates, and only `previous: null` when that window has
        fewer than 20 actions. Days follow the org's time zone. `empty` is a sentence when the
        window has no actions. Answers are cached for 60 s per org and query (`Cache-Control:
        private, no-store`). Any other parameter, or one sent twice, is 400 `validation_failed`. A
        Console session (viewer and up) or an org admin key (`admin` scope); agent keys get 403
        `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: window, in: query, required: false, description: '`7d` (default) or `30d`, ending now. Not with `from` and `to`.', schema: {type: string, enum: [7d, 30d], default: 7d}}
        - {name: from, in: query, required: false, description: The window's start (RFC 3339). Send `to` with it., schema: {type: string, format: date-time}, example: "2026-10-01T00:00:00Z"}
        - {name: to, in: query, required: false, description: The window's end (RFC 3339). Send `from` with it., schema: {type: string, format: date-time}, example: "2026-10-04T00:00:00Z"}
        - {name: by, in: query, required: false, schema: {type: string, enum: [rule, tool, agent, none], default: none}}
        - {name: agent, in: query, required: false, description: Only this agent's actions (`agt_…`)., schema: {type: string, maxLength: 64}}
        - {name: tool, in: query, required: false, description: 'A tool name, or a glob with `*` like `stripe.*`.', schema: {type: string, maxLength: 200}}
        - {name: compare, in: query, required: false, description: '`previous` adds `trend` against the window just before.', schema: {type: string, enum: [previous]}}
      responses:
        "200":
          description: The stats.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Cache-Control:
              schema: {type: string, const: "private, no-store"}
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsightsStats"
              examples:
                by_agent:
                  summary: Three days by agent, compared with the three days before (two groups and two days shown)
                  value:
                    {"object": "insights_stats", "window": {"from": "2026-10-01T00:00:00Z", "to": "2026-10-04T00:00:00Z", "days": 3},
                     "timezone": "Asia/Kolkata", "by": "agent", "filters": {"agent": null, "tool": null},
                     "totals": {"actions": 2554, "decisions": {"allow": 2373, "deny": 9, "require_approval": 172},
                                "approval": {"requested": 172, "approved": 170, "denied": 1, "expired": 1, "cancelled": 0, "pending": 0,
                                             "rate": 0.9942, "median_seconds": 1129, "p95_seconds": 6510},
                                "deny_rate": 0.0035, "verdicts": {"VERIFIED": 2508, "MISMATCH": 3, "UNVERIFIABLE": 13, "none": 30},
                                "levels": {"L0": 2037, "L1": 330, "L2": 157}, "executed": 2543, "mismatch_rate": 0.0012, "unverifiable_rate": 0.0051,
                                "latency_ms": {"p50": 58, "p95": 448}, "cost": [{"currency": "usd", "total": 718956, "p50": 2175, "p95": 7657.85}],
                                "trend": {"actions": {"delta_pct": 1.753, "previous": 2510}, "approval_rate": {"delta_pp": -0.58, "previous": 1},
                                          "deny_rate": {"delta_pp": 0.07, "previous": 0.0028}, "mismatch_rate": {"delta_pp": 0.12, "previous": 0},
                                          "unverifiable_rate": {"delta_pp": -0.29, "previous": 0.008}}},
                     "groups": [
                       {"key": {"agent": "agt_01m42v9q47mmvp6gwzj71ba643", "name": "nightly-etl"}, "actions": 1194,
                        "decisions": {"allow": 1194, "deny": 0, "require_approval": 0},
                        "approval": {"requested": 0, "approved": 0, "denied": 0, "expired": 0, "cancelled": 0, "pending": 0, "rate": null, "median_seconds": null, "p95_seconds": null},
                        "deny_rate": 0, "verdicts": {"VERIFIED": 1194, "MISMATCH": 0, "UNVERIFIABLE": 0, "none": 0}, "levels": {"L0": 1194, "L1": 0, "L2": 0},
                        "executed": 1194, "mismatch_rate": 0, "unverifiable_rate": 0, "latency_ms": {"p50": 41, "p95": 92}, "cost": [],
                        "trend": {"actions": {"delta_pct": 1.8771, "previous": 1172}, "deny_rate": {"delta_pp": 0, "previous": 0},
                                  "mismatch_rate": {"delta_pp": 0, "previous": 0}, "unverifiable_rate": {"delta_pp": 0, "previous": 0}}},
                       {"key": {"agent": "agt_01m42v9q3v1av7wvzjr1kq3mx1", "name": "support-bot"}, "actions": 737,
                        "decisions": {"allow": 730, "deny": 7, "require_approval": 0},
                        "approval": {"requested": 0, "approved": 0, "denied": 0, "expired": 0, "cancelled": 0, "pending": 0, "rate": null, "median_seconds": null, "p95_seconds": null},
                        "deny_rate": 0.0095, "verdicts": {"VERIFIED": 698, "MISMATCH": 0, "UNVERIFIABLE": 13, "none": 26}, "levels": {"L0": 227, "L1": 330, "L2": 154},
                        "executed": 730, "mismatch_rate": 0, "unverifiable_rate": 0.0178, "latency_ms": {"p50": 232, "p95": 622},
                        "cost": [{"currency": "usd", "total": 718956, "p50": 2175, "p95": 7657.85}],
                        "trend": {"actions": {"delta_pct": -7.99, "previous": 801}, "deny_rate": {"delta_pp": 0.08, "previous": 0.0087},
                                  "mismatch_rate": {"delta_pp": 0, "previous": 0}, "unverifiable_rate": {"delta_pp": -0.74, "previous": 0.0252}}}],
                     "budgets": [
                       {"agent": null, "budget": "refunds-per-day", "kind": "policy", "limit": {"amount": 400000, "currency": "usd"},
                        "max_utilisation": 0.95, "days_over_80": 1,
                        "days": [{"date": "2026-10-01", "used": 219559, "utilisation": 0.5489}, {"date": "2026-10-02", "used": 380000, "utilisation": 0.95}]}],
                     "series": [{"date": "2026-10-01", "actions": 526, "denied": 5, "verified": 510, "mismatch": 0, "unverifiable": 4},
                                {"date": "2026-10-02", "actions": 933, "denied": 2, "verified": 912, "mismatch": 3, "unverifiable": 9}],
                     "generated_at": "2026-10-04T07:05:11.547012Z", "empty": null}
                by_rule:
                  summary: The last 7 days by rule (one group shown, no compare)
                  value:
                    {"object": "insights_stats", "window": {"from": "2026-09-27T07:03:20.897006Z", "to": "2026-10-04T07:03:20.897006Z", "days": 7},
                     "timezone": "Asia/Kolkata", "by": "rule", "filters": {"agent": null, "tool": null},
                     "totals": {"actions": 5868, "decisions": {"allow": 5448, "deny": 17, "require_approval": 403},
                                "approval": {"requested": 403, "approved": 389, "denied": 1, "expired": 5, "cancelled": 0, "pending": 8,
                                             "rate": 0.9974, "median_seconds": 1118, "p95_seconds": 6508},
                                "deny_rate": 0.0029, "verdicts": {"VERIFIED": 5741, "MISMATCH": 3, "UNVERIFIABLE": 41, "none": 83},
                                "levels": {"L0": 4633, "L1": 800, "L2": 352}, "executed": 5837, "mismatch_rate": 0.0005, "unverifiable_rate": 0.007,
                                "latency_ms": {"p50": 55, "p95": 465}, "cost": [{"currency": "usd", "total": 1774668, "p50": 2206, "p95": 7839}]},
                     "groups": [
                       {"key": {"rule_id": "approve-order-updates"}, "versions": [1], "actions": 403,
                        "decisions": {"allow": 0, "deny": 0, "require_approval": 403},
                        "approval": {"requested": 403, "approved": 389, "denied": 1, "expired": 5, "cancelled": 0, "pending": 8,
                                     "rate": 0.9974, "median_seconds": 1118, "p95_seconds": 6508},
                        "deny_rate": 0, "verdicts": {"VERIFIED": 389, "MISMATCH": 0, "UNVERIFIABLE": 0, "none": 14}, "levels": {"L0": 389, "L1": 0, "L2": 0},
                        "executed": 389, "mismatch_rate": 0, "unverifiable_rate": 0, "latency_ms": {"p50": 39, "p95": 88}, "cost": []}],
                     "budgets": [], "series": [], "generated_at": "2026-10-04T07:03:20.897011Z", "empty": null}
        "400":
          description: A bad parameter (`validation_failed`), or a window that is backwards or longer than 90 days (`replay_window_invalid`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                window:
                  summary: An unknown window
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: window must be 7d or 30d, or send from and to (RFC 3339).
                      param: window
                      request_id: req_01m42vk20pnfaem7cw2nt7amza
                      doc_url: https://docs.deedglass.com/errors/validation_failed
                backwards:
                  summary: from after to
                  value:
                    error:
                      type: invalid_request_error
                      code: replay_window_invalid
                      message: The window must start before it ends, and it can be at most 90 days long. Send from before to.
                      param: from
                      request_id: req_01m42vk215p3bfyp3xasewttq4
                      doc_url: https://docs.deedglass.com/errors/replay_window_invalid
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/shadow:
    get:
      operationId: getShadowReport
      summary: Get the shadow disagreement report
      description: >-
        Where the shadow policy and the enforced one disagreed on live traffic (S27): the actions
        the shadow version evaluated, how many it agreed on (`agreement = agree / evaluated`, 4
        decimals), and every disagreement as a transition from the enforced decision to the shadow
        one, by rule (the shadow's deciding rule), tool and agent, with the first 20 action ids
        and, for actions the enforced policy sent to approval, what people did with them.
        `enforce` lists the enforced versions that decided those actions. `sentence` is the one
        line the Console and `deedglass insights shadow` print. Rows are chosen by the shadow
        version recorded on each action, never by activation time. `shadow_version` (a `pv_` id)
        reads a past shadow version; the default is the live one, and with none live the answer
        is 404 `not_found`. `from` and `to` (RFC 3339, each optional) narrow the window; `from`
        not before `to` is 400 `replay_window_invalid`. A Console session (viewer and up) or an
        org admin key (`admin` scope); agent keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: shadow_version, in: query, required: false, description: A shadow version (`pv_…`). Default the live one., schema: {type: string}, example: pv_01m42va0ryp515cvtem633vqng}
        - {name: from, in: query, required: false, description: Actions decided at or after this time (RFC 3339)., schema: {type: string, format: date-time}}
        - {name: to, in: query, required: false, description: Actions decided before this time (RFC 3339)., schema: {type: string, format: date-time}}
      responses:
        "200":
          description: The report.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShadowReport"
              examples:
                live:
                  summary: acme-sim v2 in shadow next to v1 (three samples shown per transition)
                  value:
                    {"object": "shadow_report",
                     "shadow": {"policy": "pol_01m42v9q4bjwfqmk8bd58vzkdh", "name": "acme-sim", "version": 2, "version_id": "pv_01m42va0ryp515cvtem633vqng",
                                "live": true, "since": "2026-10-04T06:58:31.330501Z"},
                     "enforce": [{"name": "acme-sim", "version": 1, "version_id": "pv_01m42v9q4bjwfqmk8bd58vzkdj", "actions": 4179}],
                     "window": {"from": null, "to": null},
                     "summary": {"evaluated": 4179, "agree": 4004, "disagree": 175, "agreement": 0.9581},
                     "transitions": [
                       {"enforce": "allow", "shadow": "require_approval", "count": 158,
                        "by_rule": [{"rule_id": "refund-approval-over-30", "count": 158}],
                        "by_tool": [{"tool": "stripe.create_refund", "count": 158}],
                        "by_agent": [{"agent": "agt_01m42v9q3v1av7wvzjr1kq3mx1", "name": "support-bot", "count": 158}],
                        "samples": ["act_01m42va0xa6fjkr9xbwsst1fzw", "act_01m42va1567ecpzdb2g1td5n56", "act_01m42va1bf0w9q777sp2eqh2mq"]},
                       {"enforce": "allow", "shadow": "deny", "count": 17,
                        "by_rule": [{"rule_id": "slack-allowlist", "count": 17}],
                        "by_tool": [{"tool": "slack.post_message", "count": 17}],
                        "by_agent": [{"agent": "agt_01m42v9q4500m4rfchvb8j53da", "name": "growth-agent", "count": 17}],
                        "samples": ["act_01m42va25mvsydk6j93ee0wc9p", "act_01m42va2ered5fbrvrzkqzbqpq", "act_01m42va3gdnqdf3acgkxan6ebb"]}],
                     "sentence": "Shadow acme-sim v2 would have sent 158 actions to approval and denied 17 that v1 allowed (refund-approval-over-30: 158, slack-allowlist: 17)."}
        "404":
          description: No shadow policy is active (or no such version).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: not_found
                  message: "No shadow policy is active in this org. Run one: deedglass policy activate <name> --version <n> --mode shadow --reason \"…\""
                  param: null
                  request_id: req_01m42vmb2c4e6g8j0m2p4r6t8v
                  doc_url: https://docs.deedglass.com/errors/not_found
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/anomalies:
    get:
      operationId: listAnomalies
      summary: List anomalies
      description: >-
        Anomaly episodes, newest window first (S27). An episode is the unresolved series of
        findings for one agent, kind and tool; anomalies are signals for a person to review, not
        verdicts. `status` is `open` and `acknowledged` by default; `resolved` or `all` for the
        rest. Filters: `agent` (an `agt_` id), `kind` and `since` (episodes whose window ends at or
        after it), with `limit` (1 to 100, default 25) and `cursor`. The list object adds what the
        inbox says about the org: `active_now` (episodes active now), `anomaly_facts` (whether this
        org's policies see `anomaly.*`), `notice` (the sentence when a policy reads anomaly facts
        but they are off) and `baseline_building` (agents with under 72 hours of history, for which
        only `new_tool` can fire). Any other parameter, or one sent twice, is 400
        `validation_failed`. A Console session (viewer and up) or an org admin key (`admin`
        scope); agent keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: status, in: query, required: false, description: Default open and acknowledged., schema: {type: string, enum: [open, acknowledged, resolved, all]}}
        - {name: agent, in: query, required: false, description: Only this agent's episodes (`agt_…`)., schema: {type: string}}
        - {name: kind, in: query, required: false, schema: {$ref: "#/components/schemas/AnomalyKind"}}
        - {name: since, in: query, required: false, description: Episodes whose window ends at or after this time (RFC 3339)., schema: {type: string, format: date-time}}
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of anomalies, with the inbox's header facts.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnomalyList"
              examples:
                inbox:
                  summary: Two open episodes (evidence trimmed to three action ids)
                  value:
                    {"object": "list", "has_more": false, "next_cursor": null, "active_now": 0, "anomaly_facts": false, "notice": null, "baseline_building": [],
                     "data": [
                       {"object": "anomaly", "id": "anm_01m42vb27wzpyw0b2n6aycjxkh", "agent": {"id": "agt_01m42v9q4500m4rfchvb8j53da", "name": "growth-agent"},
                        "kind": "volume_spike", "tool": null, "severity": "high", "score": 22.8,
                        "title": "85 calls, 14:00 to 15:00 IST · 34.8× its usual 2.4", "status": "open", "active": false,
                        "active_until": "2026-10-02T10:30:00Z", "window": {"start": "2026-10-02T08:30:00Z", "end": "2026-10-02T09:30:00Z"}, "occurrences": 1,
                        "evidence": {"hour": "14:00", "count": 85, "factor": 34.8, "writes": 85, "timezone": "IST",
                                     "points": [{"count": 85, "score": 22.8, "window_start": "2026-10-02T08:30:00.000Z", "window_end": "2026-10-02T09:30:00.000Z"}],
                                     "action_ids": ["act_01m42vak3h51dq7hv3vvv3dgv2", "act_01m42vak4fsmgfq9m2nx3cezp5", "act_01m42vak4n1bnq8sj24k4r93yk"],
                                     "window_start": "2026-10-02T08:30:00Z", "window_end": "2026-10-02T09:30:00Z",
                                     "baseline_mean": 2.44, "baseline_stddev": 3.62, "history_hours": 122},
                        "detector_version": "1", "acknowledged": null, "resolved": null,
                        "created_at": "2026-10-04T06:59:05.192752Z", "updated_at": "2026-10-04T06:59:05.192752Z"},
                       {"object": "anomaly", "id": "anm_01m42vb27n2m625kwsjpjaqam7", "agent": {"id": "agt_01m42v9q429q2mprzr6xakrx42", "name": "ops-agent"},
                        "kind": "mismatch_spike", "tool": null, "severity": "high", "score": 3,
                        "title": "3 MISMATCH verdicts, 11:00 to 12:00 IST · usually 0", "status": "open", "active": false,
                        "active_until": "2026-10-02T07:30:00Z", "window": {"start": "2026-10-02T05:30:00Z", "end": "2026-10-02T06:30:00Z"}, "occurrences": 1,
                        "evidence": {"hour": "11:00", "count": 9, "mismatches": 3, "timezone": "IST",
                                     "points": [{"count": 9, "score": 3, "window_start": "2026-10-02T05:30:00.000Z", "window_end": "2026-10-02T06:30:00.000Z"}],
                                     "action_ids": ["act_01m42vahkp6pr23rajskdegyx8", "act_01m42vahp95d8q5r4kvq25w3mz", "act_01m42vahtbb45z53wt0cjr7vrq"],
                                     "window_start": "2026-10-02T05:30:00Z", "window_end": "2026-10-02T06:30:00Z",
                                     "baseline_mean": 0, "baseline_stddev": 1, "history_hours": 119},
                        "detector_version": "1", "acknowledged": null, "resolved": null,
                        "created_at": "2026-10-04T06:59:05.192752Z", "updated_at": "2026-10-04T06:59:05.192752Z"}]}
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/anomalies/{id}:
    get:
      operationId: getAnomaly
      summary: Retrieve an anomaly
      description: >-
        One anomaly episode (S27) with its evidence: the hourly points (at most 48), the baseline
        it was compared with, and up to 10 action ids. `active` is what `anomaly.active` sees now
        (it ends `anomaly_active_minutes` after the last finding); `status` is the review inbox and
        only a person moves it, except that an episode with no new occurrence for 14 days resolves
        itself (`resolved.by` is `system`, no deed). An id that isn't one of this org's anomalies
        is 404 `not_found`. A Console session (viewer and up) or an org admin key (`admin` scope);
        agent keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - {name: id, in: path, required: true, description: The anomaly id (`anm_…`)., schema: {type: string}}
      responses:
        "200":
          description: The anomaly.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Anomaly"
              examples:
                new_tool:
                  summary: ops-agent's first github.merge_pr
                  value:
                    {"object": "anomaly", "id": "anm_01m42vb270xzbzmgbgyantw49w", "agent": {"id": "agt_01m42v9q429q2mprzr6xakrx42", "name": "ops-agent"},
                     "kind": "new_tool", "tool": "github.merge_pr", "severity": "warn", "score": 2,
                     "title": "first github.merge_pr (write), 15:00 to 16:00 IST", "status": "open", "active": false,
                     "active_until": "2026-10-01T11:30:00Z", "window": {"start": "2026-10-01T09:30:00Z", "end": "2026-10-01T10:30:00Z"}, "occurrences": 1,
                     "evidence": {"hour": "15:00", "tool": "github.merge_pr", "side_effect": "write", "count": 2, "timezone": "IST",
                                  "points": [{"count": 2, "score": 2, "window_start": "2026-10-01T09:30:00.000Z", "window_end": "2026-10-01T10:30:00.000Z"}],
                                  "action_ids": ["act_01m42vadezkscmydd9pw910e3m", "act_01m42vadr0f89h5svfvms0pj10"],
                                  "window_start": "2026-10-01T09:30:00Z", "window_end": "2026-10-01T10:30:00Z", "history_hours": 99},
                     "detector_version": "1", "acknowledged": null, "resolved": null,
                     "created_at": "2026-10-04T06:59:05.192752Z", "updated_at": "2026-10-04T06:59:05.192752Z"}
        "404":
          description: No such anomaly in this org.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: not_found
                  message: "No anomaly anm_01m42vb27wzpyw0b2n6aycjxkx in this org. List them: GET /v1/insights/anomalies."
                  param: null
                  request_id: req_01m42vkfgev8q669w3tnnvbf4p
                  doc_url: https://docs.deedglass.com/errors/not_found
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/anomalies/{id}/acknowledge:
    post:
      operationId: acknowledgeAnomaly
      summary: Acknowledge an anomaly
      description: >-
        Records who saw the episode (S27): `status` becomes `acknowledged` with `acknowledged.by`
        and `.at`. It changes nothing else: an active episode stays active until it expires, and
        no deed is sealed. Acknowledging an acknowledged episode answers it unchanged; a resolved
        one is 409 `conflict`. No body. An admin+ Console session (with the `Deedglass-CSRF`
        header) or an org admin key (`admin` scope); viewers get 403 `insufficient_role` and agent
        keys 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The anomaly id (`anm_…`)., schema: {type: string}}
      responses:
        "200":
          description: The anomaly, acknowledged.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Anomaly"
              examples:
                acknowledged:
                  summary: Acknowledged by an admin
                  value:
                    {"object": "anomaly", "id": "anm_01m42vb285wmf0ay82683pp9m4", "agent": {"id": "agt_01m42v9q4500m4rfchvb8j53da", "name": "growth-agent"},
                     "kind": "off_hours", "tool": null, "severity": "warn", "score": 12,
                     "title": "12 calls, 02:00 to 03:00 IST · usually 0.2% of its calls at this hour", "status": "acknowledged", "active": false,
                     "active_until": "2026-10-02T22:30:00Z", "window": {"start": "2026-10-02T20:30:00Z", "end": "2026-10-02T21:30:00Z"}, "occurrences": 1,
                     "evidence": {"hour": "02:00", "count": 12, "writes": 12, "share": 0.0024, "timezone": "IST",
                                  "points": [{"count": 12, "score": 12, "window_start": "2026-10-02T20:30:00.000Z", "window_end": "2026-10-02T21:30:00.000Z"}],
                                  "action_ids": ["act_01m42vap394qwexdpmpazzgq31", "act_01m42vap52y8tmnrpbxak5gm5n", "act_01m42vap5e8em83fqr9x9j7eeg"],
                                  "window_start": "2026-10-02T20:30:00Z", "window_end": "2026-10-02T21:30:00Z", "history_hours": 134},
                     "detector_version": "1", "acknowledged": {"by": "usr_01m42v9p7c3e5g7j9m1q3s5v7x", "at": "2026-10-04T07:20:44.912310Z"}, "resolved": null,
                     "created_at": "2026-10-04T06:59:05.192752Z", "updated_at": "2026-10-04T07:20:44.912310Z"}
        "409":
          description: The anomaly is resolved.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: conflict
                  message: This anomaly is already resolved. Resolved anomalies stay in the history; a new finding opens a new one.
                  param: null
                  request_id: req_01m42vmd4e6g8j0m2p4r6t8v0x
                  doc_url: https://docs.deedglass.com/errors/conflict
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/anomalies/{id}/resolve:
    post:
      operationId: resolveAnomaly
      summary: Resolve an anomaly
      description: >-
        Closes the episode (S27): `status` becomes `resolved`, any activity ends now (so
        `anomaly.active` turns false for it), and an `anomaly.resolved` admin deed is sealed with
        the reason (`resolved.deed`). `reason` is required, 3 to 280 characters with no control
        characters (400 `validation_failed` on `reason`). A resolved episode stays in the history;
        a later finding opens a new one. Resolving it again is 409 `conflict`. An admin+ Console
        session (with the `Deedglass-CSRF` header) or an org admin key (`admin` scope); viewers get
        403 `insufficient_role` and agent keys 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
        - $ref: "#/components/parameters/IdempotencyKey"
        - {name: id, in: path, required: true, description: The anomaly id (`anm_…`)., schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [reason]
              properties:
                reason: {type: string, minLength: 3, maxLength: 280, description: One line, 3 to 280 characters. It goes into the deed.}
            example: {reason: The merge was the planned release; ops-agent now owns releases.}
      responses:
        "200":
          description: The anomaly, resolved, with the deed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
            Deedglass-Idempotent-Replayed:
              $ref: "#/components/headers/DeedglassIdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Anomaly"
              examples:
                resolved:
                  summary: Resolved with a reason
                  value:
                    {"object": "anomaly", "id": "anm_01m42vb270xzbzmgbgyantw49w", "agent": {"id": "agt_01m42v9q429q2mprzr6xakrx42", "name": "ops-agent"},
                     "kind": "new_tool", "tool": "github.merge_pr", "severity": "warn", "score": 2,
                     "title": "first github.merge_pr (write), 15:00 to 16:00 IST", "status": "resolved", "active": false,
                     "active_until": "2026-10-01T11:30:00Z", "window": {"start": "2026-10-01T09:30:00Z", "end": "2026-10-01T10:30:00Z"}, "occurrences": 1,
                     "evidence": {"hour": "15:00", "tool": "github.merge_pr", "side_effect": "write", "count": 2, "timezone": "IST",
                                  "points": [{"count": 2, "score": 2, "window_start": "2026-10-01T09:30:00.000Z", "window_end": "2026-10-01T10:30:00.000Z"}],
                                  "action_ids": ["act_01m42vadezkscmydd9pw910e3m", "act_01m42vadr0f89h5svfvms0pj10"],
                                  "window_start": "2026-10-01T09:30:00Z", "window_end": "2026-10-01T10:30:00Z", "history_hours": 99},
                     "detector_version": "1", "acknowledged": null,
                     "resolved": {"by": "usr_01m42v9p7c3e5g7j9m1q3s5v7x", "at": "2026-10-04T07:22:10.204118Z",
                                  "reason": "The merge was the planned release; ops-agent now owns releases.", "deed": "deed_01m42vmf6g8j0m2p4r6t8v0x2z"},
                     "created_at": "2026-10-04T06:59:05.192752Z", "updated_at": "2026-10-04T07:22:10.204118Z"}
        "400":
          description: No reason, or one outside 3 to 280 characters.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: validation_failed
                  message: A reason is required to resolve an anomaly. It goes into the sealed record.
                  param: reason
                  request_id: req_01m42vmg7h9k1n3q5s7v9x1z3b
                  doc_url: https://docs.deedglass.com/errors/validation_failed
        "403":
          description: A viewer session (`insufficient_role`), or an agent key (`insufficient_scope`).
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: permission_error
                  code: insufficient_role
                  message: Your role in Acme is Viewer, and resolving anomalies needs Admin. Ask an owner to change your role.
                  param: null
                  request_id: req_01m42vmh8j0m2p4r6t8v0x2z4c
                  doc_url: https://docs.deedglass.com/errors/insufficient_role
        "409":
          description: The anomaly is already resolved.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: conflict
                  message: This anomaly is already resolved. Resolved anomalies stay in the history; a new finding opens a new one.
                  param: null
                  request_id: req_01m42vmj9k1n3q5s7v9x1z3b5d
                  doc_url: https://docs.deedglass.com/errors/conflict
        default:
          $ref: "#/components/responses/Error"
  /v1/insights/settings:
    get:
      operationId: getInsightsSettings
      summary: Get Insights settings
      description: >-
        This org's Insights settings (S27): whether policies see anomaly facts (`anomaly_facts`,
        off by default: then every policy sees `anomaly.active` false), how long an episode stays
        active after its last finding (`anomaly_active_minutes`, 60), and how old the last
        detection pass may be before, with the facts on, every agent counts as anomalous
        (`anomaly_stale_after_minutes`, 15). `active_policy_reads_anomaly` says whether the
        enforced or shadow policy reads `anomaly.*`; `notice` is the sentence when it does but the
        facts are off. A Console session (viewer and up) or an org admin key (`admin` scope); agent
        keys get 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
      responses:
        "200":
          description: The settings.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsightsSettings"
              examples:
                defaults:
                  summary: The defaults
                  value: {"object": "insights_settings", "anomaly_facts": false, "anomaly_active_minutes": 60, "anomaly_stale_after_minutes": 15, "active_policy_reads_anomaly": false, "notice": null}
                notice:
                  summary: A policy reads anomaly facts, but they are off
                  value: {"object": "insights_settings", "anomaly_facts": false, "anomaly_active_minutes": 60, "anomaly_stale_after_minutes": 15, "active_policy_reads_anomaly": true,
                          "notice": "acme-sim v3 reads anomaly.active, but anomaly facts are off for this org, so it always sees false. Turn them on in Console › Insights › Settings."}
        default:
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateInsightsSettings
      summary: Change Insights settings
      description: >-
        Changes the fields you send and seals an `insights.settings_changed` admin deed with the
        reason, because the settings change what policies see (`deed` in the answer; absent when
        nothing changed, and then nothing is sealed).
        `reason` is required, 3 to 280 characters (400 `validation_failed` on `reason`);
        `anomaly_active_minutes` is 5 to 1,440 and `anomaly_stale_after_minutes` 10 to 240 (400
        `validation_failed` on the field). Unknown fields are 400. Bodies up to 16 KiB. An admin+
        Console session (with the `Deedglass-CSRF` header) or an org admin key (`admin` scope);
        viewers get 403 `insufficient_role` and agent keys 403 `insufficient_scope`.
      tags: [Insights]
      security: [{bearerAuth: []}, {sessionCookie: []}]
      x-deedglass-auth: session_or_key
      x-deedglass-since: "2026-10-01"
      parameters:
        - $ref: "#/components/parameters/DeedglassVersion"
        - $ref: "#/components/parameters/CSRFWithSession"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InsightsSettingsPatch"
            example: {anomaly_facts: true, reason: Let refund-approval-over-30 send anomalous agents to approval.}
      responses:
        "200":
          description: The settings after the change, with the deed when something changed.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
            Deedglass-Version:
              $ref: "#/components/headers/DeedglassVersion"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsightsSettings"
              examples:
                facts_on:
                  summary: Anomaly facts turned on
                  value: {"object": "insights_settings", "anomaly_facts": true, "anomaly_active_minutes": 60, "anomaly_stale_after_minutes": 15, "active_policy_reads_anomaly": true,
                          "notice": null, "deed": "deed_01m42vmk0m2p4r6t8v0x2z4c6e"}
        "400":
          description: A missing or bad field.
          headers:
            Deedglass-Request-Id:
              $ref: "#/components/headers/DeedglassRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              example:
                error:
                  type: invalid_request_error
                  code: validation_failed
                  message: anomaly_active_minutes must be 5 to 1,440.
                  param: anomaly_active_minutes
                  request_id: req_01m42vmm1p3r5t7w9y1a3c5e7g
                  doc_url: https://docs.deedglass.com/errors/validation_failed
        default:
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: dg_<mode>_<prefix>_<secret>
      description: >-
        An API key: `dg_live_` or `dg_test_`, an 8-character prefix, `_`, and a 32-character secret,
        all base62 (49 characters, like `dg_live_jJbEEMWk_o0Erxxf1kgtP2QjiJ79vzW31AjZqIch8`). Agent
        keys hold `actions:execute`, `actions:read` and `deeds:read` (the default) and may add
        `actions:undo`; org admin keys hold exactly `admin`. Create agent keys in Console › Agents
        and admin keys in Console › Settings. A 401 carries `WWW-Authenticate: Bearer
        realm="deedglass", error="invalid_token", error_description="…"`; a missing scope is 403
        `insufficient_scope` with `WWW-Authenticate: Bearer realm="deedglass",
        error="insufficient_scope", scope="…"`.
    sessionCookie:
      type: apiKey
      in: cookie
      name: __Host-deedglass_session
      description: >-
        A Console session. Requests that change state also send the `Deedglass-CSRF` header,
        matching the `__Host-deedglass_csrf` cookie.
  parameters:
    DeedglassVersion:
      name: Deedglass-Version
      in: header
      required: false
      description: >-
        The API version (YYYY-MM-DD). Leave it out to use your org's pinned version (public routes
        use the latest). An unknown version is 400 `api_version_invalid`.
      schema:
        type: string
        pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
        examples: ["2026-10-01"]
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        1 to 255 printable ASCII characters. A retry with the same key and body returns the stored
        response with `Deedglass-Idempotent-Replayed: true` for 24 hours.
      schema:
        type: string
        minLength: 1
        maxLength: 255
    Limit:
      name: limit
      in: query
      required: false
      description: Page size, 1–100 (default 25).
      schema:
        type: integer
        minimum: 1
        maximum: 100
    Cursor:
      name: cursor
      in: query
      required: false
      description: '`next_cursor` from the previous page, unchanged.'
      schema:
        type: string
    FromSeq:
      name: from_seq
      in: query
      required: false
      description: The first deed seq (inclusive).
      schema:
        type: integer
        minimum: 1
    ToSeq:
      name: to_seq
      in: query
      required: false
      description: The last deed seq (inclusive).
      schema:
        type: integer
        minimum: 1
    CSRF:
      name: Deedglass-CSRF
      in: header
      required: true
      description: >-
        The CSRF token from `GET /v1/me` (it equals the `__Host-deedglass_csrf` cookie). Every
        session request that changes state needs it; without it the answer is 403 `csrf_failed`.
      schema:
        type: string
    CSRFWithSession:
      name: Deedglass-CSRF
      in: header
      required: false
      description: >-
        Needed when the request uses a Console session (it changes state); without it the answer is
        403 `csrf_failed`. Not sent with an API key.
      schema:
        type: string
    AgentId:
      name: id
      in: path
      required: true
      description: The agent id (`agt_…`).
      schema:
        type: string
    ConnectorRef:
      name: id
      in: path
      required: true
      description: The connector's id (`con_…`) or its name.
      schema:
        type: string
    KeyId:
      name: id
      in: path
      required: true
      description: The API key id (`key_…`).
      schema:
        type: string
    IntentKid:
      name: kid
      in: path
      required: true
      description: The intent key's id, 32 lowercase hex characters.
      schema:
        type: string
        pattern: "^[0-9a-f]{32}$"
    PolicyRef:
      name: id
      in: path
      required: true
      description: A pol_ id or the policy's name.
      schema:
        type: string
    PolicyVersionRef:
      name: v
      in: path
      required: true
      description: A version number, like 3, or a pv_ id.
      schema:
        type: string
    DeedglassIntent:
      name: Deedglass-Intent
      in: header
      required: false
      description: >-
        A signed intent: base64url of the intent's JCS bytes, `.`, and base64url of the 64-byte
        Ed25519 signature. The intent follows the schema `deedglass.intent/1`; the signature input
        is `"deedglass/intent/v1\n"` followed by the `intent_hash`. Clocks may differ by up to 60 s
        either way, and an intent lives at most 5 minutes. See docs/INTENTS.md.
      schema:
        type: string
        maxLength: 4096
    DeedglassOnBehalfOf:
      name: Deedglass-On-Behalf-Of
      in: header
      required: false
      description: >-
        The end user the agent acts for, in your own ids. At most once; a malformed or repeated
        header is 422 `validation_failed`.
      schema:
        type: string
        pattern: "^[A-Za-z0-9._~:@/+=-]{1,256}$"
    DeedglassSession:
      name: Deedglass-Session
      in: header
      required: false
      description: >-
        The conversation or session the call belongs to, in your own ids. At most once; a malformed
        or repeated header is 422 `validation_failed`.
      schema:
        type: string
        pattern: "^[A-Za-z0-9._~:@/+=-]{1,256}$"
  headers:
    DeedglassRequestId:
      description: This request's id. Quote it to support; it matches `error.request_id`.
      schema:
        type: string
        pattern: "^req_[0-7][0-9a-hjkmnp-tv-z]{25}$"
    DeedglassVersion:
      description: The API version this response follows.
      schema:
        type: string
    DeedglassIdempotentReplayed:
      description: Present, and `true`, when the response is a stored one replayed for an Idempotency-Key.
      schema:
        type: string
        enum: ["true"]
  responses:
    Error:
      description: An error. Every error has this envelope; `doc_url` explains the code.
      headers:
        Deedglass-Request-Id:
          $ref: "#/components/headers/DeedglassRequestId"
        Deedglass-Version:
          $ref: "#/components/headers/DeedglassVersion"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    MCPOriginRefused:
      description: >-
        A browser `Origin` other than the server's own: a JSON-RPC error without an id
        (DNS-rebinding protection). A key without `actions:execute`, or a stopped or suspended
        agent, also gets 403, with the error envelope.
      headers:
        Deedglass-Request-Id:
          $ref: "#/components/headers/DeedglassRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/JSONRPCError"
    MCPResponse:
      description: One JSON-RPC response (a result, or an error such as -32601 for a legacy unknown method).
      headers:
        Deedglass-Request-Id:
          $ref: "#/components/headers/DeedglassRequestId"
      content:
        application/json:
          schema:
            type: object
          example:
            jsonrpc: "2.0"
            id: 3
            result:
              resultType: complete
              content: [{type: text, text: "Echo: hello"}]
              isError: false
              _meta: {com.deedglass/action: act_01k6w4a3d5f7h9k1n3q5s7v9x1, com.deedglass/deed: deed_01k6w4a3d5f7h9k1n3q5s7v9x2}
    MCPRequestRefused:
      description: >-
        A header that doesn't match the body (-32020 HeaderMismatch), an unsupported protocol
        version (-32022 with data.supported and data.requested), a batch or a response (-32600), or
        JSON that can't be parsed (-32700).
      headers:
        Deedglass-Request-Id:
          $ref: "#/components/headers/DeedglassRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/JSONRPCError"
          example:
            jsonrpc: "2.0"
            id: 3
            error: {code: -32020, message: 'Header mismatch: Mcp-Name is "echo" but the body names "delete_all". Send the same tool name in both.'}
  requestBodies:
    MCPMessage:
      required: true
      description: A JSON-RPC 2.0 message.
      content:
        application/json:
          schema:
            type: object
          example: {jsonrpc: "2.0", id: 1, method: tools/list}
    OptionalReason:
      required: false
      description: An optional reason. It goes into the deed and the Console; an agent never sees it.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            properties:
              reason: {type: string, maxLength: 280, description: One line, at most 280 characters.}
          example: {reason: Refund loop in staging}
    RequiredReason:
      required: true
      description: Why it's safe now. It goes into the deed.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            required: [reason]
            properties:
              reason: {type: string, minLength: 3, maxLength: 280, description: One line, 3 to 280 characters.}
          example: {reason: "Root cause fixed: refund threshold was 100x"}
    PolicySource:
      required: true
      description: The policy YAML, and optional notes kept with the version.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: false
            required: [source]
            properties:
              source: {type: string, description: The policy YAML (deedglass policy/v1), at most 256 KiB.}
              notes: {type: string, maxLength: 2000, description: Why this version exists. Kept with the version.}
          example: {source: "deedglass: policy/v1\nname: postgres-safe\n…", notes: Allow-list writes to public.orders for the pilot.}
  schemas:
    Approval:
      type: object
      description: >-
        A person's countersign on one action (S22). The agent view leaves out `context`,
        `approvers` and `denied_by`, and `countersigned` is a count.
      required: [object, id, action, agent, tool, status, rule_id, reason, required, countersigned, decision_reason, summary, context_digest, created_at, expires_at, decided_at, deed, livemode]
      properties:
        object: {type: string, enum: [approval]}
        id: {type: string}
        action: {type: string}
        agent: {type: object, properties: {id: {type: string}, name: {type: string}}}
        tool: {type: string}
        status: {type: string, enum: [pending, approved, denied, expired, cancelled]}
        rule_id: {type: [string, "null"]}
        reason: {type: [string, "null"]}
        required: {type: integer}
        approvers: {type: array, items: {type: string}}
        countersigned:
          oneOf:
            - {type: integer}
            - type: array
              items:
                type: object
                properties:
                  user: {type: object, properties: {id: {type: string}, name: {type: string}}}
                  at: {type: string}
                  via: {type: string, enum: [console, email_link, slack, cli_dev]}
                  reason: {type: [string, "null"]}
                  deed: {type: string}
        denied_by: {type: object, properties: {id: {type: string}, name: {type: string}}}
        decision_reason: {type: [string, "null"]}
        summary: {type: string}
        context: {type: object, description: The context snapshot v1 the person countersigns (docs/APPROVALS.md).}
        context_digest: {type: [string, "null"]}
        you:
          type: object
          properties:
            eligible: {type: boolean}
            can_decide: {type: boolean}
            why_not: {type: [string, "null"]}
        created_at: {type: string}
        expires_at: {type: string}
        decided_at: {type: [string, "null"]}
        deed: {type: [string, "null"]}
        livemode: {type: boolean}
        action_status: {type: string, description: Decide answers only.}
    ApprovalList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, enum: [list]}
        data: {type: array, items: {$ref: "#/components/schemas/Approval"}}
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    ApprovalVote:
      type: object
      additionalProperties: false
      required: [context_digest]
      properties:
        reason: {type: string, maxLength: 280, description: Required to deny. The agent sees it.}
        context_digest: {type: string, description: The digest of the context the person reviewed.}
    ApprovalLinkToken:
      type: object
      required: [token]
      properties:
        token: {type: string, description: The 43-character token from the link's fragment.}
    ApprovalLinkDecision:
      type: object
      required: [token, vote, context_digest]
      properties:
        token: {type: string}
        vote: {type: string, enum: [approve, deny]}
        reason: {type: string, maxLength: 280}
        context_digest: {type: string}
    ApprovalLinkPreview:
      type: object
      properties:
        object: {type: string, enum: [approval_link_preview]}
        approval: {$ref: "#/components/schemas/Approval"}
        review_only: {type: boolean}
        org_name: {type: string}
        time_zone: {type: string}
        agent_stopped: {type: boolean}
        sql: {type: string}
        console_url: {type: string}
        expires_clock: {type: string}
        decided_clock: {type: string}
        countersign_clocks: {type: array, items: {type: string}}
    ApprovalLinkStatus:
      type: object
      properties:
        object: {type: string, enum: [approval_link_status]}
        approval: {$ref: "#/components/schemas/Approval"}
        action: {type: object}
    Webhook:
      type: object
      required: [object, id, url, events, status, api_version, created_at]
      properties:
        object: {type: string, enum: [webhook]}
        id: {type: string}
        url: {type: string}
        events: {type: array, items: {type: string}}
        description: {type: [string, "null"]}
        status: {type: string, enum: [active, disabled]}
        disabled_reason: {type: [string, "null"]}
        api_version: {type: string}
        secret_hint: {type: [string, "null"]}
        secret: {type: string, description: The signing secret (whsec_…). In the create response only.}
        failing_since: {type: [string, "null"]}
        last_success_at: {type: [string, "null"]}
        created_at: {type: string}
        deed: {type: string}
    WebhookList:
      type: object
      properties:
        object: {type: string, enum: [list]}
        data: {type: array, items: {$ref: "#/components/schemas/Webhook"}}
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    WebhookCreate:
      type: object
      required: [url, events]
      properties:
        url: {type: string, maxLength: 2048}
        events:
          type: array
          minItems: 1
          maxItems: 32
          description: Event types to receive (see Event.type), including `anomaly.detected` (S27).
          items: {type: string}
        description: {type: string, maxLength: 200}
    LoginRequest:
      type: object
      required: [object, email, expires_in, message]
      properties:
        object: {type: string, const: login_request}
        email: {type: string, description: The address, normalized.}
        expires_in: {type: integer, description: Seconds the link and code work for.}
        message: {type: string}
      examples:
        - object: login_request
          email: priya@acme.com
          expires_in: 900
          message: If priya@acme.com has a Deedglass account, a sign-in link and code are on their way.
    LoginPreview:
      type: object
      required: [object, purpose, email, org, role, invited_by, expires_at]
      properties:
        object: {type: string, const: login_preview}
        purpose: {type: string, enum: [login, invite]}
        email: {type: string}
        org:
          type: object
          required: [id, name]
          properties:
            id: {type: string}
            name: {type: string}
        role: {type: string, enum: [viewer, admin, owner]}
        invited_by:
          type: [object, "null"]
          required: [name, email]
          properties:
            name: {type: string}
            email: {type: string}
        expires_at: {type: string, format: date-time}
      examples:
        - object: login_preview
          purpose: login
          email: priya@acme.com
          org: {id: org_01k6h7kp6msmzrhdzwtcr3jnkp, name: Acme}
          role: owner
          invited_by: null
          expires_at: "2026-10-03T09:29:07.000Z"
    Me:
      type: object
      required: [object, user, org, membership, orgs, session, csrf_token]
      properties:
        object: {type: string, const: me}
        user:
          type: object
          required: [object, id, email, name, created_at]
          properties:
            object: {type: string, const: user}
            id: {type: string}
            email: {type: string}
            name: {type: string}
            created_at: {type: string, format: date-time}
        org:
          $ref: "#/components/schemas/Org"
        membership:
          type: object
          required: [object, id, role, status]
          properties:
            object: {type: string, const: membership}
            id: {type: string}
            role: {type: string, enum: [viewer, admin, owner]}
            status: {type: string}
        orgs:
          type: array
          items:
            type: object
            required: [id, name, role]
            properties:
              id: {type: string}
              name: {type: string}
              role: {type: string}
        session:
          type: object
          required: [id, idle_expires_at, expires_at]
          properties:
            id: {type: string}
            idle_expires_at: {type: string, format: date-time}
            expires_at: {type: string, format: date-time}
        csrf_token:
          type: [string, "null"]
          description: Send it back as the `Deedglass-CSRF` header on requests that change state.
      examples:
        - object: me
          user: {object: user, id: usr_01k6c9kb2zcjsyn7b8g2k1vqvp, email: priya@acme.com, name: Priya Raman, created_at: "2026-10-03T09:14:07.000Z"}
          org: {object: org, id: org_01k6h7kp6msmzrhdzwtcr3jnkp, name: Acme, slug: acme, timezone: Asia/Kolkata, status: active, created_at: "2026-10-03T09:14:07.000Z"}
          membership: {object: membership, id: mem_01k6zymkd83ftdv0mf9keseaz1, role: owner, status: active}
          orgs: [{id: org_01k6h7kp6msmzrhdzwtcr3jnkp, name: Acme, role: owner}]
          session: {id: ses_01k6ydnxs578tgcwjbknyjdh8m, idle_expires_at: "2026-10-03T21:14:07.000Z", expires_at: "2026-10-10T09:14:07.000Z"}
          csrf_token: M3q1c0Zb9xK2yV7wQ4nH8pT6rJ5sD1fG0aL3kE9uW2c
    Org:
      type: object
      required: [object, id, name, slug, timezone, status, created_at]
      properties:
        object: {type: string, const: org}
        id: {type: string}
        name: {type: string}
        slug: {type: string}
        timezone: {type: string}
        status: {type: string, enum: [active, disabled]}
        created_at: {type: string, format: date-time}
        retention_days:
          type: integer
          minimum: 1
          maximum: 3650
          description: >-
            GET and PATCH /v1/org only (S26, additive): how many days action payloads are kept, the
            org's setting or the default of 90. Omitted elsewhere.
        settings:
          type: object
          description: GET and PATCH /v1/org only (S26, additive), with defaults applied. Omitted elsewhere.
          required: [approvals]
          properties:
            approvals:
              type: object
              required: [email_links, email_detail, self_countersign]
              properties:
                email_links: {type: string, enum: [decide, review], description: Whether emailed approval links can decide (default) or only open the review page.}
                email_detail: {type: string, enum: [summary, full], description: How much of the request approval emails show (default summary).}
                self_countersign: {type: string, enum: [deny, allow], description: Whether the person an action runs on behalf of may countersign it (default deny).}
            simulated: {type: boolean, const: true, description: "Read only (S29, additive): present and true when the org was made by deedglass dev simulate; omitted otherwise. PATCH /v1/org refuses it."}
        stop:
          type: object
          description: >-
            GET /v1/org only (additive): the Stop all overlay, which the Console's org-stopped
            banner reads. Omitted elsewhere.
          required: [stopped, stopped_at, stopped_by, reason]
          properties:
            stopped: {type: boolean}
            stopped_at: {type: [string, "null"], format: date-time}
            stopped_by: {type: [string, "null"], description: Who turned it on.}
            reason: {type: [string, "null"]}
    Membership:
      type: object
      required: [object, id, org, user, role, status, invited_by, invited_at, joined_at, removed_at, created_at]
      properties:
        object: {type: string, const: membership}
        id: {type: string}
        org: {type: string}
        user:
          type: object
          required: [id, email, name]
          properties:
            id: {type: string}
            email: {type: string}
            name: {type: string}
        role: {type: string, enum: [viewer, admin, owner]}
        status: {type: string, enum: [invited, active, removed]}
        invited_by: {type: [string, "null"]}
        invited_at: {type: [string, "null"], format: date-time}
        joined_at: {type: [string, "null"], format: date-time}
        removed_at: {type: [string, "null"], format: date-time}
        created_at: {type: string, format: date-time}
        deed: {type: string, description: The admin deed this change sealed (on changes only).}
      examples:
        - object: membership
          id: mem_01k6f6evxjk5zr0t0kntw9ehx5
          org: org_01k6h7kp6msmzrhdzwtcr3jnkp
          user: {id: usr_01k6s2anqftp0685ngz82kyz6y, email: sam@acme.com, name: ""}
          role: viewer
          status: invited
          invited_by: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
          invited_at: "2026-10-03T09:20:11.000Z"
          joined_at: null
          removed_at: null
          created_at: "2026-10-03T09:20:11.000Z"
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    MembershipList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Membership"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    Labels:
      type: object
      maxProperties: 32
      description: >-
        At most 32. Keys match `^[a-z0-9][a-z0-9._/-]{0,62}$`; values are up to 128 printable
        characters. Policies match on labels, so every change seals a deed.
      propertyNames:
        pattern: "^[a-z0-9][a-z0-9._/-]{0,62}$"
      additionalProperties:
        type: string
        maxLength: 128
    Budgets:
      type: object
      additionalProperties: false
      description: >-
        An agent's limits; a limit left out means none. Money is in minor units (cents for usd,
        paise for inr). Deedglass stores and checks them now; the Gate enforces them when it ships.
      properties:
        calls_per_minute: {type: integer, minimum: 1, maximum: 1000000}
        calls_per_day: {type: integer, minimum: 1, maximum: 100000000}
        spend_per_day:
          type: object
          additionalProperties: false
          required: [amount, currency]
          properties:
            amount: {type: integer, minimum: 0, maximum: 9007199254740991, description: Minor units.}
            currency: {type: string, pattern: "^[a-z]{3}$", description: Lowercase ISO 4217, like usd or inr.}
      examples:
        - calls_per_minute: 60
          calls_per_day: 5000
          spend_per_day: {amount: 5000, currency: usd}
    Agent:
      type: object
      required: [object, id, name, description, labels, budgets, status, status_reason, status_changed_at, status_changed_by, owner, active_keys, last_used_at, created_at, updated_at, intent_mode]
      properties:
        object: {type: string, const: agent}
        id: {type: string, pattern: "^agt_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        name: {type: string, pattern: "^[a-z0-9][a-z0-9-]{1,62}$", description: Unique in the org and immutable.}
        description: {type: string, maxLength: 500}
        labels:
          $ref: "#/components/schemas/Labels"
        budgets:
          $ref: "#/components/schemas/Budgets"
        status: {type: string, enum: [active, suspended, stopped]}
        status_reason: {type: [string, "null"], maxLength: 280}
        status_changed_at: {type: [string, "null"], format: date-time}
        status_changed_by: {type: [string, "null"], description: '`usr_…`, `key_…` or `operator`.'}
        owner: {type: [string, "null"], description: The person responsible (`usr_…`).}
        active_keys: {type: integer, minimum: 0, maximum: 10}
        last_used_at: {type: [string, "null"], format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        intent_mode:
          type: string
          enum: ["off", optional, required]
          description: >-
            Signed intents (S09): off (headers ignored), optional (checked when sent) or required
            (every call needs one).
        usage:
          $ref: "#/components/schemas/AgentUsage"
        last_action_at:
          type: [string, "null"]
          format: date-time
          description: With `?expand[]=usage` only. When the agent's newest action was created.
      examples:
        - object: agent
          id: agt_01k633rrmh3bcz2p39gg4vp46q
          name: support-bot
          description: Answers refund tickets in Zendesk
          labels: {env: prod, team: support}
          budgets: {calls_per_minute: 60, calls_per_day: 5000, spend_per_day: {amount: 5000, currency: usd}}
          status: active
          status_reason: null
          status_changed_at: null
          status_changed_by: null
          owner: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
          active_keys: 0
          last_used_at: null
          created_at: "2026-10-03T10:02:11.000Z"
          updated_at: "2026-10-03T10:02:11.000Z"
          intent_mode: "off"
    AgentResult:
      allOf:
        - $ref: "#/components/schemas/Agent"
        - type: object
          required: [deed]
          properties:
            deed: {type: [string, "null"], description: The admin deed this change sealed; null when nothing changed.}
      examples:
        - object: agent
          id: agt_01k633rrmh3bcz2p39gg4vp46q
          name: support-bot
          description: Answers refund tickets in Zendesk
          labels: {env: prod, team: support}
          budgets: {calls_per_minute: 60, calls_per_day: 5000, spend_per_day: {amount: 5000, currency: usd}}
          status: active
          status_reason: null
          status_changed_at: null
          status_changed_by: null
          owner: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
          active_keys: 0
          last_used_at: null
          created_at: "2026-10-03T10:02:11.000Z"
          updated_at: "2026-10-03T10:02:11.000Z"
          intent_mode: "off"
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    AgentUsage:
      type: object
      description: >-
        With `?expand[]=usage` only (S25). The agent's standard usage counters (S11), in the windows
        its budgets use: calls this UTC minute, this UTC hour and this day in the org's time zone,
        and spend today in the agent's spend-budget currency (usd when it has none). `spend_today`
        is null when nothing was spent today and the agent has no spend budget.
      required: [calls_minute, calls_hour, calls_today, spend_today]
      properties:
        calls_minute: {type: integer, minimum: 0}
        calls_hour: {type: integer, minimum: 0}
        calls_today: {type: integer, minimum: 0}
        spend_today:
          type: [object, "null"]
          required: [amount, currency]
          properties:
            amount: {type: integer, minimum: 0, description: Minor units.}
            currency: {type: string, pattern: "^[a-z]{3}$"}
      examples:
        - {calls_minute: 3, calls_hour: 41, calls_today: 212, spend_today: {amount: 1200, currency: usd}}
    Overview:
      type: object
      description: GET /v1/overview (S25). docs/OVERVIEW_METRICS.md defines every number.
      required: [object, window, from, to, timezone, generated_at, actions, blocked, approvals, verification, agents, attention]
      properties:
        object: {type: string, const: overview}
        window: {type: string, enum: [24h, 7d]}
        from: {type: string, format: date-time, description: Inclusive.}
        to: {type: string, format: date-time, description: Exclusive; the first bucket boundary at or after now (UTC).}
        timezone: {type: string, description: "The org's time zone (IANA), the day `usage.calls_today` counts."}
        generated_at: {type: string, format: date-time, description: When this answer was read (answers are cached for 10 s).}
        actions:
          $ref: "#/components/schemas/OverviewCount"
        blocked:
          $ref: "#/components/schemas/OverviewCount"
        approvals:
          type: object
          required: [pending, expiring_soon, next_expires_at]
          properties:
            pending: {type: integer, minimum: 0, description: Every pending approval, whatever the window.}
            expiring_soon: {type: integer, minimum: 0, description: Pending approvals that expire within 15 minutes.}
            next_expires_at: {type: [string, "null"], format: date-time}
        verification:
          type: object
          required: [verified, mismatch, unverifiable, not_witnessed, by_level, series]
          properties:
            verified: {type: integer, minimum: 0}
            mismatch: {type: integer, minimum: 0}
            unverifiable: {type: integer, minimum: 0}
            not_witnessed: {type: integer, minimum: 0, description: "The window's actions with no verdict: denied, failed before running, pending or in flight."}
            by_level:
              type: object
              description: Witnessed actions by level; the three sum to verified + mismatch + unverifiable.
              required: [L0, L1, L2]
              properties:
                L0: {type: integer, minimum: 0}
                L1: {type: integer, minimum: 0}
                L2: {type: integer, minimum: 0}
            series:
              type: object
              required: [verified, mismatch, unverifiable]
              properties:
                verified: {type: array, items: {type: integer, minimum: 0}}
                mismatch: {type: array, items: {type: integer, minimum: 0}}
                unverifiable: {type: array, items: {type: integer, minimum: 0}}
        agents:
          type: object
          required: [object, has_more, data]
          properties:
            object: {type: string, const: list}
            has_more: {type: boolean}
            data:
              type: array
              maxItems: 8
              items:
                type: object
                required: [id, name, status, last_action_at, usage, budgets]
                properties:
                  id: {type: string, pattern: "^agt_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
                  name: {type: string}
                  status: {type: string, enum: [active, suspended, stopped]}
                  last_action_at: {type: [string, "null"], format: date-time}
                  usage:
                    type: object
                    required: [calls_today, spend_today]
                    properties:
                      calls_today: {type: integer, minimum: 0}
                      spend_today:
                        type: [object, "null"]
                        required: [amount, currency]
                        properties:
                          amount: {type: integer, minimum: 0}
                          currency: {type: string, pattern: "^[a-z]{3}$"}
                  budgets:
                    $ref: "#/components/schemas/Budgets"
        attention:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/AttentionItem"
      examples:
        - object: overview
          window: 24h
          from: "2026-10-08T14:00:00.000Z"
          to: "2026-10-09T14:00:00.000Z"
          timezone: Asia/Kolkata
          generated_at: "2026-10-09T13:59:58.000Z"
          actions: {total: 1204, previous: 1115, series: [31, 28, 44, 52, 61, 49, 55, 58, 60, 47, 50, 49, 52, 54, 51, 48, 46, 53, 55, 57, 50, 44, 41, 38]}
          blocked: {total: 37, previous: 41, series: [1, 0, 2, 3, 2, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 1, 1]}
          approvals: {pending: 2, expiring_soon: 1, next_expires_at: "2026-10-09T14:06:00.000Z"}
          verification:
            verified: 1021
            mismatch: 3
            unverifiable: 58
            not_witnessed: 122
            by_level: {L0: 400, L1: 262, L2: 420}
            series:
              verified: [27, 24, 37, 44, 52, 41, 47, 49, 51, 40, 43, 41, 44, 46, 43, 41, 39, 45, 47, 48, 42, 37, 35, 39]
              mismatch: [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
              unverifiable: [2, 2, 3, 3, 3, 3, 3, 2, 3, 2, 2, 2, 2, 3, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2]
          agents:
            object: list
            has_more: true
            data:
              - id: agt_01k633rrmh3bcz2p39gg4vp46q
                name: support-bot
                status: active
                last_action_at: "2026-10-09T13:59:41.000Z"
                usage: {calls_today: 212, spend_today: {amount: 1200, currency: usd}}
                budgets: {calls_per_day: 5000}
          attention:
            - object: attention_item
              kind: mismatch
              severity: high
              at: "2026-10-09T13:41:09.000Z"
              ref: act_01k6f0k9hx2q3c1m6b8v5r7w2d
              data: {tool: postgres.execute, agent: support-bot, agent_id: agt_01k633rrmh3bcz2p39gg4vp46q, summary: "Postgres changed 301 rows in public.orders. The agent reported 1.", level: L2}
    OverviewCount:
      type: object
      required: [total, previous, series]
      properties:
        total: {type: integer, minimum: 0}
        previous: {type: integer, minimum: 0, description: The same count over the window before.}
        series: {type: array, items: {type: integer, minimum: 0}, description: 24 hourly or 28 six-hour buckets, oldest first.}
    AttentionItem:
      type: object
      description: >-
        One thing on the Overview that needs a person. The server sends a kind and its data; the
        Console words it. `ref` is the id to open (an action, approval, agent, connector or deed),
        or null. Data per kind: `chain_broken` {seq, deed, code, checked, checked_at};
        `org_stopped` {since, by, reason}; `slot_invalidated` {connector, connector_id, status,
        status_reason}; `slot_lagging` {connector, connector_id, lag_bytes, threshold_bytes};
        `mismatch` {tool, agent, agent_id, summary (the witness summary, verbatim), level};
        `approval_expiring` {tool, agent, agent_id, expires_at, action}; `budget_near` {agent,
        agent_id, budget (calls_per_minute, calls_per_day or spend_per_day), used, limit, and
        currency for spend}.
      required: [object, kind, severity, at, ref, data]
      properties:
        object: {type: string, const: attention_item}
        kind: {type: string, enum: [chain_broken, org_stopped, slot_invalidated, slot_lagging, mismatch, approval_expiring, budget_near]}
        severity: {type: string, enum: [high, medium, low]}
        at: {type: string, format: date-time}
        ref: {type: [string, "null"]}
        data: {type: object}
    AgentList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Agent"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    OrgStop:
      type: object
      required: [object, org, stopped, stopped_at, stopped_by, reason, agents_affected, deed]
      description: The org's Stop all. While `stopped` is true, every agent in the org is refused with `org_stopped`.
      properties:
        object: {type: string, const: org_stop}
        org: {type: string, pattern: "^org_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        stopped: {type: boolean}
        stopped_at: {type: [string, "null"], format: date-time}
        stopped_by: {type: [string, "null"], description: '`usr_…`, `key_…` or `operator`.'}
        reason: {type: [string, "null"], maxLength: 280}
        agents_affected: {type: integer, minimum: 0}
        deed: {type: [string, "null"], description: The admin deed this change sealed; null when nothing changed.}
      examples:
        - object: org_stop
          org: org_01k6h7kp6msmzrhdzwtcr3jnkp
          stopped: true
          stopped_at: "2026-10-03T14:05:00.000Z"
          stopped_by: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
          reason: Investigating unexpected refunds
          agents_affected: 7
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    APIKey:
      type: object
      required: [object, id, agent, name, mode, prefix, scopes, expires_at, last_used_at, revoked_at, rolled_from, created_at]
      description: >-
        An agent key or an org admin key. The full key is `dg_<mode>_<prefix 8>_<secret 32>`, 49
        characters; it is shown once, as `secret`, when the key is created or rolled.
      properties:
        object: {type: string, const: api_key}
        id: {type: string, pattern: "^key_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        agent: {type: [string, "null"], description: The agent (`agt_…`); null for an org admin key.}
        name: {type: string, maxLength: 64}
        mode: {type: string, enum: [live, test]}
        prefix: {type: string, pattern: "^dg_(live|test)_[0-9A-Za-z]{8}$", description: 'How the key is displayed: `dg_live_jJbEEMWk_••••`.'}
        scopes:
          type: array
          items: {type: string, enum: ["actions:execute", "actions:read", "deeds:read", "actions:undo", admin]}
        expires_at: {type: [string, "null"], format: date-time, description: Set when the key was rolled.}
        last_used_at: {type: [string, "null"], format: date-time}
        revoked_at: {type: [string, "null"], format: date-time}
        rolled_from: {type: [string, "null"], description: The key this one replaced (`key_…`).}
        created_at: {type: string, format: date-time}
      examples:
        - object: api_key
          id: key_01k6jz12ptrvg2zn5vts5xc1ay
          agent: agt_01k633rrmh3bcz2p39gg4vp46q
          name: prod runner
          mode: live
          prefix: dg_live_jJbEEMWk
          scopes: ["actions:execute", "actions:read", "deeds:read"]
          expires_at: null
          last_used_at: "2026-10-03T14:01:58.512Z"
          revoked_at: null
          rolled_from: null
          created_at: "2026-10-03T10:03:40.000Z"
    APIKeyResult:
      allOf:
        - $ref: "#/components/schemas/APIKey"
        - type: object
          required: [deed]
          properties:
            secret:
              type: [string, "null"]
              pattern: "^dg_(live|test)_[0-9A-Za-z]{8}_[0-9A-Za-z]{32}$"
              description: >-
                The full key, on create and roll only, shown once. Null in an Idempotency-Key replay:
                Deedglass never stores it.
            deed: {type: [string, "null"], description: The admin deed this change sealed; null when nothing changed.}
            previous:
              type: object
              description: On roll only. The old key and when it stops working.
              required: [id, expires_at]
              properties:
                id: {type: string}
                expires_at: {type: [string, "null"], format: date-time}
      examples:
        - object: api_key
          id: key_01k6jz12ptrvg2zn5vts5xc1ay
          agent: agt_01k633rrmh3bcz2p39gg4vp46q
          name: prod runner
          mode: live
          prefix: dg_live_jJbEEMWk
          scopes: ["actions:execute", "actions:read", "deeds:read"]
          secret: dg_live_jJbEEMWk_o0Erxxf1kgtP2QjiJ79vzW31AjZqIch8
          expires_at: null
          last_used_at: null
          revoked_at: null
          rolled_from: null
          created_at: "2026-10-03T10:03:40.000Z"
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    APIKeyList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/APIKey"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    Whoami:
      type: object
      required: [object, org, agent, key, budgets]
      description: Never includes a stop reason or who stopped the agent.
      properties:
        object: {type: string, const: whoami}
        org:
          type: object
          required: [id, name, slug, stopped]
          properties:
            id: {type: string}
            name: {type: string}
            slug: {type: string}
            stopped: {type: boolean, description: True while Stop all is on.}
        agent:
          type: [object, "null"]
          description: Null for an org admin key.
          required: [id, name, status, labels]
          properties:
            id: {type: string}
            name: {type: string}
            status: {type: string, enum: [active, suspended, stopped]}
            labels:
              $ref: "#/components/schemas/Labels"
        key:
          type: object
          required: [id, prefix, mode, scopes, expires_at]
          properties:
            id: {type: string}
            prefix: {type: string}
            mode: {type: string, enum: [live, test]}
            scopes:
              type: array
              items: {type: string}
            expires_at: {type: [string, "null"], format: date-time}
        budgets:
          description: Null for an org admin key.
          anyOf:
            - $ref: "#/components/schemas/Budgets"
            - type: "null"
      examples:
        - object: whoami
          org: {id: org_01k6h7kp6msmzrhdzwtcr3jnkp, name: Acme, slug: acme, stopped: false}
          agent: {id: agt_01k633rrmh3bcz2p39gg4vp46q, name: support-bot, status: active, labels: {env: prod, team: support}}
          key: {id: key_01k6jt6tgx72gjr876976xak5e, prefix: dg_live_0IDw6L5O, mode: live, scopes: ["actions:execute", "actions:read", "deeds:read"], expires_at: null}
          budgets: {calls_per_minute: 60, calls_per_day: 5000, spend_per_day: {amount: 5000, currency: usd}}
    IntentKey:
      type: object
      required: [object, kid, agent, public_key, created_at, created_by]
      description: An Ed25519 public key an agent signs intents with. An agent has at most two.
      properties:
        object: {type: string, const: intent_key}
        kid: {type: string, pattern: "^[0-9a-f]{32}$", description: The key's id, 32 lowercase hex characters.}
        agent: {type: string, description: The agent (`agt_…`).}
        public_key: {type: string, pattern: "^ed25519:[A-Za-z0-9_-]{43}$"}
        created_at: {type: string, format: date-time}
        created_by: {type: string, description: '`usr_…`, `key_…` or `operator`.'}
        deed: {type: string, description: The admin deed this change sealed (on create only).}
      examples:
        - object: intent_key
          kid: 21fe31dfa154a261626bf854046fd227
          agent: agt_01k633rrmh3bcz2p39gg4vp46q
          public_key: ed25519:11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
          created_at: "2026-10-03T09:20:11.000Z"
          created_by: usr_01k6c9kb2zcjsyn7b8g2k1vqvp
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    IntentKeyList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          maxItems: 2
          items:
            $ref: "#/components/schemas/IntentKey"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    IntentKeyDeleted:
      type: object
      required: [object, kid, agent, deleted, deed]
      properties:
        object: {type: string, const: intent_key}
        kid: {type: string, pattern: "^[0-9a-f]{32}$"}
        agent: {type: string}
        deleted: {type: boolean, const: true}
        deed: {type: string, description: The admin deed this change sealed.}
      examples:
        - object: intent_key
          kid: 21fe31dfa154a261626bf854046fd227
          agent: agt_01k633rrmh3bcz2p39gg4vp46q
          deleted: true
          deed: deed_01k6vrddq2atgb2023qvhhct3g
    IntentCheck:
      type: object
      required: [object, signed, intent_hash, kid, agent, tool, args_digest, on_behalf_of, session, expires_at, nonce_consumed, intent_mode]
      description: >-
        What the Gate would see for this call. `signed` is false when no intent was sent (possible
        in `off` and `optional` mode); the intent fields are then null.
      properties:
        object: {type: string, const: intent_check}
        signed: {type: boolean}
        intent_hash: {type: [string, "null"], pattern: "^sha256:[0-9a-f]{64}$"}
        kid: {type: [string, "null"], pattern: "^[0-9a-f]{32}$"}
        agent: {type: string}
        tool: {type: string}
        args_digest: {type: string, pattern: "^sha256:[0-9a-f]{64}$", description: The SHA-256 of the arguments' JCS bytes (RFC 8785).}
        on_behalf_of: {type: [string, "null"]}
        session: {type: [string, "null"]}
        expires_at: {type: [string, "null"], format: date-time}
        nonce_consumed: {type: boolean}
        intent_mode: {type: string, enum: ["off", optional, required]}
      examples:
        - object: intent_check
          signed: true
          intent_hash: sha256:87c73e450a53f0269e33f7adb8430a6f89714606f9aae38bc732536fef7e3089
          kid: 21fe31dfa154a261626bf854046fd227
          agent: agt_01k633rrmh3bcz2p39gg4vp46q
          tool: stripe.refund
          args_digest: sha256:f7411a7cf4706333c9ba80c85aab7d8b92363ad3077a288dadc26bef42d89d36
          on_behalf_of: usr_ext_4821
          session: conv_9f2c
          expires_at: "2026-10-03T09:31:00.000Z"
          nonce_consumed: true
          intent_mode: optional
    PolicyActorRef:
      type: object
      required: [object, id]
      description: Who acted, as a person, an API key, or the system (whose id is null).
      properties:
        object: {type: string, enum: [user, api_key, system]}
        id: {type: [string, "null"], description: '`usr_…` or `key_…`; null for the system.'}
    PolicyDiagnostic:
      type: object
      required: [severity, code, message, line, column, doc_url]
      description: >-
        One finding about a policy file, with its position and the next step. Codes are stable and
        listed in docs/POLICY_ERRORS.md. In the `policy_invalid` error envelope each diagnostic
        also carries `object: policy_diagnostic`.
      properties:
        object: {type: string, const: policy_diagnostic, description: In the error envelope only.}
        severity: {type: string, enum: [error, warning]}
        code: {type: string, description: Snake case and stable, like `reason_missing` or `default_allow`.}
        message: {type: string, description: One sentence.}
        hint: {type: string, description: The next step.}
        file: {type: string}
        line: {type: integer, minimum: 0, description: 1-based; 0 means the whole file.}
        column: {type: integer, minimum: 0, description: 1-based, in characters.}
        end_line: {type: integer, minimum: 1}
        end_column: {type: integer, minimum: 1, description: Exclusive.}
        rule_id: {type: string}
        path: {type: string, description: 'The YAML path, like `rules[3].when`.'}
        doc_url: {type: string, format: uri}
      examples:
        - object: policy_diagnostic
          severity: warning
          code: default_allow
          message: "defaults.decision: allow lets every unmatched action run."
          hint: Deedglass recommends deny. Enforcing this policy will need an explicit acknowledgement.
          line: 4
          column: 13
          path: defaults.decision
          doc_url: https://docs.deedglass.com/policies/errors/default_allow
    Policy:
      type: object
      required: [object, id, name, description, status, active_version, shadow_version, latest_version, created_at, updated_at]
      description: A named policy. Its versions never change; activating one makes it decide.
      properties:
        object: {type: string, const: policy}
        id: {type: string, pattern: "^pol_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        name: {type: string, description: From the YAML. Unique in the org and immutable.}
        description: {type: string}
        status:
          type: string
          enum: [enforced, shadow, inactive]
          description: '`enforced` when a version decides, `shadow` when a version only runs in shadow, otherwise `inactive`.'
        active_version: {type: [integer, "null"], minimum: 1, description: The enforced version's number; null when none is enforced.}
        shadow_version: {type: [integer, "null"], minimum: 1, description: The shadow version's number; null when none runs in shadow.}
        latest_version: {type: integer, minimum: 1}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
      examples:
        - object: policy
          id: pol_01jb8m2x7k4q9r3t5v6w8y0z1a
          name: postgres-safe
          description: Reads run freely. Writes wait for a human. Schema changes never run.
          status: enforced
          active_version: 3
          shadow_version: null
          latest_version: 3
          created_at: "2026-10-04T09:12:44.000Z"
          updated_at: "2026-10-06T11:40:02.000Z"
    PolicyCreated:
      description: A new policy, with its first version.
      allOf:
        - $ref: "#/components/schemas/Policy"
        - type: object
          required: [version]
          properties:
            version:
              $ref: "#/components/schemas/PolicyVersion"
    PolicyList:
      type: object
      required: [object, data, has_more, next_cursor]
      description: A page of policies.
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Policy"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    PolicyVersion:
      type: object
      required: [object, id, policy, version, name, status, version_hash, source_hash, lang, origin, author, notes, rules, budgets, warnings, created_at]
      description: >-
        One immutable version of a policy. `source` is present on a single get only.
        `version_hash` is the hash of the normalized policy, so formatting and comments don't
        change it; `source_hash` is the hash of the exact bytes pushed.
      properties:
        object: {type: string, const: policy_version}
        id: {type: string, pattern: "^pv_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        policy: {type: string, pattern: "^pol_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        version: {type: integer, minimum: 1}
        name: {type: string}
        status: {type: string, enum: [enforced, shadow, inactive]}
        version_hash:
          $ref: "#/components/schemas/Digest"
        source_hash:
          $ref: "#/components/schemas/Digest"
        lang: {type: string, const: deedglass.policy/v1}
        origin:
          type: string
          enum: [manual, suggestion, starter, onboarding]
          description: Where the version came from. A person or an admin key pushing it is `manual`.
        author:
          $ref: "#/components/schemas/PolicyActorRef"
        notes: {type: [string, "null"], maxLength: 2000}
        rules: {type: integer, minimum: 0}
        budgets: {type: integer, minimum: 0}
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/PolicyDiagnostic"
        source: {type: string, description: The YAML exactly as pushed (single get only).}
        created_at: {type: string, format: date-time}
      examples:
        - object: policy_version
          id: pv_01jb8m3c0d2f4g6h8j0k2m4n6p
          policy: pol_01jb8m2x7k4q9r3t5v6w8y0z1a
          version: 3
          name: postgres-safe
          status: enforced
          version_hash: sha256:3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d
          source_hash: sha256:2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6
          lang: deedglass.policy/v1
          origin: manual
          author: {object: user, id: usr_01k6c9kb2zcjsyn7b8g2k1vqvp}
          notes: Allow-list writes to public.orders for the pilot.
          rules: 12
          budgets: 2
          warnings: []
          created_at: "2026-10-06T11:38:15.000Z"
    PolicyVersionList:
      type: object
      required: [object, data, has_more, next_cursor]
      description: A page of versions, without `source`.
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/PolicyVersion"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    PolicyActivation:
      type: object
      required: [object, id, policy, name, mode, version, previous, promoted_from_shadow, acknowledged_warnings, reason, activated_by, activated_at, deactivated_at, deactivated_by, deeds]
      description: >-
        One activation of a version, as enforce or shadow, with the deeds that sealed it. `deeds`
        is in seal order: one deed, or two for a promote from shadow (the shadow ending, then the
        version enforced).
      properties:
        object: {type: string, const: policy_activation}
        id: {type: string, pattern: "^pa_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        policy: {type: string, pattern: "^pol_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        name: {type: string}
        mode: {type: string, enum: [enforce, shadow]}
        version:
          type: object
          required: [id, version, version_hash]
          properties:
            id: {type: string, pattern: "^pv_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
            version: {type: integer, minimum: 1}
            version_hash:
              $ref: "#/components/schemas/Digest"
        previous:
          type: [object, "null"]
          required: [policy, name, version]
          description: The activation this one replaced in the same mode; null when there was none.
          properties:
            policy: {type: string}
            name: {type: string}
            version: {type: integer, minimum: 1}
        promoted_from_shadow: {type: boolean, description: True when the version was the shadow policy and is now enforced.}
        acknowledged_warnings:
          type: array
          description: The warning codes accepted for this activation, sorted.
          items: {type: string}
        reason: {type: string, minLength: 3, maxLength: 280}
        activated_by:
          $ref: "#/components/schemas/PolicyActorRef"
        activated_at: {type: string, format: date-time}
        deactivated_at: {type: [string, "null"], format: date-time}
        deactivated_by:
          description: Who switched it off; null while it is live.
          anyOf:
            - $ref: "#/components/schemas/PolicyActorRef"
            - type: "null"
        deeds:
          type: array
          minItems: 1
          items: {type: string, pattern: "^deed_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
      examples:
        - object: policy_activation
          id: pa_01jb8p0z2b4d6f8h0k2n4q6s8v
          policy: pol_01jb8m2x7k4q9r3t5v6w8y0z1a
          name: postgres-safe
          mode: enforce
          version: {id: pv_01jb8m3c0d2f4g6h8j0k2m4n6p, version: 3, version_hash: "sha256:3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"}
          previous: {policy: pol_01jb8m2x7k4q9r3t5v6w8y0z1a, name: postgres-safe, version: 2}
          promoted_from_shadow: true
          acknowledged_warnings: []
          reason: Guard the orders database before the pilot.
          activated_by: {object: user, id: usr_01k6c9kb2zcjsyn7b8g2k1vqvp}
          activated_at: "2026-10-06T11:40:02.000Z"
          deactivated_at: null
          deactivated_by: null
          deeds: [deed_01jb8p0y1a3c5e7g9j1m3p5r7t, deed_01k6vrddq2atgb2023qvhhct3g]
    PolicyActiveEntry:
      type: object
      required: [policy, name, version, version_id, version_hash, activation, activated_at, deed]
      description: One live activation in the active set.
      properties:
        policy: {type: string, pattern: "^pol_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        name: {type: string}
        version: {type: integer, minimum: 1}
        version_id: {type: string, pattern: "^pv_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        version_hash:
          $ref: "#/components/schemas/Digest"
        activation: {type: string, pattern: "^pa_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        activated_at: {type: string, format: date-time}
        deed: {type: string, description: The deed that sealed the activation.}
    PolicyActiveSet:
      type: object
      required: [object, enforce, shadow]
      description: The org's enforced and shadow policies; each is null when there is none.
      properties:
        object: {type: string, const: policy_active_set}
        enforce:
          anyOf:
            - $ref: "#/components/schemas/PolicyActiveEntry"
            - type: "null"
        shadow:
          anyOf:
            - $ref: "#/components/schemas/PolicyActiveEntry"
            - type: "null"
      examples:
        - object: policy_active_set
          enforce:
            policy: pol_01jb8m2x7k4q9r3t5v6w8y0z1a
            name: postgres-safe
            version: 3
            version_id: pv_01jb8m3c0d2f4g6h8j0k2m4n6p
            version_hash: sha256:3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d
            activation: pa_01jb8p0z2b4d6f8h0k2n4q6s8v
            activated_at: "2026-10-06T11:40:02.000Z"
            deed: deed_01k6vrddq2atgb2023qvhhct3g
          shadow: null
    PolicyLintResult:
      type: object
      required: [object, file, valid, name, version_hash, rules, budgets, errors, warnings]
      description: What the compiler found. `valid` is false when `errors` is not empty.
      properties:
        object: {type: string, const: policy_lint_result}
        file: {type: string, description: Empty for the API.}
        valid: {type: boolean}
        name: {type: [string, "null"], description: The policy's name, when the YAML gives one.}
        version_hash: {type: [string, "null"], pattern: "^sha256:[0-9a-f]{64}$", description: Null when the policy is invalid.}
        rules: {type: integer, minimum: 0}
        budgets: {type: integer, minimum: 0}
        errors:
          type: array
          items:
            $ref: "#/components/schemas/PolicyDiagnostic"
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/PolicyDiagnostic"
    PolicyTraceStep:
      type: object
      required: [step, result, detail]
      description: One line of an explain trace.
      properties:
        step: {type: string, enum: [stop, budget, rule, default]}
        id: {type: string, description: The rule or budget id.}
        line: {type: integer, minimum: 1, description: The rule's line in the policy file.}
        result: {type: string, enum: [pass, match, no_match, error, exceeded, reserved, not_evaluated]}
        detail: {type: string}
        cost: {type: integer, minimum: 0, description: The condition's CEL cost.}
    PolicyApproval:
      type: object
      required: [approvers, required, expires_in]
      description: Who approves and for how long.
      properties:
        approvers:
          type: array
          description: '`role:owner`, `role:admin` or `user:<email>`, sorted.'
          items: {type: string}
        required: {type: integer, minimum: 1}
        expires_in: {type: integer, minimum: 0, description: Seconds.}
    PolicyDecision:
      type: object
      required: [object, decision, code, source, rule_id, rule_line, reason, message, approval, verify, notify, budget, policy, shadow, facts, trace, errors, sqlguard_version, evaluated_in_us]
      description: >-
        A dry-run decision. Nothing ran and no usage was reserved. `shadow` is the shadow policy's
        decision for the same input, when one is active.
      properties:
        object: {type: string, const: policy_decision}
        decision: {type: string, enum: [allow, deny, require_approval]}
        code:
          type: [string, "null"]
          enum: [policy_denied, approval_required, budget_exceeded, agent_stopped, agent_suspended, org_stopped, policy_unavailable, null]
        source: {type: string, enum: [stop, budget, rule, default, builtin]}
        rule_id: {type: [string, "null"]}
        rule_line: {type: [integer, "null"], minimum: 1}
        reason: {type: [string, "null"]}
        message: {type: string, description: What the agent would see.}
        approval:
          anyOf:
            - $ref: "#/components/schemas/PolicyApproval"
            - type: "null"
        verify: {type: [string, "null"], enum: [L0, L1, L2, null]}
        notify:
          type: array
          items: {type: string}
        budget:
          type: [object, "null"]
          required: [id, per, window, limit, used, unit, resets_at, retry_after_s]
          description: The budget that decided; null otherwise.
          properties:
            id: {type: string}
            per: {type: string, enum: [agent, org]}
            window: {type: string, enum: [minute, hour, day]}
            limit: {type: integer, minimum: 0}
            used: {type: integer, minimum: 0}
            projected: {type: integer, minimum: 0, description: Used plus this action's measured amount, for a measured budget it would pass.}
            unit: {type: string, description: '`calls`, or a currency in minor units.'}
            resets_at: {type: string, format: date-time}
            retry_after_s: {type: integer, minimum: 0}
        policy:
          type: object
          required: [id, name, version, version_id, version_hash, mode]
          description: The policy that decided. `id`, `version` and `version_id` are null for a source sent with the request.
          properties:
            id: {type: [string, "null"]}
            name: {type: string}
            version: {type: [integer, "null"], minimum: 1}
            version_id: {type: [string, "null"]}
            version_hash: {type: string}
            mode: {type: string, enum: [enforce, shadow]}
        shadow:
          anyOf:
            - $ref: "#/components/schemas/PolicyDecision"
            - type: "null"
        facts:
          type: object
          required: [sql, time]
          description: >-
            The derived facts the rules saw. `sql` is the SQL classifier's report on `args.sql`
            (docs/SQLGUARD.md); `time` is the decision instant in the org's time zone (`now`,
            `hour`, `minute`, `weekday`, `date`).
          properties:
            sql: {type: object}
            time: {type: object}
        trace:
          type: array
          description: With `explain` only; otherwise empty.
          items:
            $ref: "#/components/schemas/PolicyTraceStep"
        errors:
          type: array
          items: {type: string}
        sqlguard_version: {type: string}
        evaluated_in_us: {type: integer, minimum: 1}
    PolicyCaseResult:
      type: object
      required: [name, pass, expected, got]
      description: One test case, what it expected and what the policy decided.
      properties:
        name: {type: string}
        line: {type: integer, minimum: 1}
        pass: {type: boolean}
        expected:
          type: object
          required: [decision]
          properties:
            decision: {type: string, enum: [allow, deny, require_approval]}
            rule: {type: string, description: 'A rule id, `default`, `budget:<id>` or `stop`.'}
            code: {type: string}
            reason_contains: {type: string}
            message: {type: string}
            verify: {type: string}
            approval:
              type: object
              properties:
                required: {type: integer}
                approvers: {type: array, items: {type: string}}
                expires_in: {type: integer}
            notify: {type: array, items: {type: string}}
        got:
          type: object
          required: [decision, rule, message]
          properties:
            decision: {type: string, enum: [allow, deny, require_approval]}
            rule: {type: string}
            code: {type: string}
            reason: {type: string}
            message: {type: string}
            verify: {type: string}
            approval:
              $ref: "#/components/schemas/PolicyApproval"
            notify: {type: array, items: {type: string}}
        problems:
          type: array
          description: 'Each difference, like "rule: expected block-destructive-sql, got allow-reads".'
          items: {type: string}
    PolicyRuleCoverage:
      type: object
      required: [id, cases]
      description: How many cases a rule or budget decided.
      properties:
        id: {type: string}
        line: {type: integer, minimum: 1}
        cases: {type: integer, minimum: 0}
    PolicyCoverage:
      type: object
      required: [rules, covered, total, uncovered, default, budgets]
      description: Which rules and budgets the cases reached.
      properties:
        rules:
          type: array
          items:
            $ref: "#/components/schemas/PolicyRuleCoverage"
        covered: {type: integer, minimum: 0, description: Rules that decided at least one case.}
        total: {type: integer, minimum: 0}
        uncovered:
          type: array
          items: {type: string}
        default: {type: integer, minimum: 0, description: Cases the default decided.}
        budgets:
          type: array
          items:
            $ref: "#/components/schemas/PolicyRuleCoverage"
    PolicyTestResult:
      type: object
      required: [object, policy, version_hash, ok, passed, failed, skipped, cases, coverage, elapsed_us]
      description: The result of a test run. `ok` is true when every case passed.
      properties:
        object: {type: string, const: policy_test_result}
        policy: {type: string, description: The policy's name.}
        file: {type: string}
        cases_file: {type: string}
        version_hash:
          $ref: "#/components/schemas/Digest"
        ok: {type: boolean}
        passed: {type: integer, minimum: 0}
        failed: {type: integer, minimum: 0}
        skipped: {type: integer, minimum: 0}
        cases:
          type: array
          items:
            $ref: "#/components/schemas/PolicyCaseResult"
        coverage:
          $ref: "#/components/schemas/PolicyCoverage"
        elapsed_us: {type: integer, minimum: 0}
    ProtectedResourceMetadata:
      type: object
      additionalProperties: false
      required: [resource, resource_name, bearer_methods_supported, scopes_supported, resource_documentation]
      description: >-
        RFC 9728 protected-resource metadata. `authorization_servers` is absent on purpose in v1:
        Deedglass v1 takes pre-issued API keys as bearer tokens and has no authorization server to
        list; v2 adds the org's issuer.
      properties:
        resource: {type: string, description: The resource identifier (the API's base URL, or an MCP endpoint's URL).}
        resource_name: {type: string}
        bearer_methods_supported:
          type: array
          items: {type: string}
        scopes_supported:
          type: array
          items: {type: string}
        resource_documentation: {type: string, format: uri}
      examples:
        - resource: https://api.deedglass.com
          resource_name: Deedglass
          bearer_methods_supported: [header]
          scopes_supported: ["actions:execute", "actions:read", "deeds:read"]
          resource_documentation: https://docs.deedglass.com/mcp/authorization
    JSONRPCError:
      type: object
      required: [jsonrpc, error]
      description: A JSON-RPC 2.0 error response.
      properties:
        jsonrpc: {type: string, const: "2.0"}
        id: {type: [string, number, "null"]}
        error:
          type: object
          required: [code, message]
          properties:
            code: {type: integer}
            message: {type: string}
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [type, code, message, param, request_id, doc_url]
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - policy_error
                - rate_limit_error
                - api_error
            code:
              type: string
              enum:
                - invalid_api_key
                - api_key_revoked
                - api_key_expired
                - agent_suspended
                - agent_stopped
                - org_stopped
                - insufficient_scope
                - policy_denied
                - approval_required
                - approval_expired
                - budget_exceeded
                - intent_required
                - intent_invalid
                - intent_replay
                - tool_unknown
                - connector_unavailable
                - upstream_error
                - timeout
                - rate_limited
                - idempotency_conflict
                - validation_failed
                - not_found
                - conflict
                - internal_error
                - authentication_required
                - method_not_allowed
                - request_too_large
                - unsupported_media_type
                - invalid_json
                - api_version_invalid
                - idempotency_key_invalid
                - idempotency_key_in_use
                - service_unavailable
                - session_expired
                - csrf_failed
                - insufficient_role
                - login_link_invalid
                - login_code_invalid
                - last_owner
                - policy_unavailable
                - policy_invalid
                - credentials_missing
                - destination_blocked
                - vault_unavailable
                - upstream_auth_failed
                - upstream_not_found
                - upstream_rate_limited
                - response_too_large
                - statement_refused
                - row_limit_exceeded
                - database_role_unsafe
                - database_not_ready
                - witness_not_configured
                - approval_decided
                - already_countersigned
                - approver_not_eligible
                - self_approval
                - approval_needs_person
                - context_changed
                - approval_unsatisfiable
                - approval_link_invalid
                - approval_link_review_only
                - approver_unmapped
                - stripe_live_key_refused
                - stripe_livemode_detected
                - upstream_approval_pending
                - schedule_needs_approval
                - action_executing
                - from_denied
                - undo_unavailable
                - undo_plan_stale
                - already_undone
                - undo_conflict
                - replay_too_large
                - replay_window_invalid
                - suggestion_stale
                - shadow_slot_taken
                - suggestion_decided
                - observe_not_enabled
                - pro_required
            message:
              type: string
              description: One or more calm sentences with the next step.
            param:
              type: [string, "null"]
              description: The request field, query parameter or header the error is about.
            request_id:
              type: string
              pattern: "^req_[0-7][0-9a-hjkmnp-tv-z]{25}$"
            doc_url:
              type: string
              format: uri
            diagnostics:
              type: array
              description: On `policy_invalid` only. Each problem with its line, code and hint.
              items:
                $ref: "#/components/schemas/PolicyDiagnostic"
            action:
              $ref: "#/components/schemas/Action"
              description: >-
                On refusals of `POST /v1/actions` only (`policy_denied`, `policy_unavailable`,
                `budget_exceeded`, `agent_stopped`, `agent_suspended`, `org_stopped`,
                `tool_unknown`): the recorded, sealed action. Additive (S13).
            checks:
              type: array
              description: On `undo_unavailable` only. Every check of the undo, each with its fix. Additive (S23).
              items:
                $ref: "#/components/schemas/UndoCheck"
      examples:
        - error:
            type: invalid_request_error
            code: not_found
            message: There's no endpoint at GET /v1/helth. Did you mean /v1/health?
            param: null
            request_id: req_01j9zr3m5k8q2w7x4t6v0b1c9d
            doc_url: https://docs.deedglass.com/errors/not_found
    Check:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok, degraded, down, pending, mismatch, unknown, not_configured]
          description: >-
            For `seal_key`, `degraded` means the key retires within its rotation overlap (restart
            with the new key) and `down` means it is retired, revoked or unreadable.
    Health:
      type: object
      required: [object, status, version, api_version, checks, time]
      properties:
        object:
          type: string
          const: health
        status:
          type: string
          enum: [ok, degraded, down, draining]
        version:
          type: string
        api_version:
          type: string
        checks:
          type: object
          required: [database, migrations, seal_key, decoder]
          properties:
            database:
              allOf:
                - $ref: "#/components/schemas/Check"
              properties:
                latency_ms:
                  type: [integer, "null"]
                  minimum: 0
            migrations:
              allOf:
                - $ref: "#/components/schemas/Check"
              properties:
                applied:
                  type: [integer, "null"]
                  minimum: 0
                expected:
                  type: [integer, "null"]
                  minimum: 0
            seal_key:
              $ref: "#/components/schemas/Check"
            decoder:
              $ref: "#/components/schemas/Check"
            policies:
              allOf:
                - $ref: "#/components/schemas/Check"
              description: >-
                `ok`, `degraded` when an org's policy failed its load checks (its actions are
                denied), or `not_configured`.
            insights:
              allOf:
                - $ref: "#/components/schemas/Check"
              description: >-
                Anomaly detection on this instance (S27): `ok`, `degraded` when its last pass
                failed or is late, or `not_configured`.
        time:
          type: string
          format: date-time
      examples:
        - object: health
          status: ok
          version: 0.1.0-dev
          api_version: "2026-10-01"
          checks:
            database: {status: ok, latency_ms: 1}
            migrations: {status: ok, applied: 9, expected: 9}
            seal_key: {status: not_configured}
            decoder: {status: not_configured}
            policies: {status: ok}
            insights: {status: ok}
          time: "2026-10-01T09:30:00.000Z"
    Digest:
      type: string
      pattern: "^sha256:[0-9a-f]{64}$"
      description: '"sha256:" + lowercase hex of SHA-256.'
    DeedTime:
      type: string
      pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3}Z$"
      description: RFC 3339, UTC, exactly three fractional digits.
    SealKey:
      type: object
      additionalProperties: false
      required: [object, id, alg, public_key, thumbprint, status, activated_at, retired_at, revoked_at, jwk]
      properties:
        object:
          type: string
          const: seal_key
        id:
          type: string
          pattern: "^sk_[0-7][0-9a-hjkmnp-tv-z]{25}$"
        alg:
          type: string
          const: Ed25519
        public_key:
          type: string
          description: The raw 32-byte Ed25519 public key, base64url without padding.
        thumbprint:
          $ref: "#/components/schemas/Digest"
        status:
          type: string
          enum: [active, retired, revoked]
        activated_at:
          $ref: "#/components/schemas/DeedTime"
        retired_at:
          type: [string, "null"]
          description: Deeds issued at or after this time are not valid under the key.
        revoked_at:
          type: [string, "null"]
          description: When set, every deed this key sealed fails verification.
        jwk:
          type: object
          additionalProperties: false
          required: [kty, crv, x, kid, use]
          description: The same key as an RFC 8037 JWK, without `alg`.
          properties:
            kty: {type: string, const: OKP}
            crv: {type: string, const: Ed25519}
            x: {type: string}
            kid: {type: string}
            use: {type: string, const: sig}
    SealKeySet:
      type: object
      additionalProperties: false
      required: [object, issuer, updated_at, keys]
      properties:
        object:
          type: string
          const: seal_key_set
        issuer:
          type: string
          format: uri
          description: This deployment's base URL.
        updated_at:
          $ref: "#/components/schemas/DeedTime"
        keys:
          type: array
          items:
            $ref: "#/components/schemas/SealKey"
      examples:
        - object: seal_key_set
          issuer: http://127.0.0.1:8080
          updated_at: "2026-10-02T09:17:14.000Z"
          keys:
            - object: seal_key
              id: sk_01j9zr8k2m4p6r8t0v2x4z6b8d
              alg: Ed25519
              public_key: pEzUz7QOOPD171uzsw5ASsNYKkd5Uq4K9zc4PQUk2e8
              thumbprint: sha256:f6b1d24a4efdc39230905e0c217093f4d1d5e301ac52c9e763a773e864d93f33
              status: active
              activated_at: "2026-10-02T09:17:14.000Z"
              retired_at: null
              revoked_at: null
              jwk: {kty: OKP, crv: Ed25519, x: pEzUz7QOOPD171uzsw5ASsNYKkd5Uq4K9zc4PQUk2e8, kid: sk_01j9zr8k2m4p6r8t0v2x4z6b8d, use: sig}
    Deed:
      type: object
      additionalProperties: false
      required: [object, id, body, body_hash, seal, seal_key_id]
      description: >-
        A sealed deed (docs/DEED_SPEC.md). The stored and exported form is the RFC 8785 canonical
        JSON of this object. `seal` is Ed25519 over the bytes "deedglass/deed/v1\n" + `body_hash`.
      properties:
        object:
          type: string
          const: deed
        id:
          type: string
          pattern: "^deed_[0-7][0-9a-hjkmnp-tv-z]{25}$"
          description: Equal to `body.deed_id` (not signed; verifiers check it).
        body:
          $ref: "#/components/schemas/DeedBody"
        body_hash:
          $ref: "#/components/schemas/Digest"
        seal:
          type: string
          pattern: "^ed25519:[A-Za-z0-9_-]{86}$"
        seal_key_id:
          type: string
          pattern: "^sk_[0-7][0-9a-hjkmnp-tv-z]{25}$"
    DeedBody:
      type: object
      additionalProperties: false
      description: >-
        Deed v1. Every key is always present; which sections are null depends on `kind`
        (docs/DEED_SPEC.md §5). Digests only, never payloads.
      required: [v, kind, deed_id, org, seq, prev_hash, issued_at, agent, on_behalf_of, session, action, policy, approval, execution, witness, cost, admin, amends, seal_key_id]
      properties:
        v: {type: integer, const: 1}
        kind: {type: string, enum: [action, admin, amendment]}
        deed_id: {type: string, pattern: "^deed_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        org: {type: string, pattern: "^org_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        seq: {type: integer, minimum: 1, maximum: 9007199254740991}
        prev_hash: {$ref: "#/components/schemas/Digest"}
        issued_at: {$ref: "#/components/schemas/DeedTime"}
        agent:
          type: [object, "null"]
          additionalProperties: false
          required: [id, name]
          properties:
            id: {type: string}
            name: {type: string, maxLength: 64}
        on_behalf_of: {type: [string, "null"], maxLength: 256}
        session: {type: [string, "null"], maxLength: 256}
        action:
          type: [object, "null"]
          additionalProperties: false
          required: [id, tool, connector, mode, request_digest, intent_hash, intent_signed]
          properties:
            id: {type: string}
            tool: {type: string}
            connector: {type: [string, "null"]}
            mode: {type: string, enum: [gateway, proxy]}
            request_digest: {$ref: "#/components/schemas/Digest"}
            intent_hash: {type: [string, "null"]}
            intent_signed: {type: boolean}
        policy:
          type: [object, "null"]
          additionalProperties: false
          required: [id, version_id, version_hash, mode, decision, rule_id]
          properties:
            id: {type: [string, "null"]}
            version_id: {type: [string, "null"]}
            version_hash: {type: [string, "null"]}
            mode: {type: string, enum: [enforce, shadow]}
            decision: {type: string, enum: [allow, deny, require_approval]}
            rule_id: {type: [string, "null"]}
        approval:
          type: [object, "null"]
          additionalProperties: false
          required: [id, outcome, decided_by, decided_at, deed]
          properties:
            id: {type: string}
            outcome: {type: string, enum: [approved, denied, expired, cancelled]}
            decided_by: {type: array, items: {type: string}}
            decided_at: {type: [string, "null"]}
            deed: {type: [string, "null"]}
        execution:
          type: [object, "null"]
          additionalProperties: false
          required: [status, started_at, finished_at, latency_ms, result_digest, error_code]
          properties:
            status: {type: string, enum: [succeeded, failed, unknown]}
            started_at: {$ref: "#/components/schemas/DeedTime"}
            finished_at: {$ref: "#/components/schemas/DeedTime"}
            latency_ms: {type: integer, minimum: 0}
            result_digest: {type: [string, "null"]}
            error_code: {type: [string, "null"]}
        witness:
          type: [object, "null"]
          additionalProperties: false
          required: [level, verdict, method, detail_digest]
          properties:
            level: {type: string, enum: [L0, L1, L2]}
            verdict: {type: string, enum: [VERIFIED, MISMATCH, UNVERIFIABLE]}
            method: {type: string}
            detail_digest: {type: [string, "null"]}
        cost:
          type: [object, "null"]
          additionalProperties: false
          required: [amount, currency]
          properties:
            amount: {type: integer, minimum: 0}
            currency: {type: string, pattern: "^[a-z]{3}$"}
        admin:
          type: [object, "null"]
          additionalProperties: false
          required: [event, action, actor, target, reason, data_digest]
          properties:
            event: {type: string}
            action: {type: string}
            actor:
              type: object
              required: [type, id]
              properties:
                type: {type: string}
                id: {type: string}
            target:
              type: [object, "null"]
              required: [type, id]
              properties:
                type: {type: string}
                id: {type: string}
            reason: {type: [string, "null"], maxLength: 280}
            data_digest: {type: [string, "null"]}
        amends: {type: [string, "null"]}
        seal_key_id: {type: string}
    NullableTime:
      type: [string, "null"]
      format: date-time
    Action:
      type: object
      required: [object, id, created_at, agent, tool, connector, mode, state, on_behalf_of, session, policy, execution, witness, cost, request_digest, result_digest, payload_pruned_at, deed, livemode, status, decision, approval, next_action]
      description: >-
        One agent action. Its sections mirror the deed's. `status` is the one field an agent
        needs; `state` is the exact lifecycle state (docs/GATE.md). The Gate's own responses
        (`POST /v1/actions` and its result report) add `execution.result` and `witness.reasons`;
        lists leave both out.
      properties:
        object: {type: string, const: action}
        id: {type: string, pattern: "^act_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        created_at: {type: string, format: date-time}
        livemode: {type: boolean, description: '`true` for a `dg_live_` key, `false` for a `dg_test_` key. Recorded, not enforced: connectors have no test mode in v1.'}
        agent:
          type: object
          required: [id, name]
          properties:
            id: {type: string}
            name: {type: string}
        tool: {type: string}
        connector: {type: [string, "null"]}
        mode: {type: string, enum: [gateway, proxy]}
        state:
          type: string
          enum: [received, authenticated, evaluated, denied, pending_approval, executing, executed, failed, witnessing, verified, mismatch, unverifiable, sealed, expired, cancelled]
        status:
          type: string
          enum: [processing, authorized, pending_approval, denied, executed, failed, expired, cancelled]
          description: >-
            `executed`: the tool ran and the action is sealed, whatever the verdict. `failed`: the
            tool certainly didn't apply the call. `authorized`: proxy mode, run the tool at the
            edge now. `processing`: still executing or witnessing. `pending_approval`: waiting for
            a person. `denied`: a policy, a budget or Stop refused it, and nothing ran. `expired`
            and `cancelled`: it waited for approval and never ran. A sealed action keeps the
            status of the state it was sealed from.
        on_behalf_of: {type: [string, "null"]}
        session: {type: [string, "null"]}
        undo:
          $ref: "#/components/schemas/ActionUndo"
        schedule:
          $ref: "#/components/schemas/ActionSchedule"
        decision:
          $ref: "#/components/schemas/ActionDecision"
        policy:
          type: object
          required: [id, version_id, mode, decision, rule_id]
          properties:
            id: {type: [string, "null"]}
            version_id: {type: [string, "null"]}
            mode: {type: string, enum: [enforce, shadow]}
            decision: {type: [string, "null"], enum: [allow, deny, require_approval, null]}
            rule_id: {type: [string, "null"]}
        approval:
          $ref: "#/components/schemas/ActionApproval"
        execution:
          type: [object, "null"]
          required: [status, started_at, finished_at, latency_ms, error_code]
          properties:
            status: {type: [string, "null"], enum: [succeeded, failed, unknown, null]}
            started_at: {$ref: "#/components/schemas/NullableTime"}
            finished_at: {$ref: "#/components/schemas/NullableTime"}
            latency_ms: {type: [integer, "null"], minimum: 0}
            error_code: {type: [string, "null"]}
            error_message: {type: [string, "null"], maxLength: 1024, description: 'A short, redacted message from the tool or the Gate (at most 1 KiB).'}
            result:
              description: >-
                The tool's result. Only on the Gate's own responses (`POST /v1/actions` and
                `POST /v1/actions/{id}/result`); never on lists. `GET /v1/actions/{id}` has it as
                the top-level `result`.
        witness:
          $ref: "#/components/schemas/Witness"
        cost:
          type: [object, "null"]
          required: [amount, currency]
          properties:
            amount: {type: integer, minimum: 0}
            currency: {type: string, pattern: "^[a-z]{3}$"}
        request_digest: {$ref: "#/components/schemas/Digest"}
        result_digest: {type: [string, "null"]}
        payload_pruned_at: {$ref: "#/components/schemas/NullableTime"}
        deed: {type: [string, "null"]}
        next_action:
          $ref: "#/components/schemas/NextAction"
      examples:
        - object: action
          id: act_01j9zt3a5c7e9g1j3m5p7r9t1v
          created_at: "2026-10-02T10:14:03.401Z"
          livemode: true
          agent: {id: agt_01j9z9b1d3f5h7k9m1p3r5t7v9, name: support-bot}
          tool: postgres.execute
          connector: null
          mode: gateway
          state: sealed
          status: executed
          on_behalf_of: u_4821
          session: chat_77
          decision: {outcome: allow, code: null, source: rule, rule_id: allow-order-updates, reason: Order updates are allowed., message: null, policy: null, policy_version: null, shadow: null}
          policy: {id: null, version_id: null, mode: enforce, decision: allow, rule_id: allow-order-updates}
          approval: null
          execution: {status: succeeded, started_at: "2026-10-02T10:14:03.455Z", finished_at: "2026-10-02T10:14:03.496Z", latency_ms: 41, error_code: null, error_message: null}
          witness: {object: witness, level: L2, verdict: MISMATCH, method: postgres.pgoutput, required_level: L0, detail_digest: "sha256:c3e1a07e5d1c9b20c3e1a07e5d1c9b20c3e1a07e5d1c9b20c3e1a07e5d1c9b20"}
          cost: null
          request_digest: sha256:9c1e5b7d0a2f4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e
          result_digest: null
          payload_pruned_at: null
          deed: deed_01j9zt3q5s7v9w1y3a5c7e9g1j
          next_action: null
    Witness:
      type: [object, "null"]
      description: >-
        The Witness's verdict about the action (S19): one level, one verdict and the method of that
        level. A verdict is never stronger than its evidence: VERIFIED needs a clean level at or
        above `required_level`; any contradicting evidence is MISMATCH; missing evidence is
        UNVERIFIABLE. Lists carry the claim only. The Gate's own responses and `GET
        /v1/actions/{id}` add `summary`, `reasons` (violations and gaps; passes and notes are in
        the detail) and `witnessed_at`; `?expand[]=witness.detail` adds `detail`. An executed
        action answers 200 whatever the verdict: the tool ran, and the verdict says what that means.
      required: [object, level, verdict, method, required_level, detail_digest]
      properties:
        object: {type: string, const: witness}
        level: {type: string, enum: [L0, L1, L2]}
        verdict: {type: string, enum: [VERIFIED, MISMATCH, UNVERIFIABLE]}
        method:
          type: [string, "null"]
          description: >-
            The claimed level's method: `reported`, `edge_reported` or `response.schema` (L0);
            `readback.<connector type>.<read tool>`, `readback.postgres.row` or
            `postcondition.<connector type>` (L1); `postgres.pgoutput` (L2).
        required_level: {type: [string, "null"], enum: [L0, L1, L2, null], description: 'The level the policy''s `verify:` asks for (null on actions recorded before S19).'}
        summary: {type: string, description: One sentence a support engineer can read aloud.}
        reasons:
          type: array
          items: {$ref: "#/components/schemas/WitnessReason"}
        witnessed_at: {type: string, format: date-time}
        detail_digest: {type: [string, "null"], description: '`sha256:` of JCS(witness_detail): the deed''s `witness.detail_digest`.'}
        detail: {$ref: "#/components/schemas/WitnessDetail"}
        history:
          type: array
          description: >-
            `GET /v1/actions/{id}` only (S21): every witness section sealed about the action, oldest
            first: the action deed, then any amendment deeds (a later verdict never edits a deed;
            at most three amendments). An entry a later amendment replaced names the reason it
            stopped where it did (`l2_timeout`, …).
          items:
            type: object
            required: [deed, kind, level, verdict, at]
            properties:
              deed: {type: string, pattern: "^deed_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
              kind: {type: string, enum: [action, amendment]}
              level: {type: string, enum: [L0, L1, L2]}
              verdict: {type: string, enum: [VERIFIED, MISMATCH, UNVERIFIABLE]}
              at: {type: string, format: date-time}
              reason: {type: string}
      examples:
        - object: witness
          level: L1
          verdict: MISMATCH
          method: readback.rest.get
          required_level: L1
          summary: 'orders.update_status reported success, but orders.get says status is "pending", not "shipped".'
          reasons:
            - code: postcondition_failed
              class: violation
              level: L1
              check: readback.expect
              message: 'orders.update_status reported success, but orders.get says status is "pending", not "shipped".'
              diff: [{path: body.status, expected: shipped, observed: pending}]
          witnessed_at: "2026-10-07T04:41:22.418Z"
          detail_digest: "sha256:c3e1a07e5d1c9b20c3e1a07e5d1c9b20c3e1a07e5d1c9b20c3e1a07e5d1c9b20"
    WitnessReason:
      type: object
      description: One piece of evidence, or one missing piece. The class and the message come from the reason catalog (docs/witness/REASONS.md).
      required: [code, class, level, check, message]
      properties:
        code: {type: string, pattern: "^[a-z][a-z0-9_]{0,63}$"}
        class: {type: string, enum: [pass, violation, gap, note]}
        level: {type: string, enum: [L0, L1, L2]}
        check: {type: string}
        message: {type: string, maxLength: 400}
        diff:
          type: array
          items:
            type: object
            required: [path, expected, observed]
            properties:
              path: {type: string}
              expected: {}
              observed: {}
        evidence: {type: object, description: Small (at most 8 KiB); never payloads or secrets.}
    WitnessDetail:
      type: object
      description: >-
        witness_detail v1 (S19): what `actions.witness_detail` holds and what the deed's
        `witness.detail_digest` is the digest of. Changes are additive only; S21 fills `levels.L2`.
      required: [object, version, claim, summary, levels, reasons, witnessed_at]
      properties:
        object: {type: string, const: witness_detail}
        version: {type: integer, const: 1}
        claim:
          type: object
          required: [level, verdict, method, required_level]
          properties:
            level: {type: string, enum: [L0, L1, L2]}
            verdict: {type: string, enum: [VERIFIED, MISMATCH, UNVERIFIABLE]}
            method: {type: string}
            required_level: {type: string, enum: [L0, L1, L2]}
        summary: {type: string}
        levels:
          type: object
          required: [L0, L1, L2]
          properties:
            L0: {$ref: "#/components/schemas/WitnessLevelDetail"}
            L1: {$ref: "#/components/schemas/WitnessLevelDetail"}
            L2: {$ref: "#/components/schemas/WitnessLevelDetail"}
        reasons:
          type: array
          items: {$ref: "#/components/schemas/WitnessReason"}
        witnessed_at: {type: string, format: date-time}
        truncated: {type: integer, minimum: 0, description: Diff rows and evidence left out to stay under witness.max_detail_bytes.}
    WitnessLevelDetail:
      type: [object, "null"]
      required: [verdict, method]
      properties:
        verdict: {type: string, enum: [VERIFIED, MISMATCH, UNVERIFIABLE]}
        method: {type: string}
        readback:
          type: object
          description: What the read-back did; the body it read is kept only as a digest.
          required: [tool, attempts, latency_ms]
          properties:
            tool: {type: string}
            target: {type: string}
            request_digest: {type: string}
            status: {type: integer}
            response_digest: {type: string}
            attempts: {type: integer, minimum: 1, maximum: 5}
            latency_ms: {type: integer, minimum: 0}
        transaction:
          type: object
          description: >-
            L2 only (S21): the committed transaction the verdict is about. Every L2 verdict that
            read a frame names its system, timeline and commit LSN; `marker_lsn` is evidence only
            and is never compared.
          required: [system_id, timeline, commit_lsn, xid, committed_at]
          properties:
            system_id: {type: string}
            timeline: {type: integer}
            commit_lsn: {type: string}
            xid: {type: integer}
            committed_at: {type: string, format: date-time}
            marker_lsn: {type: string}
            changes_digest: {type: string}
        expected:
          type: object
          description: L2 only. What the statement should touch (S21 §3.3).
          required: [derivation, tables]
          properties:
            derivation: {type: string, enum: [exact_keys, count_only, contract, explicit, unknown]}
            why: {type: string}
            tables:
              type: array
              items:
                type: object
                required: [table, ops]
                properties:
                  table: {type: string}
                  ops: {type: array, items: {type: string, enum: [insert, update, delete]}}
                  rows: {type: integer}
                  max_rows: {type: integer}
                  keys: {type: array, items: {type: object, additionalProperties: {type: string}}}
                  columns: {type: array, items: {type: string}}
        observed:
          type: object
          description: L2 only. Exact counts per table, from Postgres's change log.
          required: [total, tables]
          properties:
            total: {type: integer, minimum: 0}
            tables:
              type: array
              items:
                type: object
                required: [table, insert, update, delete]
                properties:
                  table: {type: string}
                  insert: {type: integer}
                  update: {type: integer}
                  delete: {type: integer}
                  truncate: {type: integer}
        rows:
          type: array
          description: L2 only. Changed rows, expected ones first, then unexpected ones in stream order (at most witness.diff_rows).
          items:
            type: object
            required: [table, op, key, expected, cells]
            properties:
              table: {type: string}
              op: {type: string, enum: [insert, update, delete]}
              key: {type: object, additionalProperties: {type: string}}
              expected: {type: boolean}
              cells:
                type: array
                items:
                  type: object
                  required: [column, before, after]
                  properties:
                    column: {type: string}
                    before: {type: [string, "null"]}
                    after: {type: [string, "null"], description: 'Postgres''s text output; a sensitive column is "•••".'}
        rows_shown: {type: integer, minimum: 0}
        rows_more: {type: integer, minimum: 0}
        columns_more: {type: integer, minimum: 0}
        redacted: {type: array, items: {type: string}, description: 'schema.table.column of the sensitive columns shown as "•••".'}
        frames: {type: integer, minimum: 0, description: How many transactions in the change log named the action.}
        frame_ids: {type: array, items: {type: string}}
    ActionDecision:
      type: [object, "null"]
      required: [outcome, code, source, rule_id, reason, message, policy, policy_version, shadow]
      description: >-
        The policy decision as an agent reads it: S11's decision, its source, its message verbatim,
        the policy version that made it (pinned: a later activation doesn't change it), and the
        shadow decision next to it. `null` until the action is evaluated.
      properties:
        outcome: {type: string, enum: [allow, deny, require_approval]}
        code: {type: [string, "null"], description: 'The refusal code (`policy_denied`, `budget_exceeded`, `agent_stopped`, `tool_unknown`, …), `approval_required`, or null for an allow.'}
        source: {type: [string, "null"], enum: [stop, budget, rule, default, builtin, null], description: '`builtin` is the Gate''s own decision, such as `tool_unknown`.'}
        rule_id: {type: [string, "null"]}
        reason: {type: [string, "null"]}
        message: {type: [string, "null"], description: The sentence the agent sees, verbatim. Null for an allow.}
        policy: {type: [string, "null"], description: The policy id (`pol_…`).}
        policy_version: {type: [string, "null"], description: The policy version id (`pv_…`).}
        anomaly:
          type: string
          description: >-
            S27, additive: the decision trace's line about the anomaly facts the policy saw, like
            `anomaly facts: volume_spike`, or `anomaly facts: detector_stale (…)` when detection
            was behind and the policy treated the agent as anomalous. Absent when no anomaly fact
            was active.
          examples: ["anomaly facts: volume_spike"]
        shadow:
          type: [object, "null"]
          description: What the shadow policy would have decided. Recorded, never enforced.
          required: [outcome, rule_id, policy_version]
          properties:
            outcome: {type: string, enum: [allow, deny, require_approval]}
            rule_id: {type: [string, "null"]}
            policy_version: {type: [string, "null"]}
    ActionApproval:
      type: [object, "null"]
      required: [id, status, expires_at]
      description: The approval the action waits for, or waited for. `null` when no rule asked for one.
      properties:
        id: {type: string, description: The approval id (`apr_…`).}
        status: {type: string, enum: [pending, approved, denied, expired, cancelled]}
        expires_at: {type: string, format: date-time}
        decision_reason: {type: string, description: 'S22, additive: the reason a person gave when they denied it. Absent otherwise.'}
    NextAction:
      type: [object, "null"]
      required: [type]
      description: >-
        What the caller does next. `await_approval`: wait for a person (`poll_url` long-polls the
        action). `execute_at_edge`: proxy mode, run the tool at the edge and report the result to
        `report_url` before `deadline`. `scheduled` (S40): approved, it runs between `not_before` and
        `not_after`. `null` otherwise.
      properties:
        type: {type: string, enum: [await_approval, execute_at_edge, scheduled]}
        approval: {type: string, description: '`await_approval` only: the approval id.'}
        not_before: {type: string, format: date-time, description: '`scheduled` only (S40): an approved action runs from this time.'}
        not_after: {type: string, format: date-time, description: '`scheduled` only (S40): past it the action is cancelled and sealed with `schedule_missed`; nothing runs.'}
        expires_at: {type: string, format: date-time, description: '`await_approval` only.'}
        poll_url: {type: string, description: '`await_approval` only: `/v1/actions/{id}?wait=25`.'}
        report_url: {type: string, description: '`execute_at_edge` only: `/v1/actions/{id}/result`.'}
        deadline: {type: string, format: date-time, description: '`execute_at_edge` only: after it, the action is sealed UNVERIFIABLE (`edge_lost`).'}
    ActionSchedule:
      type: object
      required: [not_before, not_after]
      description: >-
        S40, additive: the window an approved action runs in, stored once at creation and never changed.
        Absent when the request carried no schedule.
      properties:
        not_before: {type: string, format: date-time}
        not_after: {type: string, format: date-time}
    ActionRequest:
      type: object
      additionalProperties: false
      required: [tool, args]
      description: The body of `POST /v1/actions`.
      properties:
        tool:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            `<connector>.<tool>`, split at the first dot. The connector is `^[a-z][a-z0-9-]{1,31}$`.
            The tool part is `^[a-z][a-z0-9_]{0,63}$` for native connectors, and the MCP charset
            `^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$` in proxy mode. Empty, over 200 bytes or not UTF-8
            is 400 `validation_failed`; any other name that isn't a tool, or that no connector
            serves in gateway mode, is recorded, sealed and refused with 404 `tool_unknown`.
        args:
          type: object
          description: >-
            The tool's arguments, a JSON object (`{}` for none): at most `gate.max_args_bytes`
            (256 KiB), nested at most 32 levels, and no integer beyond ±9007199254740991, which RFC 8785
            would round (send it as a string). Floats are fine.
        mode:
          type: string
          enum: [gateway, proxy]
          default: gateway
          description: '`gateway`: Deedglass runs the tool through a connector. `proxy`: the edge runs it and reports the result.'
        wait:
          type: number
          minimum: 0
          maximum: 25
          description: >-
            Seconds to wait for a gateway action to finish before answering 202 `processing`. The
            default is `gate.wait_timeout` (10 s). `0` answers at once.
        context:
          type: object
          additionalProperties: false
          description: Who sent the action, as the client says. Never part of any digest; at most 4 KiB.
          properties:
            client:
              type: object
              additionalProperties: false
              properties:
                name: {type: string, maxLength: 64}
                version: {type: string, maxLength: 64}
            transport:
              type: string
              pattern: "^[a-z][a-z0-9_]{0,31}$"
              description: '`rest`, `sdk`, `mcp_stdio` or `mcp_http`. A call through `mcp_stdio` or `mcp_http` for an agent whose intent mode is `required` is refused with `intent_required`.'
            call_id:
              type: string
              pattern: "^[\\x21-\\x7e]{1,128}$"
              description: >-
                The agent framework's own id for this tool call (a LangChain `tool_call_id`, a Claude
                `tool_use_id`, an OpenAI `call_id`), stored with the context for correlation. Never part of
                a digest or a deed. Added 2026-10-05 (S32), additive.
        connector_type:
          type: string
          description: >-
            Proxy mode only: the edge's connector type (default `mcp`). `postgres` turns on the SQL
            facts for a string `args.sql`. In gateway mode it is 400 `validation_failed`.
          enum: [postgres, rest, mcp, mcp_http, sdk, github, slack, stripe, gmail, notion, fake]
        tool_digest:
          type: string
          pattern: "^sha256:[0-9a-f]{64}$"
          description: The digest of the tool's definition, sent by MCP edges. Part of `request_digest`.
        schedule:
          type: object
          additionalProperties: false
          required: [not_before, not_after]
          description: >-
            S40, additive. Gateway mode only (proxy: 400 `validation_failed`, `param: schedule`). With
            `require_approval`, the approved action runs inside the window: the countersign schedules
            the run at `not_before`, and past `not_after` it is cancelled and sealed with
            `schedule_missed`. With `allow`, it runs now when `not_before` is at most 60 seconds away
            and is denied (`schedule_needs_approval`) otherwise. At least 5 minutes long, `not_after`
            within 72 hours and still ahead. Bound into the approval's `context_digest`.
          properties:
            not_before: {type: string, format: date-time}
            not_after: {type: string, format: date-time}
    EdgeResult:
      type: object
      additionalProperties: false
      required: [status, started_at, finished_at]
      description: >-
        The body of `POST /v1/actions/{id}/result`: what the tool returned at the edge. Send
        `result`, or `result_digest` alone; both must agree when both are sent.
      properties:
        status:
          type: string
          enum: [succeeded, failed, unknown]
          description: '`failed` means the tool certainly didn''t apply the call; `unknown` means the edge can''t tell.'
        started_at: {type: string, format: date-time}
        finished_at: {type: string, format: date-time, description: Not before `started_at`.}
        result:
          description: The tool's result, any JSON, at most `gate.max_result_bytes` (256 KiB).
        result_digest:
          type: string
          pattern: "^sha256:[0-9a-f]{64}$"
          description: sha256 of the result's RFC 8785 form. Required when `result` is left out.
        error_code:
          type: [string, "null"]
          pattern: "^[a-z][a-z0-9_]{0,63}$"
          description: A short snake_case code, such as `tool_error`.
        error_message:
          type: [string, "null"]
          description: A short message. It is redacted and cut to 1 KiB before it is stored.
    ToolResolveRequest:
      type: object
      additionalProperties: false
      required: [connector, tools]
      properties:
        connector:
          type: string
          pattern: "^[a-z][a-z0-9-]{1,31}$"
          description: The connector name the wrapper was given (`--connector`); tools are matched as `<connector>.<name>`.
        connector_type:
          type: string
          description: S10's connector type the edge declares; `mcp` when left out (`postgres` for Postgres MCP servers).
        tools:
          type: array
          minItems: 0
          maxItems: 500
          items:
            type: object
            additionalProperties: false
            required: [name]
            properties:
              name: {type: string, pattern: "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$"}
              digest: {type: string, pattern: "^sha256:[0-9a-f]{64}$", description: The wrapper's pinned digest of the tool definition. Recorded nowhere yet; S18 uses it.}
    ToolVisibility:
      type: object
      required: [object, name, tool, visibility, rule_id, reason]
      properties:
        object: {type: string, const: tool_visibility}
        name: {type: string}
        tool: {type: string, description: '`<connector>.<name>`, as policies match it.'}
        visibility: {type: string, enum: [visible, hidden]}
        rule_id: {type: [string, "null"], description: The first rule that covers the tool; null when the policy's default decides.}
        reason: {type: [string, "null"], description: Why the tool is hidden; null when visible.}
    ToolVisibilityList:
      type: object
      required: [object, data, has_more, next_cursor, policy_version]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/ToolVisibility"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
        policy_version: {type: [string, "null"], description: 'The enforced policy version (`pv_…`); null when no policy is active.'}
        policy:
          type: [object, "null"]
          description: 'S22, additive: the enforced policy by name and version, for the wrapper''s startup line. Null when no policy is active.'
          required: [name, version, version_id]
          properties:
            name: {type: string}
            version: {type: integer}
            version_id: {type: string}
    Tool:
      type: object
      required: [object, name, connector, title, description, input_schema, side_effect, visibility]
      properties:
        object: {type: string, const: tool}
        name: {type: string, description: '`<connector>.<tool>`, as policies match it and /mcp/native lists it.'}
        connector: {type: string}
        title: {type: string}
        description: {type: string}
        input_schema: {type: object, description: The tool's JSON Schema for its arguments.}
        side_effect: {type: string, enum: [read, write, destructive, unknown], description: What the connector declares the tool does.}
        visibility: {type: string, enum: [visible, hidden]}
        levels: {type: array, items: {type: string, enum: [L0, L1, L2]}, description: 'S30, additive: the verification levels the tool reaches.'}
        read_back: {type: string, description: 'S30, additive: what the L1 read-back checks, in one sentence; empty when the tool is not read back.'}
    ToolList:
      type: object
      required: [object, data, has_more, next_cursor, policy_version]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Tool"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
        policy_version: {type: [string, "null"], description: 'The enforced policy version (`pv_…`); null when no policy is active.'}
    ActionList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Action"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    ActionEvent:
      type: object
      required: [object, id, seq, from, to, at, actor, data]
      properties:
        object: {type: string, const: action_event}
        id: {type: string, pattern: "^evt_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        seq: {type: integer, minimum: 1}
        from: {type: [string, "null"]}
        to: {type: string}
        at: {type: string, format: date-time}
        actor: {type: string, description: '`system`, `agt_…` or `usr_…`.'}
        data:
          type: object
          description: Short codes and digests (reason, rule_id, error_code, level, verdict, approval_id, detail_digest); never payloads.
    ActionUndo:
      type: object
      required: [undoes, undone_by, eligible]
      description: >-
        The action's undo object (S23). `undoes` names the action an undo puts back; `undone_by`
        names the undo that put this one back; `pending` an undo waiting for approval or running.
        `eligible` is cheap (L2 evidence from a bound transaction, gateway mode, sealed, not
        undone, within `undo.max_age`, the row images kept): it only decides whether the Console
        shows Preview undo. The full answer is always the preview.
      properties:
        undoes: {type: [string, "null"]}
        undone_by: {type: [string, "null"]}
        pending: {type: [string, "null"]}
        eligible: {type: boolean}
    UndoCheck:
      type: object
      required: [id, title, status, detail]
      description: One of the twelve safety checks (S23 §3.3). Available means no check failed.
      properties:
        id:
          type: string
          enum: [evidence, connector, age, not_undone, row_limit, replica_identity, primary_key, columns, triggers, cascades, unchanged, privileges]
        title: {type: string}
        status: {type: string, enum: [pass, warn, fail, skip]}
        detail: {type: string}
        fix: {type: string}
        fix_sql: {type: string, description: SQL to copy, one statement per line.}
        doc_url: {type: string, format: uri}
    UndoStep:
      type: object
      required: [op, table, key]
      description: >-
        One step of the plan as a person reads it: `expect` is the row now (what the action
        wrote), `set` what the undo writes back. A sensitive column shows as •••; NULL is null.
      properties:
        op: {type: string, enum: [update, insert, delete]}
        table: {type: string}
        key: {type: object, additionalProperties: {type: string}}
        set: {type: object, additionalProperties: {type: [string, "null"]}}
        expect: {type: object, additionalProperties: {type: [string, "null"]}}
    ObserveSession:
      type: object
      required: [object, id, connector, label, agent, state, opened_at, closed_at, opened_by, closed_by, flags, watched_tables, summary, caught, seal, undo, notes, observed, attribution]
      properties:
        object: {type: string, const: observe_session}
        id: {type: string, description: '`obs_…`'}
        connector:
          type: object
          required: [id, name]
          properties:
            id: {type: string}
            name: {type: string}
        label: {type: [string, "null"]}
        agent: {type: [string, "null"], description: An agent shown beside the session; it attributes nothing.}
        state: {type: string, enum: [open, closing, sealed, undoing, undone]}
        opened_at: {type: string, format: date-time}
        closed_at: {type: [string, "null"], format: date-time}
        opened_by: {$ref: "#/components/schemas/ObserveActor"}
        closed_by:
          oneOf:
            - {$ref: "#/components/schemas/ObserveActor"}
            - {type: "null"}
        flags: {type: array, items: {type: string, enum: [overlap, max_session, decoder_behind, caught, frames_expired]}}
        watched_tables: {type: array, items: {type: string}}
        summary:
          type: object
          required: [transactions, excluded, expired, rows, tables, first_commit, last_commit]
          properties:
            transactions: {type: integer}
            excluded: {type: integer}
            expired: {type: integer}
            rows: {type: integer}
            tables: {type: object, additionalProperties: {$ref: "#/components/schemas/ObserveCounts"}}
            first_commit: {type: [string, "null"], format: date-time}
            last_commit: {type: [string, "null"], format: date-time}
        caught: {type: array, items: {$ref: "#/components/schemas/CaughtItem"}}
        seal:
          type: [object, "null"]
          properties:
            deed: {type: string, description: 'The observe.session_sealed deed (`deed_…`).'}
        undo:
          type: object
          required: [action, possible, free_path]
          properties:
            action: {type: [string, "null"], description: The session undo action, once one started.}
            possible: {type: boolean}
            free_path: {type: string, description: The free way back (the undo SQL you run).}
        notes: {type: array, items: {type: string}}
        observed: {type: string, description: '"Observed: what Postgres committed. Nothing was compared with what the agent said."'}
        attribution: {type: string}
        transactions:
          type: object
          description: On the single read only, newest first.
          properties:
            object: {type: string, const: list}
            total: {type: integer}
            data:
              type: array
              items:
                type: object
                required: [id, commit_time, commit_lsn, xid, tables, changes, excluded, expired, flags, diff]
                properties:
                  id: {type: string, description: '`dtx_…`'}
                  commit_time: {type: [string, "null"], format: date-time}
                  commit_lsn: {type: string}
                  xid: {type: integer}
                  tables: {type: object, additionalProperties: {$ref: "#/components/schemas/ObserveCounts"}}
                  changes: {type: integer}
                  excluded: {type: boolean}
                  expired: {type: boolean}
                  flags: {type: array, items: {type: string}}
                  diff:
                    type: [object, "null"]
                    properties:
                      rows: {type: array, items: {type: object}}
                      total: {type: integer}
                      truncated: {type: boolean}
    ObserveActor:
      type: object
      required: [type, id]
      properties:
        type: {type: string, enum: [user, api_key, system]}
        id: {type: [string, "null"]}
    ObserveCounts:
      type: object
      required: [insert, update, delete, truncate]
      properties:
        insert: {type: integer}
        update: {type: integer}
        delete: {type: integer}
        truncate: {type: integer}
    CaughtItem:
      type: object
      required: [kind, line, undo_possible, undo_free_path]
      properties:
        kind: {type: string, enum: [truncate, many_rows, delete_rows, doesnt_match, destructive_ran, more_rows, retry_after_deny, undone_later]}
        session: {type: string}
        actions: {type: array, items: {type: string}}
        frames: {type: array, items: {type: string}}
        table: {type: string}
        rows: {type: integer}
        line: {type: string, description: '"Deedglass caught this: …", built from the record.'}
        undo_possible: {type: boolean}
        undo_free_path: {type: string, description: The free way back; every view that shows the item shows it.}
    ObserveUndoPreview:
      type: object
      required: [object, session, available, summary, headline, plan_digest, connector, counts, rows, tables, steps, checks, conflicts, excluded, notes, redacted, sql, refusals, pro, free_path, observed]
      properties:
        object: {type: string, const: observe_undo_preview}
        session: {type: string}
        available: {type: boolean}
        summary: {type: string}
        headline: {type: string}
        plan_digest: {type: [string, "null"]}
        connector: {type: [object, "null"]}
        counts: {type: object}
        rows: {type: integer}
        tables: {type: array, items: {type: string}}
        steps: {type: object}
        checks: {type: array, items: {type: object}}
        conflicts: {type: integer, description: Rows that changed again after the session.}
        excluded: {type: array, items: {type: string}}
        notes: {type: array, items: {type: string}}
        redacted: {type: array, items: {type: string}}
        sql: {type: [string, "null"], description: The undo SQL to review and run yourself (free).}
        refusals: {type: array, items: {type: string}}
        pro: {type: boolean, description: Whether this server may run the undo in one step.}
        free_path: {type: string}
        observed: {type: string}
    UndoPreview:
      type: object
      required: [object, action, tool, available, summary, headline, plan_digest, connector, evidence, counts, rows, tables, steps, checks, notes, redacted, policy, undone_by]
      properties:
        object: {type: string, const: undo_preview}
        action: {type: string}
        tool: {type: string, description: The original action's tool.}
        available: {type: boolean, description: No check failed.}
        summary: {type: string}
        headline: {type: string, description: '"301 rows in public.orders · 301 updates"'}
        plan_digest: {type: [string, "null"], description: 'sha256: of the plan; send it to POST /v1/actions/{id}/undo.'}
        connector:
          type: [object, "null"]
          required: [id, name, type]
          properties:
            id: {type: string}
            name: {type: string}
            type: {type: string}
        evidence:
          type: [object, "null"]
          required: [level, verdict, xid, commit_lsn, committed_at, changes]
          properties:
            level: {type: string}
            verdict: {type: string}
            xid: {type: string}
            commit_lsn: {type: string}
            committed_at: {type: string}
            changes: {type: integer}
        counts:
          type: object
          required: [update, insert, delete]
          properties:
            update: {type: integer}
            insert: {type: integer}
            delete: {type: integer}
        rows: {type: integer}
        tables: {type: array, items: {type: string}}
        steps:
          type: object
          required: [object, total, has_more, data]
          properties:
            object: {type: string, const: list}
            total: {type: integer}
            has_more: {type: boolean}
            data:
              type: array
              items:
                $ref: "#/components/schemas/UndoStep"
        checks:
          type: array
          items:
            $ref: "#/components/schemas/UndoCheck"
        notes: {type: array, items: {type: string}}
        redacted: {type: array, items: {type: string}, description: 'Columns marked sensitive, "schema.table.column": shown as ••• and compared by digest.'}
        policy:
          type: [object, "null"]
          required: [decision, rule_id, reason]
          properties:
            decision: {type: string, enum: [allow, deny, require_approval]}
            rule_id: {type: [string, "null"]}
            reason: {type: [string, "null"]}
        undone_by: {type: [string, "null"]}
    ActionDetail:
      allOf:
        - $ref: "#/components/schemas/Action"
      type: object
      required: [request, result, events, deeds, approval, undo]
      properties:
        undo:
          $ref: "#/components/schemas/ActionUndo"
        request:
          description: The tool arguments, or null after pruning.
        result:
          description: The tool result, or null after pruning.
        events:
          type: object
          required: [object, data, has_more, next_cursor]
          properties:
            object: {type: string, const: list}
            data:
              type: array
              maxItems: 200
              items:
                $ref: "#/components/schemas/ActionEvent"
            has_more: {type: boolean}
            next_cursor: {type: "null"}
        deeds:
          type: array
          items:
            type: object
            required: [id, kind, seq, issued_at]
            properties:
              id: {type: string}
              kind: {type: string, enum: [action, amendment]}
              seq: {type: integer, minimum: 1}
              issued_at: {type: string, format: date-time}
        approval:
          $ref: "#/components/schemas/ActionApproval"
    DeedList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Deed"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    LedgerCheck:
      type: object
      required: [object, org, status, checked, first_seq, last_seq, anchor, head, seal_keys, break, next_from_seq, duration_ms, checked_at]
      properties:
        object: {type: string, const: ledger_check}
        org: {type: string}
        status: {type: string, enum: [intact, broken, incomplete]}
        checked: {type: integer, minimum: 0}
        first_seq: {type: [integer, "null"]}
        last_seq: {type: [integer, "null"]}
        anchor:
          type: object
          required: [kind, seq, body_hash]
          properties:
            kind: {type: string, enum: [genesis, deed]}
            seq: {type: integer, minimum: 0}
            body_hash: {type: [string, "null"]}
        head:
          type: [object, "null"]
          required: [seq, deed, body_hash, issued_at]
          description: The last intact deed checked. Keep it outside Deedglass.
          properties:
            seq: {type: integer}
            deed: {type: string}
            body_hash: {$ref: "#/components/schemas/Digest"}
            issued_at: {$ref: "#/components/schemas/DeedTime"}
        seal_keys:
          type: array
          items: {type: string}
        break:
          type: [object, "null"]
          required: [seq, deed, code, message, next]
          properties:
            seq: {type: [integer, "null"]}
            deed: {type: [string, "null"]}
            code: {type: string, examples: [body_hash_mismatch, chain_seq_gap, chain_prev_hash_mismatch, seal_invalid]}
            message: {type: string, examples: ["Deed 812 was changed after it was sealed."]}
            next: {type: string}
        next_from_seq: {type: [integer, "null"]}
        duration_ms: {type: integer, minimum: 0}
        checked_at: {type: string, format: date-time}
      examples:
        - object: ledger_check
          org: org_01j9z8m2k4p6r8t0v2x4z6b8d0
          status: intact
          checked: 1204
          first_seq: 1
          last_seq: 1204
          anchor: {kind: genesis, seq: 0, body_hash: null}
          head: {seq: 1204, deed: deed_01j9zx3q5s7v9w1y3a5c7e9g1j, body_hash: "sha256:5e0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8fa91c", issued_at: "2026-10-02T18:29:58.004Z"}
          seal_keys: [sk_01j9zr8k2m4p6r8t0v2x4z6b8d]
          break: null
          next_from_seq: null
          duration_ms: 312
          checked_at: "2026-10-02T18:31:02.117Z"
    Connector:
      type: object
      required: [object, id, type, name, description, status, config, config_hash, tools, credentials, health, created_at, updated_at]
      properties:
        object: {type: string, const: connector}
        id: {type: string, pattern: "^con_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        type: {type: string}
        name: {type: string}
        description: {type: string}
        status: {type: string, enum: [active, disabled, error]}
        config: {type: object, description: The normalized config.}
        config_hash: {type: string, description: "sha256: of the normalized config's JCS form."}
        tools:
          type: array
          items:
            type: object
            required: [object, name, side_effect, idempotent, levels]
            properties:
              object: {type: string, const: tool}
              name: {type: string, description: "Qualified: <connector>.<tool>."}
              description: {type: string}
              side_effect: {type: string, enum: [read, write, destructive]}
              idempotent: {type: boolean}
              levels: {type: array, items: {type: string, enum: [L0, L1, L2]}}
        credentials:
          $ref: "#/components/schemas/Credential"
        health:
          type: object
          required: [status, tested_at]
          properties:
            status: {type: string, enum: [unknown, pass, warn, fail, skip]}
            tested_at: {type: [string, "null"], format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        deed: {type: string, description: The admin deed of the change this answers.}
    ConnectorList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/Connector"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    ConnectorDeleted:
      type: object
      required: [object, id, deleted, deed]
      properties:
        object: {type: string, const: connector}
        id: {type: string}
        deleted: {type: boolean, const: true}
        deed: {type: string}
    Credential:
      type: object
      description: A credential's metadata. Never a value, never a digest of one.
      required: [object, connector, set]
      properties:
        object: {type: string, const: credential}
        connector: {type: string}
        set: {type: boolean}
        kind: {type: string, enum: [bearer, header, basic, query, dsn, fields]}
        fields: {type: array, items: {type: string}, description: The field names.}
        hint: {type: string, description: "The last 4 characters of a token of 24 or more (…9f2c), the username of basic auth, or user@host:port/db of a DSN."}
        version: {type: integer, minimum: 1}
        kek_id: {type: string, pattern: "^kek:[0-9a-f]{12}$"}
        updated_at: {type: string, format: date-time}
        updated_by: {type: string}
        deleted: {type: boolean}
        deed: {type: string}
    ConnectorTest:
      type: object
      required: [object, connector, status, duration_ms, checks]
      properties:
        object: {type: string, const: connector_test}
        connector: {type: string}
        status: {type: string, enum: [pass, warn, fail, skip]}
        duration_ms: {type: integer}
        checks:
          type: array
          items:
            type: object
            required: [id, title, status, detail]
            properties:
              id: {type: string, description: "REST (S16): config, credentials, destination, tls, auth. Postgres (S17): connection, version, wal_level, slot_capacity, wal_keep_cap, idle_slot_timeout, schema, begin_action, agent_role, witness_role, witness.watched_tables, writable_watched, publication, slot, tls, pooler, emit_message_acl, probe."}
              title: {type: string}
              status: {type: string, enum: [pass, warn, fail, skip]}
              detail: {type: string}
              fix: {type: string}
              fix_sql: {type: string}
              doc_url: {type: string}
    Decoder:
      type: object
      description: >-
        A connector's decoder (S20): what Deedglass reads from the database's change stream. The
        decoder never decides a verdict; S21 turns bound frames into L2 verdicts.
      required: [object, connector, name, slot, publication, status, health, reason, detail, since, transport, server,
        positions, lag, wal_status, watching, last_commit, last_heartbeat_at, frames, recent, updated_at]
      properties:
        object: {type: string, enum: [decoder]}
        connector: {type: string, pattern: "^con_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        name: {type: string, example: postgres}
        slot: {type: string, example: deedglass_01j9zq7c4m8x2v6t0r3n5k1h7d}
        publication: {type: string, example: deedglass_pub}
        status: {type: string, enum: [pending, connecting, running, degraded, waiting, halted, dropped, disabled]}
        health: {type: string, enum: [healthy, pending, connecting, degraded, waiting, halted, dropped, disabled]}
        reason:
          type: [string, "null"]
          description: >-
            Why the status is what it is: lag_high, restart_retained, stalled, publication_changed,
            timeline_changed, peek_slow (degraded); slot_invalidated, slot_missing,
            publication_missing, publication_invalid, malformed_evidence, session_settings_changed,
            ledger_function_changed, system_changed, wal_level, not_replication, slot_conflict
            (halted); held_elsewhere (waiting); credentials_missing, unreachable (connecting);
            dropped, not_witnessed, connector_deleted, slot_orphaned.
        detail: {type: object, additionalProperties: true}
        since: {type: string, format: date-time}
        transport: {type: string, enum: [peek]}
        server:
          type: object
          required: [version, system_id, timeline]
          properties:
            version: {type: [string, "null"], example: "18.6"}
            system_id: {type: [string, "null"]}
            timeline: {type: [integer, "null"]}
        positions:
          type: object
          required: [checkpoint_lsn, confirmed_flush_lsn, restart_lsn, current_wal_lsn]
          properties:
            checkpoint_lsn: {type: [string, "null"], example: 0/3E65050}
            confirmed_flush_lsn: {type: [string, "null"]}
            restart_lsn: {type: [string, "null"]}
            current_wal_lsn: {type: [string, "null"]}
        lag:
          type: object
          required: [bytes, seconds, retained_bytes]
          properties:
            bytes: {type: [integer, "null"], description: "From the slot's confirmed position to the server's."}
            seconds: {type: [number, "null"], description: "Since the decoder last read everything flushed."}
            retained_bytes: {type: [integer, "null"], description: "WAL the slot keeps on the database (from restart_lsn)."}
        wal_status: {type: [string, "null"], example: reserved}
        watching: {type: array, items: {type: string}}
        last_commit:
          type: [object, "null"]
          required: [lsn, at]
          properties:
            lsn: {type: string}
            at: {type: [string, "null"], format: date-time}
        last_heartbeat_at: {type: [string, "null"], format: date-time}
        frames:
          type: object
          description: Frames stored, per class.
          additionalProperties: {type: integer}
        recent:
          type: array
          items:
            type: object
            required: [commit_lsn, class, action, counts, flags, at]
            properties:
              commit_lsn: {type: [string, "null"]}
              class: {type: string, enum: [bound, unattributed, forged_marker, forged_marker_nontx, marker_count, marker_mismatch,
                foreign_ledger, publication_gap, missing]}
              action: {type: [string, "null"]}
              counts:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    insert: {type: integer}
                    update: {type: integer}
                    delete: {type: integer}
                    truncate: {type: integer}
              flags: {type: array, items: {type: string}}
              at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    DecoderDropped:
      type: object
      required: [object, connector, name, slot, dropped, deed, status]
      properties:
        object: {type: string, enum: [decoder]}
        connector: {type: string}
        name: {type: string}
        slot: {type: string}
        dropped: {type: boolean, enum: [true]}
        deed: {type: string, description: "The decoder.slot_dropped admin deed."}
        status: {type: string, enum: [dropped]}
    SealedFile:
      type: object
      additionalProperties: false
      required: [object, sha256, bytes, media_type, label, deed]
      properties:
        object: {type: string, const: sealed_file}
        sha256: {type: string, pattern: "^sha256:[0-9a-f]{64}$"}
        bytes: {type: integer, minimum: 0}
        media_type: {type: string}
        label: {type: string}
        deed:
          $ref: "#/components/schemas/Deed"
    ConnectorSetup:
      type: object
      required: [object, connector, plan_sha256, changes, deed]
      properties:
        object: {type: string, const: connector_setup}
        connector: {type: string, description: "con_…"}
        plan_sha256: {type: string}
        changes: {type: integer}
        deed: {type: string, description: "deed_… of connector.postgres_initialized"}
    ConnectorTypeList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items:
            $ref: "#/components/schemas/ConnectorType"
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    ConnectorType:
      type: object
      description: >-
        One connector type in the catalog (S30 TypeInfo v2, additive to S16's shape): what it does,
        its credentials and setup, honest limits, and its fixed tools with what the Witness checks.
      required: [object, type, title, levels, credential_kinds, config_schema, docs_url, summary, status, icon, modes, setup, limits, tools_dynamic, tools]
      properties:
        object: {type: string, const: connector_type}
        type: {type: string}
        title: {type: string}
        description: {type: string}
        levels: {type: array, items: {type: string}}
        credential_kinds:
          type: array
          items:
            type: object
            properties:
              kind: {type: string}
              fields:
                type: array
                items:
                  type: object
                  properties:
                    name: {type: string}
                    secret: {type: boolean}
                    help: {type: string}
                    prefix_hint: {type: string}
        config_schema: {type: object}
        docs_url: {type: string}
        summary: {type: string}
        status: {type: string, enum: [stable, beta, dev]}
        icon: {type: string, description: A Lucide icon name.}
        modes: {type: array, items: {type: string, enum: [sandbox_only, drafts_only, dev_only]}}
        setup:
          type: array
          items:
            type: object
            required: [title]
            properties:
              title: {type: string}
              detail: {type: string}
              command: {type: string}
              url: {type: string}
        limits: {type: array, items: {type: string}}
        tools_dynamic: {type: boolean, description: True when tools come from config (rest) or the server (mcp_http).}
        tools:
          type: array
          items:
            type: object
            required: [object, name, title, description, side_effect, idempotent, levels, read_back, input_schema]
            properties:
              object: {type: string, const: tool}
              name: {type: string}
              title: {type: string}
              description: {type: string}
              side_effect: {type: string, enum: [read, write, destructive, unknown]}
              idempotent: {type: boolean}
              levels: {type: array, items: {type: string}}
              read_back: {type: string}
              requires_scope: {type: string}
              input_schema: {type: object}
    Event:
      type: object
      description: >-
        One event, on the stream and in webhooks (contract §8). On the stream, `hydrated` says
        whether `data.object` is the REST list shape (true) or the thin webhook object (false).
      required: [object, id, type, api_version, created_at, livemode, org, data]
      properties:
        object: {type: string, const: event}
        id: {type: string, description: "The event id (evt_…)."}
        type:
          type: string
          example: action.witnessed
          description: >-
            One of `action.created`, `action.decided`, `action.executed`, `action.witnessed`,
            `deed.sealed`, `approval.requested`, `approval.decided`, `agent.stopped`,
            `agent.resumed`, `agent.suspended`, `org.stopped`, `org.started`,
            `anomaly.detected` (S27), `policy.activated`, `decoder.degraded`,
            `decoder.recovered`, `decoder.alert` and `webhook.test`. New types are added.
        api_version: {type: string}
        created_at: {type: string, format: date-time}
        livemode: {type: boolean}
        org: {type: string}
        hydrated: {type: boolean, description: Stream only.}
        data:
          type: object
          required: [object]
          properties:
            object:
              type: object
              description: >-
                An action, approval or agent in its list shape, a DeedSummary, or the thin
                object (ids and states) when not hydrated. `anomaly.detected` always carries
                the thin AnomalyEventObject (`object: anomaly`, `id`, `agent`, `kind`,
                `tool` for `new_tool`, `severity`, `status`).
    DeedSummary:
      type: object
      required: [object, id, seq, kind, action, issued_at]
      properties:
        object: {type: string, const: deed_summary}
        id: {type: string}
        seq: {type: integer}
        kind: {type: string, enum: [action, amendment, admin]}
        action: {type: [string, "null"]}
        issued_at: {type: string, format: date-time}
    StreamHello:
      type: object
      required: [object, instance, resumed, heartbeat_s, server_time]
      properties:
        object: {type: string, const: stream}
        instance: {type: string, description: The instance part of this stream's cursors.}
        resumed: {type: boolean, description: True when the cursor you sent was replayed from.}
        heartbeat_s: {type: integer, example: 15}
        server_time: {type: string, format: date-time}
    StreamResync:
      type: object
      required: [object, reason]
      properties:
        object: {type: string, const: resync}
        reason: {type: string, enum: [cursor_expired, other_instance, lagged, bus_reconnected]}
    StreamSessionEnded:
      type: object
      required: [code, message]
      properties:
        code: {type: string, example: api_key_revoked}
        message: {type: string}
    Version:
      type: object
      required: [object, version, commit, built_at, go, api_versions, default_api_version]
      properties:
        object:
          type: string
          const: version
        version:
          type: string
        commit:
          type: string
        built_at:
          type: [string, "null"]
          format: date-time
        go:
          type: string
        api_versions:
          type: array
          items:
            type: string
        default_api_version:
          type: string
      examples:
        - object: version
          version: 0.1.0-dev
          commit: 5495f7e
          built_at: "2026-10-01T10:00:00Z"
          go: go1.27.1
          api_versions: ["2026-10-01"]
          default_api_version: "2026-10-01"
    ReplayRequest:
      type: object
      additionalProperties: false
      description: >-
        What to replay over. Every field is optional: the window defaults to the 7 days before
        `to`, and `to` to now; the baseline to the decisions recorded at the time.
      properties:
        from: {type: string, format: date-time, description: The window's start (RFC 3339).}
        to: {type: string, format: date-time, description: The window's end (RFC 3339). At most 90 days after `from`.}
        baseline:
          type: object
          additionalProperties: false
          properties:
            type: {type: string, enum: [recorded, version], default: recorded}
            version: {type: integer, minimum: 1, description: '`version` only: the version of the same policy to compare with.'}
    InsightsActor:
      type: object
      required: [type, id]
      description: Who asked for it.
      properties:
        type: {type: string, enum: [user, api_key, system]}
        id: {type: string, description: '`usr_…`, `key_…`, or `deedglass` for the system.'}
    InsightsWindow:
      type: object
      required: [from, to]
      properties:
        from: {type: string, format: date-time}
        to: {type: string, format: date-time}
    ReplayCandidate:
      type: object
      required: [name, version, version_hash]
      description: The version replayed.
      properties:
        policy: {type: string, description: The policy id (`pol_…`).}
        name: {type: string}
        version: {type: integer, minimum: 0, description: The version number (0 for a draft).}
        version_id: {type: string, description: The version id (`pv_…`). Absent for a draft.}
        version_hash:
          $ref: "#/components/schemas/Digest"
        source_hash:
          allOf:
            - $ref: "#/components/schemas/Digest"
          description: A draft's source hash. Absent for a stored version.
    ReplayBaseline:
      type: object
      required: [type, version]
      description: >-
        What the candidate is compared with: the decisions recorded at the time (`recorded`), or
        another version of the same policy evaluated over the same inputs (`version`).
      properties:
        type: {type: string, enum: [recorded, version]}
        version: {type: integer, minimum: 0, description: The baseline version number; 0 for `recorded`.}
        version_id: {type: string, description: '`version` only (`pv_…`).'}
        version_hash:
          allOf:
            - $ref: "#/components/schemas/Digest"
          description: '`version` only.'
    ReplayProgress:
      type: object
      required: [processed, total, percent]
      properties:
        processed: {type: integer, minimum: 0}
        total: {type: [integer, "null"], minimum: 0, description: The actions in the window, counted when the replay was queued.}
        percent: {type: integer, minimum: 0, maximum: 100, description: processed / total, an integer.}
    ReplaySummary:
      type: object
      required: [actions, evaluated, unchanged, changed, rule_changed, unknown]
      description: '`actions = evaluated + unknown` and `evaluated = unchanged + changed + rule_changed`.'
      properties:
        actions: {type: integer, minimum: 0}
        evaluated: {type: integer, minimum: 0}
        unchanged: {type: integer, minimum: 0}
        changed: {type: integer, minimum: 0, description: The decision would differ.}
        rule_changed: {type: integer, minimum: 0, description: The same decision, reached by a different rule.}
        unknown: {type: integer, minimum: 0, description: Counted but not evaluated (see `unknown`); never guessed.}
    HumanOutcome:
      type: object
      required: [approved, denied, expired, cancelled, pending]
      description: >-
        What people did with the actions of a transition out of `require_approval` (for a replay,
        a transition from it; for the shadow report, an enforced `require_approval`).
      properties:
        approved: {type: integer, minimum: 0}
        denied: {type: integer, minimum: 0}
        expired: {type: integer, minimum: 0}
        cancelled: {type: integer, minimum: 0}
        pending: {type: integer, minimum: 0}
        not_requested: {type: integer, minimum: 0, description: 'A `version` baseline: the decision recorded at the time wasn''t an approval.'}
    RuleCount:
      type: object
      required: [rule_id, count]
      properties:
        rule_id: {type: string, description: 'The deciding rule; `@default` for the policy''s default.'}
        count: {type: integer, minimum: 0}
    ToolCount:
      type: object
      required: [tool, count]
      properties:
        tool: {type: string}
        count: {type: integer, minimum: 0}
    AgentCount:
      type: object
      required: [agent, count]
      properties:
        agent: {type: string, description: The agent id (`agt_…`).}
        name: {type: string}
        count: {type: integer, minimum: 0}
    Transition:
      type: object
      required: [from, to, count, by_rule, by_tool, samples]
      description: Every action whose decision would change from `from` (the baseline) to `to` (the candidate).
      properties:
        from: {type: string, enum: [allow, deny, require_approval]}
        to: {type: string, enum: [allow, deny, require_approval]}
        count: {type: integer, minimum: 1}
        human_outcome:
          $ref: "#/components/schemas/HumanOutcome"
        by_rule:
          type: array
          description: The candidate's deciding rule, most actions first.
          items: {$ref: "#/components/schemas/RuleCount"}
        by_tool:
          type: array
          items: {$ref: "#/components/schemas/ToolCount"}
        samples:
          type: array
          maxItems: 20
          description: The first 20 action ids by decision time.
          items: {type: string}
    RuleChange:
      type: object
      required: [decision, from_rule, to_rule, count, samples]
      description: The same decision, reached by a different rule.
      properties:
        decision: {type: string, enum: [allow, deny, require_approval]}
        from_rule: {type: string}
        to_rule: {type: string}
        count: {type: integer, minimum: 1}
        samples:
          type: array
          maxItems: 20
          items: {type: string}
    UnknownCounts:
      type: object
      required: [input_pruned, input_missing, input_version_unsupported, input_unreadable, evaluation_time_limit]
      description: Actions a replay counted but couldn't evaluate, by reason.
      properties:
        input_pruned: {type: integer, minimum: 0, description: Retention removed the stored decision input.}
        input_missing: {type: integer, minimum: 0, description: Recorded before decision inputs were stored.}
        input_version_unsupported: {type: integer, minimum: 0, description: A stored input in a newer format.}
        input_unreadable: {type: integer, minimum: 0, description: A stored input that doesn't decode.}
        evaluation_time_limit: {type: integer, minimum: 0, description: The evaluation hit its time limit.}
    SQLGuardDrift:
      type: object
      required: [recorded, current]
      description: >-
        The sqlguard versions recorded with the decisions, next to the current one. SQL facts are
        computed again at replay, so a difference is shown, never hidden.
      properties:
        recorded: {type: array, items: {type: string}}
        current: {type: string}
    Replay:
      type: object
      required: [object, id, status, purpose, candidate, baseline, window, progress, requested_by, attempts, created_at, started_at, finished_at, error]
      description: >-
        A replay job (S27): what it replays, its progress and, once `status` is `succeeded`, its
        result (`summary` through `elapsed_ms`; `transitions` and `rule_changes` are absent when
        there are none). `elapsed_ms` is not part of `result_digest`.
      properties:
        object: {type: string, const: replay}
        id: {type: string, pattern: "^rpl_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        status: {type: string, enum: [queued, running, succeeded, failed, cancelled]}
        purpose: {type: string, enum: [manual, suggestion]}
        candidate:
          $ref: "#/components/schemas/ReplayCandidate"
        baseline:
          $ref: "#/components/schemas/ReplayBaseline"
        window:
          $ref: "#/components/schemas/InsightsWindow"
        progress:
          $ref: "#/components/schemas/ReplayProgress"
        requested_by:
          $ref: "#/components/schemas/InsightsActor"
        attempts: {type: integer, minimum: 0, description: Runs started. A runner that lost its lease starts again from zero.}
        created_at: {type: string, format: date-time}
        started_at: {type: [string, "null"], format: date-time}
        finished_at: {type: [string, "null"], format: date-time}
        error:
          type: [object, "null"]
          required: [code, message]
          description: A failed replay's cause.
          properties:
            code: {type: string, enum: [candidate_invalid, replay_too_large, replay_window_invalid, internal_error]}
            message: {type: string}
        summary:
          $ref: "#/components/schemas/ReplaySummary"
        transitions:
          type: array
          items: {$ref: "#/components/schemas/Transition"}
        rule_changes:
          type: array
          items: {$ref: "#/components/schemas/RuleChange"}
        unknown:
          $ref: "#/components/schemas/UnknownCounts"
        sqlguard:
          $ref: "#/components/schemas/SQLGuardDrift"
        result_digest:
          allOf:
            - $ref: "#/components/schemas/Digest"
          description: The same inputs, candidate, baseline and sqlguard version give the same digest.
        elapsed_ms: {type: integer, minimum: 0}
    ReplayList:
      type: object
      required: [object, data, has_more, next_cursor]
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/Replay"}
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
    CostRow:
      type: object
      required: [currency, total, p50, p95]
      description: One currency's cost, in integer minor units (cents for usd).
      properties:
        currency: {type: string, examples: [usd]}
        total: {type: integer}
        p50: {type: number}
        p95: {type: number}
    StatsTrend:
      type: object
      required: [previous]
      description: >-
        Against the previous window of the same length: `delta_pct` for counts, `delta_pp`
        (percentage points) for rates. Only `previous: null` when that window has fewer than 20
        actions.
      properties:
        delta_pct: {type: number}
        delta_pp: {type: number}
        previous: {type: [number, "null"]}
    StatsGroup:
      type: object
      required: [actions, decisions, approval, deny_rate, verdicts, levels, executed, mismatch_rate, unverifiable_rate, latency_ms, cost]
      description: >-
        One group's numbers (or the totals, which have no `key`). Rates are rounded to 4 decimals:
        `deny_rate = deny / actions` with Stop decisions left out of both, `mismatch_rate` and
        `unverifiable_rate` over `executed`.
      properties:
        key:
          type: object
          description: '`rule_id`, `tool`, or `agent` with its `name`, by `by`.'
          properties:
            rule_id: {type: string}
            tool: {type: string}
            agent: {type: string}
            name: {type: string}
        versions: {type: array, items: {type: integer}, description: '`by=rule`: the policy versions whose rule decided.'}
        actions: {type: integer, minimum: 0}
        decisions:
          type: object
          required: [allow, deny, require_approval]
          properties:
            allow: {type: integer, minimum: 0}
            deny: {type: integer, minimum: 0}
            require_approval: {type: integer, minimum: 0}
        approval:
          type: object
          required: [requested, approved, denied, expired, cancelled, pending, rate, median_seconds, p95_seconds]
          properties:
            requested: {type: integer, minimum: 0}
            approved: {type: integer, minimum: 0}
            denied: {type: integer, minimum: 0}
            expired: {type: integer, minimum: 0}
            cancelled: {type: integer, minimum: 0}
            pending: {type: integer, minimum: 0}
            rate: {type: [number, "null"], description: approved / (approved + denied); null when none was decided.}
            median_seconds: {type: [integer, "null"], description: From the request to the decision.}
            p95_seconds: {type: [integer, "null"]}
        deny_rate: {type: number}
        verdicts:
          type: object
          required: [VERIFIED, MISMATCH, UNVERIFIABLE, none]
          properties:
            VERIFIED: {type: integer, minimum: 0}
            MISMATCH: {type: integer, minimum: 0}
            UNVERIFIABLE: {type: integer, minimum: 0}
            none: {type: integer, minimum: 0, description: Actions with no verdict (not executed).}
        levels:
          type: object
          required: [L0, L1, L2]
          properties:
            L0: {type: integer, minimum: 0}
            L1: {type: integer, minimum: 0}
            L2: {type: integer, minimum: 0}
        executed: {type: integer, minimum: 0}
        mismatch_rate: {type: number}
        unverifiable_rate: {type: number}
        latency_ms:
          type: object
          required: [p50, p95]
          properties:
            p50: {type: [integer, "null"]}
            p95: {type: [integer, "null"]}
        cost:
          type: array
          items: {$ref: "#/components/schemas/CostRow"}
        trend:
          type: object
          description: '`compare=previous` only. Keys: `actions`, `deny_rate`, `mismatch_rate`, `unverifiable_rate`, and `approval_rate` when both windows have one.'
          additionalProperties:
            $ref: "#/components/schemas/StatsTrend"
    BudgetUse:
      type: object
      required: [agent, budget, kind, limit, max_utilisation, days_over_80, days]
      description: One budget's daily use over the window, by the org's days.
      properties:
        agent: {type: [string, "null"], description: The agent (`agt_…`) for an agent budget; null for a policy budget.}
        name: {type: string, description: The agent's name, for an agent budget.}
        budget: {type: string, description: 'A policy budget id, or `agent.calls_per_day` / `agent.spend_per_day`.'}
        kind: {type: string, enum: [policy, agent]}
        limit:
          type: object
          description: A calls limit, or an amount in minor units with its currency.
          properties:
            calls: {type: integer}
            amount: {type: integer}
            currency: {type: string}
        max_utilisation: {type: number}
        days_over_80: {type: integer, minimum: 0}
        days:
          type: array
          items:
            type: object
            required: [date, used, utilisation]
            properties:
              date: {type: string, format: date}
              used: {type: integer, minimum: 0}
              utilisation: {type: number}
    SeriesRow:
      type: object
      required: [date, actions, denied, verified, mismatch, unverifiable]
      description: One org-local day.
      properties:
        date: {type: string, format: date}
        actions: {type: integer, minimum: 0}
        denied: {type: integer, minimum: 0}
        verified: {type: integer, minimum: 0}
        mismatch: {type: integer, minimum: 0}
        unverifiable: {type: integer, minimum: 0}
    InsightsStats:
      type: object
      required: [object, window, timezone, by, filters, totals, groups, budgets, series, generated_at, empty]
      properties:
        object: {type: string, const: insights_stats}
        window:
          type: object
          required: [from, to, days]
          properties:
            from: {type: string, format: date-time}
            to: {type: string, format: date-time}
            days: {type: number}
        timezone: {type: string, description: The org's time zone; days and hours follow it.}
        by: {type: string, enum: [rule, tool, agent, none]}
        filters:
          type: object
          required: [agent, tool]
          properties:
            agent: {type: [string, "null"]}
            tool: {type: [string, "null"]}
        totals:
          $ref: "#/components/schemas/StatsGroup"
        groups:
          type: array
          description: Empty for `by=none`.
          items: {$ref: "#/components/schemas/StatsGroup"}
        budgets:
          type: array
          items: {$ref: "#/components/schemas/BudgetUse"}
        series:
          type: array
          items: {$ref: "#/components/schemas/SeriesRow"}
        generated_at: {type: string, format: date-time}
        empty: {type: [string, "null"], description: The sentence when the window has no actions.}
    ShadowVersion:
      type: object
      required: [policy, name, version, version_id, live]
      properties:
        policy: {type: string, description: The policy id (`pol_…`).}
        name: {type: string}
        version: {type: integer, minimum: 1}
        version_id: {type: string, description: The version id (`pv_…`).}
        live: {type: boolean, description: It is the active shadow version now.}
        since: {type: string, format: date-time, description: When it became the shadow version.}
    ShadowTransition:
      type: object
      required: [enforce, shadow, count, by_rule, by_tool, by_agent, samples]
      description: Every action the shadow version would have decided differently.
      properties:
        enforce: {type: string, enum: [allow, deny, require_approval]}
        shadow: {type: string, enum: [allow, deny, require_approval]}
        count: {type: integer, minimum: 1}
        human_outcome:
          $ref: "#/components/schemas/HumanOutcome"
        by_rule:
          type: array
          description: The shadow version's deciding rule.
          items: {$ref: "#/components/schemas/RuleCount"}
        by_tool:
          type: array
          items: {$ref: "#/components/schemas/ToolCount"}
        by_agent:
          type: array
          items: {$ref: "#/components/schemas/AgentCount"}
        samples:
          type: array
          maxItems: 20
          items: {type: string}
    ShadowReport:
      type: object
      required: [object, shadow, enforce, window, summary, transitions, sentence]
      properties:
        object: {type: string, const: shadow_report}
        shadow:
          $ref: "#/components/schemas/ShadowVersion"
        enforce:
          type: array
          description: The enforced versions that decided the report's actions.
          items:
            type: object
            required: [name, version, version_id, actions]
            properties:
              name: {type: string}
              version: {type: integer}
              version_id: {type: string}
              actions: {type: integer, minimum: 0}
        window:
          type: object
          required: [from, to]
          properties:
            from: {type: [string, "null"], format: date-time}
            to: {type: [string, "null"], format: date-time}
        summary:
          type: object
          required: [evaluated, agree, disagree, agreement]
          properties:
            evaluated: {type: integer, minimum: 0}
            agree: {type: integer, minimum: 0}
            disagree: {type: integer, minimum: 0}
            agreement: {type: number, description: agree / evaluated, 4 decimals.}
        transitions:
          type: array
          items: {$ref: "#/components/schemas/ShadowTransition"}
        sentence: {type: string, description: The one line the Console and the CLI print.}
    AnomalyKind:
      type: string
      enum: [new_tool, volume_spike, off_hours, deny_spike, mismatch_spike, user_ref_burst]
    Anomaly:
      type: object
      required: [object, id, agent, kind, tool, severity, score, title, status, active, active_until, window, occurrences, evidence, detector_version, acknowledged, resolved, created_at, updated_at]
      description: >-
        An anomaly episode (S27): the unresolved series of findings for one agent, kind and tool.
        A signal for a person to review, not a verdict. `active` (until `active_until`) is what
        `anomaly.active` sees; `status` is the review inbox.
      properties:
        object: {type: string, const: anomaly}
        id: {type: string, pattern: "^anm_[0-7][0-9a-hjkmnp-tv-z]{25}$"}
        agent:
          type: object
          required: [id, name]
          properties:
            id: {type: string}
            name: {type: string}
        kind:
          $ref: "#/components/schemas/AnomalyKind"
        tool: {type: [string, "null"], description: '`new_tool` only.'}
        severity: {type: string, enum: [info, warn, high]}
        score: {type: number}
        title: {type: string, description: Numbers first, in the org's time zone.}
        status: {type: string, enum: [open, acknowledged, resolved]}
        active: {type: boolean}
        active_until: {type: string, format: date-time}
        window:
          type: object
          required: [start, end]
          description: When the episode's findings happened (decision time).
          properties:
            start: {type: string, format: date-time}
            end: {type: string, format: date-time}
        occurrences: {type: integer, minimum: 1}
        evidence:
          type: object
          description: >-
            What the detector saw. Always `hour`, `timezone`, `window_start`, `window_end`,
            `history_hours`, `points` (at most 48 hourly points) and `action_ids` (at most 10).
            By kind: `count`, `factor`, `writes`, `share`, `deny_rate`, `baseline_rate`,
            `mismatches`, `new_refs`, `baseline_mean`, `baseline_stddev`, `tool`, `side_effect`.
          properties:
            hour: {type: string}
            timezone: {type: string}
            window_start: {type: string, format: date-time}
            window_end: {type: string, format: date-time}
            history_hours: {type: integer}
            count: {type: integer}
            points:
              type: array
              maxItems: 48
              items:
                type: object
                properties:
                  count: {type: number}
                  score: {type: number}
                  window_start: {type: string, format: date-time}
                  window_end: {type: string, format: date-time}
            action_ids: {type: array, maxItems: 10, items: {type: string}}
        detector_version: {type: string}
        acknowledged:
          type: [object, "null"]
          required: [by, at]
          properties:
            by: {type: string, description: '`usr_…` or `key_…`.'}
            at: {type: string, format: date-time}
        resolved:
          type: [object, "null"]
          required: [by, at, reason, deed]
          properties:
            by: {type: string, description: '`usr_…`, `key_…`, or `system` for the 14-day close.'}
            at: {type: string, format: date-time}
            reason: {type: [string, "null"]}
            deed: {type: [string, "null"], description: The `anomaly.resolved` admin deed; null for the 14-day close.}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    AnomalyList:
      type: object
      required: [object, data, has_more, next_cursor, active_now, anomaly_facts, notice, baseline_building]
      description: The list envelope, plus what the inbox says about the org (additive fields).
      properties:
        object: {type: string, const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/Anomaly"}
        has_more: {type: boolean}
        next_cursor: {type: [string, "null"]}
        active_now: {type: integer, minimum: 0, description: Episodes active now.}
        anomaly_facts: {type: boolean, description: This org's policies see `anomaly.*`.}
        notice: {type: [string, "null"], description: The sentence when a policy reads anomaly facts but they are off.}
        baseline_building:
          type: array
          description: Agents with under 72 hours of history; only `new_tool` can fire for them.
          items:
            type: object
            required: [agent, name, hours, sentence]
            properties:
              agent: {type: string}
              name: {type: string}
              hours: {type: integer, minimum: 0}
              sentence: {type: string}
    AnomalyEventObject:
      type: object
      required: [object, id, agent, kind, severity, status]
      description: >-
        `data.object` of `anomaly.detected` (S27): the episode as ids and states. Sent when an
        episode opens and on each new occurrence, never per finding.
      properties:
        object: {type: string, const: anomaly}
        id: {type: string, description: The anomaly id (`anm_…`).}
        agent: {type: string, description: The agent id (`agt_…`).}
        kind:
          $ref: "#/components/schemas/AnomalyKind"
        tool: {type: string, description: '`new_tool` only.'}
        severity: {type: string, enum: [info, warn, high]}
        status: {type: string, const: open, description: An episode is open when it is detected.}
      examples:
        - {object: anomaly, id: anm_01m42vb27wzpyw0b2n6aycjxkh, agent: agt_01m42v9q4500m4rfchvb8j53da, kind: volume_spike, severity: high, status: open}
    Suggestion:
      type: object
      description: A policy change Insights proposes, with its evidence, diff, replay and risk (S28). Nothing applies until a person accepts it, and accepting only runs it in shadow.
      required: [object, id, status, kind, generator, title, rationale, phrasing, policy, target_rule_id, evidence, evidence_digest, diff, warnings, replay, risk_note, impact, stale_reason, created_at, decided_by, decided_at, decision_reason, resulting_version, decision_deed]
      properties:
        object: {type: string, const: suggestion}
        id: {type: string, pattern: '^sug_[0-7][0-9a-hjkmnp-tv-z]{25}$'}
        status: {type: string, enum: [open, accepting, accepted, rejected, superseded]}
        kind: {type: string, enum: [loosen, tighten, budget, explicit_rule, advisory]}
        generator: {type: string, enum: [loosen.approval_rate, tighten.mismatch, tighten.verify_level, budget.near_limit, explicit_rule.default_hit, advisory.deny_heavy]}
        title: {type: string}
        rationale: {type: string}
        phrasing:
          type: object
          required: [source]
          properties:
            source: {type: string, enum: [template, claude]}
            model: {type: string}
            reason: {type: string, description: Why the template wording stands (validation:…, refusal, timeout, http_429, …).}
            input_tokens: {type: integer}
            output_tokens: {type: integer}
            cost_usd_micros: {type: integer}
            duration_ms: {type: integer}
            at: {type: string, format: date-time}
            template_title: {type: string, description: When worded by Claude, the template title it replaced.}
        policy:
          type: object
          required: [id, name, base_version, base_version_id, base_version_hash]
          properties:
            id: {type: string}
            name: {type: string}
            base_version: {type: integer, description: The enforced version the suggestion was written for.}
            base_version_id: {type: string}
            base_version_hash: {type: string}
        target_rule_id: {type: [string, "null"]}
        evidence: {type: object, description: 'The numbers that triggered it: window, approvals, median_wait_seconds, approvers, agents, tool, pattern, sample_actions (loosen); mismatches (tighten); days and limits (budget); and so on per generator.'}
        evidence_digest: {type: string}
        diff:
          type: [object, "null"]
          description: The change as unified hunks with 3 lines of context; null for advice.
          properties:
            hunks:
              type: array
              items:
                type: object
                properties:
                  old_start: {type: integer}
                  old_lines: {type: integer}
                  new_start: {type: integer}
                  new_lines: {type: integer}
                  lines: {type: array, items: {type: object, properties: {op: {type: string, enum: [" ", "+", "-"]}, text: {type: string}}}}
            text: {type: string}
            added: {type: integer}
            removed: {type: integer}
        warnings: {type: array, items: {type: object}, description: The candidate's compile warnings (severity warning).}
        replay:
          type: [object, "null"]
          description: The change replayed against the same 14 days, compared with the enforced version.
          properties:
            id: {type: string, description: The replay (`rpl_…`, purpose suggestion).}
            window: {type: object}
            summary: {type: object}
            transitions: {type: array, items: {type: object}}
            result_digest: {type: string}
        risk_note: {type: string}
        impact: {type: integer, description: Actions whose decision the replay changes.}
        stale_reason: {type: [string, "null"], enum: [base_changed, null]}
        created_at: {type: string, format: date-time}
        decided_by: {type: [object, "null"], properties: {type: {type: string, enum: [user, api_key]}, id: {type: string}}}
        decided_at: {type: [string, "null"], format: date-time}
        decision_reason: {type: [string, "null"]}
        resulting_version: {type: [object, "null"], properties: {id: {type: string}, version: {type: integer}, version_hash: {type: string}}}
        decision_deed: {type: [string, "null"]}
        deeds: {type: array, items: {type: string}, description: 'Accept and reject answers only: the deeds sealed, in order.'}
        replaced_shadow_version: {type: integer, description: Accept only, when the shadow it replaced.}
        note: {type: string, description: Accept only, when the change already existed as a version.}
    LearnRun:
      type: object
      required: [object, kind, period, policy, window, actions, created, superseded, unchanged, waiting, dropped, notice, elapsed_ms]
      properties:
        object: {type: string, const: learn_run}
        kind: {type: string, const: suggest}
        period: {type: string}
        policy: {type: [object, "null"]}
        window: {type: object}
        actions: {type: integer}
        created: {type: array, items: {type: object, properties: {id: {type: string}, kind: {type: string}, generator: {type: string}, target_rule_id: {type: string}, target: {type: string}, title: {type: string}, impact: {type: integer}}}}
        superseded: {type: integer}
        unchanged: {type: integer}
        waiting: {type: integer}
        dropped: {type: array, items: {type: object, properties: {generator: {type: string}, target: {type: string}, reason: {type: string}}}}
        notice: {type: [string, "null"]}
        elapsed_ms: {type: integer}
    InsightsSettings:
      type: object
      required: [object, anomaly_facts, anomaly_active_minutes, anomaly_stale_after_minutes, active_policy_reads_anomaly, notice]
      properties:
        object: {type: string, const: insights_settings}
        anomaly_facts: {type: boolean, description: This org's policies see `anomaly.*`. Off by default.}
        anomaly_active_minutes: {type: integer, minimum: 5, maximum: 1440, description: How long an episode stays active after its last finding (default 60).}
        anomaly_stale_after_minutes:
          type: integer
          minimum: 10
          maximum: 240
          description: >-
            With the facts on, a detection pass older than this makes every agent anomalous
            (`detector_stale`), because unknown isn't usual (default 15).
        active_policy_reads_anomaly: {type: boolean, description: The enforced or shadow policy reads `anomaly.*`.}
        notice: {type: [string, "null"], description: The sentence when a policy reads anomaly facts but they are off.}
        llm_phrasing: {type: boolean, description: 'S28: Claude may reword suggestions (off by default; needs the operator''s DEEDGLASS_ANTHROPIC_API_KEY).'}
        llm_monthly_cap_cents: {type: integer, minimum: 0, maximum: 10000, description: The month's phrasing spend cap in cents (default 200).}
        digest: {type: object, properties: {recipients: {type: string, enum: [owners, admins, "off"]}}, description: Who gets the weekly digest (default owners).}
        phrasing_available: {type: boolean, description: The server has an Anthropic key, so llm_phrasing can be on.}
        simulated: {type: boolean, description: 'S28: the org''s history came from `deedglass dev simulate` (the Console shows a banner).'}
        deed: {type: string, description: 'PATCH only, when something changed: the `insights.settings_changed` admin deed.'}
    InsightsSettingsPatch:
      type: object
      additionalProperties: false
      required: [reason]
      description: The fields to change, and why.
      properties:
        anomaly_facts: {type: boolean}
        anomaly_active_minutes: {type: integer, minimum: 5, maximum: 1440}
        anomaly_stale_after_minutes: {type: integer, minimum: 10, maximum: 240}
        llm_phrasing: {type: boolean, description: 'true without DEEDGLASS_ANTHROPIC_API_KEY on the server is 400 `validation_failed` on `llm_phrasing`.'}
        llm_monthly_cap_cents: {type: integer, minimum: 0, maximum: 10000}
        digest: {type: object, additionalProperties: false, required: [recipients], properties: {recipients: {type: string, enum: [owners, admins, "off"]}}}
        reason: {type: string, minLength: 3, maxLength: 280, description: One line, 3 to 280 characters. It goes into the deed.}
