{
  "openapi": "3.1.0",
  "info": {
    "title": "doublegate client gate — loopback HTTP",
    "version": "0.1.1.dev0",
    "summary": "The app, the gate's verbs as JSON-RPC and the MCP binding, on one loopback port.",
    "description": "One process on one loopback port, serving four kinds of route: pages that carry no data and need no credential (the app, whose API view renders this document at `/app#/api`); `POST /rpc` (the gate's verbs as JSON-RPC), `POST /mcp` and `GET /openapi.json`, which need a credential — a person's sign-in token or a program's key; two descriptions anyone may read, `GET /identity` and `GET /mcp/catalog.json`; and, while this gate is its own sign-in provider, that provider's five routes.\n\nGenerated from the gate's own route table and checked against it on every build, so this document cannot claim something the gate does not do.\n\n**A refusal is not an HTTP error.** `POST /rpc` carries a verb's JSON-RPC refusal inside a `200`. Read the body, not the status line.\n\n**There is no REST mapping of the verbs.** They reach HTTP only through `POST /rpc` as JSON-RPC, verbatim. Do not construct `POST /api/dg/<verb>` or any other `/api/*` path; this gate has no such layer, and its own app calls `/rpc` like every other client.\n\n**A receipt is not an admission.** `dg.ingest` (`doublegate.remember` over MCP) answers with the claim's id and says it is held; nobody else can read it until a reviewer who did not write it promotes it.\n\n**The role is the permission.** Every verb names the least role it needs: reader, agent, reviewer or admin. The account that runs the gate is `admin` on its local socket, any other local account is `agent`, and a credential carries the role this gate gave the person or the key. There is no separate console password.",
    "license": {
      "name": "see repository"
    },
    "x-dg-design": [
      "ADR-0082",
      "AUTH-4",
      "ADR-0119 d13",
      "GAPS 152"
    ],
    "x-dg-config": [
      "[auth].issuer"
    ]
  },
  "servers": [
    {
      "url": "/",
      "description": "this gate, on the address that served this document; where it listens is this gate's own configuration, not part of the contract",
      "x-dg-config": [
        "[console].bind"
      ]
    }
  ],
  "tags": [
    {
      "name": "description",
      "description": "the surface describing itself"
    },
    {
      "name": "pages",
      "description": "the app's host page: no credential, no data; the port alone lands on it"
    },
    {
      "name": "issuer",
      "description": "this gate's own sign-in provider; 404 when the gate signs people in with another provider",
      "x-dg-design": [
        "ADR-0082 d3"
      ],
      "x-dg-config": [
        "[auth].issuer"
      ]
    },
    {
      "name": "rpc",
      "description": "the gate's verbs as JSON-RPC over loopback HTTP"
    },
    {
      "name": "mcp",
      "description": "the MCP binding agents call"
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "summary": "This document: OpenAPI 3.1 for this gate's loopback port.",
        "tags": [
          "description"
        ],
        "description": "Generated from the same route table as the copy kept with the gate's source. It needs a credential, of any role, because it names every route and what guards it: describing the surface is access to it, not a way around it.",
        "responses": {
          "200": {
            "description": "the OpenAPI 3.1.0 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "an OpenAPI 3.1.0 document"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one the gate refuses: {\"error\":\"<kind>: <why>\",\"kind\":\"missing|malformed|expired|issuer|audience|signature|unavailable|unsupported\"}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "the request is larger than this gate accepts (4 MiB unless its configuration says otherwise). Not observed in tests: the gate refuses before reading the body, which a client sees as a reset connection rather than a status line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          }
        ],
        "x-dg-guard": "bearer",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082"
        ],
        "x-dg-config": [
          "[mcp].max_request_bytes"
        ]
      }
    },
    "/mcp/catalog.json": {
      "get": {
        "operationId": "getMcpCatalog",
        "summary": "The MCP surface as JSON Schema: methods, envelopes, content union, per-tier tools.",
        "tags": [
          "description",
          "mcp"
        ],
        "description": "Keyless, exactly like `POST /mcp` beside it: the catalog discloses the tool list that endpoint already answers to `tools/list`, and nothing else.",
        "responses": {
          "200": {
            "description": "the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpCatalog"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "none",
        "x-dg-probed": true
      }
    },
    "/identity": {
      "get": {
        "operationId": "getIdentity",
        "summary": "Who this gate is: its deployment id, how a person signs in here (`auth`), and the gates it knows.",
        "tags": [
          "description"
        ],
        "description": "No credential needed, and public by nature: a gate's id is a public key, and `auth` is what an app needs before it can sign a person in — the sign-in provider, its address, the app's client id, whether this gate can check sign-in tokens, and how many providers and configured admins it trusts; never a secret. `gates` is empty: this gate names no other service. Answers the browser's `OPTIONS` preflight, with cross-origin headers for an app origin this gate's configuration lists, and for no other.",
        "responses": {
          "200": {
            "description": "deployment_id, role, auth{issuer, issuer_url, client_id, identity_extra, trusted_issuers, admins}, gates"
          }
        },
        "security": [],
        "x-dg-guard": "none",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082 d2",
          "ADR-0112 d1"
        ],
        "x-dg-config": [
          "[console].origins"
        ]
      }
    },
    "/.well-known/openid-configuration": {
      "get": {
        "operationId": "getOpenIdConfiguration",
        "summary": "OpenID Connect discovery for this gate's own sign-in provider.",
        "tags": [
          "issuer"
        ],
        "description": "The provider, its four endpoints, `id_token_signing_alg_values_supported: [EdDSA]` and `code_challenge_methods_supported: [S256]`. 404 when this gate signs people in with another provider. Carries cross-origin headers for a listed app origin.",
        "responses": {
          "200": {
            "description": "the discovery document"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true,
        "x-dg-design": [
          "AUTH-2",
          "ADR-0082 d3"
        ],
        "x-dg-config": [
          "[auth].issuer",
          "[console].origins"
        ]
      }
    },
    "/oauth/jwks": {
      "get": {
        "operationId": "getJwks",
        "summary": "The deployment key's public half as one OKP Ed25519 JWK, kid = the deployment id.",
        "tags": [
          "issuer"
        ],
        "description": "What every gate in the group verifies this issuer's access tokens against (RFC 7517).",
        "responses": {
          "200": {
            "description": "{keys: [JWK]}"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true
      }
    },
    "/oauth/authorize": {
      "get": {
        "operationId": "getSignInPage",
        "summary": "The sign-in page: email and password, nothing else on it.",
        "tags": [
          "issuer"
        ],
        "description": "Data-free, no script from anywhere, a form that posts back here. A request whose client_id, redirect_uri or challenge fails the check is answered on this page with one line and never redirected (RFC 6749 §4.1.2.1).",
        "parameters": [
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "description": "always code",
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "description": "the console's client id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "description": "this gate's own /app or a listed console origin's /app",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "description": "the console's CSRF token, echoed back",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "description": "base64url(SHA-256(verifier)) (RFC 7636)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "description": "always S256",
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "openid email profile",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the sign-in page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "a malformed authorization request, said on the page"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true
      },
      "post": {
        "operationId": "postSignIn",
        "summary": "Verify the email and password against the people file; mint a one-minute code.",
        "tags": [
          "issuer"
        ],
        "description": "A correct password answers `303 Location: <redirect_uri>?code=…&state=…`; the code is single-use, one minute, bound to the challenge, the client id and the redirect_uri. A wrong password re-renders the page with one line, the same line for an unknown email.",
        "requestBody": {
          "description": "application/x-www-form-urlencoded: the query fields above plus email and password",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignInForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the page again, with one line",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "signed in: Location carries code and state"
          },
          "400": {
            "description": "a malformed authorization request, said on the page"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true
      }
    },
    "/oauth/token": {
      "post": {
        "operationId": "postToken",
        "summary": "Exchange a code with its PKCE verifier, rotate a refresh token, or exchange a console session for an answer ticket.",
        "tags": [
          "issuer"
        ],
        "description": "Answers `{access_token, id_token, token_type: Bearer, expires_in: 3600}` and sets the refresh token as the `dg_refresh` cookie (HttpOnly, SameSite=Lax, Path=/oauth/token). The access token is an EdDSA JWT signed by the deployment key (iss = this issuer, aud = the client id, one hour); the refresh token is opaque, thirty days, rotated on every use, read from the cookie or the `refresh_token` field. A refresh grant that presents neither answers `{session: false}` and sets nothing. While the gate names an answer origin, the token exchange takes the console's own unexpired access token as `subject_token` (type `urn:ietf:params:oauth:token-type:access_token`) and `resource` that origin exactly, and answers `{access_token, issued_token_type, token_type: Bearer, expires_in: 300}`: a ticket whose aud is the answer origin, which `/rpc` refuses; an API key is no subject token it takes, an `audience` is `invalid_target` and an `actor_token` `invalid_request`. Errors are RFC 6749 §5.2 and RFC 8693 §2.2.2: `{error, error_description}`. Carries cross-origin headers for a listed app origin.",
        "requestBody": {
          "description": "application/x-www-form-urlencoded: grant_type=authorization_code with code, code_verifier, client_id, redirect_uri; or grant_type=refresh_token with refresh_token; or grant_type=urn:ietf:params:oauth:grant-type:token-exchange with subject_token, subject_token_type, requested_token_type and resource",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the token set, or {session: false} for a refresh grant that presents no token, or a five-minute answer ticket"
          },
          "400": {
            "description": "invalid_request | invalid_grant | unsupported_grant_type | invalid_target"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "server_error: this gate was installed without its sign-in support (the SDK's identity extra), so no token can be minted"
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0084 d1",
          "GAPS 139",
          "ADR-0128 d6",
          "GAPS 452"
        ],
        "x-dg-config": [
          "[console].origins",
          "[console].answer_origin"
        ]
      }
    },
    "/oauth/logout": {
      "post": {
        "operationId": "postLogout",
        "summary": "Forget the refresh token named; the access token expires on its own.",
        "tags": [
          "issuer"
        ],
        "description": "Sign-out is the app forgetting what it holds in memory, plus this call.",
        "requestBody": {
          "description": "application/x-www-form-urlencoded or JSON: refresh_token",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogoutForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{signed_out: true, forgotten: bool}"
          },
          "404": {
            "description": "this gate is not its own issuer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "local_issuer",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082 d8"
        ]
      }
    },
    "/": {
      "get": {
        "operationId": "getRoot",
        "summary": "The port alone lands on the app.",
        "tags": [
          "pages"
        ],
        "description": "Moved into the app: `302` with `Location: /app`, no body, no query string forwarded. Keyless like the page it replaces.",
        "responses": {
          "302": {
            "description": "moved into the app: Location /app",
            "headers": {
              "Location": {
                "description": "where the route moved to",
                "schema": {
                  "type": "string",
                  "const": "/app"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "none",
        "x-dg-probed": true
      }
    },
    "/app": {
      "get": {
        "operationId": "getAppPage",
        "summary": "The app: one page for the whole gate — what waits for review, work areas, activity, status and access.",
        "tags": [
          "pages"
        ],
        "description": "No credential and no data, like every page, served under a strict same-origin Content-Security-Policy (no inline script or style). Its data block names the sign-in provider and the client id; the app signs the person in there with PKCE, keeps the tokens in memory only, and calls `POST /rpc` with the access token. Its bundle is served under `/app/assets/*` from this port; nothing comes from a third party.",
        "responses": {
          "200": {
            "description": "the page shell — static HTML, no deployment data in the bytes",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [],
        "x-dg-guard": "none",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082 d2"
        ]
      }
    },
    "/rpc": {
      "post": {
        "operationId": "callDaemonRpc",
        "summary": "The gate's verbs (dg.*) as JSON-RPC, verbatim, over loopback HTTP.",
        "tags": [
          "rpc"
        ],
        "description": "Byte for byte the message the gate's local socket reads, handled by the same code. Who is calling is the credential's identity — a person's access token or a program's `dgk_` key — and never anything in the body; what it may do is that identity's role, and a verb above the role is a JSON-RPC refusal inside a 200. There is no REST mapping of the verbs: this endpoint is their whole HTTP surface. Each verb's parameters are in components.schemas, and `dg.describe` lists the verbs with the role each needs. Answers the browser's `OPTIONS` preflight, with cross-origin headers for the one app origin this gate's configuration lists for this device, and for no other.",
        "requestBody": {
          "description": "one JSON-RPC 2.0 message",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              },
              "examples": {
                "describe": {
                  "summary": "the gate describing its own verb catalog",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "dg.describe",
                    "params": {}
                  }
                },
                "recall": {
                  "summary": "read the admitted store",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "dg.recall",
                    "params": {
                      "query": "retention policy",
                      "k": 5
                    }
                  }
                },
                "why": {
                  "summary": "the signed provenance of one artifact",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 3,
                    "method": "dg.why",
                    "params": {
                      "artifact_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the JSON-RPC response — success or a JSON-RPC error object",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "400": {
            "description": "unparsable body (-32700) or non-object JSON (-32600), in a JSON-RPC envelope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcFailure"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one the gate refuses: {\"error\":\"<kind>: <why>\",\"kind\":\"missing|malformed|expired|issuer|audience|signature|unavailable|unsupported\"}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "the request is larger than this gate accepts (4 MiB unless its configuration says otherwise). Not observed in tests: the gate refuses before reading the body, which a client sees as a reset connection rather than a status line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          }
        ],
        "x-dg-guard": "bearer",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082",
          "AUTH-1",
          "AUTH-4",
          "ADR-0080 d7"
        ],
        "x-dg-config": [
          "[console].origins",
          "[mcp].max_request_bytes"
        ],
        "x-dg-verbs": [
          {
            "method": "dg.approval",
            "role": "reader",
            "summary": "Who approved one artifact, or why it waits — a read model folded from its journal, never a state. `status` is one of rejected | refused | vetoed | withdrawn | countersigned | human_approved | ai_approved | escalated | pending | not_applicable; `signers[]` lists every decision with the signer's class (human | model | authority | deployment | agent; `authority` is the signer of a countersign or a refuse), the model label off the verdict, and whether it counts toward k.",
            "returns": "object: {artifact_id, state, status, reason, k_required, k_have, signers[], approved_by{model,human,authority}, model_labels[], provisional, human_veto_available, human_sign_available, decisions[], veto?, open_objections, revision} — below the reviewer role `revision` is a digest of the answer (GAPS 158)",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcApprovalParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.ask_to_sign",
            "role": "agent",
            "summary": "The author of a held row, a signed-in person, asks the people who may sign it (ADR-0134 d3): appends sign-asked {to}, the identities dg.who_can_sign lists, and moves nothing. Once per row a UTC day; a notifier carries it to the sinks the operator configured. Refused to every caller who is not a signed-in person (an agent asks through a question), to anyone but the row's author, and on a row that is not held.",
            "returns": "object: {artifact_id, to[], asked_at, event_id} — `asked_at` HH:MM UTC",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcAskToSignParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; each refusal in its sentence (doublegate_sdk.acts), a second ask that day `asked at HH:MM`"
            }
          },
          {
            "method": "dg.attachment",
            "role": "reader",
            "summary": "One page of one attachment of a claim the caller may read (ADR-0126 d6; 24 — Attachments §5): the served copy, an image keeping only the chunks that draw it and a PDF as stored, to every reader; the original, with original true, to the claim's writer and a reviewer that reads the space alone. Never by a hash alone: an id the caller may not read, a hash the claim does not carry, or an original below those two are answered as an unknown id. A page served whole and in order is logged, and a promoting or clearing signature counts only from an identity served every attachment to its end (`unseen_attachment` on dg.sign).",
            "returns": "object: {content_hash, media_type, name (masked), copy: served|original, size, offset, content (base64), next_offset|null}; the last page adds served_hash, stripped[], and to a reviewer metadata[{kind, text}] (masked, at most 65,536 bytes of JSON); an image whose chunks cannot be walked answers {copy: served, prepared: false} and no content",
            "required": [
              "artifact_id",
              "content_hash"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcAttachmentParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; an offset past the copy's end"
            }
          },
          {
            "method": "dg.audit",
            "role": "reader",
            "summary": "The decisions about one artifact with signatures verified. Below the reviewer role a payload's id of a row the caller may not read is null (GAPS 156). Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156).",
            "returns": "object",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcAuditParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.away",
            "role": "admin",
            "summary": "The operator declares an absence (ADR-0045): the human-review TTLs stop until `back`, the time away subtracted on restart. Nothing is promoted meanwhile.",
            "returns": "object: {away, declared, since, days, until?, note?}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcAwayParams"
          },
          {
            "method": "dg.back",
            "role": "admin",
            "summary": "The operator is back: the clocks restart with the time away subtracted (ADR-0045).",
            "returns": "object: {away, held_d, declared}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcBackParams"
          },
          {
            "method": "dg.backend_check",
            "role": "admin",
            "summary": "Ask one grading backend's provider, through this gate, whether its credential reaches its model and whether the model takes its effort and structured outputs (ADR-0125 d7). One call at a time; answers in words.",
            "returns": "object: {backend, reachable, credential, model, effort, structured_outputs, words}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcBackendCheckParams",
            "errors": {
              "-32602": "no backend table of that name"
            }
          },
          {
            "method": "dg.ban",
            "role": "admin",
            "summary": "Refuse a writer's, or a source prefix's, next ingest at the door with -32009 (ADR-0052 d6). Operator only; touches nothing already admitted (I4); not replicated.",
            "returns": "object: {kind, subject, active, event_id, until?}; with dry_run, for a writer {kind, subject, dry_run, promoted, held, last_write_ts, refused_last_7_days}, for a source {kind, subject, dry_run, sources[≤50], truncated, promoted, held} — what a ban leaves as it is",
            "required": [
              "kind",
              "subject"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcBanParams"
          },
          {
            "method": "dg.bans",
            "role": "reader",
            "summary": "The bans in effect and what they refused at the door (ADR-0052 d6). Read-only; `status` and `dg.graph` carry the same list. Below the reviewer role, only the bans whose subject is the caller's own identity and the caller's own refused writes: a ban names an identity or a source and no space, so nothing narrower is the caller's (CR14, GAPS 366).",
            "returns": "object: {bans[], refused[], truncated} — `truncated` when older refusals past the limit were left out (ADR-0133 d4)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcBansParams"
          },
          {
            "method": "dg.cancel",
            "role": "reader",
            "summary": "A signed-in person cancels a held claim they wrote (ADR-0132 d2): it goes to REJECTED with its words, events and attachments kept, and dg.approval answers `cancelled`. Refused to every caller whose role is agent, to every caller who is not a signed-in person (a program's key, a socket peer), to anyone but the claim's author, and on a claim that is promoted, signed and being promoted (L3_RATIFIED), not held, or named by a pending replacement group.",
            "returns": "object: {artifact_id, state, status, event_id}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcCancelParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; each refusal in its sentence (doublegate_sdk.acts)"
            }
          },
          {
            "method": "dg.comment",
            "role": "agent",
            "summary": "Record a remark on an artifact — an annotation, never a decision. Changes nothing about the row's standing and counts toward nothing; an open `objection` by the operator holds auto-promotion until a `resolution` replies to it or it is retracted. The author class (human/model/agent) is derived from the signing identity, never a parameter (I2). Comments are never published (I1) and never leave this gate.",
            "returns": "object: {artifact_id, event_id, comment, counts}",
            "required": [
              "artifact_id",
              "body"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcCommentParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; a bad kind, an empty body, or reply_to not a live comment here"
            }
          },
          {
            "method": "dg.comment_retract",
            "role": "agent",
            "summary": "Withdraw a comment: its author, or the operator. The comment stays as a tombstone; an open objection retracted this way releases the hold it held.",
            "returns": "object: {artifact_id, event_id, retract_event_id}",
            "required": [
              "artifact_id",
              "event_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcCommentRetractParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; an unknown comment, one already retracted, or one the caller did not write"
            }
          },
          {
            "method": "dg.comments",
            "role": "reader",
            "summary": "The comment thread on one artifact, oldest first, with counts (total, open human objections).",
            "returns": "object: {artifact_id, comments[], counts{total, open_objections, last_ts}}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcCommentsParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.complete",
            "role": "agent",
            "summary": "Mark one identity's part of a work area complete (ADR-0102 d6): the gate signs a `workarea-completed` event under its own key, and later writes by that identity into that work area are refused -32010 with reason `work_complete`. It changes no visibility, decides nothing and releases nothing — held rows stay in review exactly where they were — and there is no reopen. An agent completes its own part; a reviewer or an admin may complete any identity's by naming it. A second complete answers `changed: false` and journals nothing.",
            "returns": "object: {space, name, identity, state: complete, changed, event_id?}",
            "required": [
              "workarea"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcCompleteParams",
            "errors": {
              "-32602": "an unknown space, a name outside the grammar, a part the identity never wrote into, or an agent naming another identity"
            }
          },
          {
            "method": "dg.defer",
            "role": "agent",
            "summary": "Record that the operator looked and moved on (F40). Not a lifecycle transition; no journal event, single or bulk. `artifact_ids`, 1 to 100 ids in place of `artifact_id`, is a person's bulk Defer on Action needed, served from the reviewer role up: every id is checked readable before anything is counted, and the call adds its count to `deferred_in_bulk`, never to `deferred` (ADR-0132 d7).",
            "returns": "object: {artifact_id, deferred} or {artifact_ids, deferred: n}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcDeferParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; both or neither of artifact_id and artifact_ids; artifact_ids below the reviewer role, empty, over 100 or repeating an id"
            }
          },
          {
            "method": "dg.demote",
            "role": "reviewer",
            "summary": "Withdraw an admitted artifact from every consumer surface; the content is kept.",
            "returns": "object",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcDemoteParams"
          },
          {
            "method": "dg.describe",
            "role": "reader",
            "summary": "This catalog — the socket describing itself — plus the caller's own identity, role and kind as the gate took them, the role every verb is held to (ADR-0082), and `deployment_mode`, development or production, which says why a row may be signed by its author and grants nothing (ADR-0135 d1).",
            "returns": "object: {service, version, verbs[], documents{}, `you`{identity, role, kind, demo}, deployment_mode} — `demo` is true for a signed-in person the people file marks a demo person and false for every other caller, an API key and the socket's owner included (ADR-0135 d5)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcDescribeParams"
          },
          {
            "method": "dg.engagement",
            "role": "reader",
            "summary": "Did the operator actually review? Counts over a window: `items_deferred_in_bulk` and each history day's `deferred_in_bulk` say how many items a bulk Defer passed over without opening them (ADR-0132 d7).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcEngagementParams"
          },
          {
            "method": "dg.events",
            "role": "reader",
            "summary": "The gate's journal as events, in the order it was appended and never by `ts` (ADR-0109 d3), for a program that follows the lifecycle from a cursor it keeps: a peer. Each event names its type in the journal's own word, the row, replacement group, work area or space it is about, its space and work area, the state it leaves and enters when it moved one, who acted, and the fields `doublegate_sdk.events.CATALOG` lists for its type: identifiers, closed words and counts, never content, a title, a finding, a score or words a person wrote. One call reads at most 1000 events, served or not, and answers `next`, where the next call reads on, and `caught_up`, whether the caller has caught up with what it may read: true when nothing more it would be served lies before the end the read moves toward (the tip, or backward the journal's head), so there is nothing more to read now, and false when the caller reads on, an empty page included. A page full at `limit` reads on, serving nothing, to the next event it would serve, and `next` stands before that event, so a full page says nothing about events the caller may not read; a call that spends its 1000 events first answers false (GAPS 158). Every cursor is sealed (ADR-0117 d4, GAPS 158): encrypted and authenticated under a key derived from the deployment key, one length, and fresh on every answer, so none says where it stands or whether the journal moved, and two are never compared. Below the reviewer role a caller sees every event of a row it wrote and a row's events from its promotion on, a work area's where `dg.lineage` shows it, a group's when it may see every row the group names, and the space registry's; from reviewer up, every served event (AUTH-4). Events that belong to no space, and a pinned authority's, are not served (GAPS 118, 119). Pull only: the gate opens no connection and keeps no position for a reader. Every answer names `tip`, the cursor past the last journal event its projections hold, where a follower starts reading forward from the newest event; `direction: backward` reads from since, or from the tip, toward the journal's head, newest first, and its `next` continues backward (ADR-0111 d2, GAPS 127).",
            "returns": "object: {events[{type, object, artifact_id, space, workarea, leaves, enters, by, event_id, ts, trust_class?, content_type?, provisional?, …the type's own fields}], next, tip, caught_up, format_version} — next and tip sealed, caught_up a boolean",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcEventsParams",
            "errors": {
              "-32602": "a since this journal cannot serve (malformed, another journal's, in the readable form, or one it no longer holds, which names no position; `doublegate doctor --cursor` says whether it lies beyond the tip or was rewritten under), a type the verb does not serve, an unknown space, a work area name outside the grammar, a limit outside 1-1000, or a direction other than forward or backward"
            }
          },
          {
            "method": "dg.grade",
            "role": "reviewer",
            "summary": "Run the gate engine on one artifact: one model call per stance, one verdict per item. Unchanged bytes on an unchanged model are answered from the decision record at no cost; `fresh: true` asks past it (ADR-0129 d9). On a held row graded before the grading in force for its kind, each verdict carries `graded_before {rubric_version, kind_note}`, the grading it replaces, and every model verdict before it counts toward nothing; on any other row a grade writes no marker (ADR-0139 d2).",
            "returns": "object",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcGradeParams",
            "errors": {
              "-32602": "on a row that holds a verdict, its writer and a demo person on the developer's row: \"Refused: the claim's author cannot ask for it to be graded again.\"; outside a test home, a socket peer: \"A grading again asked over this machine's socket is refused: …\" followed by the row's who_can_sign words (ADR-0139 d2); nothing is appended"
            }
          },
          {
            "method": "dg.grade_pending",
            "role": "reviewer",
            "summary": "Grade what waits at L1, within the day's budget (fail-closed when spent, I6). A reviewer's call grades and counts rows in the spaces it reads alone (GAPS 401).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcGradePendingParams"
          },
          {
            "method": "dg.grant",
            "role": "admin",
            "summary": "Issue or revoke per-space grants as one act (ADR-0115 d16, GAPS 362): every entry names a principal (`<iss>|<sub>`, `key:<key_id>`, `uid:<n>`), one declared space and a non-empty subset of `read`, `contribute` and `submit`. The list is applied under one journal lock and refused whole if any entry is refused, one `space-grant-issued` or `-revoked` per entry that changes something; an entry already as asked answers `changed: false` and journals nothing. A connection between two crews is the `read` grants on one crew's space held by the other's keys, made and undone as one such list (ADR-0121 d8); access is never transitive. A revocation deletes nothing and applies to every answer after its event. An admin holds every capability by role and needs no grant (d15).",
            "returns": "object: {action, changed, grants[{principal, space, capabilities, changed, event_id, held}]}",
            "required": [
              "action",
              "grants"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcGrantParams",
            "errors": {
              "-32602": "an unknown space (naming the spaces that exist), a capability outside the three, a malformed entry, or a role below admin"
            }
          },
          {
            "method": "dg.grants",
            "role": "admin",
            "summary": "The open grants, one row per principal and space, each capability with the event that issued it and its reason (`upgrade` for the grants upgrade, ADR-0115 d14); `principal` and `space` narrow the list. A `uid:` principal covers every process that user runs (d17).",
            "returns": "object: {grants[{principal, space, capabilities, issued, every_process_of_that_user}], upgrade: {ts, event_id} | null, refused_not_granted: {principal: count since the daemon started}}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcGrantsParams"
          },
          {
            "method": "dg.graph",
            "role": "reader",
            "summary": "The constellation: every artifact as a node, every relation edge, every pin, every ban — the ADR-0035 projection serialized at once, bounded. `label` and `source_uri` are carried only for ACTIVE / SUPERSEDED rows (INV-SEC-10): a held artifact is its existence, never its content. `revision` is the journal tip, and below the reviewer role a digest of the answer itself, which moves only when what the caller is shown moves (GAPS 158); pass it back as `if_revision` and an unchanged graph answers {revision, unchanged: true}. With `workarea` named it is that area's work graph (ADR-0107 d5): the area's rows the caller may see — every row for a reviewer, the promoted rows and the caller's own below (AUTH-4) — plus every row one relation edge away under the same rule, each node marked with its workarea, producer, kind and whether it is foreign to the area, and `completed`, the completion markers. Computed per call, kept nowhere.",
            "returns": "object: {revision, unchanged} | {revision, full, nodes[], edges[], pins[], bans[], total, truncated, counts, spaces, states, viewer} — `total`, `truncated`, `counts` and `spaces` are over the rows the caller may see, below reviewer the promoted rows and its own; every node carries {workarea, producer, kind, addressed_to, writer_kind}, so both answers draw one node shape (ADR-0111 d2), writer_kind the principal kind its ingest recorded (person, program, local; ADR-0131 d8); a question's node in the constellation also carries cited_by[{artifact_id, kind, state, writer_identity, writer_kind, ts, state_ts, replaces?, cancels?}], the claims citing it the caller may read, replaces and cancels on a citing question whose body names it there; with workarea, each node also carries `foreign` and the answer {space, workarea, completed[{writer_identity, ts}]}; a node's `ts` is its ingest time in ms",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcGraphParams",
            "errors": {
              "-32602": "an unknown space (naming the spaces that exist); a work area nobody wrote into, or one where the caller sees no row, in one sentence; space beside spaces, workarea beside spaces, or space without workarea; kinds empty, naming a word outside the closed table, or beside workarea"
            }
          },
          {
            "method": "dg.health",
            "role": "reviewer",
            "summary": "Six counts of trouble among the promoted claims of the spaces the caller reads, the newest 2,000 by promotion (ADR-0133 d2): `same_words` (bodies equal after NFC, case folding and collapsed white space), `similar` (a cosine of at least 0.92 in this gate's embedding index, words differing; not computed without one), `cites_retired` (derives from a superseded, demoted or wholly hidden claim), `not_served` (promoted more than stale_days ago and returned by no recall since; not computed until the served days are that old), `unrelated` (no relation and no derives_from either way; steering excepted) and `not_reviewed` (a promotion that rested on its author's or a demo person's signature, ADR-0135 d3). Writes nothing, ranks nothing.",
            "returns": "object: {checked_at, rows_checked, truncated, checks{same_words, similar, cites_retired, not_served, unrelated, not_reviewed: {count, at_least, artifact_ids[], computed, why_not?}}}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcHealthParams",
            "errors": {
              "-32602": "an unknown space, or stale_days or limit outside its bounds"
            }
          },
          {
            "method": "dg.hide",
            "role": "reviewer",
            "summary": "Stop serving a promoted artifact to one audience, or to all — reversibly (ADR-0052 d4). The row stays ACTIVE and why-able; include_hidden returns it. A total hide by anyone but a signed-in person is journaled `lowered`.",
            "returns": "object: {artifact_id, audience, hidden, event_id, hidden_for[], lowered?}; with dry_run {artifact_id, audience, dry_run, already_hidden_for[], derived_served, derived_ids[≤20]} — the claims deriving from it that stay served, which a hide does not reach",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcHideParams"
          },
          {
            "method": "dg.ingest",
            "role": "agent",
            "summary": "Propose content into a work area (ADR-0102 d1): a gated class named by `kind`, whose trust class the gate derives (memory, derived_fact, summary, steering, question, answer → T-1; skill, prompt_template → T-2; imported_document, third-party content → T-4; the scanner may only raise it), or a recorded-path class named by `content_type`, entered as T-1 and never graded. Scanned before this answers (ADR-0053 d1) and lands in quarantine at L1 (or L1b on a finding); nothing is readable until promoted. A repeat of the same bytes in the same space by the same identity, or of bytes already promoted there, answers `duplicate: true` with that row and the work area it was first written into; bytes another identity holds unpromoted become the caller's own new row (ADR-0102 d3). Writer identity is the connection's, never a parameter (I2). A request that names no `workarea` is refused, whatever else it carries (ADR-0102 d1). A `steering` row is a direction to the agents at its work area and every area below it: held, scanned and graded like any row, and once promoted shown by its place beside a recall's hits, never among them (ADR-0116 d16, d17). A `question` is a member's question to the people of its crew, addressed to people: its body is the closed form `question/1` (`doublegate_sdk.question`), checked here, and it is never a hit in any scope; one writer holds at most eight in one work area (ADR-0131 d1, d2). The `ingest` event records the writer's principal (AUTH-5).",
            "returns": "object: {artifact_id, state, space, workarea?, producer, kind?|content_type?, findings_count, scan_event, trust_class} — the scanner's rule pass ran before this answered (ADR-0053 d1); state is L1_SCANNED or L1B_FLAGGED (RECORDED on the recorded path), and a finding's text is never in the answer. A duplicate answers {artifact_id, state, space, workarea, producer, duplicate: true}, the producer the row was first written with",
            "required": [
              "content",
              "workarea",
              "source_uri"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcIngestParams",
            "errors": {
              "-32602": "invalid arguments: an unknown space (named, with the spaces that exist), no workarea, a work area name outside the grammar, both or neither of kind and content_type, a trust_class, an unknown kind or recorded class, a producer outside ADR-0107 d1's shape, an addressed_to on any kind but steering or question or outside ADR-0116 d18's shape, or on a question any address but people; a question's body outside question/1, named by its first broken rule, a replaces or cancels its derives_from does not name, or a ninth question from one writer in one work area (data.reason `question_limit`); an answer's body outside answer/1, a question it names that derives_from does not name first, a question held or ended, values its form does not admit, a second answer or referral of the crew while one stands unless replaces names it, or a writer that is not a member key of the crew the question was routed to (data.reason `not_the_answering_crew`; ADR-0143 d2); a derives_from id: unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; for a member key of a routed crew, the question routed to it, its charter rows and its cited claims are readable here, and for a caller who may read a question, the routed crew's answer in force to it (ADR-0143 d2, d4)",
              "-32001": "request too large",
              "-32006": "quarantine at max_depth; data.retry_after_ms (below the reviewer role it names no number of held rows, GAPS 157)",
              "-32010": "data.reason `work_complete`: this identity completed that work area; nothing was written (ADR-0102 d6)",
              "-32015": "attachment storage at its bound: data.reason upload_quota (with data.retry_after_ms until the caller's oldest upload expires) or attachment_budget (a review frees it)"
            }
          },
          {
            "method": "dg.ingest_bundle",
            "role": "agent",
            "summary": "Inspect a ZIP without execution. Retain the original archive, its complete canonical manifest and every scanned file occurrence in quarantine, all at T-5 (the daemon proposes the class; the verb takes none). The bytes are vouched for by the identity that proposes them; no fetch record travels (ADR-0110 d6). Writer is the connection identity. Validation never authorizes installation or promotion.",
            "returns": "object: {state: quarantined, validation: valid|failed, archive_artifact_id, manifest_artifact_id, members: [{path, content_hash, size_bytes, classification, artifact_id, state, scan_event}], workarea, space?, producer, reason?}. Failed validation retains the archive only, and `reason` is archive_invalid:<detail>. No model judgment or installation. Every row of the unit carries the producer.",
            "required": [
              "content",
              "source_uri",
              "workarea"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcIngestBundleParams",
            "errors": {
              "-32602": "invalid arguments, an unknown space, no workarea, or a work area name outside the grammar",
              "-32001": "request too large",
              "-32006": "quarantine full; interrupted writes remain held (below the reviewer role it names no number of held rows, GAPS 157)",
              "-32010": "data.reason `work_complete`: this identity completed that work area (ADR-0102 d6)"
            }
          },
          {
            "method": "dg.ingest_map",
            "role": "agent",
            "summary": "Admit a project map: one tree's nodes and edges as a single gated artifact at T-5 (the daemon proposes the class; the verb takes none). The manifest IS the map and its edges are bytes inside it — they never enter the relation graph, never appear in `tip` or `relations`, and can never move what recall serves (ADR-0089). The gate validates and admits; it parses no source and knows no language (ADR-0090). Validation is whole against the SDK's `project_map/v1` contract: a manifest that does not check admits nothing at all. Send the archive beside it and the member table, the file and directory nodes and the `contains` edges are recomputed from those bytes, and every content node is retained as an occurrence of this map. Writer is the connection identity.",
            "returns": "object: {state: quarantined|refused, validation: valid|structural|failed, map_artifact_id, artifact_state, space, workarea, producer, project, produced_by, nodes, edges, members: [{path, content_hash, size_bytes, classification, artifact_id, state}], duplicate, reason?}. `validation` names which of 16 §9's two checks ran and a check that did not happen is never reported as one (GAPS 83): `valid` when the archive travelled and recomputed, `structural` when the manifest arrived alone, and a map with no archive carries no member rows. A refusal retains nothing and `reason` is one of: map_invalid:<detail> | archive_invalid:<detail>. The map's bytes are vouched for by the identity that proposes them; no fetch record travels (ADR-0110 d6).",
            "required": [
              "manifest",
              "source_uri",
              "workarea"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcIngestMapParams",
            "errors": {
              "-32602": "invalid arguments, an unknown space, no workarea, or a work area name outside the grammar; a derives_from id: unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing",
              "-32001": "request too large",
              "-32006": "quarantine full (below the reviewer role it names no number of held rows, GAPS 157)",
              "-32010": "data.reason `work_complete`: this identity completed that work area (ADR-0102 d6)"
            }
          },
          {
            "method": "dg.inventory",
            "role": "reader",
            "summary": "Every artifact in EVERY lifecycle state, as metadata, paged and filterable — the operator workspace's list. Deliberately wider than dg.pending, which answers about quarantine only and so can never show the ACTIVE row a handoff is made from. Carries the spaces the store actually holds, because a caller that cannot name a space cannot pass `spaces` to dg.recall, and a recall without it falls back to `main` and answers nothing. Metadata only: no content, ever. Below the reviewer role the rows, `total`, `counts` and `spaces` cover only the promoted rows and the caller's own, as dg.graph's do (INV-SEC-10).",
            "returns": "object: {artifacts[], total, offset, limit, counts{state: n}, spaces[], states[]} — `counts` is over the filtered set, `spaces` the spaces holding a row the caller may list and `states` the lifecycle's vocabulary, so a filter control can be rendered from the answer itself",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcInventoryParams"
          },
          {
            "method": "dg.keys",
            "role": "reviewer",
            "summary": "API keys (AUTH-3): list the key table, issue a `dgk_…` key (shown once; stored as its hash in an `apikey-issued` event) with a role no higher than the caller's, or revoke one by id. Below admin, list and revoke reach a key the caller issued and a key of a role no higher than its own whose open grants lie within the spaces it reads; any other key is answered as one that does not exist (GAPS 402).",
            "returns": "object: list → {keys[{key_id, label, role, issued_by, issued_at, expires_at, revoked_at, scope}]}; issue → {key, key_id, role, label, expires_at, event_id, scope}; revoke → {key_id, revoked, already?, event_id?} + the list",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcKeysParams",
            "errors": {
              "-32602": "unknown action, bad role, unknown key_id, a role above the issuer's, or a runner key below admin"
            }
          },
          {
            "method": "dg.lineage",
            "role": "reader",
            "summary": "The lineage of a space, or of one work area with its ancestors, its descendants and every area one relation or derivation away (ADR-0107 d3): a view computed on each call, never a store. Nodes are work areas with their writers, each writer's open or complete state, the producers with their counts and last writes, and promoted, held and recorded counts. Every edge points from the area that depends to the one it depends on and names its provenance: `path` (a/b under a, by name alone), `declared` (a relation in effect) or `derived` (promoted rows whose derives_from crosses two areas, counted). Below the reviewer role an area appears only when it holds a promoted row or the caller wrote into it, counts are over promoted rows and the caller's own, a path the caller sees nothing in is drawn `implicit`, and a derived edge is a count, never rows (AUTH-4). An orchestrator with subagents, a pipeline and a swarm are shapes of this graph; none is stored.",
            "returns": "object: {space, workarea, nodes[{name, implicit, writers[], states{identity: open|complete}, producers[{name, kind, rows, last_write}], promoted, held, recorded, first_write, last_write, rows?[{artifact_id, state, kind, held, trust_class, producer}]}], edges[{source, target, provenance: path|declared|derived, kind?, by?, producer?, count?, rows?}]} — `first_write`, `last_write` and a producer's `last_write` are ingest times in ms, a producer's the newest among the rows it is counted over; `recorded` counts the recorded evidence rows by the rule the producer counts follow, so an area holding only hook rows reads as evidence (ADR-0111 d2); a node's `rows`, the claims each producer wrote, only on the named area's node and only those the caller may see there (ADR-0107 d5, GAPS 107); an edge's `rows`, the (child, parent) id pairs behind a derived edge, only for a reviewer",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcLineageParams",
            "errors": {
              "-32602": "an unknown space or a work area name outside the grammar"
            }
          },
          {
            "method": "dg.node",
            "role": "reader",
            "summary": "One node in full for a review surface: the status row, the approval trail, the comment thread, relations, the served tip, bundle members (`derives_from` children), and a bounded MASKED preview (INV-SEC-7; null when the content is erased or the blob is gone). Below the reviewer role the members and relations are the rows the caller may read. Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156).",
            "returns": "object: {artifact_id, status, approval, comments, relations, tip, members[], preview, preview_chars, self_authored, refusal?, revision, supersedes} — `supersedes` is {artifact_ids, operation, via, group_id} when the row is a child of a pending replacement group, its ids the parents the caller may read, else null (ADR-0133 d1); below the reviewer role `revision` is a digest of the answer (GAPS 158); with body, also {body, body_exact, content_hash, size_bytes, source_uri, signers[{identity, class, decision}]}, `body` null and `body_exact` false when the content is erased or the blob is gone",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcNodeParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; with body, a held row the caller did not write: \"refused: dg.node serves a body for a promoted row or the caller's own; …\"; body not a boolean"
            }
          },
          {
            "method": "dg.override_tip",
            "role": "reviewer",
            "summary": "Pin a chain's served head to this member (time travel). Reversible by a second override. A crew sheet is never pinned: promote and activation, both checked, are the only acts that change the sheet in force (ADR-0143 d8).",
            "returns": "object",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcOverrideTipParams",
            "errors": {
              "-32602": "data.reason crew_sheet_not_pinned, \"Refused: a crew sheet is not pinned as tip; propose it in place of the sheet in force so its connections are checked.\""
            }
          },
          {
            "method": "dg.pending",
            "role": "reader",
            "summary": "What is waiting in quarantine, as metadata (state, class, kind) — never content.",
            "returns": "object: {pending[], truncated} — `truncated` when rows past the limit were left out (ADR-0133 d4)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcPendingParams"
          },
          {
            "method": "dg.people",
            "role": "admin",
            "summary": "The role table (ADR-0082 d5, AUTH-4): list every identity this gate has a role for, assign a role to an (iss, sub), or remove one. A role never exceeds the assigner's. `list` also shows the built-in issuer's people and `[auth].admins`.",
            "returns": "object: list → {issuer, people[{iss, sub, role, source, demo, email?, name?, assigned_by?, assigned_at?}]}; assign → {iss, sub, role, event_id} + the list; remove → {iss, sub, removed, was?, event_id?} + the list — `demo` is true where the people file marks a demo person (ADR-0135 d5)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcPeopleParams",
            "errors": {
              "-32602": "unknown action, bad role, missing sub, or a role above the assigner's"
            }
          },
          {
            "method": "dg.ping",
            "role": "reader",
            "summary": "Liveness and identity: deployment id, role, retrieval paths, degraded flags, quorum profile.",
            "returns": "object: {pong, deployment_id, role, paths, degraded, quorum_profile}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcPingParams"
          },
          {
            "method": "dg.promote",
            "role": "reviewer",
            "summary": "Admit an artifact whose quorum is met. On every profile the promotion is provisional until a second authority countersigns (R3), and T-3 and T-5 are refused and held until one has (trust model §3, GAPS 109). It sets no weight a read multiplies in (ADR-0110 d6).",
            "returns": "object: {artifact_id, state, provisional?}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcPromoteParams",
            "errors": {
              "-32602": "data.reason runner_lacks_type with data.missing, or runner_full, on a crew sheet whose space's assigned runner runs no model for a type it uses or has made no report since the gate started (ADR-0141 d5); data.reason answering_not_a_crew on a person's crew sheet naming in answered_by a space with no crew sheet in force, answering_chain when the named crew names an answering crew of its own or a crew in force names this one, and, from a caller below admin, connection_needs_admin on a crew sheet that a writer below admin wrote and that adds a read or an answering crew against the space's last sheet in force, an admin's own sheet not held whether it replaces a sheet or none (ADR-0143 d1, d8)"
            }
          },
          {
            "method": "dg.question",
            "role": "reader",
            "summary": "One question's routing state, to a caller that may read the question (ADR-0143 d4; 17 — Crews §11.5): held; with_crew with its crew and the minutes left; with_people with no_crew, wait_over, crew_gone or referred; answered by a person or by the crew, a person's standing above; or ended as ADR-0131 d6 lists. The route is the question's question-routed event, never the sheets in force. A held answer or referral is served to nobody here.",
            "returns": "object: {question, state, reason, crew, wait_minutes, minutes_left, answers[{artifact_id, body, space, promoted_at}], person_answer, referral} — answers are the crew's in force citing it, newest first; referral {artifact_id, body} is served to a person alone, null to every other caller (GAPS 1382, 1387)",
            "required": [
              "question"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcQuestionParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.rebuild",
            "role": "admin",
            "summary": "Replay the journal into every projection; the result must equal the live state (I5).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRebuildParams"
          },
          {
            "method": "dg.recall",
            "role": "reader",
            "summary": "Read the active store as far as a scope says to look (ADR-0102 d5), plus the caller's own scanned claims still in quarantine, appended after the ranked hits and marked held (ADR-0053 d2). `closest` starts at a work area and walks up its path to the first level any promoted row answers; `workarea` is that area alone; `space` is every row in one space, those written before work areas included; `all` is every space this gate declares. Default: `closest` with a work area, `space` without. A scope chooses which rows are searched, never how they rank. Nobody else's quarantine, ever (I1). Provisional hits are marked; superseded are hidden unless asked for. A recall that names a work area (`closest`, `workarea`) also answers `steering`, the steering in force there: every promoted steering row in the space at the area or one of its ancestors, whatever the query, less what a supersession or a reviewer's hide withdraws, nearest place first and the newest write first within a place, cut at `steering_k` with `steering_omitted` counting the rest (ADR-0116 d17-d19). A steering row is never a ranked hit. With a work area the query may be left out: the answer is then the steering alone, no hits (GAPS 251).",
            "returns": "object: {scope, space, workarea, answered_by, hits[{artifact_id, score, body, space, workarea, producer, held, distance, provisional, provenance, signals}], paths, audience?, steering[{artifact_id, body, space, workarea, distance, producer, writer{identity, kind}, addressed_to, promoted_at, provisional}], steering_omitted} — the ranked hits first, then the caller's own held rows. `distance` is the levels walked up from the named work area (0 under workarea; null under space and all); `answered_by` the level that answered; `workarea` null on a row written before work areas. `steering` is empty and `steering_omitted` 0 under space and all; a steering entry's `writer.kind` is the principal kind its ingest recorded (person, program, local), null where none was recorded, and `promoted_at` the promotion's time in ms",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRecallParams",
            "errors": {
              "-32602": "an unknown scope or space, a scope missing its operand, an operand the scope does not use (producer and steering_k under space and all among them), a recall naming neither a query nor a work area, a steering_k outside 1-20, a producer outside ADR-0107 d1's shape, or a work area name outside the grammar"
            }
          },
          {
            "method": "dg.reconcile",
            "role": "admin",
            "summary": "Re-derive the consumer surfaces of every space from the journal (the integrator's rebuild): the deployment's own work, beside dg.rebuild (GAPS 401).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReconcileParams"
          },
          {
            "method": "dg.records",
            "role": "reviewer",
            "summary": "The bodies of recorded rows whose class is event, usage_record or state_snapshot, in a space the caller holds a read grant in, newest first (ADR-0132 d19). Each body is the JSON object the row's masked body carries when it parses and is at most 4,096 bytes, else null with body_refused. Never session, the plugin's transcript tail, or any other class; an events key is refused it.",
            "returns": "object: {items[{artifact_id, class, workarea, writer_identity, ts, body, body_refused?}], truncated}",
            "required": [
              "space",
              "classes",
              "limit"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRecordsParams",
            "errors": {
              "-32602": "an unknown space; not granted: read on it; a class outside the three; limit or since outside its bounds"
            }
          },
          {
            "method": "dg.reject",
            "role": "reviewer",
            "summary": "Refuse an artifact in quarantine, with a category and optionally what it lost to (ADR-0037).",
            "returns": "object",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRejectParams"
          },
          {
            "method": "dg.relate",
            "role": "reviewer",
            "summary": "Append a typed relation from artifact_id (child) to parent: supersedes | restates | corrects | contradicts | extends. Only supersedes hides.",
            "returns": "object: {artifact_id, relation, parent, event_id}",
            "required": [
              "artifact_id",
              "kind",
              "parent"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRelateParams"
          },
          {
            "method": "dg.relations",
            "role": "reader",
            "summary": "The typed relation edges touching one artifact (ADR-0035); below the reviewer role only the edges whose other end the caller may read.",
            "returns": "object: {artifact_id, relations[]}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRelationsParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.reopen",
            "role": "reviewer",
            "summary": "Return a rejected claim to the held state its scan left (L1B while a finding is at veto severity, else L1), with a reason (ADR-0133 d5). It makes nothing readable: the count of signatures starts again from the reopen, and the verdicts before it stay in the trail. Refused to a caller who is not a signed-in person, to the claim's author (in development the one person reopens their own, ADR-0135 d2), on a claim that is not rejected or that its author cancelled, and past three reopens of one claim.",
            "returns": "object: {artifact_id, state, reopens, event_id}",
            "required": [
              "artifact_id",
              "reason"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReopenParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; each refusal in its sentence (doublegate_sdk.acts)"
            }
          },
          {
            "method": "dg.replacement_abort",
            "role": "reviewer",
            "summary": "Close a pending group and keep everything it held (graph operations §4) — not a deletion and not a demotion: nothing it prepared was ever served. There is no abort after activation.",
            "returns": "object: the group's status plus {event_id | recorded}",
            "required": [
              "group_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReplacementAbortParams",
            "errors": {
              "-32602": "an unknown reason, a role below reviewer, or a group already activated"
            }
          },
          {
            "method": "dg.replacement_activate",
            "role": "reviewer",
            "summary": "The one signed commit that flips a whole group (graph operations §4): every prepared child becomes servable together and every listed parent becomes SUPERSEDED together. Curation, never the author's: the proposer is refused by name, and a stale `graph_revision` is refused with the current one rather than activated against a graph that moved. In a development home the developer activates their own group, and `replacement-activated` carries `self_signed: true` (ADR-0135 d2's dated line). A crew sheet it brings into force is checked as a promote is, and one that adds a read or an answering crew is activated by an admin, or by any reviewer who did not propose it when an admin wrote it and the group replaces the sheet in force (ADR-0143 d8).",
            "returns": "object: the group's status plus {event_id, promoted[], superseded[], frontier[], published}",
            "required": [
              "group_id",
              "graph_revision"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReplacementActivateParams",
            "errors": {
              "-32602": "the proposer, an unprepared child, a stale revision or pin, a competing group, a cycle, or a child the admission boundary refuses; data.reason answering_not_a_crew, answering_chain or connection_needs_admin on a crew sheet child, in the record's words (ADR-0143 d1, d8)"
            }
          },
          {
            "method": "dg.replacement_prepare",
            "role": "reviewer",
            "summary": "Take one child of a pending group to ratified-and-not-serving (graph operations §3). It appends no promote, so the child reaches no serving surface by any flag; a child its reviewers rejected aborts the whole group instead.",
            "returns": "object: the group's status plus {child_id, event_id | recorded}",
            "required": [
              "group_id",
              "child_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReplacementPrepareParams",
            "errors": {
              "-32602": "not a member, body not here yet, not ratifiable, or the group is terminal"
            }
          },
          {
            "method": "dg.replacement_propose",
            "role": "reviewer",
            "summary": "Propose one replacement group: a split, a merge or a supersede, with its membership fixed at acceptance (graph operations §3). The proposer is the connection's identity (I2) and is the one identity that may never activate the result (§3: proposer is not ratifier). Nothing is admitted and nothing is served by this call; a retry of the same manifest returns the recorded group.",
            "returns": "object: {group_id, state, operation, parent_ids, child_ids, prepared[], pending_children[], manifest_digest, proposer, proposed_event, event_id | recorded}",
            "required": [
              "operation",
              "parent_ids",
              "child_ids",
              "reason"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReplacementProposeParams",
            "errors": {
              "-32602": "the field that refused it: a membership, an arity, a trust class, a space, a competing group, a cycle, or an id already recorded with a different manifest"
            }
          },
          {
            "method": "dg.replacements",
            "role": "reviewer",
            "summary": "Every replacement group with its membership, its manifest and why it is held — the reviewer's view of what is waiting to be activated. Metadata, never content (I1). `revision` is the graph revision this reading was taken at: pass it to dg.replacement_activate.",
            "returns": "object: {groups[{group_id, state, operation, members[{artifact_id, side, state, prepared}], manifest, held, ready, prepared[], pending_children[], activation?, abort?}], total, counts{state: n}, revision, scope}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReplacementsParams",
            "errors": {
              "-32602": "no such replacement group"
            }
          },
          {
            "method": "dg.review",
            "role": "reviewer",
            "summary": "The operator's queue: what needs a human, capped. Each row is annotated with a bounded content preview, whether the operator wrote it (I2), and the verdicts its state accepts.",
            "returns": "object: {held_steering[{…the item's shape}], held_steering_truncated, items[{artifact_id, state, waits_for{blocker, code, words, rubric?, missing?}, stances?[{stance, backend_label, provider, decision, reason, kind_note, stale}], waiting_since, supersedes, content, content_type, trust_class, writer_identity, ingest_ts, source_uri, findings[], verdicts[], expires_at, expires_in_s, space, workarea, producer, kind, attachments[{content_hash, size_bytes, media_type, name, source_uri, extracts}], …}], held_back, total_waiting, cap} — `attachments` lists each attachment entry, its name masked, never a byte (ADR-0126); `space`, `workarea`, `producer` and `kind` say where the held row was written and what produced it, the row's own fields (ADR-0111 d2); `workarea` and `producer` are null on a row that named none; `waits_for` is why the row waits, the first that holds of ADR-0070's blockers with the gate's own code and sentence beside it, and `waiting_since` the ts of the event that put it in its held state (ADR-0132 d6, d9); `supersedes` is dg.node's (ADR-0133 d1); `who_can_sign` is dg.who_can_sign's answer for the row, one letter per person across the whole answer, and `self_authored` whether the caller wrote it (ADR-0134 d3); `held_steering` is every held steering row the caller reads, its own included, oldest wait first, at most 50 with `held_steering_truncated`, before the digest and never counted in `cap`, `items` or `held_back` (ADR-0134 d4); `waits_for.code` `rubric_changed`, on the `semantic_review` blocker, is a row graded before the grading in force for its kind, with `rubric {was, now}`, each a kind paragraph's id or \"claim\"; `waits_for.code` `runner_lacks_type`, on the `hard_constraint` blocker, is a crew sheet whose space's assigned runner runs no model for a type it uses, with `missing` the types, and `runner_full` one whose runner has made no report since the gate started (ADR-0141 d5); `stances` is on an item whose code is a disagreement, an abstention, low confidence or rubric_changed alone: each current model verdict, first pass then review, then the latest superseded one of each stance, `reason` its rationale's first sentence of at most 200 characters and `stale` true where its grading is not the one the row is read under (ADR-0139 d2, d3)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReviewParams"
          },
          {
            "method": "dg.review_pending",
            "role": "reviewer",
            "summary": "ADR-0048: the second opinion. Under review.mode model|auto, every L2 row whose first pass escalated gets one `gate-review` verdict, charged to the same budget; a no-op under `human`. Never touches L1b (INV-SEC-5). A reviewer's call reviews rows in the spaces it reads alone (GAPS 401).",
            "returns": "object: {reviewed, cost_tokens, paused, budget_remaining}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReviewPendingParams"
          },
          {
            "method": "dg.review_runs",
            "role": "reader",
            "summary": "The review-run log: bounded, filterable, searchable, newest first. Deliberately separate from dg.status's queue depths — a depth is what is WAITING, a run is what is being PROCESSED, and reading one as the other is the misreport this split prevents. Below the reviewer role, only the runs on rows the caller may read.",
            "returns": "object: {reported, runs[], total, offset, limit, counts, in_flight[], last_failure, detail?} — `reported: false` when this worker emits no run events, and then the list is empty rather than inferred",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcReviewRunsParams"
          },
          {
            "method": "dg.routed_questions",
            "role": "agent",
            "summary": "The questions routed to the crew of `space` and still with it, oldest promotion first, at most 50, each with the one set that crosses between two crews: the question's body, the asking crew's brief, its crew sheet and the asker's crew role's instructions in force at `claude`, and the promoted claims of the asking space it cites, at most four (ADR-0143 d4). Served only to an unrevoked agent key the gate issued by dg.runner_keys for a slot of `space`, read off its `apikey-issued` and never off its label, and to a person at reviewer or above holding read on `space`; a crew connected to it by a read grant is served nothing of it. Grants nothing.",
            "returns": "object: {space, questions[{question, body, space, asker{producer, role}, promoted_at, wait_minutes, minutes_left, brief, sheet, instructions, citations[]}], truncated} — each charter row and citation is {artifact_id, body} or null; minutes_left is counted on the daemon's monotonic clock, never on dg.status's now (GAPS 1382, 1386)",
            "required": [
              "space"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRoutedQuestionsParams",
            "errors": {
              "-32602": "an unknown space; any other caller: data.reason `not_the_answering_crew`, \"Refused: only the members of crew <space> and its reviewers are served the questions routed to it.\""
            }
          },
          {
            "method": "dg.runner_assign",
            "role": "admin",
            "summary": "Assign a crew's space to a runner, switch it, or unassign it with `runner: null` (ADR-0140 d2, d3): journalled `runner-assigned {space, from, to, slots, sheet, key_days}`; the runner is granted `read` and `contribute` on the space; a switch, `reissue: true` or a change of slots revokes every member key of the old batch at the act, reason `reassigned`, and keeps every row. An assignment already in force journals nothing. An admin's act, never a claim.",
            "returns": "object: {space, from, to, changed, revoked[], event_id}",
            "required": [
              "space",
              "runner"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerAssignParams",
            "errors": {
              "-32602": "data.reason not_a_runner, runner_unconfigured (the runner's command, plugin_dir, workdir_root or models unset, ADR-0142 d2), runner_lacks_type with data.missing the role types the sheet uses that the runner's last report runs no model for (ADR-0141 d5), or runner_full; a field outside its rule, named; a sheet that names no row of the space, or a row that is no crew sheet of it, naming the field"
            }
          },
          {
            "method": "dg.runner_hello",
            "role": "runner",
            "summary": "A runner's report of itself, at most once a minute (ADR-0140 d1): the gate keeps the newest per key in memory, on its own clock, as the runner's last report, never checked and never a claim, and journals only what it acts on: `runner-first-seen` on a key's first hello ever, `runner-keys-acknowledged` when a space's `holds` equals its batch and the hello comes from the batch's instance, `runner-keys-refused` when it comes from another, and `runner-released` when a space reassigned away is no longer listed, revoking the runner's `read` and `contribute` there. The body is `doublegate.runner-hello/1` (`doublegate_sdk.crew.hello`). The hello carries the machine's facts, the settings version it holds, each setting it could not apply and its endpoint token's version (ADR-0142 d4); the answer carries the whole settings only when that version differs from the gate's.",
            "returns": "object: {now, settings_version, settings? (every field of the runner's settings, only when the hello's settings_version differs), label, token: none|waiting|served|held|lost|unreadable, assignments[{space, slots[], sheet, keys: waiting|issued|held}], questions?[{space, asked[{question, slot, state, reason}], routed[], truncated}]} — `questions`, present when an assigned space holds one, is per assigned space its open questions, oldest first, with the slot whose served key asked each and its routing state, and the ids of the questions routed to it and still with it, at most 50 each; ids and states, never a body (ADR-0143 d5)",
            "required": [
              "format",
              "instance",
              "runner",
              "harness",
              "host",
              "backend",
              "models",
              "models_floor",
              "capacity",
              "recorded",
              "budget",
              "serving",
              "facts",
              "settings_version",
              "refused",
              "token"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerHelloParams",
            "errors": {
              "-32602": "a field outside its rule, named; or any caller but a runner key"
            }
          },
          {
            "method": "dg.runner_keys",
            "role": "runner",
            "summary": "A crew's member keys, issued once to the runner the space is assigned to (ADR-0140 d3): one `agent` key per slot, labelled with the slot, expiring after the assignment's `key_days`, granted `read` and `contribute` on the space and `read` on the crew's connections, each journalled `apikey-issued` with `issued_by` the admin who assigned and `served_to` the runner. The raw keys are in this answer alone; the gate keeps their hashes (INV-SEC-9). The batch is bound to the one instance heard on the key; a second call before the runner's hello acknowledges it revokes it, reason `superseded`, and issues anew; after, it is refused.",
            "returns": "object: {space, members[{slot, role, key}]}",
            "required": [
              "space"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerKeysParams",
            "errors": {
              "-32602": "data.reason not_assigned (another runner, none assigned, or no hello since the gate started), two_instances, already_served; or any caller but a runner key"
            }
          },
          {
            "method": "dg.runner_set",
            "role": "admin",
            "summary": "Set one or more of a runner's settings (ADR-0140 d4; ADR-0142 d2; 17 — Crews §12): each field checked on its own, then the settings with the change checked whole; journalled `runner-settings-set {runner, version, fields}` under the admin, the version the count of the runner's settings lines, and served in its next answer. The limits and intervals take effect at the runner's next pass; the launch settings, the endpoint and the map at its next launch or resume. No setting is a secret: the endpoint's token is `dg.runner_token_set`'s.",
            "returns": "object: {runner, settings_version, settings, event_id}",
            "required": [
              "runner"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerSetParams",
            "errors": {
              "-32602": "data.reason not_a_runner; a field outside its rule, named in data.field, with data.reason inside_workdir for a sandbox path inside workdir_root"
            }
          },
          {
            "method": "dg.runner_token",
            "role": "runner",
            "summary": "The caller's own endpoint token, once (ADR-0142 d3): to the one instance heard on its key within 120 seconds, with the origin it was set for. In the act the gate removes its sealed copy and journals `runner-token-served {runner, instance, version}`; the runner's next hello acknowledges it, and one not acknowledged within 600 seconds is lost.",
            "returns": "object: {version, token, origin}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerTokenParams",
            "errors": {
              "-32602": "data.reason no_token (none waiting, lost, or no hello heard), two_instances, already_served, unreadable (the gate cannot open its sealed copy); or any caller but a runner key"
            }
          },
          {
            "method": "dg.runner_token_set",
            "role": "admin",
            "summary": "Enter, rotate or clear a runner's endpoint token (ADR-0142 d3; 17 — Crews §12): sealed under the deployment's `keys/runner-secrets.key` until the runner takes it once with `dg.runner_token`, then held nowhere by the gate. Journalled `runner-token-set {runner, version, origin, rotated}` or `runner-token-cleared {runner, reason: admin}`; no answer, log line or event carries the value. A change of the runner's `base_url` origin clears it.",
            "returns": "object: {runner, version?, token: waiting|none, event_id}",
            "required": [
              "runner",
              "token"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnerTokenSetParams",
            "errors": {
              "-32602": "data.reason not_a_runner, or no_endpoint while the runner has no base_url; a token outside its rule, named and never repeated"
            }
          },
          {
            "method": "dg.runners",
            "role": "reviewer",
            "summary": "Every runner key (ADR-0140 d5; 17 — Crews §12): its served label, standing, state (the first of revoked, two_instances, settings_refused, needs_settings, no_report_since_start, not_seen, budget_spent, paused, serving, idle, never_seen that holds), when it was first seen, its last report (with the machine's facts and the settings it could not apply, ADR-0142 d4) and that receipt on the gate's clock (`received_at`), that report's age and the instances heard in 120 seconds on the daemon's monotonic clock, never on `now` (ADR-0140 d3, 2026-10-14; GAPS 1432), its settings with their version and, for each field ever set, the identity that set it and when (ADR-0142 d2), its assignments with each batch's state, the spaces reassigned away and not yet released, and the refused acknowledgements journalled on it. The report carries the hello's `backend`, `models` and `models_floor` (ADR-0141 d5), and each assignment the role types its sheet uses that the map runs no model for, `missing`. Below admin the assignments and the report's spaces are those the caller reads.",
            "returns": "object: {now, started_at, runners[{runner, label, standing, state, first_seen_at, report, received_at, age_ms, instances[], settings, settings_version, settings_set{<field>: {by, at}}, assignments[{space, slots, sheet, keys, missing[], key_ids, issued_at, revoke_at}], released_pending[{space, running}], refused, token{state, version, origin, set_at, served_at, reason, box}}], token_store: the last token set refused for keys/, or null}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcRunnersParams"
          },
          {
            "method": "dg.scan",
            "role": "reviewer",
            "summary": "Run the deterministic scanner on one L0 artifact; a finding ≥ medium routes to L1b and no score clears it (INV-SEC-5). An explicit outcome is the manual override.",
            "returns": "object: {artifact_id, state, outcome, findings[]}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcScanParams"
          },
          {
            "method": "dg.scan_pending",
            "role": "reviewer",
            "summary": "Scan every L0 artifact whose delay elapsed. Counts, never content. A reviewer's call scans rows in the spaces it reads alone (GAPS 401).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcScanPendingParams"
          },
          {
            "method": "dg.semantic_review",
            "role": "reviewer",
            "summary": "ADR-0048/ADR-0053: ask the server-owned reviewer to look at one row. Every disposition it writes is reviewer_class='model' — a model is never laundered into a human signature by being triggered from a console. The caller supplies the subject and nothing else; the reviewer identity, the rendering, the scan event and the registry revision are read from the row.",
            "returns": "object: the reviewer's disposition payload",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSemanticReviewParams",
            "errors": {
              "-32602": "unknown artifact: the id names no row in this quarantine",
              "-32014": "the reviewer refused (no reviewable finding, unavailable backend, unusable reply); the row is unchanged"
            }
          },
          {
            "method": "dg.settings",
            "role": "admin",
            "summary": "The review worker's configuration: configured (on disk) beside effective (in this process), plus the drift between them and the field/choice tables a configuration surface renders.",
            "returns": "object: {configured, explicit, secrets, effective, etag, config_rev, drift[], fields, choices, runs, caps[], defaults} — `caps` as dg.status serves them (ADR-0132 d18); `defaults` names each setting \"Use the default\" returns to, `{set, value, why}`, the value the gate resolves when the file is silent (ADR-0134 d2, d6)",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSettingsParams"
          },
          {
            "method": "dg.settings_save",
            "role": "admin",
            "summary": "Compare-and-swap the configuration file, then apply it to this running daemon. Always both: a save that needs a restart to mean anything is a save the operator has to be told about twice.",
            "returns": "object: {ok, saved, applied, etag, config_rev, configured, secrets, effective, drift[], defaults, note} or {ok: false, conflict|errors}",
            "required": [
              "values",
              "etag"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSettingsSaveParams",
            "errors": {
              "-32602": "`deployment.mode` in values or unset: the deployment mode is set in config.toml and read at start (ADR-0135 d1)"
            }
          },
          {
            "method": "dg.settings_validate",
            "role": "admin",
            "summary": "Run a configuration draft through the real loader. Writes nothing, applies nothing — the call a form may make on every keystroke.",
            "returns": "object: {ok, errors?} — field-keyed errors, never an exception",
            "required": [
              "values"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSettingsValidateParams"
          },
          {
            "method": "dg.settle_pending",
            "role": "reviewer",
            "summary": "ADR-0048: under review.mode auto, promote every L2 row that meets k with no human required on its class — the deployment signs the promote, the integrator's boundary check applies unchanged. A no-op otherwise. A reviewer's call settles and counts rows in the spaces it reads alone (GAPS 401).",
            "returns": "object: {promoted, rejected, held}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSettlePendingParams"
          },
          {
            "method": "dg.sign",
            "role": "reviewer",
            "summary": "Record a verdict as the calling principal (ADR-0082): a signed-in person's verdict is a human's, a key's or a socket peer's is a reviewer's and never satisfies a human requirement. With `promote: true` the signature and the promotion it was for happen in one call; a promotion the boundary refuses is reported in `promote.refused` and the signature stands. The claim's author is refused, except in development, where the one person's promote on a held row they wrote counts once, marked `self_signed` (ADR-0135 d2); a demo person's verdict is marked `demo_signed`, counts for nothing on the developer's row and is refused outside development (d5). On a promoted row marked either way, a promote from a signed-in person who neither wrote it nor is a demo person appends `confirm-sign`, and `review-confirmed` once such signatures meet k (d3).",
            "returns": "object: {artifact_id, state, event_id, promote?} — on a marked promoted row {artifact_id, state, event_id, confirmed, confirmation?}",
            "required": [
              "artifact_id",
              "decision"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSignParams",
            "errors": {
              "-32602": "data.reason runner_lacks_type with data.missing, or runner_full, on a crew sheet whose space's assigned runner runs no model for a type it uses or has made no report since the gate started (ADR-0141 d5), and answering_not_a_crew, answering_chain or connection_needs_admin on a crew sheet as dg.promote refuses it (ADR-0143 d1, d8); with `promote: true` it is refused before the signature, which is not recorded"
            }
          },
          {
            "method": "dg.space_create",
            "role": "admin",
            "summary": "Declare a space (ADR-0023 d1). Until a space is declared, writing to its name is refused -32602 rather than quietly opening a partition nobody can find again — the reason this verb exists. `main` exists at install and needs no creation. An operator's act: an agent may name a space it was granted and may never open one.",
            "returns": "object: {name, owner, policy, created, event_id} — or the existing row with `duplicate: true`",
            "required": [
              "name"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSpaceCreateParams",
            "errors": {
              "-32602": "a name outside the charset, or a role below admin"
            }
          },
          {
            "method": "dg.spaces",
            "role": "reader",
            "summary": "The store's partitions (ADR-0023 d1): every declared space with its owner, its review policy and how many artifacts sit in it (below the reviewer role, how many the caller may read: the promoted rows and its own, in a space it holds `read` on, and its own anywhere), `granted`, the capabilities the caller holds there (ADR-0115 d6; every one for an admin), and which space a write defaults to. `unregistered` names spaces the store holds rows in that nothing declared — empty on a deployment this daemon has started, because startup registers exactly those. Names and counts, never content: this is a directory, not a read.",
            "returns": "object: {default, spaces[{name, owner, policy, artifacts, granted[], event_id, identity, ts, builtin?}], unregistered[]}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcSpacesParams"
          },
          {
            "method": "dg.status",
            "role": "reader",
            "summary": "Queue depths, journal tip, gate budget, role-specific gauges — or one artifact's lifecycle row. Below the reviewer role `queues`, `replacements` and `runs` count only the rows the caller may read: the promoted rows and its own (INV-SEC-10), and `tip`, the journal's newest event, is null (GAPS 158). From the reviewer role up it carries `caps[]`, every cap that pauses a pass, `{setting, words, spent, limit, unit, day, resets_at}`, counted in the daemon's own process on its own clock, and `paused[]`, `{pass, cause, words, since, until?, cap?}` for each pass (grade, second_opinion, settle, re_review) that stopped on its last tick, cause `cap` or `backend` (ADR-0132 d5, d18). An API key of scope `events` is answered `{paused, caps, now}` alone (d6). `deployment {mode, in_file, why, since}` is the mode the gate resolved, the file's value (null when the line is absent), the first condition that keeps a file that says development out of it, and the time of the journal's last `deployment-mode`, null when it holds none (ADR-0135 d1). `now` is the daemon's clock in epoch milliseconds, the one every event's ts is stamped on, on every answer, an events key's and one row's included (ADR-0139 d6). From the reviewer role up, `grade_again {stale, oldest, per_tick}` is what the last grade pass left to grade again: the held claims graded before the grading in force for their kind, the oldest one's state ts, and `[gating].grade_again_per_tick` (ADR-0139 d2). To a caller who reads every space, `routing {open[{question, space, crew, wait_minutes, minutes_left, starts, held_clock}], held_sheets[{artifact_id, space, code, crew}], only_admin_proposed[{group_id, space, proposer}], starts_file}` is what doctor's questions and crews lines read: each question still with its answering crew, how many daemon starts restarted its count and whether its wait ran while the served clock was held, each held crew sheet the routing refuses, and each widening crew sheet version only its proposer, the one admin, could commit, and why routing-starts.json could not be read or written, null when it could (ADR-0143 d1, d8; GAPS 1386, 1391). Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156).",
            "returns": "object",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcStatusParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.tip",
            "role": "reader",
            "summary": "The served head of an artifact's relation chain (ADR-0037).",
            "returns": "object: {artifact_id, root, tip, chain[]}",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcTipParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.upload",
            "role": "agent",
            "summary": "Open or resume the caller's upload of one file before the dg.ingest that binds it (ADR-0126 d3; 24 — Attachments §3): a PNG, JPEG or WebP image up to 7 MiB, or a PDF up to 16 MiB, keyed by the caller and the file's SHA-256, so a second call answers the recorded offset. An upload is no content: no event names it, nobody but its uploader addresses it, and it is removed when it expires. Bytes another claim holds start a fresh upload. Refused when the caller may contribute nowhere. Writer identity is the connection's (I2).",
            "returns": "object: {upload_id, offset, size_bytes, part_bytes: 524288, complete, expires_at}",
            "required": [
              "sha256",
              "size_bytes",
              "media_type",
              "name"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcUploadParams",
            "errors": {
              "-32602": "data.reason media_type, attachment_too_large, upload_conflict, attachment_name or not_granted, each with its sentence (24 §3.1)",
              "-32015": "attachment storage at its bound: data.reason upload_quota (with data.retry_after_ms until the caller's oldest upload expires) or attachment_budget (a review frees it)"
            }
          },
          {
            "method": "dg.upload_part",
            "role": "agent",
            "summary": "One part of the caller's open upload at the recorded offset, strict base64 of 1 to 524,288 bytes (24 §3). The last part re-hashes the file and checks its magic bytes, frames, sides and encryption; bytes that can never bind are removed and the sentence says so (GAPS 493). Another identity's upload id is answered as unknown.",
            "returns": "object: {offset}; on the last part {offset, complete: true, sha256}",
            "required": [
              "upload_id",
              "offset",
              "content"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcUploadPartParams",
            "errors": {
              "-32602": "data.reason upload_unknown, encoding, part_offset (data.recorded), part_size, hash_mismatch, media_mismatch, animated, image_dimensions or pdf_encrypted, each with its sentence (24 §3.1)"
            }
          },
          {
            "method": "dg.verify",
            "role": "reader",
            "summary": "Walk the journal chain and verify every signature. Below the reviewer role `events` counts the events the caller may read, as dg.events serves them, and `tip` names the last of them (GAPS 158).",
            "returns": "object: {events, tip}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcVerifyParams"
          },
          {
            "method": "dg.who_can_sign",
            "role": "reviewer",
            "summary": "Who may sign a held row, or a row of a kind the caller would write (ADR-0134 d3): the people who hold reviewer or admin and read on its space, never its writer, by name to an admin and by letter (A, B, ... in the people file's order) to anyone else, and the graders whose promote can count toward its k under the review mode and its class, each by its backend's words. In development the developer comes first on their own row with `self_signs`, and no demo person is listed on it; outside development no demo person is listed (ADR-0135 d2, d5). Refused to every API key, whatever its role or scope.",
            "returns": "object: {people[{who, role, `you?`, self_signs?}], graders[{label, provider}], enough, words, notify} — `enough` is whether those listed can bring the row to its k with no one else, read through the count the auto pass reads; `words` the sentence a console prints; `notify` whether an events key reads the space, so an ask reaches a sink",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcWhoCanSignParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing; an API key is refused (doublegate_sdk.acts.WHO_CAN_SIGN_API_KEY)"
            }
          },
          {
            "method": "dg.why",
            "role": "reader",
            "summary": "Every signed event about one artifact, in order — the provenance answer, with `self_signed` and `demo_signed`, whether its promotion rested on its author's or a demo person's signature (ADR-0135 d3). A reviewer may also name a journal key that is no row (a work area's, a group's); below the reviewer role only a row the caller may read is answered, and a payload's id of a row the caller may not read is null (GAPS 156).",
            "returns": "object: {artifact_id, producer, events[]} — `producer` is the row's, off its first ingest, null when it named none (ADR-0107 d1)",
            "required": [
              "artifact_id"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcWhyParams",
            "errors": {
              "-32602": "unknown artifact: <the id's first 16 characters> — an id that names no row, and, below the reviewer role, a row the caller may not read: another identity's row that is not promoted, answered exactly as an id that names nothing"
            }
          },
          {
            "method": "dg.workarea",
            "role": "agent",
            "summary": "The caller's own work area from the inside (ADR-0102 d8): its own rows with their state, the promoted rows in it whoever wrote them, counts over those two by state, and the caller's own open or complete state. Another identity's rows that are not promoted are never in it, nor counted (ADR-0053, INV-SEC-10). The caller's recorded rows are evidence and are counted apart. Metadata only — ids, states and kinds, never content. The identity is the connection's (I2).",
            "returns": "object: {space, name, state: open|complete, counts{state: n}, rows[{artifact_id, state, kind, held, trust_class, producer}], recorded, producers[{name, kind, rows}]} — `producers` counts the rows shown by the producer each names (ADR-0107 d1)",
            "required": [
              "workarea"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcWorkareaParams",
            "errors": {
              "-32602": "an unknown space, a name outside the grammar, or a work area the caller never wrote into — the same answer whether or not another identity did"
            }
          },
          {
            "method": "dg.workarea_relate",
            "role": "agent",
            "summary": "Relate one work area to another in the same space (ADR-0107 d2): `opened_by` (this area was opened by work in target: a subagent's area under its orchestrator's), `continues` (this area carries on target, which is complete) or `derived_from` (this area's work derives from target's promoted rows). The gate signs a `workarea-related` event under its own key, naming the caller as `by`. It changes no visibility, decides nothing and ranks nothing. An agent relates only an area it wrote into, to an area it wrote into or that holds a promoted row; a reviewer or an admin relates any area someone wrote into. A cycle, a missing target and a `continues` whose target is not complete are refused; the same relation again answers `changed: false` and journals nothing. `lift: true` withdraws a relation with a second event and is a reviewer's: nothing erases one.",
            "returns": "object: {space, workarea, kind, target, by, producer, note, lifted, changed, event_id?}",
            "required": [
              "workarea",
              "kind",
              "target"
            ],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcWorkareaRelateParams",
            "errors": {
              "-32602": "an unknown space, a name outside the grammar, an unknown kind, a relation that would make a cycle among every relation in effect in the space (the refusal names no area; GAPS 106), a target this caller cannot relate to (one answer, whichever test failed), a continues whose target is not complete, an area the caller never wrote into, or an agent's lift"
            }
          },
          {
            "method": "dg.workareas",
            "role": "reviewer",
            "summary": "Every work area in a space, with each writer's own open or complete state and its rows counted by state (ADR-0102 d8): the reviewer's list of who is working where. Recorded rows are evidence and are not among the counts; `recorded` and `evidence_producers` count them beside them. Counts, never content.",
            "returns": "object: {space, workareas[{space, name, writers[{identity, state: open|complete, counts{state: n}, producers[{name, kind, rows}], recorded, evidence_producers[{name, kind, rows}]}]}]}",
            "required": [],
            "dispatched": true,
            "paramsSchema": "#/components/schemas/RpcWorkareasParams",
            "errors": {
              "-32602": "an unknown space, or a caller below the reviewer role"
            }
          }
        ]
      }
    },
    "/mcp": {
      "post": {
        "operationId": "callMcp",
        "summary": "The client tier's MCP surface: the stateless request/response subset.",
        "tags": [
          "mcp"
        ],
        "description": "Needs a credential, like `/rpc`: a harness presents the key this gate issued, and its tools act as that program. Origin is validated before the body is read (the spec's DNS-rebinding MUST). No Mcp-Session-Id is ever issued; one that arrives is ignored.",
        "requestBody": {
          "description": "one JSON-RPC 2.0 MCP message",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpRequest"
              },
              "examples": {
                "discover": {
                  "summary": "handshake, 2026-07-28 style",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "server/discover"
                  }
                },
                "initialize": {
                  "summary": "handshake, the pre-2026 method every shipped SDK still opens with",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "initialize",
                    "params": {
                      "protocolVersion": "2025-11-25",
                      "capabilities": {},
                      "clientInfo": {
                        "name": "demo",
                        "version": "0"
                      }
                    }
                  }
                },
                "toolsList": {
                  "summary": "the tools this tier serves",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 3,
                    "method": "tools/list"
                  }
                },
                "toolsCall": {
                  "summary": "a read-only tool call",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 4,
                    "method": "tools/call",
                    "params": {
                      "name": "doublegate.recall",
                      "arguments": {
                        "query": "retention policy",
                        "k": 5
                      }
                    }
                  }
                },
                "notification": {
                  "summary": "a notification — answered 202 with no body",
                  "value": {
                    "jsonrpc": "2.0",
                    "method": "notifications/initialized"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the MCP response object",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpResponse"
                }
              }
            }
          },
          "202": {
            "description": "a notification or a client response: no body (spec MUST)"
          },
          "400": {
            "description": "malformed JSON, carrying -32700 Parse error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpFailure"
                }
              }
            }
          },
          "401": {
            "description": "no credential, or one the gate refuses: {\"error\":\"<kind>: <why>\",\"kind\":\"missing|malformed|expired|issuer|audience|signature|unavailable|unsupported\"}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "origin refused (DNS rebinding guard)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "the request is larger than this gate accepts (4 MiB unless its configuration says otherwise). Not observed in tests: the gate refuses before reading the body, which a client sees as a reset connection rather than a status line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          }
        ],
        "x-dg-guard": "bearer",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0082"
        ],
        "x-dg-config": [
          "[mcp].max_request_bytes"
        ],
        "x-dg-mcp-catalog": "/mcp/catalog.json"
      },
      "get": {
        "operationId": "getMcpNotAllowed",
        "summary": "No SSE stream is offered on this binding.",
        "tags": [
          "mcp"
        ],
        "description": "405 by design: the streaming half of Streamable HTTP is not offered, so a client must not open a GET stream and wait.",
        "responses": {
          "405": {
            "description": "method not allowed; no body"
          }
        },
        "security": [],
        "x-dg-guard": "none",
        "x-dg-probed": true,
        "x-dg-design": [
          "ADR-0050"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "A person's access token from the sign-in provider this gate names (an OpenID Connect JWT; `GET /identity` says which), or a program's `dgk_…` key this gate issued. Either resolves to one identity with one role, and every verb is held to that role.",
        "x-dg-design": [
          "AUTH-3",
          "ADR-0082"
        ]
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "a transport-level refusal from the listener itself"
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "The listener's own refusal shape (401/404/413/500/502). A *domain* refusal is not this: it arrives as HTTP 200 carrying RpcErrorEnvelope."
      },
      "RpcErrorEnvelope": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/RpcError"
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "A JSON-RPC refusal carried inside a 200 by POST /rpc. Read the body, not the status line."
      },
      "RpcError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "integer",
            "description": "-32700 parse error — the body is not JSON; -32600 invalid request — not a JSON-RPC 2.0 object; -32601 method not found; -32602 invalid params, or a refusal carrying its reason; a field the verb does not declare is refused by name; -32603 internal error — the /rpc frame's own guard; the listener survives; -32000 the caller's identity could not be established; -32001 request too large; -32006 the place where new claims are held is full; data.retry_after_ms says when to try again, and below the reviewer role the refusal says nothing of how many claims are held; -32009 writer or source under an operator-signed ban"
          },
          "message": {
            "type": "string"
          },
          "data": {
            "description": "code-specific detail, e.g. retry_after_ms on -32006"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "additionalProperties": false,
        "x-dg-design": [
          "I6",
          "I7"
        ]
      },
      "JsonRpcRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ],
            "description": "omit or null for a notification; /rpc still answers 200 with the response object"
          },
          "method": {
            "type": "string",
            "pattern": "^dg\\.[a-z_]+$"
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "closed per verb: a field the verb does not declare is refused by name. `null` values are refused — omit the field."
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ],
        "additionalProperties": false,
        "x-dg-design": [
          "I7"
        ]
      },
      "JsonRpcResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/JsonRpcSuccess"
          },
          {
            "$ref": "#/components/schemas/JsonRpcFailure"
          }
        ],
        "description": "Exactly one of result or error. Both arrive with HTTP 200; a JSON-RPC error is not an HTTP error."
      },
      "JsonRpcSuccess": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "jsonrpc",
          "id",
          "result"
        ],
        "additionalProperties": false
      },
      "JsonRpcFailure": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "error": {
            "$ref": "#/components/schemas/RpcError"
          }
        },
        "required": [
          "jsonrpc",
          "id",
          "error"
        ],
        "additionalProperties": false
      },
      "RetrievalPaths": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string"
          },
          "vector": {
            "type": "string"
          },
          "embedder": {
            "type": "string"
          },
          "hybrid": {
            "type": "string"
          }
        },
        "description": "Which retrieval path answered, and with what. A named degradation ('keyword-only …') is reported here rather than silently producing thinner results."
      },
      "StatusPayload": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "client"
            ]
          },
          "queues": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "depth per lifecycle state; a depth is what is waiting, never evidence of processing"
          },
          "gate": {
            "type": "object",
            "additionalProperties": true
          },
          "storage": {
            "type": "object",
            "additionalProperties": true
          },
          "paths": {
            "$ref": "#/components/schemas/RetrievalPaths"
          },
          "tip": {
            "type": [
              "string",
              "null"
            ],
            "description": "the journal head"
          },
          "policy": {
            "type": "object",
            "additionalProperties": true
          },
          "operator": {
            "type": "object",
            "additionalProperties": true
          },
          "degraded": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "named degradations in force; empty when none"
          },
          "quorum_profile": {
            "type": "string"
          },
          "limbo_max_depth": {
            "type": "integer"
          },
          "bans": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "absence": {
            "type": "object",
            "properties": {
              "away": {
                "type": "boolean"
              },
              "since_ms": {
                "type": "integer"
              },
              "declared": {
                "type": "string"
              },
              "held_ms": {
                "type": "integer"
              },
              "ceiling_ms": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "away"
            ]
          },
          "absences": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "since": {
                  "type": "integer"
                },
                "open": {
                  "type": "boolean"
                },
                "declared": {
                  "type": "string"
                },
                "deadline": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          },
          "countersigner": {
            "type": [
              "string",
              "null"
            ],
            "description": "the id of the second, independent gate pinned to countersign here, or null when none is pinned"
          },
          "runs": {
            "$ref": "#/components/schemas/RunSnapshot"
          },
          "reviewer": {
            "type": "object",
            "additionalProperties": true
          },
          "config_rev": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "role",
          "countersigner",
          "runs",
          "queues",
          "paths"
        ],
        "description": "dg.status for this gate, plus what the app's banners are drawn from.",
        "x-dg-design": [
          "ADR-0047 §4",
          "ADR-0112 d2"
        ]
      },
      "ReviewQueue": {
        "type": "object",
        "properties": {
          "held_steering": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReviewItem"
            },
            "description": "every held steering row the caller reads, oldest wait first, at most 50, never counted in cap, items or held_back"
          },
          "held_steering_truncated": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReviewItem"
            }
          },
          "held_back": {
            "type": "integer"
          },
          "total_waiting": {
            "type": "integer"
          },
          "cap": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "items",
          "held_back",
          "total_waiting"
        ],
        "additionalProperties": false
      },
      "ReviewItem": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "state": {
            "type": "string"
          },
          "state_ts": {
            "type": [
              "integer",
              "null"
            ]
          },
          "space": {
            "type": "string"
          },
          "envelope": {
            "type": "object",
            "additionalProperties": true
          },
          "verdicts": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "quorum": {
            "type": "object",
            "additionalProperties": true
          },
          "blocking": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "findings": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "dispositions": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "ranks": {
            "type": "object",
            "additionalProperties": true
          },
          "hidden_for": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "provisional": {
            "type": "boolean"
          },
          "countersigned_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "content_erased": {
            "type": "boolean"
          },
          "last_scanner": {
            "type": [
              "string",
              "null"
            ]
          },
          "self_authored": {
            "type": "boolean",
            "description": "the caller wrote this claim, so the gate refuses their signature: the reviewer is never the author"
          },
          "refusal": {
            "type": "string",
            "description": "present only when self_authored"
          },
          "decisions": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "promote",
                "clear",
                "archive"
              ]
            },
            "description": "the affirmative verdicts dg.sign accepts from this state; reject is its own verb"
          },
          "review_findings_available": {
            "type": "boolean"
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "the claim's attachment entries, names masked, never a byte"
          },
          "who_can_sign": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "dg.who_can_sign's answer for the row: people by name to an admin and by letter to anyone else, graders, enough, words, notify"
          }
        },
        "required": [
          "artifact_id",
          "state",
          "decisions",
          "self_authored"
        ],
        "x-dg-design": [
          "I2",
          "ADR-0134 d3"
        ]
      },
      "RunSnapshot": {
        "type": "object",
        "properties": {
          "reported": {
            "type": "boolean"
          },
          "counts": {
            "type": "object",
            "additionalProperties": true
          },
          "in_flight": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "last_failure": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          }
        },
        "description": "Review-run processing state. `reported: false` means this worker emits no run events — queue depth is not evidence of processing and is not substituted for one."
      },
      "RunsPage": {
        "type": "object",
        "properties": {
          "reported": {
            "type": "boolean"
          },
          "runs": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "total": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "counts": {
            "type": "object",
            "additionalProperties": true
          },
          "in_flight": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "last_failure": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "detail": {
            "type": "string",
            "description": "present when reported is false"
          }
        },
        "required": [
          "reported",
          "runs",
          "total",
          "offset",
          "limit"
        ]
      },
      "RecallResult": {
        "type": "object",
        "properties": {
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecallHit"
            }
          },
          "paths": {
            "$ref": "#/components/schemas/RetrievalPaths"
          },
          "audience": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "hits",
          "paths"
        ]
      },
      "RecallHit": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "score": {
            "type": "number"
          },
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "content_type": {
            "type": "string"
          },
          "space": {
            "type": "string"
          },
          "trust_class": {
            "type": "string"
          },
          "ratified": {
            "type": "boolean",
            "description": "false on the caller's own claims that are still held, unreviewed"
          },
          "provenance": {
            "type": "object",
            "properties": {
              "unreviewed": {
                "type": "boolean"
              },
              "hidden": {
                "type": "boolean"
              },
              "provisional": {
                "type": "boolean"
              }
            }
          }
        },
        "required": [
          "artifact_id"
        ],
        "x-dg-design": [
          "ADR-0053 d2"
        ]
      },
      "Challenge": {
        "type": "object",
        "properties": {
          "nonce": {
            "type": "string"
          },
          "ttl_ms": {
            "type": "integer"
          }
        },
        "required": [
          "nonce",
          "ttl_ms"
        ],
        "additionalProperties": false,
        "description": "A nonce for the operator key to sign. Worthless without the private key.",
        "x-dg-design": [
          "ADR-0028 d1"
        ]
      },
      "SettingsPayload": {
        "type": "object",
        "properties": {
          "configured": {
            "type": "object",
            "additionalProperties": true
          },
          "explicit": {
            "type": "object",
            "additionalProperties": true
          },
          "secrets": {
            "type": "object",
            "additionalProperties": true
          },
          "effective": {
            "type": "object",
            "additionalProperties": true
          },
          "etag": {
            "type": "string"
          },
          "config_rev": {
            "type": [
              "string",
              "null"
            ]
          },
          "drift": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "each field whose effective value differs from the configured one; empty when the file and this process agree"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "choices": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "runs": {
            "$ref": "#/components/schemas/RunSnapshot"
          }
        },
        "required": [
          "configured",
          "effective",
          "etag"
        ],
        "description": "Configured (on disk) beside effective (in this process), plus the drift between them. `secrets` reports presence only — never a value."
      },
      "SettingsValues": {
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "values"
        ],
        "description": "The draft tables to validate or save."
      },
      "SettingsSave": {
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "additionalProperties": true
          },
          "etag": {
            "type": "string"
          }
        },
        "required": [
          "etag"
        ],
        "description": "Compare-and-swap: the etag the page was shown is required."
      },
      "SettingsValidation": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "ok"
        ]
      },
      "SettingsSaveResult": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean",
                "const": true
              },
              "saved": {
                "type": "boolean"
              },
              "applied": {
                "type": "boolean"
              },
              "etag": {
                "type": "string"
              },
              "config_rev": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "configured": {
                "type": "object",
                "additionalProperties": true
              },
              "secrets": {
                "type": "object",
                "additionalProperties": true
              },
              "effective": {
                "type": "object",
                "additionalProperties": true
              },
              "drift": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "ok",
              "saved",
              "applied",
              "etag"
            ]
          },
          {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean",
                "const": false
              },
              "conflict": {
                "type": "boolean"
              },
              "message": {
                "type": "string"
              },
              "etag": {
                "type": "string"
              },
              "current": {
                "type": "object",
                "additionalProperties": true
              },
              "errors": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            },
            "required": [
              "ok"
            ]
          }
        ],
        "description": "A stale or absent etag is a 200 carrying ok:false — the page is told what is actually there rather than asked to guess."
      },
      "SignInForm": {
        "type": "object",
        "properties": {
          "response_type": {
            "type": "string",
            "enum": [
              "code"
            ]
          },
          "client_id": {
            "type": "string"
          },
          "redirect_uri": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "code_challenge": {
            "type": "string"
          },
          "code_challenge_method": {
            "type": "string",
            "enum": [
              "S256"
            ]
          },
          "scope": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string"
          }
        },
        "required": [
          "response_type",
          "client_id",
          "redirect_uri",
          "state",
          "code_challenge",
          "code_challenge_method",
          "email",
          "password"
        ],
        "additionalProperties": false,
        "description": "The authorization request's fields plus the person's email and password; form-encoded."
      },
      "TokenForm": {
        "type": "object",
        "properties": {
          "grant_type": {
            "type": "string",
            "enum": [
              "authorization_code",
              "refresh_token",
              "urn:ietf:params:oauth:grant-type:token-exchange"
            ]
          },
          "code": {
            "type": "string"
          },
          "code_verifier": {
            "type": "string"
          },
          "client_id": {
            "type": "string"
          },
          "redirect_uri": {
            "type": "string"
          },
          "refresh_token": {
            "type": "string"
          },
          "subject_token": {
            "type": "string"
          },
          "subject_token_type": {
            "type": "string"
          },
          "requested_token_type": {
            "type": "string"
          },
          "resource": {
            "type": "string"
          }
        },
        "required": [
          "grant_type"
        ],
        "additionalProperties": false,
        "description": "RFC 6749 §4.1.3 with the PKCE verifier (RFC 7636 §4.5), or §6 with the refresh token, or RFC 8693 §2.1's token exchange for the answer origin alone; form-encoded."
      },
      "LogoutForm": {
        "type": "object",
        "properties": {
          "client_id": {
            "type": "string"
          },
          "refresh_token": {
            "type": "string"
          },
          "id_token_hint": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "description": "The refresh token to forget; the other two are accepted and unused."
      },
      "SignRequest": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "decision": {
            "type": "string",
            "enum": [
              "promote",
              "clear",
              "archive",
              "hold"
            ]
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id",
          "decision"
        ],
        "additionalProperties": false,
        "description": "The signer is whoever the credential names; no proof, identity or role is a field of this body.",
        "x-dg-design": [
          "ADR-0082"
        ]
      },
      "RejectRequest": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "reason": {
            "type": "string"
          },
          "in_favour_of": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false
      },
      "DeferRequest": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false
      },
      "IngestRequest": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "utf-8 text"
          },
          "workarea": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "source_uri": {
            "type": "string"
          },
          "space": {
            "type": "string"
          },
          "producer": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "one segment of the work-area grammar"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "agent",
                  "hook",
                  "person",
                  "tool"
                ]
              }
            },
            "required": [
              "name",
              "kind"
            ],
            "additionalProperties": false,
            "description": "what produced this write, in the writer's own words; read by people, never an identity and never an authorization"
          }
        },
        "required": [
          "content",
          "workarea",
          "source_uri"
        ],
        "additionalProperties": false,
        "description": "Every write names a work area and exactly one of kind (a claim that is reviewed) or content_type (a record that is kept, never reviewed or recalled); how strictly it is checked is the gate's to decide, never the writer's. writer_identity and deployment_id come from the connection; sending either is refused. A producer is optional and changes no claim's id.",
        "x-dg-design": [
          "ADR-0102 d1",
          "ADR-0102 d8",
          "ADR-0102 d9",
          "I2",
          "FR-65",
          "ADR-0107 d1"
        ]
      },
      "AwayRequest": {
        "type": "object",
        "properties": {
          "note": {
            "type": "string"
          },
          "until_ms": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "BackRequest": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "EvaluationReviewRequest": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "settle": {
            "type": "boolean",
            "default": true
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "Starts the server-owned model reviewer. Every disposition it writes is reviewer_class='model'; no human signature is produced."
      },
      "WriteResult": {
        "oneOf": [
          {
            "type": "object",
            "additionalProperties": true
          },
          {
            "$ref": "#/components/schemas/RpcErrorEnvelope"
          }
        ],
        "description": "The verb's result, or the verb's refusal — both inside HTTP 200."
      },
      "IngestResult": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "bare sha256 hex on this surface; the MCP surface prefixes it `sha256:`"
          },
          "state": {
            "type": "string"
          },
          "space": {
            "type": "string"
          },
          "event_id": {
            "type": "string"
          },
          "scan_event": {
            "type": [
              "string",
              "null"
            ]
          },
          "findings_count": {
            "type": "integer"
          },
          "trust_class": {
            "type": "string"
          },
          "registered_type": {
            "type": "boolean"
          }
        },
        "required": [
          "artifact_id",
          "state"
        ],
        "description": "A receipt, not an admission: the claim is held and readable to nobody else until a reviewer who did not write it promotes it. A finding's text is never here."
      },
      "McpRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ],
            "description": "absent or null makes this a notification: 202 with no body (spec MUST)"
          },
          "method": {
            "type": "string",
            "enum": [
              "server/discover",
              "initialize",
              "ping",
              "tools/list",
              "tools/call",
              "notifications/initialized"
            ]
          },
          "params": {
            "type": "object",
            "properties": {
              "_meta": {
                "type": "object",
                "properties": {
                  "io.modelcontextprotocol/protocolVersion": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ],
        "additionalProperties": false
      },
      "McpResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/McpSuccess"
          },
          {
            "$ref": "#/components/schemas/McpFailure"
          }
        ],
        "description": "Errors are JSON-RPC errors inside a 200 — never isError: true."
      },
      "McpSuccess": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer"
            ]
          },
          "result": {
            "$ref": "#/components/schemas/McpResult"
          }
        },
        "required": [
          "jsonrpc",
          "id",
          "result"
        ],
        "additionalProperties": false
      },
      "McpFailure": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer",
                "description": "-32700 parse — the body is not JSON (surfaces as HTTP 400); -32600 invalid request — not a JSON-RPC 2.0 object, or no string method; -32601 method not found — unknown protocol method, or initialize with legacy_initialize off; -32602 invalid params — unknown tool, unknown/missing/null argument, wrong type, over maximum, forbidden writer_identity/deployment_id, or a bare (unprefixed) artifact id; -32603 internal — unexpected backend failure; -32000 the caller's identity could not be established; -32001 the request is larger than this gate accepts; -32002 gate down — the stdio binding cannot reach the daemon socket; -32003 store down; -32004 the deployment is degraded; -32005 UnsupportedProtocolVersionError — _meta protocol version not in the supported list; -32006 the place where new claims are held is full; data.retry_after_ms says when to try again, and below the reviewer role the refusal says nothing of how many claims are held; -32007 plugin tier, no gate reachable; -32008 declared but not yet built; -32009 writer or source under an operator-signed ban"
              },
              "message": {
                "type": "string"
              },
              "data": {}
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc",
          "id",
          "error"
        ],
        "additionalProperties": false
      },
      "McpResult": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "resultType": {
                "type": "string",
                "const": "complete"
              },
              "_meta": {
                "type": "object",
                "properties": {
                  "io.modelcontextprotocol/serverInfo": {
                    "$ref": "#/components/schemas/McpServerInfo"
                  }
                },
                "required": [
                  "io.modelcontextprotocol/serverInfo"
                ]
              }
            },
            "required": [
              "resultType",
              "_meta"
            ]
          },
          {
            "oneOf": [
              {
                "$ref": "#/components/schemas/McpDiscoverResult"
              },
              {
                "$ref": "#/components/schemas/McpInitializeResult"
              },
              {
                "$ref": "#/components/schemas/McpToolsListResult"
              },
              {
                "$ref": "#/components/schemas/McpToolCallResult"
              },
              {
                "type": "object",
                "properties": {
                  "resultType": {
                    "type": "string"
                  },
                  "_meta": {
                    "type": "object",
                    "properties": {}
                  }
                },
                "additionalProperties": false,
                "description": "ping: only the common result metadata"
              }
            ]
          }
        ]
      },
      "McpServerInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "const": "doublegate"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "version"
        ],
        "additionalProperties": false
      },
      "McpDiscoverResult": {
        "type": "object",
        "properties": {
          "protocolVersions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "serverInfo": {
            "$ref": "#/components/schemas/McpServerInfo"
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "tools": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "required": [
              "tools"
            ]
          },
          "instructions": {
            "type": "string"
          }
        },
        "required": [
          "protocolVersions",
          "serverInfo",
          "capabilities"
        ]
      },
      "McpInitializeResult": {
        "type": "object",
        "properties": {
          "protocolVersion": {
            "type": "string"
          },
          "serverInfo": {
            "$ref": "#/components/schemas/McpServerInfo"
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "tools": {
                "type": "object",
                "properties": {
                  "listChanged": {
                    "type": "boolean"
                  }
                }
              }
            },
            "required": [
              "tools"
            ]
          },
          "instructions": {
            "type": "string"
          }
        },
        "required": [
          "protocolVersion",
          "serverInfo",
          "capabilities"
        ]
      },
      "McpToolsListResult": {
        "type": "object",
        "properties": {
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/McpTool"
            }
          }
        },
        "required": [
          "tools"
        ]
      },
      "McpTool": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^doublegate\\.[a-z_]+$"
          },
          "description": {
            "type": "string"
          },
          "inputSchema": {
            "type": "object",
            "properties": {},
            "description": "JSON Schema, closed (additionalProperties: false)"
          },
          "outputSchema": {
            "type": "object",
            "properties": {}
          },
          "annotations": {
            "type": "object",
            "properties": {
              "readOnlyHint": {
                "type": "boolean"
              },
              "destructiveHint": {
                "type": "boolean"
              },
              "idempotentHint": {
                "type": "boolean"
              }
            }
          }
        },
        "required": [
          "name",
          "description",
          "inputSchema"
        ]
      },
      "McpToolCallResult": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/McpContentBlock"
            }
          },
          "structuredContent": {
            "type": "object",
            "additionalProperties": true
          },
          "isError": {
            "type": "boolean",
            "const": false,
            "description": "always false: this build reports failures as JSON-RPC errors, not as tool results"
          }
        },
        "required": [
          "content",
          "structuredContent",
          "isError"
        ],
        "description": "The text block is the JSON encoding of structuredContent. Read structuredContent."
      },
      "McpContentBlock": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "text"
              },
              "text": {
                "type": "string"
              }
            },
            "required": [
              "type",
              "text"
            ],
            "additionalProperties": false,
            "description": "the only block this build emits"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "image"
              },
              "data": {
                "type": "string"
              },
              "mimeType": {
                "type": "string"
              }
            },
            "required": [
              "type",
              "data",
              "mimeType"
            ],
            "additionalProperties": false,
            "description": "declared by the MCP content union; not emitted by this server"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "resource"
              },
              "resource": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "required": [
              "type",
              "resource"
            ],
            "additionalProperties": false,
            "description": "declared by the MCP content union; not emitted by this server"
          }
        ],
        "description": "The MCP content union. Only the text variant is produced here; the others are declared so a client that models the union is not surprised by the schema."
      },
      "McpCatalog": {
        "type": "object",
        "properties": {
          "protocol": {
            "type": "string"
          },
          "endpoint": {
            "type": "object",
            "additionalProperties": true
          },
          "protocolVersions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "methods": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "envelopes": {
            "type": "object",
            "additionalProperties": true
          },
          "tiers": {
            "type": "object",
            "additionalProperties": true
          },
          "served_tier": {
            "type": "string"
          }
        },
        "required": [
          "protocol",
          "protocolVersions",
          "methods",
          "tiers",
          "served_tier"
        ]
      },
      "RpcPingParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.ping — Liveness and identity: deployment id, role, retrieval paths, degraded flags, quorum profile. (role: reader; returns object: {pong, deployment_id, role, paths, degraded, quorum_profile})"
      },
      "RpcDescribeParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.describe — This catalog — the socket describing itself — plus the caller's own identity, role and kind as the gate took them, the role every verb is held to (ADR-0082), and `deployment_mode`, development or production, which says why a row may be signed by its author and grants nothing (ADR-0135 d1). (role: reader; returns object: {service, version, verbs[], documents{}, `you`{identity, role, kind, demo}, deployment_mode} — `demo` is true for a signed-in person the people file marks a demo person and false for every other caller, an API key and the socket's owner included (ADR-0135 d5))"
      },
      "RpcPeopleParams": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "list | assign | remove; default list"
          },
          "iss": {
            "type": "string",
            "description": "the issuer; default this gate's person issuer"
          },
          "sub": {
            "type": "string",
            "description": "the subject at that issuer (assign, remove)"
          },
          "role": {
            "type": "string",
            "description": "reader | agent | reviewer | admin (assign)"
          }
        },
        "additionalProperties": false,
        "description": "dg.people — The role table (ADR-0082 d5, AUTH-4): list every identity this gate has a role for, assign a role to an (iss, sub), or remove one. A role never exceeds the assigner's. `list` also shows the built-in issuer's people and `[auth].admins`. (role: admin; returns object: list → {issuer, people[{iss, sub, role, source, demo, email?, name?, assigned_by?, assigned_at?}]}; assign → {iss, sub, role, event_id} + the list; remove → {iss, sub, removed, was?, event_id?} + the list — `demo` is true where the people file marks a demo person (ADR-0135 d5))"
      },
      "RpcKeysParams": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "list | issue | revoke; default list"
          },
          "label": {
            "type": "string",
            "description": "who or what holds the key (issue)"
          },
          "role": {
            "type": "string",
            "description": "reader | agent | reviewer | admin | runner; default agent (issue). A runner key is an admin's alone to issue or revoke (ADR-0140 d1)"
          },
          "expires_in_days": {
            "type": "integer",
            "description": "optional expiry (issue)"
          },
          "scope": {
            "type": "string",
            "description": "events — the key reads dg.events and dg.status's paused and caps and is refused every other method; a reviewer key alone (issue; ADR-0132 d6)"
          },
          "key_id": {
            "type": "string",
            "description": "the key to revoke (revoke)"
          },
          "reason": {
            "type": "string",
            "description": "why (revoke)"
          }
        },
        "additionalProperties": false,
        "description": "dg.keys — API keys (AUTH-3): list the key table, issue a `dgk_…` key (shown once; stored as its hash in an `apikey-issued` event) with a role no higher than the caller's, or revoke one by id. Below admin, list and revoke reach a key the caller issued and a key of a role no higher than its own whose open grants lie within the spaces it reads; any other key is answered as one that does not exist (GAPS 402). (role: reviewer; returns object: list → {keys[{key_id, label, role, issued_by, issued_at, expires_at, revoked_at, scope}]}; issue → {key, key_id, role, label, expires_at, event_id, scope}; revoke → {key_id, revoked, already?, event_id?} + the list)"
      },
      "RpcIngestBundleParams": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "base64 ZIP bytes (strict decoding)"
          },
          "source_uri": {
            "type": "string",
            "description": "where the archive came from"
          },
          "manifest": {
            "type": "object",
            "description": "optional SDK v1 manifest, recomputed completely against the archive bytes"
          },
          "space": {
            "type": "string",
            "description": "default main; must already exist (ADR-0091 d2)"
          },
          "workarea": {
            "type": "string",
            "description": "the work area this is remembered into (ADR-0102 d1): a path of 1-8 segments joined by `/`, each segment the space-name grammar, at most 200 characters (d4). Comes into being on the first write that names it; grants nothing; never creates a space. Required: a write that names none is refused"
          },
          "producer": {
            "type": "object",
            "description": "what produced this write, in the writer's own words (ADR-0107 d1): {name, kind}, kind one of agent, hook, person, tool and name one segment of the work-area grammar. Read by people and never checked as an authorization; the identity stays the connection's (I2). Rides the ingest event beside the work area, never the envelope, so it changes no artifact id"
          }
        },
        "required": [
          "content",
          "source_uri",
          "workarea"
        ],
        "additionalProperties": false,
        "description": "dg.ingest_bundle — Inspect a ZIP without execution. Retain the original archive, its complete canonical manifest and every scanned file occurrence in quarantine, all at T-5 (the daemon proposes the class; the verb takes none). The bytes are vouched for by the identity that proposes them; no fetch record travels (ADR-0110 d6). Writer is the connection identity. Validation never authorizes installation or promotion. (role: agent; returns object: {state: quarantined, validation: valid|failed, archive_artifact_id, manifest_artifact_id, members: [{path, content_hash, size_bytes, classification, artifact_id, state, scan_event}], workarea, space?, producer, reason?}. Failed validation retains the archive only, and `reason` is archive_invalid:<detail>. No model judgment or installation. Every row of the unit carries the producer.)"
      },
      "RpcIngestMapParams": {
        "type": "object",
        "properties": {
          "manifest": {
            "type": "object",
            "description": "an SDK `project_map/v1` manifest: map_version, project, produced_by, members, nodes, edges, and archive_hash with archive_size when the members came from a container — `doublegate_sdk.project_map.check_manifest` is the schema"
          },
          "source_uri": {
            "type": "string",
            "description": "where the map was taken, e.g. repo://client-gate@316e2cb"
          },
          "content": {
            "type": "string",
            "description": "optional: the base64 ZIP the manifest was computed from (strict decoding). With it the answer is `valid`; without it, `structural`"
          },
          "encoding": {
            "type": "string",
            "description": "base64 — required with content, refused without it"
          },
          "space": {
            "type": "string",
            "description": "default main; must already exist (ADR-0091 d2)"
          },
          "workarea": {
            "type": "string",
            "description": "the work area this is remembered into (ADR-0102 d1): a path of 1-8 segments joined by `/`, each segment the space-name grammar, at most 200 characters (d4). Comes into being on the first write that names it; grants nothing; never creates a space. Required: a write that names none is refused"
          },
          "producer": {
            "type": "object",
            "description": "what produced this write, in the writer's own words (ADR-0107 d1): {name, kind}, kind one of agent, hook, person, tool and name one segment of the work-area grammar. Read by people and never checked as an authorization; the identity stays the connection's (I2). Rides the ingest event beside the work area, never the envelope, so it changes no artifact id. Carried by the map and every member row"
          },
          "derives_from": {
            "type": "array",
            "description": "parent artifact ids (provenance edges, frozen in the envelope); below the reviewer role each a row the caller may read, checked after the write's other checks, the space first (ADR-0102 d2)",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "manifest",
          "source_uri",
          "workarea"
        ],
        "additionalProperties": false,
        "description": "dg.ingest_map — Admit a project map: one tree's nodes and edges as a single gated artifact at T-5 (the daemon proposes the class; the verb takes none). The manifest IS the map and its edges are bytes inside it — they never enter the relation graph, never appear in `tip` or `relations`, and can never move what recall serves (ADR-0089). The gate validates and admits; it parses no source and knows no language (ADR-0090). Validation is whole against the SDK's `project_map/v1` contract: a manifest that does not check admits nothing at all. Send the archive beside it and the member table, the file and directory nodes and the `contains` edges are recomputed from those bytes, and every content node is retained as an occurrence of this map. Writer is the connection identity. (role: agent; returns object: {state: quarantined|refused, validation: valid|structural|failed, map_artifact_id, artifact_state, space, workarea, producer, project, produced_by, nodes, edges, members: [{path, content_hash, size_bytes, classification, artifact_id, state}], duplicate, reason?}. `validation` names which of 16 §9's two checks ran and a check that did not happen is never reported as one (GAPS 83): `valid` when the archive travelled and recomputed, `structural` when the manifest arrived alone, and a map with no archive carries no member rows. A refusal retains nothing and `reason` is one of: map_invalid:<detail> | archive_invalid:<detail>. The map's bytes are vouched for by the identity that proposes them; no fetch record travels (ADR-0110 d6).)"
      },
      "RpcIngestParams": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "utf-8 text, or base64 with encoding='base64'"
          },
          "workarea": {
            "type": "string",
            "description": "the work area this is remembered into (ADR-0102 d1): a path of 1-8 segments joined by `/`, each segment the space-name grammar, at most 200 characters (d4). Comes into being on the first write that names it; grants nothing; never creates a space. Required: a write that names none is refused"
          },
          "kind": {
            "type": "string",
            "description": "the gated class this is, which decides its trust class (ADR-0102 d8); exactly one of kind and content_type",
            "enum": [
              "memory",
              "derived_fact",
              "summary",
              "steering",
              "skill",
              "prompt_template",
              "imported_document",
              "question",
              "answer"
            ]
          },
          "content_type": {
            "type": "string",
            "description": "answer | bundle_asset | bundle_document | bundle_python | bundle_skill | derived_fact | imported_document | map_asset | map_document | map_source | memory | project_map | prompt_template | question | skill | skill_archive | steering | summary | bundle_manifest | event | generation | message | message_part | scratchpad_state | session | state_snapshot | tool_call | trace | usage_record | verdict; the second group is the recorded path (taxonomy §2.2) — written and replayable, never graded as a belief; an unregistered type enters at the strictest class (FR-18). On this verb it names a recorded-path class only; a gated class travels as kind (ADR-0102 d9)",
            "enum": [
              "answer",
              "bundle_asset",
              "bundle_document",
              "bundle_python",
              "bundle_skill",
              "derived_fact",
              "imported_document",
              "map_asset",
              "map_document",
              "map_source",
              "memory",
              "project_map",
              "prompt_template",
              "question",
              "skill",
              "skill_archive",
              "steering",
              "summary",
              "bundle_manifest",
              "event",
              "generation",
              "message",
              "message_part",
              "scratchpad_state",
              "session",
              "state_snapshot",
              "tool_call",
              "trace",
              "usage_record",
              "verdict"
            ]
          },
          "source_uri": {
            "type": "string",
            "description": "where it came from, e.g. agent://hermes/session-42"
          },
          "trust_class": {
            "type": "string",
            "description": "refused by name: the class follows the kind, a recorded write enters at T-1, and the scanner may only raise either (ADR-0102 d8, ADR-0021)"
          },
          "space": {
            "type": "string",
            "description": "the folder within the deployment; default main; must already exist — an unknown one is refused before anything is written (ADR-0091 d2)"
          },
          "encoding": {
            "type": "string",
            "description": "'base64', decoded strictly, or absent for text; the decoded body must be UTF-8 text with no NUL, and a file is an attachment (24 — Attachments §3.1, `content_not_text`)"
          },
          "producer": {
            "type": "object",
            "description": "what produced this write, in the writer's own words (ADR-0107 d1): {name, kind}, kind one of agent, hook, person, tool and name one segment of the work-area grammar. Read by people and never checked as an authorization; the identity stays the connection's (I2). Rides the ingest event beside the work area, never the envelope, so it changes no artifact id"
          },
          "addressed_to": {
            "type": "object",
            "description": "on a steering write or a question only: whom it is for, {names, kinds}, either or both and at least one a nonempty list — names 1-64 producer names, kinds among agent, hook, person, tool, none repeated. A recall that declares a producer is shown the rows with no address and those whose names, where given, hold its name and whose kinds, where given, hold its kind. The writer's word: it narrows, and grants and hides nothing (ADR-0116 d18). On a question kinds is exactly [\"person\"], names optional (ADR-0131 d1). Refused on any other kind and on a recorded write. Rides the ingest event beside the producer, never the envelope"
          },
          "derives_from": {
            "type": "array",
            "description": "parent artifact ids (provenance edges, frozen in the envelope); below the reviewer role each a row the caller may read, checked after the write's other checks, the space first (ADR-0102 d2)",
            "items": {
              "type": "string"
            }
          },
          "attachments": {
            "type": "array",
            "description": "up to 4 {sha256, name?, source_uri?}, each a completed dg.upload of the caller's, bound to this claim by hash (ADR-0126 d3); the envelope is then v2, a source_uri lifts the claim to T-4 (d5), and the duplicate key is the words' hash and the ordered attachment hashes. steering, skill, prompt_template and recorded classes carry none",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "content",
          "workarea",
          "source_uri"
        ],
        "additionalProperties": false,
        "description": "dg.ingest — Propose content into a work area (ADR-0102 d1): a gated class named by `kind`, whose trust class the gate derives (memory, derived_fact, summary, steering, question, answer → T-1; skill, prompt_template → T-2; imported_document, third-party content → T-4; the scanner may only raise it), or a recorded-path class named by `content_type`, entered as T-1 and never graded. Scanned before this answers (ADR-0053 d1) and lands in quarantine at L1 (or L1b on a finding); nothing is readable until promoted. A repeat of the same bytes in the same space by the same identity, or of bytes already promoted there, answers `duplicate: true` with that row and the work area it was first written into; bytes another identity holds unpromoted become the caller's own new row (ADR-0102 d3). Writer identity is the connection's, never a parameter (I2). A request that names no `workarea` is refused, whatever else it carries (ADR-0102 d1). A `steering` row is a direction to the agents at its work area and every area below it: held, scanned and graded like any row, and once promoted shown by its place beside a recall's hits, never among them (ADR-0116 d16, d17). A `question` is a member's question to the people of its crew, addressed to people: its body is the closed form `question/1` (`doublegate_sdk.question`), checked here, and it is never a hit in any scope; one writer holds at most eight in one work area (ADR-0131 d1, d2). The `ingest` event records the writer's principal (AUTH-5). (role: agent; returns object: {artifact_id, state, space, workarea?, producer, kind?|content_type?, findings_count, scan_event, trust_class} — the scanner's rule pass ran before this answered (ADR-0053 d1); state is L1_SCANNED or L1B_FLAGGED (RECORDED on the recorded path), and a finding's text is never in the answer. A duplicate answers {artifact_id, state, space, workarea, producer, duplicate: true}, the producer the row was first written with)"
      },
      "RpcUploadParams": {
        "type": "object",
        "properties": {
          "sha256": {
            "type": "string",
            "description": "the file's SHA-256, 64 lowercase hex"
          },
          "size_bytes": {
            "type": "integer",
            "description": "the file's exact length"
          },
          "media_type": {
            "type": "string",
            "description": "image/png, image/jpeg, image/webp or application/pdf; checked against the magic bytes at the last part"
          },
          "name": {
            "type": "string",
            "description": "1 to 255 UTF-8 bytes, no control character, slash, backslash or NUL; display only, never a file name on disk"
          }
        },
        "required": [
          "sha256",
          "size_bytes",
          "media_type",
          "name"
        ],
        "additionalProperties": false,
        "description": "dg.upload — Open or resume the caller's upload of one file before the dg.ingest that binds it (ADR-0126 d3; 24 — Attachments §3): a PNG, JPEG or WebP image up to 7 MiB, or a PDF up to 16 MiB, keyed by the caller and the file's SHA-256, so a second call answers the recorded offset. An upload is no content: no event names it, nobody but its uploader addresses it, and it is removed when it expires. Bytes another claim holds start a fresh upload. Refused when the caller may contribute nowhere. Writer identity is the connection's (I2). (role: agent; returns object: {upload_id, offset, size_bytes, part_bytes: 524288, complete, expires_at})"
      },
      "RpcUploadPartParams": {
        "type": "object",
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "from dg.upload"
          },
          "offset": {
            "type": "integer",
            "description": "the recorded offset dg.upload or the last part answered"
          },
          "content": {
            "type": "string",
            "description": "the part, strict base64"
          }
        },
        "required": [
          "upload_id",
          "offset",
          "content"
        ],
        "additionalProperties": false,
        "description": "dg.upload_part — One part of the caller's open upload at the recorded offset, strict base64 of 1 to 524,288 bytes (24 §3). The last part re-hashes the file and checks its magic bytes, frames, sides and encryption; bytes that can never bind are removed and the sentence says so (GAPS 493). Another identity's upload id is answered as unknown. (role: agent; returns object: {offset}; on the last part {offset, complete: true, sha256})"
      },
      "RpcAttachmentParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the claim; held to the one read rule by the verb itself, and a claim the caller may not read is answered as an unknown id"
          },
          "content_hash": {
            "type": "string",
            "description": "an attachment hash the claim's envelope names"
          },
          "offset": {
            "type": "integer",
            "description": "default 0; pages are 524,288 bytes"
          },
          "original": {
            "type": "boolean",
            "description": "the stored bytes; the writer and reviewers only; default false"
          }
        },
        "required": [
          "artifact_id",
          "content_hash"
        ],
        "additionalProperties": false,
        "description": "dg.attachment — One page of one attachment of a claim the caller may read (ADR-0126 d6; 24 — Attachments §5): the served copy, an image keeping only the chunks that draw it and a PDF as stored, to every reader; the original, with original true, to the claim's writer and a reviewer that reads the space alone. Never by a hash alone: an id the caller may not read, a hash the claim does not carry, or an original below those two are answered as an unknown id. A page served whole and in order is logged, and a promoting or clearing signature counts only from an identity served every attachment to its end (`unseen_attachment` on dg.sign). (role: reader; returns object: {content_hash, media_type, name (masked), copy: served|original, size, offset, content (base64), next_offset|null}; the last page adds served_hash, stripped[], and to a reviewer metadata[{kind, text}] (masked, at most 65,536 bytes of JSON); an image whose chunks cannot be walked answers {copy: served, prepared: false} and no content)"
      },
      "RpcCompleteParams": {
        "type": "object",
        "properties": {
          "workarea": {
            "type": "string",
            "description": "the work area, by its path"
          },
          "space": {
            "type": "string",
            "description": "default main; must already exist"
          },
          "note": {
            "type": "string",
            "description": "why, for the journal"
          },
          "identity": {
            "type": "string",
            "description": "whose part: reviewer and admin only; an agent naming any identity but its own is refused (I2). Default the caller"
          }
        },
        "required": [
          "workarea"
        ],
        "additionalProperties": false,
        "description": "dg.complete — Mark one identity's part of a work area complete (ADR-0102 d6): the gate signs a `workarea-completed` event under its own key, and later writes by that identity into that work area are refused -32010 with reason `work_complete`. It changes no visibility, decides nothing and releases nothing — held rows stay in review exactly where they were — and there is no reopen. An agent completes its own part; a reviewer or an admin may complete any identity's by naming it. A second complete answers `changed: false` and journals nothing. (role: agent; returns object: {space, name, identity, state: complete, changed, event_id?})"
      },
      "RpcWorkareaParams": {
        "type": "object",
        "properties": {
          "workarea": {
            "type": "string",
            "description": "the work area, by its path"
          },
          "space": {
            "type": "string",
            "description": "default main; must already exist"
          }
        },
        "required": [
          "workarea"
        ],
        "additionalProperties": false,
        "description": "dg.workarea — The caller's own work area from the inside (ADR-0102 d8): its own rows with their state, the promoted rows in it whoever wrote them, counts over those two by state, and the caller's own open or complete state. Another identity's rows that are not promoted are never in it, nor counted (ADR-0053, INV-SEC-10). The caller's recorded rows are evidence and are counted apart. Metadata only — ids, states and kinds, never content. The identity is the connection's (I2). (role: agent; returns object: {space, name, state: open|complete, counts{state: n}, rows[{artifact_id, state, kind, held, trust_class, producer}], recorded, producers[{name, kind, rows}]} — `producers` counts the rows shown by the producer each names (ADR-0107 d1))"
      },
      "RpcWorkareasParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "default main; must already exist"
          }
        },
        "additionalProperties": false,
        "description": "dg.workareas — Every work area in a space, with each writer's own open or complete state and its rows counted by state (ADR-0102 d8): the reviewer's list of who is working where. Recorded rows are evidence and are not among the counts; `recorded` and `evidence_producers` count them beside them. Counts, never content. (role: reviewer; returns object: {space, workareas[{space, name, writers[{identity, state: open|complete, counts{state: n}, producers[{name, kind, rows}], recorded, evidence_producers[{name, kind, rows}]}]}]})"
      },
      "RpcWorkareaRelateParams": {
        "type": "object",
        "properties": {
          "workarea": {
            "type": "string",
            "description": "the area that depends, by its path"
          },
          "kind": {
            "type": "string",
            "description": "how it relates to target (ADR-0107 d2)",
            "enum": [
              "opened_by",
              "continues",
              "derived_from"
            ]
          },
          "target": {
            "type": "string",
            "description": "the area it depends on, in the same space"
          },
          "space": {
            "type": "string",
            "description": "default main; must already exist. Both areas are in it: cross-space lineage stays claim-level derives_from (ADR-0035 d1)"
          },
          "note": {
            "type": "string",
            "description": "why, for the journal"
          },
          "producer": {
            "type": "object",
            "description": "what produced this write, in the writer's own words (ADR-0107 d1): {name, kind}, kind one of agent, hook, person, tool and name one segment of the work-area grammar. Read by people and never checked as an authorization; the identity stays the connection's (I2). Rides the ingest event beside the work area, never the envelope, so it changes no artifact id"
          },
          "lift": {
            "type": "boolean",
            "description": "reviewer and admin only: withdraw the relation with a second event; a lift of a relation not in effect answers `changed: false`"
          }
        },
        "required": [
          "workarea",
          "kind",
          "target"
        ],
        "additionalProperties": false,
        "description": "dg.workarea_relate — Relate one work area to another in the same space (ADR-0107 d2): `opened_by` (this area was opened by work in target: a subagent's area under its orchestrator's), `continues` (this area carries on target, which is complete) or `derived_from` (this area's work derives from target's promoted rows). The gate signs a `workarea-related` event under its own key, naming the caller as `by`. It changes no visibility, decides nothing and ranks nothing. An agent relates only an area it wrote into, to an area it wrote into or that holds a promoted row; a reviewer or an admin relates any area someone wrote into. A cycle, a missing target and a `continues` whose target is not complete are refused; the same relation again answers `changed: false` and journals nothing. `lift: true` withdraws a relation with a second event and is a reviewer's: nothing erases one. (role: agent; returns object: {space, workarea, kind, target, by, producer, note, lifted, changed, event_id?})"
      },
      "RpcLineageParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "default main; must already exist"
          },
          "workarea": {
            "type": "string",
            "description": "the area to centre on; default the whole space"
          }
        },
        "additionalProperties": false,
        "description": "dg.lineage — The lineage of a space, or of one work area with its ancestors, its descendants and every area one relation or derivation away (ADR-0107 d3): a view computed on each call, never a store. Nodes are work areas with their writers, each writer's open or complete state, the producers with their counts and last writes, and promoted, held and recorded counts. Every edge points from the area that depends to the one it depends on and names its provenance: `path` (a/b under a, by name alone), `declared` (a relation in effect) or `derived` (promoted rows whose derives_from crosses two areas, counted). Below the reviewer role an area appears only when it holds a promoted row or the caller wrote into it, counts are over promoted rows and the caller's own, a path the caller sees nothing in is drawn `implicit`, and a derived edge is a count, never rows (AUTH-4). An orchestrator with subagents, a pipeline and a swarm are shapes of this graph; none is stored. (role: reader; returns object: {space, workarea, nodes[{name, implicit, writers[], states{identity: open|complete}, producers[{name, kind, rows, last_write}], promoted, held, recorded, first_write, last_write, rows?[{artifact_id, state, kind, held, trust_class, producer}]}], edges[{source, target, provenance: path|declared|derived, kind?, by?, producer?, count?, rows?}]} — `first_write`, `last_write` and a producer's `last_write` are ingest times in ms, a producer's the newest among the rows it is counted over; `recorded` counts the recorded evidence rows by the rule the producer counts follow, so an area holding only hook rows reads as evidence (ADR-0111 d2); a node's `rows`, the claims each producer wrote, only on the named area's node and only those the caller may see there (ADR-0107 d5, GAPS 107); an edge's `rows`, the (child, parent) id pairs behind a derived edge, only for a reviewer)"
      },
      "RpcEventsParams": {
        "type": "object",
        "properties": {
          "since": {
            "type": "string",
            "description": "the `next` or `tip` of an earlier answer from this gate, sealed and opaque; omit to read from the journal's start, or backward from the tip. One this journal did not issue, one in the readable form it no longer issues, an altered one, or one it can no longer serve (beyond its tip after a restore, or rewritten under it), is refused naming since (GAPS 117, 158)"
          },
          "types": {
            "type": "array",
            "description": "event types to serve, each one `doublegate_sdk.events` serves; a type the catalog lacks or does not serve is refused by name",
            "items": {
              "type": "string"
            }
          },
          "space": {
            "type": "string",
            "description": "only events in this space; must already exist"
          },
          "workarea": {
            "type": "string",
            "description": "only events in this work area, exactly"
          },
          "limit": {
            "type": "integer",
            "description": "most events served; 1-1000, default 100"
          },
          "direction": {
            "type": "string",
            "description": "forward (default) or backward, newest first",
            "enum": [
              "forward",
              "backward"
            ]
          }
        },
        "additionalProperties": false,
        "description": "dg.events — The gate's journal as events, in the order it was appended and never by `ts` (ADR-0109 d3), for a program that follows the lifecycle from a cursor it keeps: a peer. Each event names its type in the journal's own word, the row, replacement group, work area or space it is about, its space and work area, the state it leaves and enters when it moved one, who acted, and the fields `doublegate_sdk.events.CATALOG` lists for its type: identifiers, closed words and counts, never content, a title, a finding, a score or words a person wrote. One call reads at most 1000 events, served or not, and answers `next`, where the next call reads on, and `caught_up`, whether the caller has caught up with what it may read: true when nothing more it would be served lies before the end the read moves toward (the tip, or backward the journal's head), so there is nothing more to read now, and false when the caller reads on, an empty page included. A page full at `limit` reads on, serving nothing, to the next event it would serve, and `next` stands before that event, so a full page says nothing about events the caller may not read; a call that spends its 1000 events first answers false (GAPS 158). Every cursor is sealed (ADR-0117 d4, GAPS 158): encrypted and authenticated under a key derived from the deployment key, one length, and fresh on every answer, so none says where it stands or whether the journal moved, and two are never compared. Below the reviewer role a caller sees every event of a row it wrote and a row's events from its promotion on, a work area's where `dg.lineage` shows it, a group's when it may see every row the group names, and the space registry's; from reviewer up, every served event (AUTH-4). Events that belong to no space, and a pinned authority's, are not served (GAPS 118, 119). Pull only: the gate opens no connection and keeps no position for a reader. Every answer names `tip`, the cursor past the last journal event its projections hold, where a follower starts reading forward from the newest event; `direction: backward` reads from since, or from the tip, toward the journal's head, newest first, and its `next` continues backward (ADR-0111 d2, GAPS 127). (role: reader; returns object: {events[{type, object, artifact_id, space, workarea, leaves, enters, by, event_id, ts, trust_class?, content_type?, provisional?, …the type's own fields}], next, tip, caught_up, format_version} — next and tip sealed, caught_up a boolean)"
      },
      "RpcRecallParams": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "natural-language query; may be left out when a work area is named, and the answer is then the steering in force there and no hits (ADR-0116 d19)"
          },
          "scope": {
            "type": "string",
            "description": "closest | workarea | space | all (ADR-0102 d5); default closest when a workarea is named, space when none is",
            "enum": [
              "closest",
              "workarea",
              "space",
              "all"
            ]
          },
          "workarea": {
            "type": "string",
            "description": "where closest starts, or the one area workarea searches; refused under space and all (ADR-0102 d9)"
          },
          "space": {
            "type": "string",
            "description": "the space searched, default main; must already exist; refused under all"
          },
          "spaces": {
            "type": "array",
            "description": "the space scope over several spaces, the earlier operand; refused beside space or any other scope (GAPS 96)",
            "items": {
              "type": "string"
            }
          },
          "k": {
            "type": "integer",
            "description": "max hits; default 10. The held rows are capped at k apart from the ranked"
          },
          "mode": {
            "type": "string",
            "description": "hybrid | lexical | vector; default hybrid"
          },
          "include_provisional": {
            "type": "boolean",
            "description": "default true"
          },
          "include_superseded": {
            "type": "boolean",
            "description": "default false (ADR-0035 d6)"
          },
          "audience": {
            "type": "string",
            "description": "ADR-0052 d2: the reader's preference, an argument never an identity — hides for it apply; it weights nothing, since the audience weight retired with dg.rank (ADR-0110 d6); omitted = only a hide for all applies"
          },
          "include_hidden": {
            "type": "boolean",
            "description": "ADR-0052 d4: rows hidden for this audience (or for all) are dropped before fusion; true returns them, each marked provenance.hidden"
          },
          "include_own_pending": {
            "type": "boolean",
            "description": "ADR-0053 d2 (FR-13): default true — the caller's OWN scanned, unflagged claims still in quarantine, from the levels the scope searched, are appended after the ranked hits, each `held: true`, `ratified: false`, `provenance.unreviewed: true`, with its state; never anyone else's, never L0 or L1b"
          },
          "producer": {
            "type": "object",
            "description": "the producer the caller declares itself to be, {name, kind} as dg.ingest takes it: the steering is then the rows with no address and those addressed to it (ADR-0116 d18). It narrows, never orders; refused under space and all"
          },
          "steering_k": {
            "type": "integer",
            "description": "how many rows of the steering in force are answered; 1-20, default 5; the rest are counted in steering_omitted (ADR-0116 d19). Refused under space and all"
          }
        },
        "additionalProperties": false,
        "description": "dg.recall — Read the active store as far as a scope says to look (ADR-0102 d5), plus the caller's own scanned claims still in quarantine, appended after the ranked hits and marked held (ADR-0053 d2). `closest` starts at a work area and walks up its path to the first level any promoted row answers; `workarea` is that area alone; `space` is every row in one space, those written before work areas included; `all` is every space this gate declares. Default: `closest` with a work area, `space` without. A scope chooses which rows are searched, never how they rank. Nobody else's quarantine, ever (I1). Provisional hits are marked; superseded are hidden unless asked for. A recall that names a work area (`closest`, `workarea`) also answers `steering`, the steering in force there: every promoted steering row in the space at the area or one of its ancestors, whatever the query, less what a supersession or a reviewer's hide withdraws, nearest place first and the newest write first within a place, cut at `steering_k` with `steering_omitted` counting the rest (ADR-0116 d17-d19). A steering row is never a ranked hit. With a work area the query may be left out: the answer is then the steering alone, no hits (GAPS 251). (role: reader; returns object: {scope, space, workarea, answered_by, hits[{artifact_id, score, body, space, workarea, producer, held, distance, provisional, provenance, signals}], paths, audience?, steering[{artifact_id, body, space, workarea, distance, producer, writer{identity, kind}, addressed_to, promoted_at, provisional}], steering_omitted} — the ranked hits first, then the caller's own held rows. `distance` is the levels walked up from the named work area (0 under workarea; null under space and all); `answered_by` the level that answered; `workarea` null on a row written before work areas. `steering` is empty and `steering_omitted` 0 under space and all; a steering entry's `writer.kind` is the principal kind its ingest recorded (person, program, local), null where none was recorded, and `promoted_at` the promotion's time in ms)"
      },
      "RpcStatusParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "one artifact instead of the deployment"
          }
        },
        "additionalProperties": false,
        "description": "dg.status — Queue depths, journal tip, gate budget, role-specific gauges — or one artifact's lifecycle row. Below the reviewer role `queues`, `replacements` and `runs` count only the rows the caller may read: the promoted rows and its own (INV-SEC-10), and `tip`, the journal's newest event, is null (GAPS 158). From the reviewer role up it carries `caps[]`, every cap that pauses a pass, `{setting, words, spent, limit, unit, day, resets_at}`, counted in the daemon's own process on its own clock, and `paused[]`, `{pass, cause, words, since, until?, cap?}` for each pass (grade, second_opinion, settle, re_review) that stopped on its last tick, cause `cap` or `backend` (ADR-0132 d5, d18). An API key of scope `events` is answered `{paused, caps, now}` alone (d6). `deployment {mode, in_file, why, since}` is the mode the gate resolved, the file's value (null when the line is absent), the first condition that keeps a file that says development out of it, and the time of the journal's last `deployment-mode`, null when it holds none (ADR-0135 d1). `now` is the daemon's clock in epoch milliseconds, the one every event's ts is stamped on, on every answer, an events key's and one row's included (ADR-0139 d6). From the reviewer role up, `grade_again {stale, oldest, per_tick}` is what the last grade pass left to grade again: the held claims graded before the grading in force for their kind, the oldest one's state ts, and `[gating].grade_again_per_tick` (ADR-0139 d2). To a caller who reads every space, `routing {open[{question, space, crew, wait_minutes, minutes_left, starts, held_clock}], held_sheets[{artifact_id, space, code, crew}], only_admin_proposed[{group_id, space, proposer}], starts_file}` is what doctor's questions and crews lines read: each question still with its answering crew, how many daemon starts restarted its count and whether its wait ran while the served clock was held, each held crew sheet the routing refuses, and each widening crew sheet version only its proposer, the one admin, could commit, and why routing-starts.json could not be read or written, null when it could (ADR-0143 d1, d8; GAPS 1386, 1391). Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156). (role: reader; returns object)"
      },
      "RpcPendingParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "default 100"
          }
        },
        "additionalProperties": false,
        "description": "dg.pending — What is waiting in quarantine, as metadata (state, class, kind) — never content. (role: reader; returns object: {pending[], truncated} — `truncated` when rows past the limit were left out (ADR-0133 d4))"
      },
      "RpcInventoryParams": {
        "type": "object",
        "properties": {
          "states": {
            "type": "array",
            "description": "restrict to these lifecycle states; default all. Unknown names are ignored",
            "items": {
              "type": "string"
            }
          },
          "spaces": {
            "type": "array",
            "description": "restrict to these spaces; default all",
            "items": {
              "type": "string"
            }
          },
          "q": {
            "type": "string",
            "description": "substring match on artifact id, content type or writer identity"
          },
          "offset": {
            "type": "integer",
            "description": "page offset; default 0"
          },
          "limit": {
            "type": "integer",
            "description": "page size; default 50, capped at 200"
          }
        },
        "additionalProperties": false,
        "description": "dg.inventory — Every artifact in EVERY lifecycle state, as metadata, paged and filterable — the operator workspace's list. Deliberately wider than dg.pending, which answers about quarantine only and so can never show the ACTIVE row a handoff is made from. Carries the spaces the store actually holds, because a caller that cannot name a space cannot pass `spaces` to dg.recall, and a recall without it falls back to `main` and answers nothing. Metadata only: no content, ever. Below the reviewer role the rows, `total`, `counts` and `spaces` cover only the promoted rows and the caller's own, as dg.graph's do (INV-SEC-10). (role: reader; returns object: {artifacts[], total, offset, limit, counts{state: n}, spaces[], states[]} — `counts` is over the filtered set, `spaces` the spaces holding a row the caller may list and `states` the lifecycle's vocabulary, so a filter control can be rendered from the answer itself)"
      },
      "RpcWhyParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.why — Every signed event about one artifact, in order — the provenance answer, with `self_signed` and `demo_signed`, whether its promotion rested on its author's or a demo person's signature (ADR-0135 d3). A reviewer may also name a journal key that is no row (a work area's, a group's); below the reviewer role only a row the caller may read is answered, and a payload's id of a row the caller may not read is null (GAPS 156). (role: reader; returns object: {artifact_id, producer, events[]} — `producer` is the row's, off its first ingest, null when it named none (ADR-0107 d1))"
      },
      "RpcAuditParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.audit — The decisions about one artifact with signatures verified. Below the reviewer role a payload's id of a row the caller may not read is null (GAPS 156). Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156). (role: reader; returns object)"
      },
      "RpcTipParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.tip — The served head of an artifact's relation chain (ADR-0037). (role: reader; returns object: {artifact_id, root, tip, chain[]})"
      },
      "RpcRelationsParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.relations — The typed relation edges touching one artifact (ADR-0035); below the reviewer role only the edges whose other end the caller may read. (role: reader; returns object: {artifact_id, relations[]})"
      },
      "RpcApprovalParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.approval — Who approved one artifact, or why it waits — a read model folded from its journal, never a state. `status` is one of rejected | refused | vetoed | withdrawn | countersigned | human_approved | ai_approved | escalated | pending | not_applicable; `signers[]` lists every decision with the signer's class (human | model | authority | deployment | agent; `authority` is the signer of a countersign or a refuse), the model label off the verdict, and whether it counts toward k. (role: reader; returns object: {artifact_id, state, status, reason, k_required, k_have, signers[], approved_by{model,human,authority}, model_labels[], provisional, human_veto_available, human_sign_available, decisions[], veto?, open_objections, revision} — below the reviewer role `revision` is a digest of the answer (GAPS 158))"
      },
      "RpcGraphParams": {
        "type": "object",
        "properties": {
          "spaces": {
            "type": "array",
            "description": "restrict to these spaces; not with workarea",
            "items": {
              "type": "string"
            }
          },
          "states": {
            "type": "array",
            "description": "restrict to these lifecycle states; unknown names are ignored",
            "items": {
              "type": "string"
            }
          },
          "limit": {
            "type": "integer",
            "description": "nodes, newest state change first (the work graph: the area's rows in write order, then the rows one edge away); default and ceiling 2000"
          },
          "if_revision": {
            "type": "string",
            "description": "the revision the caller already holds"
          },
          "kinds": {
            "type": "array",
            "description": "restrict the constellation to these kinds of the closed table, in the query before the cut, so total, counts and the cut are over them; not with workarea (ADR-0131 d8)",
            "items": {
              "type": "string"
            }
          },
          "workarea": {
            "type": "string",
            "description": "draw this work area's work graph instead of the constellation"
          },
          "space": {
            "type": "string",
            "description": "the work area's space; default main; only with workarea"
          }
        },
        "additionalProperties": false,
        "description": "dg.graph — The constellation: every artifact as a node, every relation edge, every pin, every ban — the ADR-0035 projection serialized at once, bounded. `label` and `source_uri` are carried only for ACTIVE / SUPERSEDED rows (INV-SEC-10): a held artifact is its existence, never its content. `revision` is the journal tip, and below the reviewer role a digest of the answer itself, which moves only when what the caller is shown moves (GAPS 158); pass it back as `if_revision` and an unchanged graph answers {revision, unchanged: true}. With `workarea` named it is that area's work graph (ADR-0107 d5): the area's rows the caller may see — every row for a reviewer, the promoted rows and the caller's own below (AUTH-4) — plus every row one relation edge away under the same rule, each node marked with its workarea, producer, kind and whether it is foreign to the area, and `completed`, the completion markers. Computed per call, kept nowhere. (role: reader; returns object: {revision, unchanged} | {revision, full, nodes[], edges[], pins[], bans[], total, truncated, counts, spaces, states, viewer} — `total`, `truncated`, `counts` and `spaces` are over the rows the caller may see, below reviewer the promoted rows and its own; every node carries {workarea, producer, kind, addressed_to, writer_kind}, so both answers draw one node shape (ADR-0111 d2), writer_kind the principal kind its ingest recorded (person, program, local; ADR-0131 d8); a question's node in the constellation also carries cited_by[{artifact_id, kind, state, writer_identity, writer_kind, ts, state_ts, replaces?, cancels?}], the claims citing it the caller may read, replaces and cancels on a citing question whose body names it there; with workarea, each node also carries `foreign` and the answer {space, workarea, completed[{writer_identity, ts}]}; a node's `ts` is its ingest time in ms)"
      },
      "RpcNodeParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "body": {
            "type": "boolean",
            "description": "the whole masked body of a promoted row or the caller's own, with body_exact, content_hash, size_bytes, source_uri and signers; never another writer's held row, whatever the role (ADR-0127 d8; GAPS 395)"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.node — One node in full for a review surface: the status row, the approval trail, the comment thread, relations, the served tip, bundle members (`derives_from` children), and a bounded MASKED preview (INV-SEC-7; null when the content is erased or the blob is gone). Below the reviewer role the members and relations are the rows the caller may read. Below the reviewer role the envelope's `derives_from` names only the parents the caller may read, and `derives_from_withheld`, present when any is left out, counts the rest (GAPS 156). (role: reader; returns object: {artifact_id, status, approval, comments, relations, tip, members[], preview, preview_chars, self_authored, refusal?, revision, supersedes} — `supersedes` is {artifact_ids, operation, via, group_id} when the row is a child of a pending replacement group, its ids the parents the caller may read, else null (ADR-0133 d1); below the reviewer role `revision` is a digest of the answer (GAPS 158); with body, also {body, body_exact, content_hash, size_bytes, source_uri, signers[{identity, class, decision}]}, `body` null and `body_exact` false when the content is erased or the blob is gone)"
      },
      "RpcReviewParams": {
        "type": "object",
        "properties": {
          "cap": {
            "type": "integer",
            "description": "override the digest cap"
          }
        },
        "additionalProperties": false,
        "description": "dg.review — The operator's queue: what needs a human, capped. Each row is annotated with a bounded content preview, whether the operator wrote it (I2), and the verdicts its state accepts. (role: reviewer; returns object: {held_steering[{…the item's shape}], held_steering_truncated, items[{artifact_id, state, waits_for{blocker, code, words, rubric?, missing?}, stances?[{stance, backend_label, provider, decision, reason, kind_note, stale}], waiting_since, supersedes, content, content_type, trust_class, writer_identity, ingest_ts, source_uri, findings[], verdicts[], expires_at, expires_in_s, space, workarea, producer, kind, attachments[{content_hash, size_bytes, media_type, name, source_uri, extracts}], …}], held_back, total_waiting, cap} — `attachments` lists each attachment entry, its name masked, never a byte (ADR-0126); `space`, `workarea`, `producer` and `kind` say where the held row was written and what produced it, the row's own fields (ADR-0111 d2); `workarea` and `producer` are null on a row that named none; `waits_for` is why the row waits, the first that holds of ADR-0070's blockers with the gate's own code and sentence beside it, and `waiting_since` the ts of the event that put it in its held state (ADR-0132 d6, d9); `supersedes` is dg.node's (ADR-0133 d1); `who_can_sign` is dg.who_can_sign's answer for the row, one letter per person across the whole answer, and `self_authored` whether the caller wrote it (ADR-0134 d3); `held_steering` is every held steering row the caller reads, its own included, oldest wait first, at most 50 with `held_steering_truncated`, before the digest and never counted in `cap`, `items` or `held_back` (ADR-0134 d4); `waits_for.code` `rubric_changed`, on the `semantic_review` blocker, is a row graded before the grading in force for its kind, with `rubric {was, now}`, each a kind paragraph's id or \"claim\"; `waits_for.code` `runner_lacks_type`, on the `hard_constraint` blocker, is a crew sheet whose space's assigned runner runs no model for a type it uses, with `missing` the types, and `runner_full` one whose runner has made no report since the gate started (ADR-0141 d5); `stances` is on an item whose code is a disagreement, an abstention, low confidence or rubric_changed alone: each current model verdict, first pass then review, then the latest superseded one of each stance, `reason` its rationale's first sentence of at most 200 characters and `stale` true where its grading is not the one the row is read under (ADR-0139 d2, d3))"
      },
      "RpcReviewRunsParams": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "description": "queued | running | completed | failed | blocked | all; default all"
          },
          "q": {
            "type": "string",
            "description": "search run id, artifact, reviewer or configuration revision"
          },
          "offset": {
            "type": "integer",
            "description": "page offset; default 0"
          },
          "limit": {
            "type": "integer",
            "description": "page size; default 25"
          }
        },
        "additionalProperties": false,
        "description": "dg.review_runs — The review-run log: bounded, filterable, searchable, newest first. Deliberately separate from dg.status's queue depths — a depth is what is WAITING, a run is what is being PROCESSED, and reading one as the other is the misreport this split prevents. Below the reviewer role, only the runs on rows the caller may read. (role: reader; returns object: {reported, runs[], total, offset, limit, counts, in_flight[], last_failure, detail?} — `reported: false` when this worker emits no run events, and then the list is empty rather than inferred)"
      },
      "RpcSettingsParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.settings — The review worker's configuration: configured (on disk) beside effective (in this process), plus the drift between them and the field/choice tables a configuration surface renders. (role: admin; returns object: {configured, explicit, secrets, effective, etag, config_rev, drift[], fields, choices, runs, caps[], defaults} — `caps` as dg.status serves them (ADR-0132 d18); `defaults` names each setting \"Use the default\" returns to, `{set, value, why}`, the value the gate resolves when the file is silent (ADR-0134 d2, d6))"
      },
      "RpcSettingsValidateParams": {
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "description": "dotted `table.key` → value; only the keys being changed"
          }
        },
        "required": [
          "values"
        ],
        "additionalProperties": false,
        "description": "dg.settings_validate — Run a configuration draft through the real loader. Writes nothing, applies nothing — the call a form may make on every keystroke. (role: admin; returns object: {ok, errors?} — field-keyed errors, never an exception)"
      },
      "RpcSettingsSaveParams": {
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "description": "dotted `table.key` → value; a decision kind's caps as `decisions.<kind>.typed_tokens_per_day` and `writer_tokens_per_day`, written into its `[decisions.<kind>]` table (ADR-0132 d18)"
          },
          "etag": {
            "type": "string",
            "description": "the revision this draft was editing"
          },
          "unset": {
            "type": "array",
            "description": "\"Use the default\": names whose line the save removes, so the gate's own resolution answers: `review.mode`, `gating.budget_tokens_per_day` and `decisions.<kind>.writer_tokens_per_day` (ADR-0134 d2, d6)",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "values",
          "etag"
        ],
        "additionalProperties": false,
        "description": "dg.settings_save — Compare-and-swap the configuration file, then apply it to this running daemon. Always both: a save that needs a restart to mean anything is a save the operator has to be told about twice. (role: admin; returns object: {ok, saved, applied, etag, config_rev, configured, secrets, effective, drift[], defaults, note} or {ok: false, conflict|errors})"
      },
      "RpcBackendCheckParams": {
        "type": "object",
        "properties": {
          "backend": {
            "type": "string",
            "description": "a [gating.backend.<name>] table's name; omitted, the first-pass positive stance's"
          }
        },
        "additionalProperties": false,
        "description": "dg.backend_check — Ask one grading backend's provider, through this gate, whether its credential reaches its model and whether the model takes its effort and structured outputs (ADR-0125 d7). One call at a time; answers in words. (role: admin; returns object: {backend, reachable, credential, model, effort, structured_outputs, words})"
      },
      "RpcDeferParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "one held row; or name artifact_ids"
          },
          "artifact_ids": {
            "type": "array",
            "description": "1 to 100 distinct ids, reviewer and up (ADR-0132 d7)",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": false,
        "description": "dg.defer — Record that the operator looked and moved on (F40). Not a lifecycle transition; no journal event, single or bulk. `artifact_ids`, 1 to 100 ids in place of `artifact_id`, is a person's bulk Defer on Action needed, served from the reviewer role up: every id is checked readable before anything is counted, and the call adds its count to `deferred_in_bulk`, never to `deferred` (ADR-0132 d7). (role: agent; returns object: {artifact_id, deferred} or {artifact_ids, deferred: n})"
      },
      "RpcEngagementParams": {
        "type": "object",
        "properties": {
          "days": {
            "type": "integer",
            "description": "default 14"
          }
        },
        "additionalProperties": false,
        "description": "dg.engagement — Did the operator actually review? Counts over a window: `items_deferred_in_bulk` and each history day's `deferred_in_bulk` say how many items a bulk Defer passed over without opening them (ADR-0132 d7). (role: reader; returns object)"
      },
      "RpcScanParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "description": "clean | flagged — manual override"
          },
          "findings": {
            "type": "array",
            "description": "with a manual outcome",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.scan — Run the deterministic scanner on one L0 artifact; a finding ≥ medium routes to L1b and no score clears it (INV-SEC-5). An explicit outcome is the manual override. (role: reviewer; returns object: {artifact_id, state, outcome, findings[]})"
      },
      "RpcScanPendingParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "default 100"
          },
          "now": {
            "type": "boolean",
            "description": "ignore the scan delay"
          }
        },
        "additionalProperties": false,
        "description": "dg.scan_pending — Scan every L0 artifact whose delay elapsed. Counts, never content. A reviewer's call scans rows in the spaces it reads alone (GAPS 401). (role: reviewer; returns object)"
      },
      "RpcGradeParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "fresh": {
            "type": "boolean",
            "description": "ask past the decision record, the typed answer's and the writer's"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.grade — Run the gate engine on one artifact: one model call per stance, one verdict per item. Unchanged bytes on an unchanged model are answered from the decision record at no cost; `fresh: true` asks past it (ADR-0129 d9). On a held row graded before the grading in force for its kind, each verdict carries `graded_before {rubric_version, kind_note}`, the grading it replaces, and every model verdict before it counts toward nothing; on any other row a grade writes no marker (ADR-0139 d2). (role: reviewer; returns object)"
      },
      "RpcGradePendingParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "default 50"
          }
        },
        "additionalProperties": false,
        "description": "dg.grade_pending — Grade what waits at L1, within the day's budget (fail-closed when spent, I6). A reviewer's call grades and counts rows in the spaces it reads alone (GAPS 401). (role: reviewer; returns object)"
      },
      "RpcReviewPendingParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "default 50"
          }
        },
        "additionalProperties": false,
        "description": "dg.review_pending — ADR-0048: the second opinion. Under review.mode model|auto, every L2 row whose first pass escalated gets one `gate-review` verdict, charged to the same budget; a no-op under `human`. Never touches L1b (INV-SEC-5). A reviewer's call reviews rows in the spaces it reads alone (GAPS 401). (role: reviewer; returns object: {reviewed, cost_tokens, paused, budget_remaining})"
      },
      "RpcSettlePendingParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "default 100"
          }
        },
        "additionalProperties": false,
        "description": "dg.settle_pending — ADR-0048: under review.mode auto, promote every L2 row that meets k with no human required on its class — the deployment signs the promote, the integrator's boundary check applies unchanged. A no-op otherwise. A reviewer's call settles and counts rows in the spaces it reads alone (GAPS 401). (role: reviewer; returns object: {promoted, rejected, held})"
      },
      "RpcSignParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "decision": {
            "type": "string",
            "description": "promote | reject | hold"
          },
          "note": {
            "type": "string"
          },
          "promote": {
            "type": "boolean",
            "description": "also promote once signed (default false)"
          }
        },
        "required": [
          "artifact_id",
          "decision"
        ],
        "additionalProperties": false,
        "description": "dg.sign — Record a verdict as the calling principal (ADR-0082): a signed-in person's verdict is a human's, a key's or a socket peer's is a reviewer's and never satisfies a human requirement. With `promote: true` the signature and the promotion it was for happen in one call; a promotion the boundary refuses is reported in `promote.refused` and the signature stands. The claim's author is refused, except in development, where the one person's promote on a held row they wrote counts once, marked `self_signed` (ADR-0135 d2); a demo person's verdict is marked `demo_signed`, counts for nothing on the developer's row and is refused outside development (d5). On a promoted row marked either way, a promote from a signed-in person who neither wrote it nor is a demo person appends `confirm-sign`, and `review-confirmed` once such signatures meet k (d3). (role: reviewer; returns object: {artifact_id, state, event_id, promote?} — on a marked promoted row {artifact_id, state, event_id, confirmed, confirmation?})"
      },
      "RpcPromoteParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "supersedes": {
            "type": "string",
            "description": "parent id this artifact supersedes (single-head chains, ADR-0035)"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.promote — Admit an artifact whose quorum is met. On every profile the promotion is provisional until a second authority countersigns (R3), and T-3 and T-5 are refused and held until one has (trust model §3, GAPS 109). It sets no weight a read multiplies in (ADR-0110 d6). (role: reviewer; returns object: {artifact_id, state, provisional?})"
      },
      "RpcAwayParams": {
        "type": "object",
        "properties": {
          "until": {
            "type": "integer",
            "description": "ms epoch; the hold's own ceiling — past it the TTLs run again"
          },
          "note": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "description": "dg.away — The operator declares an absence (ADR-0045): the human-review TTLs stop until `back`, the time away subtracted on restart. Nothing is promoted meanwhile. (role: admin; returns object: {away, declared, since, days, until?, note?})"
      },
      "RpcBackParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.back — The operator is back: the clocks restart with the time away subtracted (ADR-0045). (role: admin; returns object: {away, held_d, declared})"
      },
      "RpcDemoteParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.demote — Withdraw an admitted artifact from every consumer surface; the content is kept. (role: reviewer; returns object)"
      },
      "RpcRejectParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "in_favour_of": {
            "type": "string",
            "description": "artifact id"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.reject — Refuse an artifact in quarantine, with a category and optionally what it lost to (ADR-0037). (role: reviewer; returns object)"
      },
      "RpcRecordsParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "a declared space the caller reads"
          },
          "classes": {
            "type": "array",
            "description": "one or more of event, usage_record, state_snapshot",
            "items": {
              "type": "string"
            }
          },
          "limit": {
            "type": "integer",
            "description": "1 to 200"
          },
          "workarea": {
            "type": "string",
            "description": "this area and every area below it"
          },
          "since": {
            "type": "integer",
            "description": "a ts; only later records are served"
          }
        },
        "required": [
          "space",
          "classes",
          "limit"
        ],
        "additionalProperties": false,
        "description": "dg.records — The bodies of recorded rows whose class is event, usage_record or state_snapshot, in a space the caller holds a read grant in, newest first (ADR-0132 d19). Each body is the JSON object the row's masked body carries when it parses and is at most 4,096 bytes, else null with body_refused. Never session, the plugin's transcript tail, or any other class; an events key is refused it. (role: reviewer; returns object: {items[{artifact_id, class, workarea, writer_identity, ts, body, body_refused?}], truncated})"
      },
      "RpcRoutedQuestionsParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "the answering crew's space"
          }
        },
        "required": [
          "space"
        ],
        "additionalProperties": false,
        "description": "dg.routed_questions — The questions routed to the crew of `space` and still with it, oldest promotion first, at most 50, each with the one set that crosses between two crews: the question's body, the asking crew's brief, its crew sheet and the asker's crew role's instructions in force at `claude`, and the promoted claims of the asking space it cites, at most four (ADR-0143 d4). Served only to an unrevoked agent key the gate issued by dg.runner_keys for a slot of `space`, read off its `apikey-issued` and never off its label, and to a person at reviewer or above holding read on `space`; a crew connected to it by a read grant is served nothing of it. Grants nothing. (role: agent; returns object: {space, questions[{question, body, space, asker{producer, role}, promoted_at, wait_minutes, minutes_left, brief, sheet, instructions, citations[]}], truncated} — each charter row and citation is {artifact_id, body} or null; minutes_left is counted on the daemon's monotonic clock, never on dg.status's now (GAPS 1382, 1386))"
      },
      "RpcQuestionParams": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "description": "the question's artifact id"
          }
        },
        "required": [
          "question"
        ],
        "additionalProperties": false,
        "description": "dg.question — One question's routing state, to a caller that may read the question (ADR-0143 d4; 17 — Crews §11.5): held; with_crew with its crew and the minutes left; with_people with no_crew, wait_over, crew_gone or referred; answered by a person or by the crew, a person's standing above; or ended as ADR-0131 d6 lists. The route is the question's question-routed event, never the sheets in force. A held answer or referral is served to nobody here. (role: reader; returns object: {question, state, reason, crew, wait_minutes, minutes_left, answers[{artifact_id, body, space, promoted_at}], person_answer, referral} — answers are the crew's in force citing it, newest first; referral {artifact_id, body} is served to a person alone, null to every other caller (GAPS 1382, 1387))"
      },
      "RpcHealthParams": {
        "type": "object",
        "properties": {
          "spaces": {
            "type": "array",
            "description": "declared space names; default every space the caller reads",
            "items": {
              "type": "string"
            }
          },
          "stale_days": {
            "type": "integer",
            "description": "1 to 365, default 30"
          },
          "limit": {
            "type": "integer",
            "description": "ids listed per check, 1 to 100, default 20"
          }
        },
        "additionalProperties": false,
        "description": "dg.health — Six counts of trouble among the promoted claims of the spaces the caller reads, the newest 2,000 by promotion (ADR-0133 d2): `same_words` (bodies equal after NFC, case folding and collapsed white space), `similar` (a cosine of at least 0.92 in this gate's embedding index, words differing; not computed without one), `cites_retired` (derives from a superseded, demoted or wholly hidden claim), `not_served` (promoted more than stale_days ago and returned by no recall since; not computed until the served days are that old), `unrelated` (no relation and no derives_from either way; steering excepted) and `not_reviewed` (a promotion that rested on its author's or a demo person's signature, ADR-0135 d3). Writes nothing, ranks nothing. (role: reviewer; returns object: {checked_at, rows_checked, truncated, checks{same_words, similar, cites_retired, not_served, unrelated, not_reviewed: {count, at_least, artifact_ids[], computed, why_not?}}})"
      },
      "RpcCancelParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "a held claim the caller wrote"
          },
          "reason": {
            "type": "string",
            "description": "why, 1 to 500 characters; kept in the journal and never served"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.cancel — A signed-in person cancels a held claim they wrote (ADR-0132 d2): it goes to REJECTED with its words, events and attachments kept, and dg.approval answers `cancelled`. Refused to every caller whose role is agent, to every caller who is not a signed-in person (a program's key, a socket peer), to anyone but the claim's author, and on a claim that is promoted, signed and being promoted (L3_RATIFIED), not held, or named by a pending replacement group. (role: reader; returns object: {artifact_id, state, status, event_id})"
      },
      "RpcWhoCanSignParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "a held row; exactly one of artifact_id and content_type"
          },
          "content_type": {
            "type": "string",
            "description": "a kind the caller would write",
            "enum": [
              "answer",
              "derived_fact",
              "imported_document",
              "memory",
              "prompt_template",
              "question",
              "skill",
              "steering",
              "summary"
            ]
          },
          "trust_class": {
            "type": "string",
            "description": "with content_type: the class, else the kind's own"
          },
          "space": {
            "type": "string",
            "description": "with content_type: the space it would be written in; absent, a space the caller would create, which the admins alone may sign in"
          }
        },
        "additionalProperties": false,
        "description": "dg.who_can_sign — Who may sign a held row, or a row of a kind the caller would write (ADR-0134 d3): the people who hold reviewer or admin and read on its space, never its writer, by name to an admin and by letter (A, B, ... in the people file's order) to anyone else, and the graders whose promote can count toward its k under the review mode and its class, each by its backend's words. In development the developer comes first on their own row with `self_signs`, and no demo person is listed on it; outside development no demo person is listed (ADR-0135 d2, d5). Refused to every API key, whatever its role or scope. (role: reviewer; returns object: {people[{who, role, `you?`, self_signs?}], graders[{label, provider}], enough, words, notify} — `enough` is whether those listed can bring the row to its k with no one else, read through the count the auto pass reads; `words` the sentence a console prints; `notify` whether an events key reads the space, so an ask reaches a sink)"
      },
      "RpcAskToSignParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "a held row the caller wrote"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.ask_to_sign — The author of a held row, a signed-in person, asks the people who may sign it (ADR-0134 d3): appends sign-asked {to}, the identities dg.who_can_sign lists, and moves nothing. Once per row a UTC day; a notifier carries it to the sinks the operator configured. Refused to every caller who is not a signed-in person (an agent asks through a question), to anyone but the row's author, and on a row that is not held. (role: agent; returns object: {artifact_id, to[], asked_at, event_id} — `asked_at` HH:MM UTC)"
      },
      "RpcReopenParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "a rejected claim"
          },
          "reason": {
            "type": "string",
            "description": "why, 1 to 500 characters"
          }
        },
        "required": [
          "artifact_id",
          "reason"
        ],
        "additionalProperties": false,
        "description": "dg.reopen — Return a rejected claim to the held state its scan left (L1B while a finding is at veto severity, else L1), with a reason (ADR-0133 d5). It makes nothing readable: the count of signatures starts again from the reopen, and the verdicts before it stay in the trail. Refused to a caller who is not a signed-in person, to the claim's author (in development the one person reopens their own, ADR-0135 d2), on a claim that is not rejected or that its author cancelled, and past three reopens of one claim. (role: reviewer; returns object: {artifact_id, state, reopens, event_id})"
      },
      "RpcRelateParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the child"
          },
          "kind": {
            "type": "string"
          },
          "parent": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id",
          "kind",
          "parent"
        ],
        "additionalProperties": false,
        "description": "dg.relate — Append a typed relation from artifact_id (child) to parent: supersedes | restates | corrects | contradicts | extends. Only supersedes hides. (role: reviewer; returns object: {artifact_id, relation, parent, event_id})"
      },
      "RpcOverrideTipParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.override_tip — Pin a chain's served head to this member (time travel). Reversible by a second override. A crew sheet is never pinned: promote and activation, both checked, are the only acts that change the sheet in force (ADR-0143 d8). (role: reviewer; returns object)"
      },
      "RpcHideParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "a promoted artifact"
          },
          "audience": {
            "type": "string",
            "description": "default `all`"
          },
          "reason": {
            "type": "string",
            "description": "required unless lift"
          },
          "lift": {
            "type": "boolean",
            "description": "true reverses an earlier hide for this audience — a second event, both kept"
          },
          "dry_run": {
            "type": "boolean",
            "description": "true answers what the hide would reach and appends nothing (ADR-0133 d3)"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.hide — Stop serving a promoted artifact to one audience, or to all — reversibly (ADR-0052 d4). The row stays ACTIVE and why-able; include_hidden returns it. A total hide by anyone but a signed-in person is journaled `lowered`. (role: reviewer; returns object: {artifact_id, audience, hidden, event_id, hidden_for[], lowered?}; with dry_run {artifact_id, audience, dry_run, already_hidden_for[], derived_served, derived_ids[≤20]} — the claims deriving from it that stay served, which a hide does not reach)"
      },
      "RpcBanParams": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "writer | source"
          },
          "subject": {
            "type": "string",
            "description": "the identity, or a source_uri prefix"
          },
          "reason": {
            "type": "string",
            "description": "required unless lift — it is what the refused writer is told"
          },
          "until": {
            "type": "integer",
            "description": "ms epoch; omitted = until lifted"
          },
          "lift": {
            "type": "boolean",
            "description": "true lifts a ban in effect"
          },
          "dry_run": {
            "type": "boolean",
            "description": "true answers what the ban would reach and appends nothing (ADR-0133 d3)"
          }
        },
        "required": [
          "kind",
          "subject"
        ],
        "additionalProperties": false,
        "description": "dg.ban — Refuse a writer's, or a source prefix's, next ingest at the door with -32009 (ADR-0052 d6). Operator only; touches nothing already admitted (I4); not replicated. (role: admin; returns object: {kind, subject, active, event_id, until?}; with dry_run, for a writer {kind, subject, dry_run, promoted, held, last_write_ts, refused_last_7_days}, for a source {kind, subject, dry_run, sources[≤50], truncated, promoted, held} — what a ban leaves as it is)"
      },
      "RpcBansParams": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "description": "refused-at-door records to return; default 100"
          }
        },
        "additionalProperties": false,
        "description": "dg.bans — The bans in effect and what they refused at the door (ADR-0052 d6). Read-only; `status` and `dg.graph` carry the same list. Below the reviewer role, only the bans whose subject is the caller's own identity and the caller's own refused writes: a ban names an identity or a source and no space, so nothing narrower is the caller's (CR14, GAPS 366). (role: reader; returns object: {bans[], refused[], truncated} — `truncated` when older refusals past the limit were left out (ADR-0133 d4))"
      },
      "RpcSpacesParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.spaces — The store's partitions (ADR-0023 d1): every declared space with its owner, its review policy and how many artifacts sit in it (below the reviewer role, how many the caller may read: the promoted rows and its own, in a space it holds `read` on, and its own anywhere), `granted`, the capabilities the caller holds there (ADR-0115 d6; every one for an admin), and which space a write defaults to. `unregistered` names spaces the store holds rows in that nothing declared — empty on a deployment this daemon has started, because startup registers exactly those. Names and counts, never content: this is a directory, not a read. (role: reader; returns object: {default, spaces[{name, owner, policy, artifacts, granted[], event_id, identity, ts, builtin?}], unregistered[]})"
      },
      "RpcGrantParams": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "issue | revoke"
          },
          "grants": {
            "type": "array",
            "description": "[{principal, space, capabilities[]}], one to 500 entries, no principal on one space twice",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "action",
          "grants"
        ],
        "additionalProperties": false,
        "description": "dg.grant — Issue or revoke per-space grants as one act (ADR-0115 d16, GAPS 362): every entry names a principal (`<iss>|<sub>`, `key:<key_id>`, `uid:<n>`), one declared space and a non-empty subset of `read`, `contribute` and `submit`. The list is applied under one journal lock and refused whole if any entry is refused, one `space-grant-issued` or `-revoked` per entry that changes something; an entry already as asked answers `changed: false` and journals nothing. A connection between two crews is the `read` grants on one crew's space held by the other's keys, made and undone as one such list (ADR-0121 d8); access is never transitive. A revocation deletes nothing and applies to every answer after its event. An admin holds every capability by role and needs no grant (d15). (role: admin; returns object: {action, changed, grants[{principal, space, capabilities, changed, event_id, held}]})"
      },
      "RpcGrantsParams": {
        "type": "object",
        "properties": {
          "principal": {
            "type": "string",
            "description": "only this principal's grants"
          },
          "space": {
            "type": "string",
            "description": "only grants on this space"
          }
        },
        "additionalProperties": false,
        "description": "dg.grants — The open grants, one row per principal and space, each capability with the event that issued it and its reason (`upgrade` for the grants upgrade, ADR-0115 d14); `principal` and `space` narrow the list. A `uid:` principal covers every process that user runs (d17). (role: admin; returns object: {grants[{principal, space, capabilities, issued, every_process_of_that_user}], upgrade: {ts, event_id} | null, refused_not_granted: {principal: count since the daemon started}})"
      },
      "RpcRunnerHelloParams": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "description": "exactly doublegate.runner-hello/1"
          },
          "instance": {
            "type": "string",
            "description": "32 lower-case hex characters, made by the runner process at start"
          },
          "runner": {
            "type": "string",
            "description": "the runner program's version, MAJOR.MINOR.PATCH"
          },
          "harness": {
            "type": "string",
            "description": "Claude Code's version, or empty"
          },
          "host": {
            "type": "string",
            "description": "what the runner says its machine is called, 1-64 printable characters"
          },
          "backend": {
            "type": "string",
            "description": "anthropic, or the endpoint's host, lower-case, 1-253 characters, never a scheme, path, port or token (ADR-0141 d5)"
          },
          "models": {
            "type": "object",
            "description": "0-5 {<role type>: {model, effort?}}: each type of reasoning, research, coding, review, fast the runner serves, its model and the effort its line names (ADR-0141 d4)"
          },
          "models_floor": {
            "type": "string",
            "description": "the highest Claude Code floor among the map's models, or empty"
          },
          "capacity": {
            "type": "object",
            "description": "{crews 1-16, members 1-64}"
          },
          "recorded": {
            "type": "object",
            "description": "{version, ok}: the recorded session's version and its check"
          },
          "budget": {
            "type": "object",
            "description": "{day, spent, of}: the UTC day, calls charged, the day's budget"
          },
          "serving": {
            "type": "array",
            "description": "0-16 {space, running, holds[]}: per space, members running and the key ids held",
            "items": {
              "type": "string"
            }
          },
          "facts": {
            "type": "object",
            "description": "{node, claude{path, found}, plugin{path, mcp}, workdir{path, writable}, git, home}: what the machine shows, the runner's word (ADR-0142 d1)"
          },
          "settings_version": {
            "type": "integer",
            "description": "the settings version the runner holds, 0 before any"
          },
          "refused": {
            "type": "array",
            "description": "0-16 {field, code}: each setting it could not apply at this pass, code one of not_found, no_mcp_json, not_the_plugin, missing, not_writable, inside_home, inside_workdir, not_a_git_checkout",
            "items": {
              "type": "string"
            }
          },
          "token": {
            "type": "object",
            "description": "{version, held}: the endpoint token version it holds, 0 for none"
          }
        },
        "required": [
          "format",
          "instance",
          "runner",
          "harness",
          "host",
          "backend",
          "models",
          "models_floor",
          "capacity",
          "recorded",
          "budget",
          "serving",
          "facts",
          "settings_version",
          "refused",
          "token"
        ],
        "additionalProperties": false,
        "description": "dg.runner_hello — A runner's report of itself, at most once a minute (ADR-0140 d1): the gate keeps the newest per key in memory, on its own clock, as the runner's last report, never checked and never a claim, and journals only what it acts on: `runner-first-seen` on a key's first hello ever, `runner-keys-acknowledged` when a space's `holds` equals its batch and the hello comes from the batch's instance, `runner-keys-refused` when it comes from another, and `runner-released` when a space reassigned away is no longer listed, revoking the runner's `read` and `contribute` there. The body is `doublegate.runner-hello/1` (`doublegate_sdk.crew.hello`). The hello carries the machine's facts, the settings version it holds, each setting it could not apply and its endpoint token's version (ADR-0142 d4); the answer carries the whole settings only when that version differs from the gate's. (role: runner; returns object: {now, settings_version, settings? (every field of the runner's settings, only when the hello's settings_version differs), label, token: none|waiting|served|held|lost|unreadable, assignments[{space, slots[], sheet, keys: waiting|issued|held}], questions?[{space, asked[{question, slot, state, reason}], routed[], truncated}]} — `questions`, present when an assigned space holds one, is per assigned space its open questions, oldest first, with the slot whose served key asked each and its routing state, and the ids of the questions routed to it and still with it, at most 50 each; ids and states, never a body (ADR-0143 d5))"
      },
      "RpcRunnerKeysParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "the crew's space"
          }
        },
        "required": [
          "space"
        ],
        "additionalProperties": false,
        "description": "dg.runner_keys — A crew's member keys, issued once to the runner the space is assigned to (ADR-0140 d3): one `agent` key per slot, labelled with the slot, expiring after the assignment's `key_days`, granted `read` and `contribute` on the space and `read` on the crew's connections, each journalled `apikey-issued` with `issued_by` the admin who assigned and `served_to` the runner. The raw keys are in this answer alone; the gate keeps their hashes (INV-SEC-9). The batch is bound to the one instance heard on the key; a second call before the runner's hello acknowledges it revokes it, reason `superseded`, and issues anew; after, it is refused. (role: runner; returns object: {space, members[{slot, role, key}]})"
      },
      "RpcRunnersParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.runners — Every runner key (ADR-0140 d5; 17 — Crews §12): its served label, standing, state (the first of revoked, two_instances, settings_refused, needs_settings, no_report_since_start, not_seen, budget_spent, paused, serving, idle, never_seen that holds), when it was first seen, its last report (with the machine's facts and the settings it could not apply, ADR-0142 d4) and that receipt on the gate's clock (`received_at`), that report's age and the instances heard in 120 seconds on the daemon's monotonic clock, never on `now` (ADR-0140 d3, 2026-10-14; GAPS 1432), its settings with their version and, for each field ever set, the identity that set it and when (ADR-0142 d2), its assignments with each batch's state, the spaces reassigned away and not yet released, and the refused acknowledgements journalled on it. The report carries the hello's `backend`, `models` and `models_floor` (ADR-0141 d5), and each assignment the role types its sheet uses that the map runs no model for, `missing`. Below admin the assignments and the report's spaces are those the caller reads. (role: reviewer; returns object: {now, started_at, runners[{runner, label, standing, state, first_seen_at, report, received_at, age_ms, instances[], settings, settings_version, settings_set{<field>: {by, at}}, assignments[{space, slots, sheet, keys, missing[], key_ids, issued_at, revoke_at}], released_pending[{space, running}], refused, token{state, version, origin, set_at, served_at, reason, box}}], token_store: the last token set refused for keys/, or null})"
      },
      "RpcRunnerAssignParams": {
        "type": "object",
        "properties": {
          "space": {
            "type": "string",
            "description": "the crew's space"
          },
          "runner": {
            "type": "string",
            "description": "a runner key id, or null to unassign"
          },
          "slots": {
            "type": "array",
            "description": "the member labels <space>/<role>/<n>",
            "items": {
              "type": "string"
            }
          },
          "sheet": {
            "type": "string",
            "description": "the id of the crew sheet, a row of the space"
          },
          "key_days": {
            "type": "integer",
            "description": "the member keys' expiry in days, 1-90"
          },
          "reissue": {
            "type": "boolean",
            "description": "true replaces the crew's keys on the same runner"
          }
        },
        "required": [
          "space",
          "runner"
        ],
        "additionalProperties": false,
        "description": "dg.runner_assign — Assign a crew's space to a runner, switch it, or unassign it with `runner: null` (ADR-0140 d2, d3): journalled `runner-assigned {space, from, to, slots, sheet, key_days}`; the runner is granted `read` and `contribute` on the space; a switch, `reissue: true` or a change of slots revokes every member key of the old batch at the act, reason `reassigned`, and keeps every row. An assignment already in force journals nothing. An admin's act, never a claim. (role: admin; returns object: {space, from, to, changed, revoked[], event_id})"
      },
      "RpcRunnerSetParams": {
        "type": "object",
        "properties": {
          "runner": {
            "type": "string",
            "description": "the runner key id"
          },
          "label": {
            "type": "string",
            "description": "the runner's display name, a space name of 1-64 characters"
          },
          "paused": {
            "type": "boolean",
            "description": "launch and resume nothing; a run under way ends at its own end"
          },
          "max_running": {
            "type": "integer",
            "description": "1-64, never above the capacity the runner reports"
          },
          "budget_per_day": {
            "type": "integer",
            "description": "2,880 or more; default 50,000"
          },
          "command": {
            "type": "string",
            "description": "one bare program name the runner resolves on its own PATH: no path separator, no white space, 1-255 characters"
          },
          "plugin_dir": {
            "type": "string",
            "description": "an absolute path or one beginning ~/, never inside workdir_root"
          },
          "workdir_root": {
            "type": "string",
            "description": "an absolute path or one beginning ~/, where members' directories go"
          },
          "repository": {
            "type": "string",
            "description": "a path as workdir_root's, or null to clear"
          },
          "deny_read": {
            "type": "array",
            "description": "0-64 paths a member may not read, none inside workdir_root",
            "items": {
              "type": "string"
            }
          },
          "managed_settings": {
            "type": "array",
            "description": "0-8 absolute paths, none inside workdir_root, or null to clear",
            "items": {
              "type": "string"
            }
          },
          "poll_seconds": {
            "type": "number",
            "description": "0.1-60; default 5"
          },
          "idle_poll_seconds": {
            "type": "number",
            "description": "poll_seconds-600; default 60"
          },
          "grace_seconds": {
            "type": "number",
            "description": "0.5-300; default 10"
          },
          "key_seconds": {
            "type": "number",
            "description": "1-3,600; default 60"
          },
          "progress_seconds": {
            "type": "number",
            "description": "0 (off) or 1-3,600; default 600"
          },
          "base_url": {
            "type": "string",
            "description": "https://, or http:// to a loopback host, no user, query or fragment; null for Anthropic's own endpoint. A change of origin clears the token"
          },
          "models": {
            "type": "object",
            "description": "{<role type>: {model, effort?}, retention_days_accepted?} (ADR-0141 d4)"
          }
        },
        "required": [
          "runner"
        ],
        "additionalProperties": false,
        "description": "dg.runner_set — Set one or more of a runner's settings (ADR-0140 d4; ADR-0142 d2; 17 — Crews §12): each field checked on its own, then the settings with the change checked whole; journalled `runner-settings-set {runner, version, fields}` under the admin, the version the count of the runner's settings lines, and served in its next answer. The limits and intervals take effect at the runner's next pass; the launch settings, the endpoint and the map at its next launch or resume. No setting is a secret: the endpoint's token is `dg.runner_token_set`'s. (role: admin; returns object: {runner, settings_version, settings, event_id})"
      },
      "RpcRunnerTokenSetParams": {
        "type": "object",
        "properties": {
          "runner": {
            "type": "string",
            "description": "the runner key id"
          },
          "token": {
            "type": "string",
            "description": "1-4,096 printable characters with no white space, or null to clear"
          }
        },
        "required": [
          "runner",
          "token"
        ],
        "additionalProperties": false,
        "description": "dg.runner_token_set — Enter, rotate or clear a runner's endpoint token (ADR-0142 d3; 17 — Crews §12): sealed under the deployment's `keys/runner-secrets.key` until the runner takes it once with `dg.runner_token`, then held nowhere by the gate. Journalled `runner-token-set {runner, version, origin, rotated}` or `runner-token-cleared {runner, reason: admin}`; no answer, log line or event carries the value. A change of the runner's `base_url` origin clears it. (role: admin; returns object: {runner, version?, token: waiting|none, event_id})"
      },
      "RpcRunnerTokenParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.runner_token — The caller's own endpoint token, once (ADR-0142 d3): to the one instance heard on its key within 120 seconds, with the origin it was set for. In the act the gate removes its sealed copy and journals `runner-token-served {runner, instance, version}`; the runner's next hello acknowledges it, and one not acknowledged within 600 seconds is lost. (role: runner; returns object: {version, token, origin})"
      },
      "RpcSpaceCreateParams": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "1-64 characters, starting alphanumeric, then letters, digits, dot, dash or underscore"
          },
          "owner": {
            "type": "string",
            "description": "who owns it; default the calling identity"
          },
          "policy": {
            "type": "string",
            "description": "its review policy; default `inherit` — this deployment's configured policy. ADR-0023 d1 requires a space to carry one and names no vocabulary, so the string is stored verbatim"
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "description": "dg.space_create — Declare a space (ADR-0023 d1). Until a space is declared, writing to its name is refused -32602 rather than quietly opening a partition nobody can find again — the reason this verb exists. `main` exists at install and needs no creation. An operator's act: an agent may name a space it was granted and may never open one. (role: admin; returns object: {name, owner, policy, created, event_id} — or the existing row with `duplicate: true`)"
      },
      "RpcSemanticReviewParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the row to review"
          },
          "settle": {
            "type": "boolean",
            "description": "default true — settle the row if the disposition clears it"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.semantic_review — ADR-0048/ADR-0053: ask the server-owned reviewer to look at one row. Every disposition it writes is reviewer_class='model' — a model is never laundered into a human signature by being triggered from a console. The caller supplies the subject and nothing else; the reviewer identity, the rendering, the scan event and the registry revision are read from the row. (role: reviewer; returns object: the reviewer's disposition payload)"
      },
      "RpcCommentParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the row to comment on; any state, and below the reviewer role a row the caller may read"
          },
          "body": {
            "type": "string",
            "description": "the remark, plain text"
          },
          "kind": {
            "type": "string",
            "description": "note | question | objection | resolution; default note"
          },
          "reply_to": {
            "type": "string",
            "description": "a live comment on the same artifact; required for a resolution"
          },
          "findings": {
            "type": "array",
            "description": "finding kinds on the row this remark is about",
            "items": {
              "type": "string"
            }
          },
          "audience": {
            "type": "string",
            "description": "all (default) | people — a note served to signed-in people alone, and to every other caller as if it did not exist; refused from a caller whose role is agent or who is not a signed-in person, and on any kind but note (ADR-0132 d3, GAPS 664)"
          }
        },
        "required": [
          "artifact_id",
          "body"
        ],
        "additionalProperties": false,
        "description": "dg.comment — Record a remark on an artifact — an annotation, never a decision. Changes nothing about the row's standing and counts toward nothing; an open `objection` by the operator holds auto-promotion until a `resolution` replies to it or it is retracted. The author class (human/model/agent) is derived from the signing identity, never a parameter (I2). Comments are never published (I1) and never leave this gate. (role: agent; returns object: {artifact_id, event_id, comment, counts})"
      },
      "RpcCommentsParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the row"
          },
          "include_retracted": {
            "type": "boolean",
            "description": "default false — tombstones are kept in the projection"
          }
        },
        "required": [
          "artifact_id"
        ],
        "additionalProperties": false,
        "description": "dg.comments — The comment thread on one artifact, oldest first, with counts (total, open human objections). (role: reader; returns object: {artifact_id, comments[], counts{total, open_objections, last_ts}})"
      },
      "RpcCommentRetractParams": {
        "type": "object",
        "properties": {
          "artifact_id": {
            "type": "string",
            "description": "the row"
          },
          "event_id": {
            "type": "string",
            "description": "the comment's event id"
          },
          "reason": {
            "type": "string",
            "description": "why, for the audit trail"
          }
        },
        "required": [
          "artifact_id",
          "event_id"
        ],
        "additionalProperties": false,
        "description": "dg.comment_retract — Withdraw a comment: its author, or the operator. The comment stays as a tombstone; an open objection retracted this way releases the hold it held. (role: agent; returns object: {artifact_id, event_id, retract_event_id})"
      },
      "RpcReplacementsParams": {
        "type": "object",
        "properties": {
          "group_id": {
            "type": "string",
            "description": "one group instead of the list"
          },
          "state": {
            "type": "string",
            "description": "PENDING | ACTIVATED | ABORTED"
          },
          "limit": {
            "type": "integer",
            "description": "default 50"
          }
        },
        "additionalProperties": false,
        "description": "dg.replacements — Every replacement group with its membership, its manifest and why it is held — the reviewer's view of what is waiting to be activated. Metadata, never content (I1). `revision` is the graph revision this reading was taken at: pass it to dg.replacement_activate. (role: reviewer; returns object: {groups[{group_id, state, operation, members[{artifact_id, side, state, prepared}], manifest, held, ready, prepared[], pending_children[], activation?, abort?}], total, counts{state: n}, revision, scope})"
      },
      "RpcReplacementProposeParams": {
        "type": "object",
        "properties": {
          "operation": {
            "type": "string",
            "description": "merge | split | supersede"
          },
          "parent_ids": {
            "type": "array",
            "description": "the artifacts this group retires; a split has exactly one",
            "items": {
              "type": "string"
            }
          },
          "child_ids": {
            "type": "array",
            "description": "the replacement set; a merge has exactly one, a split at least two",
            "items": {
              "type": "string"
            }
          },
          "reason": {
            "type": "string",
            "description": "why — the line `why` shows a later reader"
          },
          "policy_revision": {
            "type": "string",
            "description": "the admission policy this was proposed against"
          },
          "group_id": {
            "type": "string",
            "description": "an id to retry under; derived from the manifest when omitted"
          }
        },
        "required": [
          "operation",
          "parent_ids",
          "child_ids",
          "reason"
        ],
        "additionalProperties": false,
        "description": "dg.replacement_propose — Propose one replacement group: a split, a merge or a supersede, with its membership fixed at acceptance (graph operations §3). The proposer is the connection's identity (I2) and is the one identity that may never activate the result (§3: proposer is not ratifier). Nothing is admitted and nothing is served by this call; a retry of the same manifest returns the recorded group. (role: reviewer; returns object: {group_id, state, operation, parent_ids, child_ids, prepared[], pending_children[], manifest_digest, proposer, proposed_event, event_id | recorded})"
      },
      "RpcReplacementPrepareParams": {
        "type": "object",
        "properties": {
          "group_id": {
            "type": "string"
          },
          "child_id": {
            "type": "string",
            "description": "a member of that group"
          }
        },
        "required": [
          "group_id",
          "child_id"
        ],
        "additionalProperties": false,
        "description": "dg.replacement_prepare — Take one child of a pending group to ratified-and-not-serving (graph operations §3). It appends no promote, so the child reaches no serving surface by any flag; a child its reviewers rejected aborts the whole group instead. (role: reviewer; returns object: the group's status plus {child_id, event_id | recorded})"
      },
      "RpcReplacementActivateParams": {
        "type": "object",
        "properties": {
          "group_id": {
            "type": "string"
          },
          "graph_revision": {
            "type": "string",
            "description": "the revision dg.replacements showed the caller"
          }
        },
        "required": [
          "group_id",
          "graph_revision"
        ],
        "additionalProperties": false,
        "description": "dg.replacement_activate — The one signed commit that flips a whole group (graph operations §4): every prepared child becomes servable together and every listed parent becomes SUPERSEDED together. Curation, never the author's: the proposer is refused by name, and a stale `graph_revision` is refused with the current one rather than activated against a graph that moved. In a development home the developer activates their own group, and `replacement-activated` carries `self_signed: true` (ADR-0135 d2's dated line). A crew sheet it brings into force is checked as a promote is, and one that adds a read or an answering crew is activated by an admin, or by any reviewer who did not propose it when an admin wrote it and the group replaces the sheet in force (ADR-0143 d8). (role: reviewer; returns object: the group's status plus {event_id, promoted[], superseded[], frontier[], published})"
      },
      "RpcReplacementAbortParams": {
        "type": "object",
        "properties": {
          "group_id": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "cancelled | child_rejected | admission_refused | expired; default cancelled"
          },
          "evidence": {
            "type": "array",
            "description": "artifact or event ids the reason rests on",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "group_id"
        ],
        "additionalProperties": false,
        "description": "dg.replacement_abort — Close a pending group and keep everything it held (graph operations §4) — not a deletion and not a demotion: nothing it prepared was ever served. There is no abort after activation. (role: reviewer; returns object: the group's status plus {event_id | recorded})"
      },
      "RpcReconcileParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.reconcile — Re-derive the consumer surfaces of every space from the journal (the integrator's rebuild): the deployment's own work, beside dg.rebuild (GAPS 401). (role: admin; returns object)"
      },
      "RpcVerifyParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.verify — Walk the journal chain and verify every signature. Below the reviewer role `events` counts the events the caller may read, as dg.events serves them, and `tip` names the last of them (GAPS 158). (role: reader; returns object: {events, tip})"
      },
      "RpcRebuildParams": {
        "type": "object",
        "properties": {},
        "additionalProperties": false,
        "description": "dg.rebuild — Replay the journal into every projection; the result must equal the live state (I5). (role: admin; returns object)"
      }
    }
  },
  "security": [],
  "externalDocs": {
    "url": "docs/api/http-and-mcp.md",
    "description": "the prose companion: probed status codes, refusal messages, coverage"
  },
  "x-dg-documents": {
    "mcp_catalog": "/mcp/catalog.json",
    "rendered": [
      "docs/api/openapi.json",
      "docs/api/mcp-catalog.json",
      "docs/api/daemon-rpc.json",
      "docs/api/mcp-tools.json",
      "docs/api/note.schema.json"
    ]
  },
  "x-dg-unprobed": []
}
