Chio/Docs
LOGIN · JOIN

ReferenceSpec

Receipt Query API

The trust-control receipt read routes: filters, cursor pagination, analytics and operator-report bodies, and the CLI and SDK clients.

Source

The crate is the contract; no specification file covers these routes. This page reflects crates/platform/chio-control-plane/src/trust_control/service_types/paths.rs (the path constants and the list limits), service_types/requests.rs (the query and response types), service_runtime/router.rs (which handler each path is registered against), trust_control/receipt_handlers.rs and trust_control/report_validation.rs (authentication and status codes).

The response bodies come from crates/kernel/chio-kernel/src/receipt_analytics.rs and crates/kernel/chio-kernel/src/operator_report/, and the SQL behind them from crates/platform/chio-store-sqlite/src/receipt_store/reports/. The CLI flags come from crates/products/chio-cli/src/cli/types/receipt.rs, and the clients from sdks/typescript/chio-ts/src/receipt_query_client.ts and sdks/python/chio-py/src/chio/receipt_query.py. Status: shipped.


Synopsis

4 routes read receipts. Each takes its filters from the query string and returns JSON.

MethodPathQuery typeResponse type
GET/v1/receipts/queryReceiptQueryHttpQueryReceiptQueryResponse
GET/v1/agents/{subject_key}/receiptsAgentReceiptsHttpQueryReceiptQueryResponse
GET/v1/receipts/analyticsReceiptAnalyticsQueryReceiptAnalyticsResponse
GET/v1/reports/operatorOperatorReportQueryOperatorReport

Endpoint summary

Two further receipt routes read the same store behind a different envelope. /v1/receipts/tools takes ToolReceiptQuery and /v1/receipts/children takes ChildReceiptQuery; both accept a receiptId that point-loads exactly one row, which is how a deployment resolves a parent or child receipt the kernel's bounded in-memory mirror has evicted. Both return ReceiptListResponse: configured, backend, kind, count, filters, and receipts, with no cursor. The child listing is admin-only: a tenant read token gets 403, because child receipts carry no tenant attribution. POST appends a receipt, requires the service token, forwards to the cluster leader, and carries a body-size limit.

MethodPathPurpose
GET/v1/receipts/queryReceipt query with filters and cursor pagination
GET/v1/agents/{subject_key}/receiptsAgent-scoped listing; accepts limit and cursor only
GET/v1/receipts/analyticsAggregate counters and groupings over the same corpus
GET/v1/reports/operatorComposed operator report over the receipt and budget stores
GET, POST/v1/receipts/toolsList or append tool receipts
GET, POST/v1/receipts/childrenList or append child receipts

Every route requires a bearer token in the Authorization header. The filters combine with AND semantics, and omitting a parameter disables that filter.


Authentication

The trust-control server compares the bearer token against two sets in constant time and resolves one of two principals: the admin service token, or a per-tenant read token. The principal becomes the read context the store query runs under, so a tenant token reads only that tenant's receipts.

bash
curl -sS https://trust.example.com/v1/receipts/query \
  -H "Authorization: Bearer my-service-token"

The operator provisions tokens out of band, alongside chio trust serve. /v1/receipts/query and /v1/agents/{subject_key}/receipts accept either principal. The analytics and operator-report routes accept the admin service token only: a tenant read token gets 403 with the message <surface> requires admin receipt read authority.

Treat tokens like keys

A trust-control token grants read access to the receipt log it is scoped to. Store it in a secrets manager, rotate it on personnel change, and keep it out of source control. Prefer the CHIO_CONTROL_TOKEN environment variable over the argv form, which leaks through ps.

Filter parameters

Every parameter is a query-string parameter. The names are camelCase on the wire, because ReceiptQueryHttpQuery carries serde(rename_all = "camelCase").

ParameterTypeDescription
capabilityIdstringExact match on capability id
toolServerstringExact match on tool server name
toolNamestringExact match on tool name
outcomestringOne of allow, deny, cancelled, incomplete; maps to the decision_kind column
sinceu64Only receipts with timestamp >= since (Unix seconds, inclusive)
untilu64Only receipts with timestamp <= until (Unix seconds, inclusive)
minCostu64Minimum cost_charged in minor units. Receipts without financial metadata are excluded when it is set.
maxCostu64Maximum cost_charged in minor units. Receipts without financial metadata are excluded when it is set.
costCurrencystringCurrency for the cost bounds, a three-letter uppercase code. Required whenever minCost or maxCost is present.
agentSubjectstringHex-encoded Ed25519 agent subject key, resolved through the capability-lineage join rather than by replaying issuance logs
cursoru64Pagination cursor. Returns only receipts with seq > cursor (exclusive).
limitusizeResults per page. Clamped to the range 1 to 200; the default when absent is 50.

The server validates the cost filters before it touches the store. A minimum above the maximum, a cost bound with no costCurrency, or a currency that is not three uppercase ASCII letters each return 400 with the reason in the error string.

The server cap is authoritative

limit passes through list_limit, which clamps the requested value into the range fixed by DEFAULT_LIST_LIMIT and MAX_LIST_LIMIT. A larger value is reduced without an error. The TypeScript and Python ReceiptQueryClient helpers forward limit unchanged and follow nextCursor; they add no higher logical cap and no double fetch. Size bandwidth around 200-item pages.

Response shape

json
{
  "totalCount": 1024,
  "nextCursor": 47,
  "receipts": []
}

ReceiptQueryResponse has exactly those three fields. totalCount counts every receipt matching the filters, independent of page limit and cursor, so a caller renders a total without paginating to the end.

nextCursor is the seq of the last receipt in the page, and the store sets it only when the page came back full. A short page means no more rows matched, and the field serializes as null. Pass a non-null value back as cursor to fetch the next page.

receipts holds the stored ChioReceipt values, serialized one for one and ordered by seq ascending. The handler serializes the stored receipt without rewriting it, so the signature still covers what the array carries and a client can check it against the kernel key. Receipt Format documents the fields.


Cursor pagination

The cursor is the seq column value of the last receipt in a page. Pagination is forward-only and append-safe: new receipts land at higher seq values and appear on later pages without moving the boundaries of a page already read.

bash
# Page 1
GET /v1/receipts/query?toolServer=shell&limit=50

# Response carries nextCursor: 147

# Page 2
GET /v1/receipts/query?toolServer=shell&limit=50&cursor=147

# nextCursor is null on the last page.

The cursor is a raw u64. Treat the value as opaque: round-trip what the server returned in nextCursor rather than parsing or constructing cursors.


Example request

bash
GET /v1/receipts/query?outcome=deny&since=1700000000&limit=2
Authorization: Bearer my-service-token

The array elements are the receipts the kernel wrote. The CLI reads the same store and prints the same objects, one per line, so the shape below is what an HTTP caller iterates over as well.

quickstart · receipt-jsontranscript
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
    | jq 'select(.decision.verdict == "deny")'
{
  "id": "28b6e2576ca2ccbd031e769d8d2bd504317161115a600f27771a7f932307a284",
  "timestamp": 1788529912,
  "capability_id": "cap-01a06cb0-aae3-7ad3-8ba7-01350a3e01a0",
  "tool_server": "hello",
  "tool_name": "drop_tables",
  "action": {
    "parameters": {},
    "parameter_hash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
  },
  "decision": {
    "verdict": "deny",
    "reason": "requested tool drop_tables on server hello is not in capability scope",
    "guard": "kernel"
  },
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "tool_origin": "caller_executed",
  "redaction_mode": "none",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
  "metadata": {
    "attribution": {
      "delegation_depth": 0,
      "issuer_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
      "subject_key": "af2da8097f2c133affd89145302f029c7a5854e4e3a692a92b0cd20f84d07b37"
    },
    "chio_receipt_signing_nonce": "rcpt-01a06cb0-ab51-70d0-bcb3-72a8b78fe057",
    "receipt_context": {
      "request_id": "check-001"
    }
  },
  "trust_level": "mediated",
  "kernel_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
  "signature": "e89f7a9cbe6c6b207cf5ac0c3e197a3957d62d2994217a7dcaeeeb9869abef1233680328432ee7a502a76238eb6ee9e72b19715e2458a3ef85aebfbed0df9202"
}
exit 0denyin my-agent

A denial is a row in the same corpus as an allow, with the refused parameters still attached. Filtering outcome=deny is how an auditor reads the refusals without reading the whole log.


Agent-scoped route

A shorter URL covers per-agent lookups.

bash
GET /v1/agents/{subject_key}/receipts?limit=50&cursor=0
Authorization: Bearer my-service-token

AgentReceiptsHttpQuery carries cursor and limit and nothing else, so the subject key in the path is the only filter. The response type is the same ReceiptQueryResponse. Use it when a routing layer makes per-agent path scoping easier than a query string.


TypeScript SDK

@chio-protocol/sdk ships a ReceiptQueryClient over this route. The constructor takes positional arguments: new ReceiptQueryClient(baseUrl, authToken, fetchImpl?). query(params) fetches one page. paginate(params) is an async generator yielding one array of receipts per page, and it throws QueryError when the returned cursor fails to advance rather than looping.

typescript
import { ReceiptQueryClient } from "@chio-protocol/sdk";

const client = new ReceiptQueryClient(
  "https://trust.example.com",
  process.env.CHIO_CONTROL_TOKEN!,
);

// Single page.
const page = await client.query({
  outcome: "deny",
  since: 1700000000,
  limit: 50,
});

console.log(`${page.totalCount} total denies`);
for (const receipt of page.receipts) {
  console.log(receipt.id, receipt.decision);
}

// paginate() yields one array of receipts per page.
for await (const receipts of client.paginate({
  toolServer: "payment-server",
  since: 1700000000,
})) {
  for (const receipt of receipts) {
    if (receipt.decision?.verdict === "deny") {
      console.warn(`denied: ${receipt.decision.reason}`);
    }
  }
}

Python SDK

The chio.ReceiptQueryClient constructor is ReceiptQueryClient(base_url, auth_token, *, client=None). The keyword is auth_token, not bearer_token. query() and paginate() are synchronous: the default transport is a blocking urllib request, and passing client= substitutes an object with a get() method. Filters are a dict of the same camelCase keys the route accepts, and paginate() yields one list of receipts per page.

python
import os
from chio import ReceiptQueryClient

client = ReceiptQueryClient(
    "https://trust.example.com",
    auth_token=os.environ["CHIO_CONTROL_TOKEN"],
)

# Single page. query() and paginate() are synchronous.
page = client.query({"outcome": "deny", "since": 1700000000, "limit": 50})
print(f"{page['totalCount']} total denies")
for receipt in page["receipts"]:
    print(receipt["id"], receipt.get("decision"))

# paginate() yields one list of receipts per page.
for receipts in client.paginate({"toolServer": "payment-server", "since": 1700000000}):
    for receipt in receipts:
        decision = receipt.get("decision")
        if decision and decision["verdict"] == "deny":
            print(f"denied: {decision['reason']}")

Both clients thread the cursor and stop when the server returns no nextCursor. Neither refreshes tokens, retries a transient error, or verifies receipt signatures.


Pagination without an SDK

In a language with no client, the loop is short.

bash
cursor=""
while : ; do
  resp=$(curl -sS \
    "https://trust.example.com/v1/receipts/query?outcome=deny&limit=200${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $CHIO_CONTROL_TOKEN")

  echo "$resp" | jq -r '.receipts[].id'

  cursor=$(echo "$resp" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done

Analytics route

/v1/receipts/analytics aggregates the same corpus in the store. ReceiptAnalyticsQuery takes capabilityId, agentSubject, toolServer, toolName, since, and until, plus two aggregation parameters. It has no cost filters and no cursor.

ParameterDescription
groupLimitRows per grouped dimension. The store clamps it to the range 1 to MAX_ANALYTICS_GROUP_LIMIT, which is 200; the query default is 50.
timeBucketBucket width, hour (3600 seconds) or day (86400 seconds). Default day.
json
{
  "summary": {
    "totalReceipts": 12,
    "allowCount": 9,
    "denyCount": 1,
    "cancelledCount": 1,
    "incompleteCount": 1,
    "totalCostCharged": 750,
    "totalAttemptedCost": 500,
    "reliabilityScore": 0.8181818182,
    "complianceRate": 0.9166666667,
    "budgetUtilizationRate": 0.6
  },
  "byAgent": [{ "subjectKey": "", "metrics": {} }],
  "byTool": [{ "toolServer": "shell", "toolName": "bash", "metrics": {} }],
  "byTime": [{ "bucketStart": 1700000000, "bucketEnd": 1700086400, "metrics": {} }]
}

Every row carries the same ReceiptAnalyticsMetrics under metrics. The three ratios are Option<f64> and are omitted from the JSON when their denominator is zero: reliabilityScore divides allows by allows plus cancelled plus incomplete, complianceRate divides non-denies by total receipts, and budgetUtilizationRate divides charged cost by charged plus attempted cost. The other seven fields are u64 and always present.


Operator report route

/v1/reports/operator composes the analytics, cost-attribution, budget, compliance, settlement, metered-billing, authorization-context, and shared-evidence slices into one body, reading the receipt store and the budget store in one request.

OperatorReportQuery takes the analytics filters and aggregation parameters, plus six row limits, each defaulting to the value shown.

ParameterDefaultBounds the rows in
attributionLimit100costAttribution
budgetLimit50budgetUtilization
settlementLimit50settlementReconciliation
meteredLimit50meteredBillingReconciliation
authorizationLimit50authorizationContext
economicLimit50the economic receipt projection

OperatorReport declares no optional top-level key, so a client indexes every one below without a presence check.

json
{
  "generatedAt": 1700000000,
  "filters": { "toolServer": "shell", "toolName": "bash" },
  "activity": {},
  "costAttribution": {},
  "budgetUtilization": { "summary": {}, "rows": [] },
  "compliance": {},
  "settlementReconciliation": { "summary": {}, "receipts": [] },
  "meteredBillingReconciliation": { "summary": {}, "receipts": [] },
  "authorizationContext": { "schema": "", "profile": {}, "summary": {}, "receipts": [] },
  "sharedEvidence": { "summary": {}, "references": [] }
}
KeyTypeCarries
generatedAtu64Unix seconds the report was built
filtersOperatorReportQueryThe filters as the server resolved them; the read context is never serialized
activityReceiptAnalyticsResponseThe same body the analytics route returns
costAttributionCostAttributionReportThe same body /v1/reports/cost-attribution returns
budgetUtilizationBudgetUtilizationReportA summary and one row per capability grant, with invocation and cost usage
complianceComplianceReportCheckpoint and lineage coverage, settlement backlog counts, and the evidence-export query
settlementReconciliationSettlementReconciliationReportA summary and the receipts awaiting settlement reconciliation
meteredBillingReconciliationMeteredBillingReconciliationReportA summary and the receipts awaiting metered-billing reconciliation
authorizationContextAuthorizationContextReportA schema id, the OAuth authorization profile, a summary, and one row per receipt
sharedEvidenceSharedEvidenceReferenceReportA summary and one row per federated evidence share matched to local receipts

compliance carries childReceiptScope, whose value is one of full_query_window, time_window_context_only, or omitted_no_join_path. It says how far an evidence export over the same filters would reach for child receipts.

The other report routes

The operator report is one of the routes the trust-control router registers under /v1/reports/. The other 32 cover credit, liability, settlement, authorization, and underwriting surfaces, and each has its own request and response type.

MethodPathHandler
GET/v1/reports/authorization-contexthandle_authorization_context_report
GET/v1/reports/authorization-profile-metadatahandle_authorization_profile_metadata_report
GET/v1/reports/authorization-review-packhandle_authorization_review_pack_report
GET/v1/reports/behavioral-feedhandle_behavioral_feed_report
GET/v1/reports/bond-loss-policyhandle_credit_loss_lifecycle_report
GET/v1/reports/bond-losseshandle_query_credit_loss_lifecycle
GET/v1/reports/bond-policyhandle_credit_bond_report
POST/v1/reports/bonded-execution-simulationhandle_credit_bonded_execution_simulation_report
GET/v1/reports/bondshandle_query_credit_bonds
GET/v1/reports/capital-bookhandle_capital_book_report
GET/v1/reports/comptroller-surfacehandle_comptroller_surface_report
GET/v1/reports/cost-attributionhandle_cost_attribution_report
GET/v1/reports/credit-backtesthandle_credit_backtest_report
GET/v1/reports/credit-scorecardhandle_credit_scorecard_report
GET/v1/reports/economic-completion-flowhandle_economic_completion_flow_report
GET/v1/reports/economic-receiptshandle_economic_receipt_report
GET/v1/reports/exposure-ledgerhandle_exposure_ledger_report
GET/v1/reports/facilitieshandle_query_credit_facilities
GET/v1/reports/facility-policyhandle_credit_facility_report
GET/v1/reports/liability-claimshandle_query_liability_claim_workflows
GET/v1/reports/liability-markethandle_query_liability_market_workflows
GET/v1/reports/liability-providershandle_query_liability_providers
GET/v1/reports/metered-billinghandle_metered_billing_report
GET/v1/reports/provider-risk-packagehandle_credit_provider_risk_package_report
POST/v1/reports/runtime-attestation-appraisalhandle_runtime_attestation_appraisal_report
POST/v1/reports/runtime-attestation-appraisal-resulthandle_runtime_attestation_appraisal_result_export
POST/v1/reports/runtime-attestation-appraisal/importhandle_runtime_attestation_appraisal_import
GET/v1/reports/settlementshandle_settlement_report
GET/v1/reports/underwriting-decisionhandle_underwriting_decision_report
GET/v1/reports/underwriting-decisionshandle_query_underwriting_decisions
GET/v1/reports/underwriting-inputhandle_underwriting_policy_input
POST/v1/reports/underwriting-simulationhandle_underwriting_simulation_report

The chio receipt list subcommand

chio receipt list reads the same corpus, from a local SQLite receipt store or over HTTP. It prints one JSON receipt per line.

text
chio receipt list [OPTIONS]

Options:
  --capability     <ID>      Filter by capability ID
  --tool-server    <NAME>    Filter by tool server ID
  --tool-name      <NAME>    Filter by tool name
  --outcome        <OUT>     allow | deny | cancelled | incomplete
  --since          <SEC>     Receipts with timestamp >= this Unix seconds value
  --until          <SEC>     Receipts with timestamp <= this Unix seconds value
  --min-cost       <UNITS>   Minimum cost in minor units; requires --cost-currency
  --max-cost       <UNITS>   Maximum cost in minor units; requires --cost-currency
  --cost-currency  <CODE>    Three-letter uppercase currency code for the cost filters
  --limit          <N>       Maximum receipts per page [default: 50]
  --cursor         <SEQ>     Cursor for pagination (seq value to start after)
  --tenant         <ID>      Tenant read boundary; conflicts with --admin-all
  --admin-all                Read across all tenants as an administrative operation

Global options that apply here:
  --receipt-db     <PATH>    SQLite database path for durable receipt persistence
  --control-url    <URL>     Shared trust-control service base URL
  --control-token  <TOK>     Bearer token for the trust-control service

--min-cost and --max-cost each declare requires = "cost_currency", so clap rejects either one on its own. That mirrors the HTTP route, where a cost bound without costCurrency is a 400.

Local reads need an explicit read boundary

In local --receipt-db mode the read path fails closed unless exactly one of --tenant <ID> or --admin-all is supplied. The two conflict: --admin-all declares conflicts_with = "tenant". Against a running server the scope comes from the control token instead.
quickstart · receipt-listtranscript
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
    | jq -r '[.decision.verdict, .tool_server, .tool_name, .id] | @tsv'
allow	hello	hello_world	448440b65fa15559a364d24512cbe4f08631befe2c3d2ab471dad7204b8b69c8
deny	hello	drop_tables	28b6e2576ca2ccbd031e769d8d2bd504317161115a600f27771a7f932307a284
exit 0in my-agent

Piping the same command through jq selects one verdict and prints the whole receipt.

quickstart · receipt-jsontranscript
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
    | jq 'select(.decision.verdict == "deny")'
{
  "id": "28b6e2576ca2ccbd031e769d8d2bd504317161115a600f27771a7f932307a284",
  "timestamp": 1788529912,
  "capability_id": "cap-01a06cb0-aae3-7ad3-8ba7-01350a3e01a0",
  "tool_server": "hello",
  "tool_name": "drop_tables",
  "action": {
    "parameters": {},
    "parameter_hash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
  },
  "decision": {
    "verdict": "deny",
    "reason": "requested tool drop_tables on server hello is not in capability scope",
    "guard": "kernel"
  },
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "tool_origin": "caller_executed",
  "redaction_mode": "none",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
  "metadata": {
    "attribution": {
      "delegation_depth": 0,
      "issuer_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
      "subject_key": "af2da8097f2c133affd89145302f029c7a5854e4e3a692a92b0cd20f84d07b37"
    },
    "chio_receipt_signing_nonce": "rcpt-01a06cb0-ab51-70d0-bcb3-72a8b78fe057",
    "receipt_context": {
      "request_id": "check-001"
    }
  },
  "trust_level": "mediated",
  "kernel_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
  "signature": "e89f7a9cbe6c6b207cf5ac0c3e197a3957d62d2994217a7dcaeeeb9869abef1233680328432ee7a502a76238eb6ee9e72b19715e2458a3ef85aebfbed0df9202"
}
exit 0denyin my-agent

Omitting the boundary is an error rather than a default, and so is supplying both.

bash
chio receipt list --receipt-db ./chio.db --outcome deny
chio receipt list --receipt-db ./chio.db --tenant acme --admin-all

The first exits with the CLI error domain, because the read path refuses to infer a boundary. The second is rejected by clap before the command runs, from the conflicts_with declaration.

To paginate from the CLI, take nextCursor from one invocation and pass it as --cursor on the next. The SDK paginate() helpers do that threading for a caller.


Rate limits

The receipt query, analytics, and report routes carry no per-token request-rate middleware. The router applies a body-size limit to the POST routes and one response-header layer that sets a Content-Security-Policy on every response; the rate limiters in the crate govern federation admission and cluster peer authentication, not these read paths. Size a scan accordingly: prefer /v1/reports/operator for a dashboard, and reuse nextCursor rather than re-scanning from the start.


Error responses

Every error path on these handlers returns a JSON object with one error string, built by plain_http_error. There is no nested code object and no machine-readable error registry on these routes. Read the status for the class of failure and the string for the detail.

json
{ "error": "missing or invalid control bearer token" }

A request with no bearer token and a request with a token the server does not hold land on the same response: 401, that body, and a www-authenticate: Bearer header.

HTTPMeaning
400Cost filters failed validation: a minimum above the maximum, a bound with no costCurrency, or a currency that is not three uppercase letters
401Missing or invalid bearer token
403A tenant read token reached an admin-only surface, or asked for another tenant
500The service config failed validation, the store would not open, or serialization failed; the string carries the detail

Operational notes

  • Ordering. A page returns receipts by seq ascending. New receipts append at higher seq values, so a long-running forward pagination reaches new data without repeating old data.
  • Signature verification. The handler serializes the stored receipt unchanged, so the kernel signature over the canonical body survives the round trip. Verify it against the kernel key with the bindings helpers before treating a copied receipt as evidence.
  • Backfill. A large historical log takes a while to scan end to end. Start with a tight since bound and widen the window. The operator report gives a current-state picture in one request.
  • Read authority. The read context comes from the token, never from a query parameter. The query types that carry a read_context field mark it serde(skip), so a caller cannot set it on the wire.