From e05ebdff3e7f9c13dc1aaf0e41006b3065977e44 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 14:07:42 +0800 Subject: [PATCH 01/23] docs: refresh Cloud verification guides --- cloud/private-inference.mdx | 36 +-- cloud/verification/chat.mdx | 359 ++++------------------ cloud/verification/gateway.mdx | 168 ++--------- cloud/verification/index.mdx | 99 +++--- cloud/verification/model.mdx | 309 ++++--------------- cloud/verification/provenance.mdx | 50 ++++ cloud/verification/tls.mdx | 479 +++--------------------------- docs.json | 3 +- 8 files changed, 310 insertions(+), 1193 deletions(-) create mode 100644 cloud/verification/provenance.mdx diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index 05e01b1..c215ee1 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -26,7 +26,7 @@ NEAR AI Cloud's private inference provides three core guarantees: Your prompts, model weights, and outputs are encrypted and isolated in hardware-secured environments. Infrastructure providers, model providers, and NEAR cannot access your data at any point in the process. - Every computation generates cryptographic proof that it occurred inside a genuine, secure TEE. You can independently verify that your AI requests were processed in a protected environment without trusting any third party. + Attestation reports provide cryptographic evidence for a specific runtime and request path. When a response signature is available, you can also verify response integrity. Hardware-accelerated TEEs with [NVIDIA Confidential Computing](https://www.nvidia.com/en-us/data-center/solutions/confidential-computing/) deliver high-throughput inference with minimal latency overhead, making private inference practical for real-world applications. @@ -54,13 +54,13 @@ NEAR AI Cloud combines Intel TDX and NVIDIA TEE technologies to create isolated, If you're using a standard OpenAI SDK or curl, your prompts are automatically protected by TLS encryptionβ€”no additional setup required. -NEAR AI Cloud supports two connection modes β€” both provide TLS termination inside a TEE: +NEAR AI Cloud supports two connection modes. Use [TLS attestation](/cloud/verification/tls) when your application requires evidence for the TLS endpoint of a particular connection: **Gateway mode** routes through `cloud-api.near.ai`, which runs in its own TEE before forwarding to the model TEE: ```mermaid flowchart LR - A["Your Machine"] -->|"πŸ”’ Encrypted"| B["Gateway TEE"] -->|"πŸ”’ Encrypted"| C["Model TEE: TLS Termination β†’ LLM"] + A["Your Machine"] -->|"πŸ”’ HTTPS"| B["Cloud API gateway"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style B fill:#1b5e20,stroke:#66bb6a,stroke-width:2px,color:#fff @@ -71,23 +71,23 @@ flowchart LR ```mermaid flowchart LR - A["Your Machine"] -->|"πŸ”’ Encrypted"| C["Model TEE: TLS Termination β†’ LLM"] + A["Your Machine"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style C fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff ``` -**Key insight:** In both modes, TLS termination happens inside the TEE (green boxes), not at an external load balancer. Your prompts remain encrypted until they reach the secure enclave. +**Key insight:** TLS protects traffic in transit. When the [TLS attestation flow](/cloud/verification/tls) passes for a particular connection, it binds that connection's endpoint to verified attestation evidence. Here's why this works: 1. **Standard HTTPS = TLS encryption**: When you make API calls using the OpenAI SDK or curl, you're connecting via HTTPS. This means TLS encryption is applied automatically by your client before any data leaves your machine. -2. **TLS terminates inside the TEE**: Unlike traditional cloud services where TLS terminates at an external load balancer, NEAR AI Cloud terminates TLS connections **inside the Trusted Execution Environment**. Your encrypted data travels from your laptop directly into the secure enclave before being decrypted. +2. **TLS endpoint verification**: Use TLS attestation when your application needs to verify the endpoint for a particular connection. The same TLS connection must supply both the peer certificate and attestation report. -3. **No plaintext exposure**: Because TLS termination happens within the TEE, your prompts are never exposed in plaintext outside the hardware-secured environmentβ€”not to network infrastructure, not to cloud providers, not to anyone. +3. **Evidence-driven trust**: Use attestation and the workload policy appropriate for your application to decide which environment properties are required. -This means you get the same seamless developer experience as any OpenAI-compatible API, while your data remains encrypted from your machine all the way into the secure TEE. +This gives you the same OpenAI-compatible development flow while allowing your application to verify the properties it requires. ### Direct Completions @@ -148,7 +148,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Gateway (`cloud-api.near.ai`):** 1. **Request Initiation:** - You send chat completion requests via HTTPS to the LLM Gateway. TLS encryption protects your data in transit, and the TLS connection terminates inside the TEE, ensuring your prompts are decrypted only within the secure environment. + You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [TLS attestation](/cloud/verification/tls) when your application needs to verify the endpoint for a particular connection. 2. **Secure Request Routing:** The LLM Gateway routes your request to the appropriate Private LLM Node based on the requested model, availability, and load balancing requirements. @@ -157,27 +157,27 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel AI inference computations execute inside the Private LLM Node's TEE, where all data and model weights are protected by hardware-enforced isolation. 4. **Attestation Generation** - The TEE generates CPU and GPU attestation reports that provide cryptographic proof of the environment's integrity and configuration. + Attestation reports provide evidence about the environment and its measured configuration. - 5. **Cryptographic Signing:** - The TEE cryptographically signs both your original request and the inference results to ensure authenticity and prevent tampering. + 5. **Response Signing:** + Supported flows can make a response signature available for integrity verification. 6. **Verifiable Response:** - You receive the AI response along with cryptographic signatures and attestation data for independent verification. + Use [verification](/cloud/verification) to check the evidence required by your application. A response without an available signature is not response-verified. **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain. TLS terminates inside the model's TEE β€” there is no intermediate gateway. + You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [TLS attestation](/cloud/verification/tls) when endpoint verification is required. 2. **Secure Inference:** The model processes your request entirely within its TEE, where all data and model weights are protected by hardware-enforced isolation. 3. **Attestation & Signing:** - The TEE generates attestation reports and cryptographically signs both your request and the inference results. + Attestation reports provide environment evidence; supported flows can make response signatures available for integrity verification. 4. **Verifiable Response:** - You receive the response with cryptographic signatures. Only [model verification](/cloud/verification/model) is needed β€” no gateway attestation to check. + Use [model verification](/cloud/verification/model) for the direct endpoint. If response verification is required, also check that a signature is available and valid. --- @@ -215,7 +215,7 @@ NEAR AI Cloud's private inference implements multiple layers of security to prot TEEs create isolated execution environments enforced at the hardware level, preventing unauthorized access to memory and computation even from privileged system administrators or cloud providers. - **Secure Communication** : -All communication between your applications and the LLM infrastructure uses TLS encryption. Critically, TLS termination occurs inside the TEEβ€”not at an external load balancerβ€”so your prompts remain encrypted until they reach the secure enclave. +All communication between your applications and the LLM infrastructure uses TLS encryption. Use [TLS attestation](/cloud/verification/tls) to verify endpoint binding for a particular connection when that property is required. - **Cryptographic Attestation** : Every TEE environment generates cryptographic proofs that verify the integrity of the execution environment, allowing you to independently confirm your computations occurred in a genuine, unmodified TEE. @@ -248,7 +248,7 @@ Cryptographic signatures generated inside the TEE ensure that responses cannot b Understand how to verify and validate secure interactions with AI models - Verify that your HTTPS connection terminates inside the TEE using hardware-backed TLS attestation + Verify TLS endpoint binding for a particular HTTPS connection using hardware-backed attestation Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 9e92f47..38fb96f 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -1,334 +1,103 @@ --- -title: "Chat Message Verification" -sidebarTitle: "Chat Verification" +title: "Chat Verification" +description: "Verify an available chat-completion signature against the exact request and response bytes." --- +Chat verification checks the integrity of a response for which a signature is available. It combines three pieces of evidence: -You can verify each chat message with NEAR AI Cloud. For this you will need: +1. SHA-256 hashes of the exact request and response bytes; +2. a valid signature over the expected payload; and +3. a matching [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) attestation for the signer. -1. [Chat Message **REQUEST** Hash](#chat-message-request-hash) -2. [Chat Message **RESPONSE** Hash](#chat-message-response-hash) -3. [Chat Message Signature](#chat-message-signature) - - - See an example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. - - - **Two signature kinds** - - There are two kinds of chat signatures, distinguished by the `signature_kind` field on the gateway's signature response β€” see [Signature Kinds](#signature-kinds): - - - **`provider_tee`** β€” generated **inside the model TEE** over the exact bytes the TEE received and sent. This is always what you get from a model's [direct completions endpoint](/cloud/private-inference#direct-completions) (`{slug}.completions.near.ai`) β€” the examples below use `qwen35-122b.completions.near.ai`. - - **`gateway`** β€” generated **inside the gateway TEE** (`cloud-api.near.ai`) over the exact bytes the gateway returned to you. Used when the gateway rewrites streamed response bytes (OpenAI-spec usage accounting), so the model TEE's byte-exact signature could no longer match what you received. - ---- - -## Chat Message Request Hash - -The value is calculated from the **exact JSON request body string** as it was sent over the wire. - -**_Example:_** - -```json -{"messages":[{"content":"Respond with only two words.","role":"user"}],"stream":true,"model":"Qwen/Qwen3.5-122B-A10B","chat_template_kwargs":{"enable_thinking":false}} -``` - -Which hashes to: - -```bash -2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad -``` - -Here is an example of how to get the sha256 hash of your message request body: - - - - -```js -import crypto from 'crypto'; - -const requestBody = JSON.stringify({ - "messages": [ - { - "content": "Respond with only two words.", - "role": "user" - } - ], - "stream": true, - "model": "Qwen/Qwen3.5-122B-A10B", - "chat_template_kwargs": {"enable_thinking": false} -}); - -const hash = crypto.createHash('sha256').update(requestBody).digest('hex'); -console.log(hash); //2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad -``` - - - + + Not every response has an available signature. An unavailable signature is not a successful verification result. If response verification is required by your policy, treat that response as unverified. + -```python -import hashlib -import json +## Preserve the exact bytes -request_body = { - "messages": [ - { - "content": "Respond with only two words.", - "role": "user" - } - ], - "stream": True, - "model": "Qwen/Qwen3.5-122B-A10B", - "chat_template_kwargs": {"enable_thinking": False} -} +Build the request body once and retain the exact UTF-8 bytes that your HTTP client sends. Read and retain the raw response body before parsing it as JSON or Server-Sent Events. -# Convert to JSON string with same formatting as JavaScript -request_body_str = json.dumps(request_body, separators=(',', ':')) +Calculate: -# Calculate SHA-256 hash -hash_obj = hashlib.sha256(request_body_str.encode()) -hash_hex = hash_obj.hexdigest() -print(hash_hex) #2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) ``` - - - ---- - -## Chat Message Response Hash +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct the response from parsed events. Any of those changes can produce a different hash. -This value is calculated from the **exact response payload string**. - - - Please note that the streaming response contains two new lines at the end and should not be omitted when copying the response as the hash value will change. - -**_Example Response Body:_** - -```bash -data: {"choices":[{"delta":{"content":"","reasoning_content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} +For a streaming response, capture all bytes delivered by the HTTP response body, including framing such as the final SSE delimiter when present. -data: {"choices":[{"delta":{"content":"Under","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[{"delta":{"content":"stood.","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null,"matched_stop":null}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[{"delta":{"reasoning_content":null},"finish_reason":"stop","index":0,"logprobs":null,"matched_stop":248046}],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk"} - -data: {"choices":[],"created":1780404899,"id":"afa7975eaf844b1888776cf41548e230","model":"Qwen/Qwen3.5-122B-A10B","object":"chat.completion.chunk","usage":{"completion_tokens":4,"prompt_tokens":18,"prompt_tokens_details":null,"reasoning_tokens":0,"total_tokens":22}} - -data: [DONE] - -``` +## Fetch the signature -Which hashes to: - -```bash -8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - -Here is an example of how to get the sha256 hash of your message response payload: +After completion, use the completion ID as `chat_id` and request a signature with the algorithm you want to verify. + - - -```js -const response = await fetch('https://qwen35-122b.completions.near.ai/v1/chat/completions', { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.NEARAI_CLOUD_API_KEY}` - }, - body: requestBody -}); - -const responseBody = await response.text(); -const hash = crypto.createHash('sha256').update(responseBody).digest('hex'); -console.log(hash); // 8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - - - - -```python -import hashlib -import requests -import os - -response = requests.post( - 'https://qwen35-122b.completions.near.ai/v1/chat/completions', - headers={ - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {os.environ["NEARAI_CLOUD_API_KEY"]}' - }, - data=request_body_str # Uses the exact string from the previous example -) - -response_body = response.text -hash_obj = hashlib.sha256(response_body.encode()) -hash_hex = hash_obj.hexdigest() -print(hash_hex) # 8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c -``` - - - - ---- - -## Chat Message Signature - -From the Chat Message Response you will get a unique chat `id` that is used to fetch the Chat Message Signature from NEAR AI Cloud. - -You can query the signature API with the value of `id` from the response after chat completion. - -Use one of the following endpoints to get the signature: - -**Via Direct Completions:** -```bash -GET https://{slug}.completions.near.ai/v1/signature/{chat_id}?signing_algo=ecdsa -``` - -**Via Gateway:** ```bash -GET https://cloud-api.near.ai/v1/signature/{chat_id}?model={model_id}&signing_algo=ecdsa +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` -For example, the `id` from the response in the previous section is: - - `afa7975eaf844b1888776cf41548e230` + + ```bash -# Via direct completions: -curl -X GET 'https://qwen35-122b.completions.near.ai/v1/signature/afa7975eaf844b1888776cf41548e230?signing_algo=ecdsa' \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer " +curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` - - A model can be served by multiple TEE nodes behind the same domain. The signature is cached on the node that served your chat completion, so a lookup may transiently return `Chat id not found or expired` if it lands on a different node β€” simply retry until you hit the right one. - -***Example Response:*** - -```json -{ - "text":"Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c", - "signature":"0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b", - "signing_address":"0x6525e128afcffebf7eed05d485d7be983cdae934", - "signing_algo":"ecdsa" -} -``` - -The above response gives us all of the crucial information we need to verify that the message was executed in our trusted environment: - -- `text` - The signed payload. Its format depends on the [signature kind](#signature-kinds): `{model_id}:{request_hash}:{response_hash}` for a model-TEE signature (as in this example), `{request_hash}:{response_hash}` for a gateway signature -- `signature` - This is the cryptographic signature of the `text` field, generated using the TEE's private key -- `signing_address` - Public key of the TEE that signed (the model TEE for `provider_tee`, the gateway TEE for `gateway`) -- `signing_algo` - Cryptography curve used to sign -- `signature_kind` - Which key produced the signature: `"provider_tee"` or `"gateway"` (gateway endpoint only β€” see [Signature Kinds](#signature-kinds)) - -You can see that `text` is: - -`Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c` - -This exactly matches the model we requested and the values we calculated in the previous sections: - -- Model: `Qwen/Qwen3.5-122B-A10B` -- Request hash: `2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad` -- Response hash: `8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c` - ---- - -## Signature Kinds - -Signatures fetched from the gateway (`GET https://cloud-api.near.ai/v1/signature/{chat_id}`) carry a `signature_kind` field that tells you which key signed and what the `text` payload contains: - -### `provider_tee` - -- **Who signs:** the model TEE that served your request, with the signing key from its [model attestation](/cloud/verification/model). Verify `signing_address` against that attestation. -- **Payload:** `text = "{model_id}:{request_hash}:{response_hash}"`. -- **What it covers:** the exact request bytes the model TEE received and the exact response bytes it sent. When you use a [direct completions endpoint](/cloud/private-inference#direct-completions), the bytes you sent and received are those bytes, so both hashes verify byte-for-byte against your own traffic. (Direct completions signature responses come straight from the model TEE and do not carry a `signature_kind` field β€” they are always model-TEE signatures.) - -### `gateway` - -- **Who signs:** the gateway TEE (`cloud-api.near.ai`), with the same signing key whose address is returned by the [gateway attestation report](/cloud/verification/gateway) (`GET /v1/attestation/report`). Verify `signing_address` against that report. -- **Payload:** `text = "{request_hash}:{response_hash}"` β€” no model-ID prefix. -- **What it covers:** the exact request body you sent to the gateway and the exact response bytes the gateway returned to you, so both hashes verify byte-for-byte against your own traffic. - -The gateway signs a streamed response itself when it has rewritten the stream bytes for OpenAI compatibility, which makes the model TEE's byte-exact signature unable to match what you received: - -- **Usage accounting** β€” with `stream_options: {"include_usage": true}`, the gateway rewrites intermediate chunks to carry `usage: null` and appends a single final usage chunk. -- **Usage stripping** β€” on attested models, default streaming (no `stream_options`) and `"include_usage": false` streams have per-chunk usage nulled out and the upstream's trailing usage-only chunk dropped. - -In both cases the gateway stores its signature **before** emitting the final `data: [DONE]` line, so the signature is retrievable the moment the stream ends. The gateway signature also covers attested third-party fallback serving (`x-serving-provider: chutes` response header): those backends protect response integrity with an end-to-end encrypted channel rather than a per-response model signature, so a rewritten stream served by them is signed by the gateway alone. - - - **Legacy signatures** - - Signatures stored before `signature_kind` was recorded omit the field β€” their provenance is unknown rather than guessed. You can still distinguish them structurally: a `text` with three `:`-separated fields (`{model_id}:{request_hash}:{response_hash}`) is a model-TEE payload; one with two (`{request_hash}:{response_hash}`) is a gateway payload. - ---- - -## Verify Signature - -Signature verification can be easily done with any standard ECDSA verification library such as [ethers](https://www.npmjs.com/package/ethers) or even an online tool such as [etherscan's VerifySignatures](https://etherscan.io/verifiedSignatures). - -These tools will require: - -- `Address`: What the expected address is for the signature. In our case it will be the one retrieved from your [attestation API query](/cloud/verification/model) (see Model Verification page). -- `Message`: The original message before signing. In our case it will be the `text` field from [Chat Message Signature](#chat-message-signature) (`{model_id}:{request_hash}:{response_hash}`) -- `Signature`: The signed message from above + + -Here is an example of how to verify the Chat Message signature using `ethers`: +The returned object contains: - - +| Field | Meaning | +| --- | --- | +| `text` | The payload that was signed. | +| `signature` | The cryptographic signature over `text`. | +| `signing_address` | The algorithm-specific identity that made the signature. | +| `signing_algo` | The algorithm required to verify the signature. | +| `signature_kind` | On a gateway response, identifies whether the model-serving signer or gateway signer produced the signature. | -```js -import { ethers } from 'ethers'; +## Check the signed payload -const text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c"; -const signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b"; -const expectedAddress = "0x6525e128afcffebf7eed05d485d7be983cdae934"; +First calculate your `request_hash` and `response_hash`. Then construct the expected payload from `signature_kind`: -// Recover the address from the signature -const recoveredAddress = ethers.verifyMessage(text, signature); +| `signature_kind` | Expected `text` | Match signer to | +| --- | --- | --- | +| `provider_tee` | `::` | A model attestation with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | A gateway attestation with the same `signing_address` and `signing_algo`. | -// Compare with expected address (case-insensitive) -const isValid = recoveredAddress.toLowerCase() === expectedAddress.toLowerCase(); +For a direct completion signature, use the model-attestation path. For a gateway response whose `signature_kind` is absent or unfamiliar, do not infer its classification from the shape of `text`; treat the signer classification as unavailable unless your endpoint contract supplies it. -console.log("Text:", text); -console.log("Expected address:", expectedAddress); -console.log("Recovered address:", recoveredAddress); -console.log("Signature valid:", isValid); -``` +Reject the signature if `text` does not exactly equal the expected payload. Then verify `signature` locally with a cryptographic library that supports the reported `signing_algo`. - - + + `ecdsa` signatures use an Ethereum-style signing address. `ed25519` signatures use an Ed25519 public key. Use the verification method appropriate to the reported algorithm; do not send prompts, responses, or signatures to a third-party verification website. + -```python -from eth_account import Account -from eth_account.messages import encode_defunct +## Match the signature to attestation -text = "Qwen/Qwen3.5-122B-A10B:2974f24b2a687856d2a0cf08d813902965c25e6552ba7062e4fa303432b6d2ad:8cb30eef9d133bdc6bfe812772dc4a62336d2827caea843546cbeff3f004c42c" -signature = "0xed381e84d059198d1826e44dbbbac9501caaa8f79f913f27578acafa5be852e6103fe34fabd6446d7fde3f5250c0a16e109fe088a562bae08ac13d081a66d0761b" -expected_address = "0x6525e128afcffebf7eed05d485d7be983cdae934" +Fetch or select an attestation for the signer reported in the signature. Verify the quote, your client-generated nonce, the signer binding, and the workload policy as described in [model verification](/cloud/verification/model) or [gateway verification](/cloud/verification/gateway). -# Create a message object that can be signed -message = encode_defunct(text=text) +Record the response time and attestation time, and enforce the maximum attestation age your policy requires. A matching report establishes the signer binding for that attestation; your policy determines how close in time it must be to the response. -# Recover the address from the signature -recovered_address = Account.recover_message(message, signature=signature) +## Signature availability and retries -# Compare with expected address (case-insensitive) -is_valid = recovered_address.lower() == expected_address.lower() +The signature can be unavailable immediately after completion or when the response did not complete. Retry for a short, bounded interval, then make a decision according to your policy. -print("Text:", text) -print("Expected address:", expected_address) -print("Recovered address:", recovered_address) -print("Signature valid:", is_valid) -``` +| Result | Recommended handling | +| --- | --- | +| Signature returned | Complete the payload, cryptographic-signature, and attestation checks. | +| Temporary lookup failure | Retry briefly with the same completion ID and signing algorithm. | +| No signature after the retry window | Treat the response as unverified if a signature is required. | +| Signature or signer mismatch | Reject the verification result; do not substitute a different response or attestation. | - - +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for signature-related verification steps. diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index 25404c6..cfcd3de 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,166 +1,52 @@ --- title: "Gateway Verification" +description: "Verify the attestation evidence for requests sent through the NEAR AI Cloud API gateway." --- +Use gateway verification when your request is sent to `https://cloud-api.near.ai`. It verifies evidence for the gateway request path. It does not replace [model verification](/cloud/verification/model) when you also need to verify the environment that served the model. - **Direct Completions β€” No Gateway Verification Needed** - - If you are using [direct completions endpoints](/cloud/private-inference#direct-completions) (e.g., `https://qwen35-122b.completions.near.ai`), your requests go straight to the model's TEE with no gateway in the path. In that case, only [Model Verification](/cloud/verification/model) is needed β€” you can skip gateway verification entirely. - -To verify the NEAR AI Cloud private inference gateway is operating in a secure trusted environment, you need to verify the gateway attestation. The gateway attestation proves that the API gateway itself runs in a Trusted Execution Environment (TEE). - -- [Request Gateway Attestation](#request-gateway-attestation) report from NEAR AI Cloud -- [Verify Gateway Attestation](#verifying-gateway-attestation) report using Intel attestation authenticators - - - See an example implementation in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. + A [direct completions endpoint](/cloud/private-inference#direct-completions) connects to the model endpoint without the Cloud API gateway in the request path. Start with [model verification](/cloud/verification/model) for that flow. ---- - -## Request Gateway Attestation - -The gateway attestation can be requested standalone or is included in the response when you request a model attestation. To request gateway attestation standalone, use the following `GET` API endpoint: -```bash -https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce} -``` - -The `signing_algo` parameter specifies the signing algorithm used (`ecdsa` or `ed25519`). The `nonce` parameter is optional but recommended. It should be a randomly generated 64 character hexadecimal string (32 bytes) that ensures attestation freshness and prevents replay attacks. If not provided, the server will generate one for you. - -The endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. +## Request a fresh gateway report -If you want to verify that the HTTPS connection to `cloud-api.near.ai` terminates inside the gateway TEE, add `include_tls_fingerprint=true` and follow [TLS Attestation Verification](/cloud/verification/tls). That opt-in flag binds the gateway's TLS certificate fingerprint into `report_data`; it is disabled by default for compatibility with existing clients. - - - +Generate a new 32-byte nonce in your client for each verification attempt. Encode it as 64 hexadecimal characters. ```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -curl "https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${NONCE}" \ - -H 'accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - +NONCE="$(openssl rand -hex 32)" -```js -import crypto from 'crypto'; - -// Generate a random 64-character hex nonce (optional but recommended) -const nonce = crypto.randomBytes(32).toString('hex'); - -const response = await fetch( - `https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${nonce}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, - }, - } -); -``` - - - - -```python -import os -import requests -import secrets - -# Generate a random 64-character hex nonce (optional but recommended) -nonce = secrets.token_hex(32) - -response = requests.get( - f'https://cloud-api.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce}', - headers={ - 'accept': 'application/json', - 'Authorization': f'Bearer {os.environ["NEAR_AI_CLOUD_API_KEY"]}', - } -) +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` - - - - - ```json - { - "gateway_attestation": { - "request_nonce": "...", \\ The nonce you provided in the request - "intel_quote": "...", \\ Attestation report used to verify w/ Intel TDX - "event_log": [...], \\ TDX event log - "info": { \\ TCB information including Docker compose manifest - "compose": "...", - ... - } - } - } - ``` - - - `gateway_attestation`: Attestation report for the private inference gateway (API gateway) - - `request_nonce`: The nonce you provided in the request - - `intel_quote`: Intel TDX quote for the gateway TEE - - `event_log`: TDX event log - - `info`: TCB information including Docker compose manifest - - - - **Implementation**: This endpoint is defined in the [NEAR AI Cloud API](https://github.com/nearai/cloud-api). - ---- - -## Verifying Gateway Attestation - -Once you have [requested a gateway attestation](#request-gateway-attestation) from NEAR AI Cloud, you can use the returned payload to verify its authenticity. You can verify: - -- **Intel TDX quote**: Verifies TDX quote with [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library -- **TDX report data**: Validates that report data includes the nonce in request -- **Compose manifest**: Displays Docker compose manifest and verifies it matches the mr_config measurement -- **Source Code provenance**: Verifies container image provenance - -### Verify TDX Quote - -You can verify the Intel TDX quote with the value of `intel_quote` from `gateway_attestation` using the [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library. This verifies: - -- The CPU TEE measurements are valid -- The quote is authentic and signed by Intel -- The TEE environment is genuine - -Alternatively, you can verify the Intel TDX quote at [TEE Attestation Explorer](https://proof.t16z.com/). - -### Verify TDX Report Data +Add `model=` when you also want the request to include model attestation evidence. Add `include_tls_fingerprint=true` when you need to verify the HTTPS connection; see [TLS attestation](/cloud/verification/tls). -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce +## Verify the gateway evidence -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. +Verify the Intel TDX quote before trusting report fields supplied over HTTP. Then check the following: -### Verify Compose Manifest +1. The verified quote satisfies your Intel DCAP and advisory policy. +2. The quote's report data contains the nonce generated by your client and binds the reported gateway signing identity. +3. The signing identity is paired with its reported `signing_algo`. +4. The measured configuration satisfies the policy your application accepts. +5. If you require source and build provenance, verify it for each image digest in the measured configuration. See [image provenance](/cloud/verification/provenance). -The attestation response includes Docker compose manifest information in the `gateway_attestation.info` field. To verify: +When the report includes `gateway_attestation`, use that entry for gateway verification. A request that includes a model can also include `model_attestations`; verify those separately when model evidence is required. -1. Extract the Docker compose manifest from the attestation -2. Calculate the SHA-256 hash of the compose manifest -3. Compare it with the `mr_config` measurement from the verified TDX quote -4. Verify they match, proving the exact container configuration +## Verify responses signed by the gateway -This ensures the exact Docker compose file is deployed to the TEE environment. +When a response signature reports `signature_kind: "gateway"`, verify its signature against the gateway attestation whose `signing_address` and `signing_algo` match the signature. Then verify that the signed payload contains the SHA-256 hashes of the exact request and response bytes you observed. -### Verify Source Code Provenance +Follow [chat verification](/cloud/verification/chat) for the full response-signature flow. -Extract the `nearaidev/cloud-api` container image digests from the Docker compose manifest (matching `@sha256:xxx` patterns) and fetch the source code provenance from Sigstore for each image. This allows you to: +## Freshness and failure handling -1. Verify the container images were built from the expected source repository with exact release tag -2. Review the GitHub Actions workflow that built the images -3. Audit the build provenance and supply chain metadata -4. Audit the source code of a given release +The nonce proves freshness only when your client created and retained it. Do not accept a report merely because an HTTP field echoes a nonce; verify the binding inside the validated quote. -The source code of the [cloud-api gateway](https://github.com/nearai/cloud-api) is reproducible, which means you can build from the source code of a given release and verify that the resulting Docker image digest matches the digest from the attestation (`@sha256:xxx`). +An unavailable report, failed quote, nonce mismatch, identity mismatch, or policy mismatch is not an equivalent result. If your application requires that property, mark the verification as failed or unverified and fetch new evidence rather than reusing a report from a different instance. -This ensures the exact version of source code is built into the `nearaidev/cloud-api` Docker image and makes it easy to audit and validate the source code. You can find one [Sigstore link example](https://search.sigstore.dev/?hash=sha256:f75c2a8f1a3d8a36ed6cd7479e848edf0a0e814381d7b83993a703201594bc14) in the [v0.1.7 release of cloud-api](https://github.com/nearai/cloud-api/releases/tag/v0.1.7). +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for individual verification steps. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 9d3ce6d..35ff5b5 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,57 +1,80 @@ --- title: "Verification" +description: "Verify NEAR AI Cloud attestation reports, TLS endpoints, signed chat responses, and deployed image provenance." --- -[NEAR AI Cloud](https://cloud.near.ai) operates in Trusted Execution Environments (TEEs) which use cryptographic proofs to verify that your private AI conversations actually happened in secure, isolated environments that are not on compromised systems or with unauthorized access. +Verification lets you independently check evidence for a specific NEAR AI Cloud request path. Each check answers a different question: whether an attestation is fresh, whether a TLS connection reaches the attested endpoint, whether a response was signed by an attested signer, or whether deployed images have the provenance your policy requires. -This section will show you step-by-step processes for checking these proofs, validating digital signatures, and confirming that your AI interactions haven't been tampered with. +Start with the result you need to verify. - - See an easy-to-follow example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. + + + Verify a model attestation, including its Intel TDX quote, nonce, signer, GPU evidence, and measured configuration. + + + Verify the Cloud API gateway when requests are sent through cloud-api.near.ai. + + + Verify that one HTTPS connection terminates at the endpoint named by its attestation. + + + Verify the integrity and signer of a chat completion. + + + Apply a source and build-provenance policy to every image pinned in an attested deployment. + + - A more complete verifier implementation is available in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. - - - **Simplified Verification with Direct Completions** +## Choose the checks you need - When using [direct completions endpoints](/cloud/private-inference#direct-completions) (e.g., `https://qwen35-122b.completions.near.ai`), your requests go straight to the model's TEE. This means only **model verification** is needed β€” there is no gateway in the path, so [gateway verification](/cloud/verification/gateway) can be skipped entirely. - - - **TLS Termination Inside the TEE** +| Goal | Required evidence | +| --- | --- | +| Verify a model environment | A fresh model attestation, a verified Intel TDX quote, the request nonce, signer binding, and GPU evidence when present. | +| Verify the gateway | A fresh gateway attestation and a verified Intel TDX quote. Verify the model separately when you also need evidence for model execution. | +| Verify an HTTPS connection | A TLS-bound attestation and the live certificate's SPKI hash from the **same TLS connection**. | +| Verify a chat response | The exact request and response bytes, a valid signature, and an attestation for the signature's signer. | +| Verify deployed software provenance | A verified compose measurement plus a provenance decision for every image digest in that compose file. | + +## Recommended verification flow - If you also need to prove that your HTTPS connection terminates inside the same TEE, request attestation with `include_tls_fingerprint=true`. That opt-in flag changes the attested `report_data` from `signing_address || nonce` to `SHA256(signing_address || tls_cert_fingerprint) || nonce`. See [TLS Attestation Verification](/cloud/verification/tls). +For a production integration, use the following order: + +1. Generate a fresh, random 32-byte nonce in your client. Send it as 64 hexadecimal characters with the attestation request. +2. Request the attestation for the endpoint and model you are using. When you need TLS endpoint verification, set `include_tls_fingerprint=true`. +3. Verify the Intel TDX quote with an Intel DCAP verifier. Read `report_data` and measurements from the **verified quote**, not from unverified response fields. +4. Check that the verified quote binds your nonce and the reported signing identity. Verify NVIDIA GPU evidence when the report contains it. +5. Apply your workload policy: inspect the measured compose configuration and, when required, verify provenance for every pinned image digest. +6. For a chat response, verify the signature over the exact bytes you sent and received, then match its signer to the appropriate attestation. + + + The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual checks. Treat any example as a starting point for your own policy: your application should fail verification when a check it requires is missing or does not pass. -## How NEAR AI Cloud Verification Works -1. **Secure Key Generation:** When NEAR AI Cloud initializes, it generates a unique cryptographic signing key pair inside the Trusted Execution Environment (TEE). The private key never leaves the secure hardware. +## Direct completions and the gateway -2. **Hardware Attestation:** The system generates attestation reports that cryptographically prove it's running on genuine NVIDIA H100/H200 hardware in TEE mode within a confidential VM. +Requests sent to a [direct completions endpoint](/cloud/private-inference#direct-completions) go directly to the model endpoint. In that path, start with [model verification](/cloud/verification/model); gateway verification is not required for the direct request path. -3. **Key Binding:** These attestation reports include the public key from step 1, creating a verifiable link between the secure hardware and the signing capability. +Requests sent to `https://cloud-api.near.ai` pass through the Cloud API gateway. Verify the gateway when you need evidence for that request path, and verify the model when you need evidence for the model environment. -4. **Message Signing:** Every AI inference request and response is digitally signed using the private key that remains secured within the TEE. +## Important details for load-balanced endpoints -5. **End-to-End Verification:** You can verify that your AI interactions were genuinely processed in the secure environment by: +An endpoint can be served by more than one attested instance. Do not assume that two independent requests reached the same instance. - - Checking the hardware attestation reports - - Validating the digital signatures on your messages - - Confirming the signing key matches the attested hardware +- When verifying a chat response, use the response's `signing_address` and `signing_algo` to select the matching attestation. +- When verifying TLS, obtain the live certificate and the attestation over the same TLS connection. Fetching them over separate connections can produce a false mismatch after load balancing or certificate rotation. +- When an instance is redeployed or its certificate changes, fetch a new attestation before accepting the new evidence. ---- +## What attestation does and does not establish -## What You Can Verify +Attestation verifies specific cryptographic evidence about a runtime and its measured configuration. It does not, by itself, validate the quality of a model response, secure a compromised client device, or establish source provenance for an image whose build evidence you have not verified. - - - Verify that AI models are running in secure TEE environments using NVIDIA and Intel attestation - - - Verify that the private inference gateway is running in secure TEE environments using Intel attestation - - - Verify that your HTTPS connection terminates inside the attested TEE by binding the TLS certificate into report_data - - - Verify individual chat messages and responses through cryptographic signatures - - +Use the individual guides in this section to decide which guarantees your application requires. + +## Common verification failures + +| Symptom | What to do | +| --- | --- | +| Nonce mismatch | Reject the report. Generate a new nonce in the client and request a new attestation. | +| TLS fingerprint mismatch | Reject the connection. Repeat the TLS flow using one connection for both the certificate and attestation request. | +| Signature is unavailable | Do not mark the response as verified. Retry briefly after completion; if it remains unavailable, retain the response ID and request ID for support. | +| Image provenance is unavailable | Treat the image as unverified under a provenance-required policy. Fetch a fresh attestation after a deployment change, then repeat the provenance check. | diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 9dfd65c..44aaec1 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -1,297 +1,88 @@ --- title: "Model Verification" +description: "Verify the attestation evidence for a NEAR AI Cloud model endpoint." --- +Use model verification to check the environment serving a model. It is the required attestation check for [direct completions](/cloud/private-inference#direct-completions), and it complements [gateway verification](/cloud/verification/gateway) when you call `cloud-api.near.ai`. -To verify a NEAR AI model is operating in a secure trusted environment, there are two main steps: +Model verification does not prove a particular response by itself. To verify a response, also follow [chat verification](/cloud/verification/chat). -- [Request Model Attestation](#request-model-attestation) report from NEAR AI Cloud -- [Verify Model Attestation](#verifying-model-attestation) report using NVIDIA & Intel attestation authenticators +## Request a fresh report - - See an easy-to-follow example implementation in the [NEAR AI Cloud Verification Example](https://github.com/near-examples/nearai-cloud-verification-example) repo. - - A more complete verifier implementation is available in the [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) repo. - ---- - -## Request Model Attestation - -To request a model attestation from NEAR AI cloud, use one of the following `GET` API endpoints: - -**Via Gateway:** -```bash -https://cloud-api.near.ai/v1/attestation/report?model={model_name}&signing_algo=ecdsa&nonce={nonce} -``` - -**Via Direct Completions:** -```bash -https://{slug}.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce} -``` - -The `signing_algo` parameter specifies the signing algorithm used (`ecdsa` or `ed25519`). The `nonce` parameter is optional but recommended. It should be a randomly generated 64 character hexadecimal string (32 bytes) that ensures attestation freshness and prevents replay attacks. If not provided, the server will generate one for you. - -The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. The direct completions endpoint is served by the model TEE itself and needs no API key. - -With direct completions, you can also pass `include_tls_fingerprint=true` to bind the TLS certificate fingerprint to the attestation report. This is disabled by default for compatibility with existing clients. Without the flag, `report_data` binds only the signing key and nonce. With the flag, `report_data[0:32]` becomes `SHA256(signing_address || tls_cert_fingerprint)`, which lets you verify that the HTTPS connection terminates inside the model TEE. See [TLS Attestation Verification](/cloud/verification/tls). +Generate the nonce in your client. Use 32 random bytes encoded as 64 hexadecimal characters, and retain it until verification has completed. - + ```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ecdsa&nonce=${NONCE}" \ - -H 'accept: application/json' \ - -H 'Authorization: Bearer ' +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` - - + + ```bash -# Generate a random 64-character hex nonce (optional but recommended) -NONCE=$(openssl rand -hex 32) - -# Direct completions β€” no model parameter needed -curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${NONCE}&include_tls_fingerprint=true" \ - -H 'accept: application/json' -``` - - - - -```js -import crypto from 'crypto'; - -const MODEL_NAME = 'zai-org/GLM-5.1-FP8' -// Generate a random 64-character hex nonce (optional but recommended) -const nonce = crypto.randomBytes(32).toString('hex'); - -// Via gateway (requires an API key): -const response = await fetch( - `https://cloud-api.near.ai/v1/attestation/report?model=${MODEL_NAME}&signing_algo=ecdsa&nonce=${nonce}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, - }, - } -); - -// Or via direct completions: -// const response = await fetch( -// `https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce=${nonce}&include_tls_fingerprint=true`, -// { headers: { 'accept': 'application/json' } } -// ); -``` - - - +NONCE="$(openssl rand -hex 32)" -```python -import os -import requests -import secrets - -MODEL_NAME = 'zai-org/GLM-5.1-FP8' -# Generate a random 64-character hex nonce (optional but recommended) -nonce = secrets.token_hex(32) - -# Via gateway (requires an API key): -response = requests.get( - f'https://cloud-api.near.ai/v1/attestation/report?model={MODEL_NAME}&signing_algo=ecdsa&nonce={nonce}', - headers={ - 'accept': 'application/json', - 'Authorization': f'Bearer {os.environ["NEAR_AI_CLOUD_API_KEY"]}', - } -) - -# Or via direct completions: -# response = requests.get( -# f'https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ecdsa&nonce={nonce}&include_tls_fingerprint=true', -# headers={'accept': 'application/json'} -# ) +curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' ``` - + - - ```json - { - "model_attestations": [ \\ List of all GPU nodes in the network - { - "signing_address": "...", \\ TEE Public Key - "nvidia_payload": "...", \\ Attestation report used to verify w/ NVIDIA - "intel_quote": "..." \\ Attestation report used to verify w/ Intel - } - ] - } - ``` +Use the same `signing_algo` as the signature you plan to verify. Supported values are `ecdsa` and `ed25519`. If you need to bind the report to the HTTPS endpoint, add `include_tls_fingerprint=true` and follow the [TLS attestation guide](/cloud/verification/tls). - - `model_attestations`: List attestations from all GPU nodes as multiple TEE nodes may be used to serve inference requests. - - - `signing_address`: Account address generated inside TEE that will be used to sign the chat response. You can utilize the `signing_address` from `model_attestations` to select the appropriate TEE node for verifying its integrity. - - - `nvidia_payload` and `intel_quote`: Attestation report formatted for NVIDIA TEE and Intel TEE respectively. You can use them to verify the integrity of the TEE. See [Verifying Model Attestation](#verifying-model-attestation) for more details. - +The Cloud API request accepts a `model` parameter. A direct endpoint already identifies the model through its hostname. If you are verifying a response signature and the endpoint supports it, include its `signing_address` and `signing_algo` to request evidence for that signer. - **Implementation**: This endpoint is defined in the [NEAR AI Private ML SDK](https://github.com/nearai/private-ml-sdk/blob/a23fa797dfd7e676fba08cba68471b51ac9a13d9/vllm-proxy/src/app/api/v1/openai.py#L170). + A report can contain evidence from more than one serving instance. Do not assume a fixed number of entries. Select the entry that matches the signer and algorithm of the response you are verifying. -## Verifying Model Attestation - -Once you have [requested a model attestation](#request-model-attestation) from NEAR AI Cloud, you can use the returned payload to verify its authenticity. You can verify: - -- **GPU attestation**: Submit GPU evidence payload to NVIDIA NRAS and verify the nonce matches -- **Intel TDX quote**: Verifies TDX quote with [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library -- **TDX report data**: Validates that report data binds the signing key (ECDSA or Ed25519) and nonce -- **Compose manifest**: Displays Docker compose manifest and verifies it matches the mr_config measurement - -### Verify GPU Attestation - -NVIDIA offers a [Remote Attestation Service](https://docs.nvidia.com/attestation/cloud-services/latest/nras/nras_introduction.html) that allows you to verify that you are using a trusted environment with one of their GPUs. To verify: - -1. Submit the GPU evidence payload (`nvidia_payload`) to NVIDIA NRAS -2. Verify the nonce in the GPU payload matches the request nonce -3. Validate the NVIDIA attestation verdict is PASS - -The `nvidia_payload` contains: -- `nonce` - The nonce from your attestation request -- `arch` - Architecture of the GPU _(HOPPER or BLACKWELL)_ -- `evidence_list` - A list of GPU evidence items, each containing an evidence and a corresponding certificate - -The `evidence_list` contains Base64 encoded data that lists the GPU's: - -- Hardware Identity -- Firmware & Software measurements -- Security configuration state -- Endorsement certificates (Signed measurements from the GPU's unique key) - -The private key of this GPU is how we can securely verify the authenticity. NVIDIA burns this unique private key into each GPU during the manufacturing process and only retains the corresponding public key, which is used to verify the signature of attestations provided to them. - -All of this data is provided to you from the [Model Attestation response](#request-model-attestation) as `nvidia_payload`. - -Simply use this JSON Object with your API call: - -```bash -curl -X POST https://nras.attestation.nvidia.com/v3/attest/gpu \ - -H "accept: application/json" \ - -H "content-type: application/json" \ - -d "" -``` - -See official documentation: https://docs.api.nvidia.com/attestation/reference/attestmultigpu_1 - - - ```json - [ - [ - "JWT", - "eyJraWQiOiJudi1lYXQta2lkLXByb2QtMjAyNTA4MjQxNzI2MzczMzEtMGM4YzM2MzQtY2ZkMC00YmViLWFmNWYtMTE2MzliOWUxOTIyIiwiYWxnIjoiRVMzODQifQ.eyJzdWIiOiJOVklESUEtUExBVEZPUk0tQVRURVNUQVRJT04iLCJ4LW52aWRpYS12ZXIiOiIyLjAiLCJuYmYiOjE3NTYxNjg5MjYsImlzcyI6Imh0dHBzOi8vbnJhcy5hdHRlc3RhdGlvbi5udmlkaWEuY29tIiwieC1udmlkaWEtb3ZlcmFsbC1hdHQtcmVzdWx0Ijp0cnVlLCJzdWJtb2RzIjp7IkdQVS0wIjpbIkRJR0VTVCIsWyJTSEEtMjU2IiwiMDJmYzJmMTg3M2JkZjg5Y2VlNGYzZTQzYzU3ZTE3YzI0ODUxODcwMmQ4ZGZjMzcwNmE3YjdmZTgwMzZlOTNkMCJdXX0sImVhdF9ub25jZSI6IjRkNmUwYzQ5MzIxZDIyZGFhOWJkN2ZjMjIwNWUzODFmOTUwNmMyMGU3N2RkNTA4MmVjZjVlMTI0ZWMwZjQ2MTgiLCJleHAiOjE3NTYxNzI1MjYsImlhdCI6MTc1NjE2ODkyNiwianRpIjoiYzFhM2NkYzktZWUyMi00MmFkLTljZDEtNDRhMTE5OWYyZGVlIn0.199S4bah6SVZpy4lpBvRBc975tmf25gkf_mLDwR9-fwrc_kWYePNxGygTRQUzGbRdbrZOQHXWP0eALUPkJvmwGIV_MVfHRIKaBIRdr1e2_7jEP1-mqkbCmbefimiZN8t" - ], - { - "GPU-0": "eyJraWQiOiJudi1lYXQta2lkLXByb2QtMjAyNTA4MjQxNzI2MzczMzEtMGM4YzM2MzQtY2ZkMC00YmViLWFmNWYtMTE2MzliOWUxOTIyIiwiYWxnIjoiRVMzODQifQ.eyJ4LW52aWRpYS1ncHUtZHJpdmVyLXJpbS1zY2hlbWEtdmFsaWRhdGVkIjp0cnVlLCJpc3MiOiJodHRwczovL25yYXMuYXR0ZXN0YXRpb24ubnZpZGlhLmNvbSIsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtY2VydC1jaGFpbi12YWxpZGF0ZWQiOnRydWUsImVhdF9ub25jZSI6IjRkNmUwYzQ5MzIxZDIyZGFhOWJkN2ZjMjIwNWUzODFmOTUwNmMyMGU3N2RkNTA4MmVjZjVlMTI0ZWMwZjQ2MTgiLCJ4LW52aWRpYS1ncHUtdmJpb3MtcmltLXNpZ25hdHVyZS12ZXJpZmllZCI6dHJ1ZSwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1mZXRjaGVkIjp0cnVlLCJleHAiOjE3NTYxNzI1MjYsImlhdCI6MTc1NjE2ODkyNiwidWVpZCI6IjY0Mjk2MDE4OTI5ODAwNzUxMTI1MDk1ODAzMDUwMDc0OTE1MjczMDIyMTE0MjQ2OCIsImp0aSI6IjFhMzhjMTAzLWMyODAtNDQyMi1hZDc1LTRkMTA3OTkyMGI2MyIsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtbm9uY2UtbWF0Y2giOnRydWUsIngtbnZpZGlhLWdwdS12Ymlvcy1pbmRleC1uby1jb25mbGljdCI6dHJ1ZSwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1jZXJ0LXZhbGlkYXRlZCI6dHJ1ZSwic2VjYm9vdCI6dHJ1ZSwieC1udmlkaWEtZ3B1LWF0dGVzdGF0aW9uLXJlcG9ydC1wYXJzZWQiOnRydWUsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLXNpZ25hdHVyZS12ZXJpZmllZCI6dHJ1ZSwieC1udmlkaWEtZ3B1LWFyY2gtY2hlY2siOnRydWUsIngtbnZpZGlhLWF0dGVzdGF0aW9uLXdhcm5pbmciOm51bGwsIm5iZiI6MTc1NjE2ODkyNiwieC1udmlkaWEtZ3B1LWRyaXZlci12ZXJzaW9uIjoiNTcwLjEzMy4yMCIsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLW1lYXN1cmVtZW50cy1hdmFpbGFibGUiOnRydWUsIngtbnZpZGlhLWdwdS1hdHRlc3RhdGlvbi1yZXBvcnQtc2lnbmF0dXJlLXZlcmlmaWVkIjp0cnVlLCJod21vZGVsIjoiR0gxMDAgQTAxIEdTUCBCUk9NIiwiZGJnc3RhdCI6ImRpc2FibGVkIiwieC1udmlkaWEtZ3B1LWRyaXZlci1yaW0tZmV0Y2hlZCI6dHJ1ZSwib2VtaWQiOiI1NzAzIiwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1zY2hlbWEtdmFsaWRhdGVkIjp0cnVlLCJtZWFzcmVzIjoic3VjY2VzcyIsIngtbnZpZGlhLWdwdS1kcml2ZXItcmltLWNlcnQtdmFsaWRhdGVkIjp0cnVlLCJ4LW52aWRpYS1ncHUtdmJpb3MtdmVyc2lvbiI6Ijk2LjAwLkNGLjAwLjAyIiwieC1udmlkaWEtZ3B1LXZiaW9zLXJpbS1tZWFzdXJlbWVudHMtYXZhaWxhYmxlIjp0cnVlfQ.Zjac1Al0OsYbrXu7lOKDAH7lLNnRU_G2R1UJBnUpvZKL1EE8mjPyy-4sqRvE_d8uZJ4GuhXoy_EonyuUIXESd3sxjY0Eohe9Rtlzatj14iLOdcVrF_eOq12ZHNIYs4Go" - } - ] - ``` - - - - NVIDIA's attestation verification response returns a "Entity Attestation Token" (EAT) encoded as a JSON Web Token (JWT) - - To decode these values, you can use an online tool such as [jwt.io](https://www.jwt.io) or a library such as [Jose](https://www.npmjs.com/package/jose). - - ```json +## Verify the report - "JWT": - { - "sub": "NVIDIA-PLATFORM-ATTESTATION", - "x-nvidia-ver": "2.0", - "nbf": 1756168926, - "iss": "https://nras.attestation.nvidia.com", - "x-nvidia-overall-att-result": true, - "submods": { - "GPU-0": [ - "DIGEST", - [ - "SHA-256", - "02fc2f1873bdf89cee4f3e43c57e17c248518702d8dfc3706a7b7fe8036e93d0" - ] - ] - }, - "eat_nonce": "4d6e0c49321d22daa9bd7fc2205e381f9506c20e77dd5082ecf5e124ec0f4618", - "exp": 1756172526, - "iat": 1756168926, - "jti": "c1a3cdc9-ee22-42ad-9cd1-44a1199f2dee" - } +Verify the Intel TDX quote before trusting fields copied into the HTTP response. Your verifier should read the report data and measurements from the verified quote. - "GPU-0": - { - "x-nvidia-gpu-driver-rim-schema-validated": true, - "iss": "https://nras.attestation.nvidia.com", - "x-nvidia-gpu-attestation-report-cert-chain-validated": true, - "eat_nonce": "4d6e0c49321d22daa9bd7fc2205e381f9506c20e77dd5082ecf5e124ec0f4618", - "x-nvidia-gpu-vbios-rim-signature-verified": true, - "x-nvidia-gpu-vbios-rim-fetched": true, - "exp": 1756172526, - "iat": 1756168926, - "ueid": "642960189298007511250958030500749152730221142468", - "jti": "1a38c103-c280-4422-ad75-4d1079920b63", - "x-nvidia-gpu-attestation-report-nonce-match": true, - "x-nvidia-gpu-vbios-index-no-conflict": true, - "x-nvidia-gpu-vbios-rim-cert-validated": true, - "secboot": true, - "x-nvidia-gpu-attestation-report-parsed": true, - "x-nvidia-gpu-driver-rim-signature-verified": true, - "x-nvidia-gpu-arch-check": true, - "x-nvidia-attestation-warning": null, - "nbf": 1756168926, - "x-nvidia-gpu-driver-version": "570.133.20", - "x-nvidia-gpu-driver-rim-measurements-available": true, - "x-nvidia-gpu-attestation-report-signature-verified": true, - "hwmodel": "GH100 A01 GSP BROM", - "dbgstat": "disabled", - "x-nvidia-gpu-driver-rim-fetched": true, - "oemid": "5703", - "x-nvidia-gpu-vbios-rim-schema-validated": true, - "measres": "success", - "x-nvidia-gpu-driver-rim-cert-validated": true, - "x-nvidia-gpu-vbios-version": "96.00.CF.00.02", - "x-nvidia-gpu-vbios-rim-measurements-available": true - } +For each applicable model-attestation entry, check the following: - ``` - - -### Verify TDX Quote +1. Verify the Intel TDX quote using an Intel DCAP verifier and apply your policy for its result and advisories. +2. Verify that the quote's report data contains the nonce your client generated and binds the reported signing identity. +3. Match `signing_address` and `signing_algo` to the identity that signed the response, when you are verifying a response. +4. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +5. Check the measured configuration against the policy your application accepts. If you require source and build provenance, continue with [image provenance verification](/cloud/verification/provenance). -You can verify the Intel TDX quote with the value of `intel_quote` using the [`dcap-qvl`](https://github.com/Phala-Network/dcap-qvl) library. This verifies: +The response's `request_nonce` and `report_data` fields are useful for diagnostics, but the verified quote is the cryptographic source of truth. -- The CPU TEE measurements are valid -- The quote is authentic and signed by Intel -- The TEE environment is genuine +## Signing identities -Alternatively, you can verify the Intel TDX quote at [TEE Attestation Explorer](https://proof.t16z.com/). +`signing_address` is an algorithm-specific identity, not always a public key in the same format: -### Verify TDX Report Data +| `signing_algo` | `signing_address` | +| --- | --- | +| `ecdsa` | An Ethereum-style address derived from the signing key. | +| `ed25519` | The Ed25519 public-key bytes, encoded as hexadecimal. | -The TDX report data validates that: -- The report data binds the signing address (ECDSA or Ed25519) -- The report data embeds the request nonce +Always compare both the identity and `signing_algo`. Do not compare identities across algorithms. -This ensures cryptographic binding between the signing address and the hardware, and prevents replay attacks through nonce freshness. +## Handling rotations and load balancing -### Verify Compose Manifest +An endpoint may be served by several instances. A report fetched before or after a completion can therefore describe a different signer. -The attestation response includes Docker compose manifest information in the `info` field. To verify: +- Fetch a new report when the signer, certificate, or measured configuration changes. +- For a signed response, request or select the attestation entry that matches its signer. +- Record the report time and enforce the maximum attestation age your policy permits. +- When a required check cannot be completed, treat that result as unverified rather than substituting evidence from a different instance. -1. Extract the Docker compose manifest from the attestation -2. Calculate the SHA-256 hash of the compose manifest -3. Compare it with the `mr_config` measurement from the verified TDX quote -4. Verify they match, proving the exact container configuration +## Reference implementation -This ensures the exact Docker compose file is deployed to the TEE environment. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) provides implementation examples for quote, GPU, TLS, signature, and image-digest inspection. Integrate the checks and acceptance policy appropriate for your application. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx new file mode 100644 index 0000000..e303e0a --- /dev/null +++ b/cloud/verification/provenance.mdx @@ -0,0 +1,50 @@ +--- +title: "Image Provenance" +description: "Verify source and build provenance for images named in an attested deployment." +--- + +An attestation can establish that a measured deployment configuration was running. Image provenance answers a separate question: whether each image in that configuration was built from the source and build process your policy accepts. + +Use both checks when your application requires a source-to-runtime trust chain. + +## Verification flow + +1. Verify the Intel TDX quote. Obtain the compose configuration text from the report, calculate its hash, and compare it with the configuration measurement in the verified quote. Only then treat that text as the attested configuration. +2. Extract every container image pinned by immutable digest (`@sha256:…`) from the matched configuration. +3. For each digest, retrieve provenance evidence from the relevant build-provenance system. +4. Cryptographically verify the provenance statement and confirm that its subject digest exactly matches the image digest in the attested configuration. +5. Apply your policy to the verified statement, such as the expected repository, build identity, workflow, ref, and source revision. +6. Accept the deployment only when every image required by your policy passes the checks. + + + An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. + + +## Define a provenance policy + +Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: + +- the expected source repository or repositories; +- the accepted build identity and workflow; +- the allowed source branch, tag, or commit policy; and +- whether provenance is required for every image or only selected images. + +Pin every evaluated image by digest. If a configuration references an image without an immutable digest, it cannot satisfy a digest-matching provenance check. + +## When a provenance record is missing + +A missing record, including an HTTP `404`, means provenance for the reported image digest could not be verified. It does not by itself identify a security incident. + +Deployments can change. Fetch a fresh attestation report and repeat the verification flow. If the current report still references a digest without the provenance required by your policy, treat that image as unverified. + +When requesting help, include the endpoint, time, report identifier, image name, and full digest. Do not include API keys, prompts, or response content. + +## Relationship to attestation + +| Check | What it establishes | +| --- | --- | +| Quote and configuration measurement | The configuration text you matched is bound to a measurement in the verified quote. | +| Image provenance | A particular image digest satisfies your source and build policy. | +| Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | + +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for inspecting image digests. Use a provenance verifier that validates the cryptographic evidence required by your policy. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 4bb7aea..b8be96a 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -1,463 +1,60 @@ --- -title: "TLS Attestation Verification" -description: "How to verify that your HTTPS connection to NEAR AI terminates inside a Trusted Execution Environment using TLS attestation." +title: "TLS Attestation" +description: "Verify that a specific HTTPS connection terminates at the endpoint bound to an attestation report." --- +TLS attestation links a live HTTPS connection to the signing identity in a verified attestation. It is the check to use when your policy requires evidence that the TLS endpoint for a particular connection is the attested endpoint. -NEAR AI runs inference models inside **Confidential VMs (CVMs)** β€” virtual machines backed by Intel TDX hardware that provide a Trusted Execution Environment (TEE). The key property: even the cloud operator cannot see or tamper with what runs inside the TEE. + + Obtain the peer certificate and the attestation report over the **same TLS connection**. Fetching them over separate connections can reach different instances or observe a rotated certificate, especially for load-balanced endpoints. + -TLS attestation proves that the TLS connection used for your HTTPS requests terminates directly **inside the TEE**, ensuring your messages remain encrypted end-to-end. +## What the check establishes - - Check our [example](https://github.com/nearai/nearai-cloud-verifier/tree/main) on how to perform TLS attestation verification. - -## What TLS Attestation Proves +When you request an attestation with `include_tls_fingerprint=true`, the report includes `tls_cert_fingerprint`: the SHA-256 hash of the certificate's SubjectPublicKeyInfo (SPKI). The verified TDX quote binds that fingerprint, the signing identity, and your nonce. -TLS attestation binds the server's **TLS certificate** to the **TEE hardware attestation**: +After you compare the live peer certificate's SPKI hash with `tls_cert_fingerprint`, the result establishes that the TLS connection you observed is bound to that verified attestation. -1. You request `/v1/attestation/report?include_tls_fingerprint=true`. -2. The TEE computes the certificate's **SPKI hash** (SHA-256 of the SubjectPublicKeyInfo) and binds it into the attested `report_data`. -3. Intel TDX hardware signs this quote β€” it cannot be forged. -4. You verify the quote, then confirm the live TLS certificate matches the attested hash. +Your application must make an explicit certificate-validation decision. TLS attestation becomes a security conclusion only after the quote, binding, and same-connection checks pass; an unverified report is not a reason to relax TLS validation. -This proves the TLS private key is held by the TEE. Your HTTPS traffic is end-to-end encrypted all the way to the hardware enclave. +## Verification flow - - **Opt-In Behavior** +1. Generate a fresh, random 32-byte nonce in your client and encode it as 64 hexadecimal characters. +2. Open a TLS connection using the trust configuration appropriate for your application. +3. From that connection, read the peer certificate and calculate `SHA-256(SPKI_DER)`. +4. Reuse the **same open connection** to request `/v1/attestation/report` with `include_tls_fingerprint=true`, your nonce, and the signing algorithm you require. +5. Verify the Intel TDX quote and verify that its report data binds the nonce, signing identity, and reported TLS fingerprint. +6. Compare the SPKI hash you calculated from the peer certificate with `tls_cert_fingerprint` from the report. +7. Apply the same measurement and provenance policy you use for [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) verification. - `include_tls_fingerprint` is disabled by default so existing attestation clients keep the original `report_data` layout. Without the flag, `report_data[0:32]` contains the signing address bytes (padded or truncated to 32 bytes). With the flag, `report_data[0:32]` becomes `SHA256(signing_address || tls_cert_fingerprint)`. - -## Trust Model +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for TLS-related verification steps. Independently ensure that your client keeps the certificate read and report request on one connection. -You trust: -- **Intel TDX** hardware (CPU attestation) -- **NVIDIA GPU** hardware (GPU attestation) -- **The code** running inside the TEE (verified via compose file hash) +## Request parameters -You do **not** need to trust the cloud operator, network infrastructure, or certificate authorities. +Use the normal attestation endpoint for your request path, adding `include_tls_fingerprint=true`: -```mermaid -flowchart TD - A["Intel TDX Hardware"] --> Q["TDX Quote
(signed by CPU)"] - B["NVIDIA GPU Hardware"] --> G["GPU Attestation
(NVIDIA NRAS)"] - Q --> C["mr_config_id
↓
Docker Compose file
↓
Container images"] - Q --> D["report_data[0:32]
↓
SHA256(signing_address || tls_cert_fingerprint)"] - Q --> E["report_data[32:64]
↓
nonce (freshness)"] - D --> F["Live TLS cert match
↓
Your HTTPS connection"] +```text +GET /v1/attestation/report?signing_algo=ecdsa&nonce=&include_tls_fingerprint=true ``` ---- - -## Prerequisites - -Install the required Python packages: - -```bash -pip install dcap-qvl cryptography requests pyyaml -``` - ---- -## Verifications - -### Discover Available Endpoints - -The `completions.near.ai/endpoints` API lists all available model endpoints and their domains. - -```python -import http.client -import json - -conn = http.client.HTTPSConnection("completions.near.ai") -conn.request("GET", "/endpoints") -resp = conn.getresponse() -endpoints_data = json.loads(resp.read()) -conn.close() - -# Build a lookup: domain -> models -domain_models = {} -for entry in endpoints_data.get("endpoints", []): - domain = entry["domain"] - domain_models[domain] = entry.get("models", []) - print(f"{domain:<45} {', '.join(domain_models[domain])}") - -# Pick an endpoint to verify -TARGET_DOMAIN = "glm-5-1.completions.near.ai" -print(f"\nVerifying: {TARGET_DOMAIN}") -``` - ---- - -### Fetch Attestation + Live SPKI Hash - -Multiple backend CVMs may serve the same domain via load balancing. Making two separate connections β€” one for attestation, one to check the certificate β€” could hit different backends and produce a false SPKI mismatch. - -The solution: extract the live certificate **and** make the attestation request over the **same TLS connection**. - - - CA verification is intentionally skipped. The TEE generates its own TLS key pair β€” it is not CA-signed. Trust comes from the TEE hardware attestation, not from Certificate Authorities. - -```python -import ssl -import secrets -from hashlib import sha256 -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization - -def compute_spki_hash(cert_der: bytes) -> str: - """Compute SHA-256 of a certificate's SubjectPublicKeyInfo DER encoding. - - Hashing only the public key info (not the full certificate) keeps the hash - stable across certificate renewals that reuse the same key. - """ - cert = x509.load_der_x509_certificate(cert_der, default_backend()) - spki_der = cert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - return sha256(spki_der).hexdigest() - -def fetch_attestation_and_spki(hostname, port=443, signing_algo="ecdsa"): - """Fetch attestation report and live TLS cert SPKI hash over one connection.""" - nonce = secrets.token_hex(32) - - # Skip CA verification β€” trust comes from the TEE hardware attestation - context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - - conn = http.client.HTTPSConnection(hostname, port, context=context, timeout=60) - conn.connect() - - # Extract the live certificate from this TLS session - cert_der = conn.sock.getpeercert(binary_form=True) - if not cert_der: - conn.close() - raise Exception("Failed to get certificate from server") - live_spki_hash = compute_spki_hash(cert_der) - - # Make the attestation request over the same connection - path = ( - f"/v1/attestation/report" - f"?include_tls_fingerprint=true&nonce={nonce}&signing_algo={signing_algo}" - ) - conn.request("GET", path, headers={"Host": hostname}) - resp = conn.getresponse() - body = resp.read() - conn.close() - - if resp.status != 200: - raise Exception(f"HTTP {resp.status}: {body.decode()}") - - attestation = json.loads(body) - return attestation, live_spki_hash, nonce - -attestation, live_spki_hash, request_nonce = fetch_attestation_and_spki(TARGET_DOMAIN) - -print(f"Request nonce: {request_nonce}") -print(f"Live SPKI hash: {live_spki_hash}") -print(f"TLS fingerprint: {attestation.get('tls_cert_fingerprint')}") -print(f"Model name: {attestation.get('model_name')}") -print(f"Signing address: {attestation['signing_address']}") -print(f"Intel quote length: {len(attestation.get('intel_quote', ''))} hex chars") -print(f"NVIDIA payload: {bool(attestation.get('nvidia_payload'))}") -``` - -Query parameters used in the attestation request: - -| Parameter | Description | -|-----------|-------------| -| `include_tls_fingerprint=true` | Tells the TEE to include its TLS certificate's SPKI hash in the report data | -| `nonce` | A random 32-byte hex string you generate; included in the report to prove freshness | -| `signing_algo` | `ecdsa` (secp256k1) or `ed25519` β€” which signing key to bind | - ---- - -### Verify the Intel TDX Quote - -The attestation report contains an Intel TDX quote β€” a hardware-signed data structure that proves what code is running and what data the TEE attests to. Verify it using `dcap-qvl`, which validates the quote against Intel's DCAP (Data Center Attestation Primitives) collateral. - -```python -import dcap_qvl - -intel_quote_bytes = bytes.fromhex(attestation["intel_quote"]) -print(f"Quote size: {len(intel_quote_bytes)} bytes") -print("Verifying against Intel DCAP collateral...") - -result = await dcap_qvl.get_collateral_and_verify(intel_quote_bytes) -result_json = json.loads(result.to_json()) - -print(f"Verification status: {result.status}") -print(f"Advisory IDs: {result.advisory_ids}") - -# Extract key fields from the verified quote -td10 = result_json["report"]["TD10"] -report_data_hex = td10["report_data"] -mr_config_id = td10["mr_config_id"] -``` - -| Status | Meaning | -|--------|---------| -| `UpToDate` | Quote is valid, TCB firmware is current | -| `SWHardeningNeeded` | Quote is valid but has advisories β€” check `advisory_ids` | -| Any other value | Verification failed | - -Key fields extracted from the verified quote: - -| Field | What it proves | -|-------|----------------| -| `report_data[0:32]` | `SHA256(signing_address βˆ₯ tls_cert_fingerprint)` β€” binds signing key + TLS cert to this TEE | -| `report_data[32:64]` | The nonce β€” proves the attestation is fresh, not a replay | -| `mr_config_id` | `"01" + SHA256(app_compose)` β€” binds the Docker Compose file to this TEE | - ---- - -### Verify Report Data Binds Signing Key + TLS Cert - -Check that the 64-byte `report_data` in the TDX quote contains the expected values. This cryptographically binds the signing key, TLS certificate, and nonce all to the same hardware attestation. - -```python -report_data = bytes.fromhex(report_data_hex.removeprefix("0x")) -signing_algo = attestation.get("signing_algo", "ecdsa").lower() - -if signing_algo == "ecdsa": - signing_address_bytes = bytes.fromhex(attestation["signing_address"].removeprefix("0x")) -else: - signing_address_bytes = bytes.fromhex(attestation["signing_address"]) - -# report_data[0:32] = SHA256(signing_address || tls_cert_fingerprint) -cert_fp_bytes = bytes.fromhex(attestation["tls_cert_fingerprint"]) -expected_hash = sha256(signing_address_bytes + cert_fp_bytes).digest() - -binds_key_and_tls = report_data[:32] == expected_hash -print(f"Signing key + TLS cert bound to TEE: {binds_key_and_tls}") - -# report_data[32:64] = nonce -embeds_nonce = report_data[32:].hex() == request_nonce -print(f"Nonce matches (fresh attestation): {embeds_nonce}") -``` - ---- - -### Verify Live TLS Certificate Matches Attested Fingerprint +For the Cloud API gateway, include your Bearer API key and, when needed, `model=`. For a direct endpoint, call that endpoint's `/v1/attestation/report` route. A command-line request can retrieve a report, but it does not by itself prove the same-connection property unless your client also reads the peer certificate from that very connection. -Compare the SPKI hash extracted from the live TLS session (Step 2) against the `tls_cert_fingerprint` from the attestation report. A match proves the TLS connection terminates inside the TEE. +## Load balancing and certificate rotation -```python -attested_fingerprint = attestation["tls_cert_fingerprint"] +Do not use a cached fingerprint as evidence for a new connection. It can be useful as an operational hint, but it is not a fresh attestation result. -print(f"Live SPKI hash: {live_spki_hash}") -print(f"Attested fingerprint: {attested_fingerprint}") +Request new evidence when any of the following changes: -tls_match = live_spki_hash == attested_fingerprint -if tls_match: - print("\nThe TLS connection terminates inside the TEE.") - print("Your HTTPS traffic is end-to-end encrypted to the hardware enclave.") -else: - print("\nWARNING: TLS certificate mismatch! The connection may not terminate in the TEE.") -``` - ---- - -### Verify Model Name - -The attestation report's `model_name` field is self-reported by the inference proxy running inside the TEE. Since it originates from within the attested enclave, it tells you which model is actually serving your requests. Compare it against the model listed in `/endpoints` for this domain. - -```python -attested_model = attestation.get("model_name") -expected_models = domain_models.get(TARGET_DOMAIN, []) - -print(f"Attested model (from TEE): {attested_model}") -print(f"Expected models (from /endpoints): {expected_models}") - -if attested_model: - model_match = attested_model in expected_models - print(f"\nModel verified: {model_match}") -else: - print("\nModel name not present β€” older proxy version.") -``` - ---- - -### Verify GPU Attestation - -In addition to CPU-level attestation, the inference GPUs provide hardware attestation via NVIDIA's Remote Attestation Service (NRAS). The GPU evidence is collected inside the TEE and included as `nvidia_payload` in the report. It's bound to the same nonce as the TDX quote, ensuring both attestations are fresh and from the same request. - -```python -import base64 -import requests - -GPU_VERIFIER_API = "https://nras.attestation.nvidia.com/v3/attest/gpu" - -def decode_jwt_payload(jwt_token): - payload_b64 = jwt_token.split(".")[1] - padded = payload_b64 + "=" * ((4 - len(payload_b64) % 4) % 4) - return json.loads(base64.urlsafe_b64decode(padded).decode()) - -nvidia_payload_str = attestation.get("nvidia_payload") -if nvidia_payload_str: - payload = json.loads(nvidia_payload_str) - - gpu_nonce_matches = payload["nonce"].lower() == request_nonce.lower() - print(f"GPU nonce matches request nonce: {gpu_nonce_matches}") - - print("Submitting GPU evidence to NVIDIA NRAS...") - nras_resp = requests.post(GPU_VERIFIER_API, json=payload, timeout=30) - jwt_token = nras_resp.json()[0][1] - verdict = decode_jwt_payload(jwt_token) - overall_result = verdict["x-nvidia-overall-att-result"] - - print(f"NVIDIA attestation verdict: {overall_result}") - if overall_result == "success": - print("GPU hardware attestation verified.") - else: - print(f"WARNING: GPU attestation result is '{overall_result}'") -else: - print("No NVIDIA payload β€” GPU attestation not available for this endpoint.") -``` - ---- - -### Verify Docker Compose File - -The TDX quote's `mr_config_id` encodes `"01" + SHA256(app_compose_json)`, binding the exact Docker Compose configuration β€” including pinned container image digests β€” to the hardware attestation. Verifying this confirms the TEE is running exactly the code defined in that compose file. - -```python -import re -import yaml # pip install pyyaml - -tcb_info = attestation["info"]["tcb_info"] -if isinstance(tcb_info, str): - tcb_info = json.loads(tcb_info) - -app_compose_str = tcb_info.get("app_compose") -if app_compose_str: - app_compose = json.loads(app_compose_str) - docker_compose_yaml = app_compose["docker_compose_file"] - - # List services and their pinned image digests - compose_parsed = yaml.safe_load(docker_compose_yaml) - print("Services in this TEE:") - for svc_name, svc_config in compose_parsed.get("services", {}).items(): - print(f" {svc_name}: {svc_config.get('image', '(no image)')[:80]}") - - # Verify hash matches mr_config_id - compose_hash = sha256(app_compose_str.encode()).hexdigest() - expected_mr_config = "01" + compose_hash - compose_matches = mr_config_id.lower().startswith(expected_mr_config.lower()) - print(f"\nCompose hash matches mr_config_id: {compose_matches}") - if compose_matches: - print("The TEE is running exactly the code defined in this compose file.") - - # Extract pinned image digests for Sigstore checks - digest_pattern = r'@sha256:([0-9a-f]{64})' - unique_digests = list(dict.fromkeys(re.findall(digest_pattern, docker_compose_yaml))) - print(f"\nPinned image digests ({len(unique_digests)}):") - for digest in unique_digests: - print(f" sha256:{digest[:24]}...") -else: - print("No app_compose in tcb_info β€” cannot verify compose file.") -``` - - - `mr_config_id` may be all zeros on some endpoints during TEE configuration transitions. In that case, skip this check β€” the TDX quote verification, report data binding, and TLS fingerprint checks above still provide strong guarantees. - -**Cross-reference with GitHub:** The compose files deployed to CVMs come from the public repository [nearai/cvm-compose-files](https://github.com/nearai/cvm-compose-files). You can fetch the file by its tag and compare it byte-for-byte with what the TEE attests to. - -**Sigstore provenance:** Container images are pinned by `@sha256:` digest. Check `https://search.sigstore.dev/?hash=sha256:` for each image to confirm it was built by a traceable GitHub Actions pipeline, not pushed manually. - ---- - -## Caching Verified Certificates for Production - -Running the full attestation flow is thorough but takes several seconds. For production use, cache the verified SPKI hash and check it on every connection as a lightweight trust anchor. - -### The key insight: SPKI hash as trust anchor - -Once you've verified a particular SPKI hash is bound to a TEE via hardware attestation, you can reuse it: -1. Connect to the endpoint via TLS -2. Extract the live certificate's SPKI hash -3. Compare against your cached, previously-verified hash -4. If it matches β†’ you're talking to the same TEE β€” no full attestation needed - -### Build the cache - -```python -trusted_certs = {} # domain -> {"spki_hash": ..., "model_name": ..., "signing_address": ...} - -for domain in domain_models: - try: - att, spki, nonce = fetch_attestation_and_spki(domain) - tls_fp = att.get("tls_cert_fingerprint") - if tls_fp and spki == tls_fp: - trusted_certs[domain] = { - "spki_hash": spki, - "model_name": att.get("model_name"), - "signing_address": att["signing_address"], - } - print(f"PASS {domain}") - else: - print(f"FAIL {domain}") - except Exception as e: - print(f"ERROR {domain}: {e}") -``` - -### Verify on every connection - -```python -def verified_request(domain, method, path, body=None, headers=None, port=443): - """Make an HTTPS request verified against the cached SPKI hash.""" - if domain not in trusted_certs: - raise Exception(f"No cached SPKI for {domain}. Run full attestation first.") - - cached_spki = trusted_certs[domain]["spki_hash"] - - context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - - conn = http.client.HTTPSConnection(domain, port, context=context, timeout=30) - conn.connect() - - live_spki = compute_spki_hash(conn.sock.getpeercert(binary_form=True)) - if live_spki != cached_spki: - conn.close() - raise Exception( - f"SPKI mismatch for {domain}! " - f"Certificate may have rotated β€” re-run full attestation." - ) - - # SPKI matches β€” this connection terminates in the verified TEE - all_headers = {"Host": domain, **(headers or {})} - conn.request(method, path, body=body, headers=all_headers) - resp = conn.getresponse() - data = resp.read() - conn.close() - return resp.status, json.loads(data) -``` - -**When to re-verify:** Re-run full attestation when the SPKI hash changes (certificate rotated with a new key), when the TEE is redeployed, or when your cached entry expires (a 24-hour TTL is a reasonable default). - ---- - -## Verification Summary - -| Check | What it proves | -|-------|----------------| -| **Intel TDX quote** | Attestation comes from genuine Intel TDX hardware | -| **Report data binding** | Signing key + TLS cert are bound to this specific TEE | -| **Live TLS match** | Your HTTPS connection terminates inside the TEE | -| **Model name** | The TEE is running the model you expect | -| **GPU attestation** | NVIDIA GPUs are in verified confidential computing mode | -| **Compose file hash** | `mr_config_id` matches `SHA256(app_compose)` β€” exact code verified | -| **GitHub cross-reference** | Compose file matches the public source at `nearai/cvm-compose-files` | -| **Sigstore provenance** | Container images were built by a traceable CI pipeline | - ---- +- the live certificate's public key; +- the report's signing identity or signing algorithm; +- the measured configuration; or +- your policy's allowed attestation age. -## Resources +## Failure handling -- [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) β€” Reference client that performs TLS certificate and attestation verification -- [Private Inference](/cloud/private-inference) β€” How TEE isolation protects your data -- [E2EE Chat Completions](/cloud/guides/e2ee-chat-completions) β€” Defense-in-depth encryption on top of TLS -- [Verification](/cloud/verification) β€” Verify chat signatures and model attestation +| Result | Required action when TLS endpoint verification is required | +| --- | --- | +| No `tls_cert_fingerprint` | Treat the TLS binding as unavailable; request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs from the report | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signing-identity check fails | Reject the report and fetch fresh evidence. | +| Certificate read and report fetch used different connections | Treat the result as inconclusive, not verified. | diff --git a/docs.json b/docs.json index 3efe7e9..e0b79c9 100644 --- a/docs.json +++ b/docs.json @@ -62,7 +62,8 @@ "cloud/verification/model", "cloud/verification/gateway", "cloud/verification/tls", - "cloud/verification/chat" + "cloud/verification/chat", + "cloud/verification/provenance" ] } ] From 6615761b0c0023b1e0f5915b4f5febf3bae1d886 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 15:10:17 +0800 Subject: [PATCH 02/23] Clarify Compose Manager provenance checks --- cloud/verification/provenance.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index e303e0a..c52bf6e 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -20,6 +20,12 @@ Use both checks when your application requires a source-to-runtime trust chain. An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. +## Compose Manager images + +When the attested configuration contains platform images such as `compose-manager` or `compose-manager-launcher`, evaluate their exact digests the same way as every other image in the configuration. First bind the configuration to the verified quote, then verify build provenance for each digest under your policy. + +An integrity check for a reported action list is a separate check. It does not establish the provenance of the image that ran the action, and it does not replace quote or image-provenance verification. + ## Define a provenance policy Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: @@ -47,4 +53,4 @@ When requesting help, include the endpoint, time, report identifier, image name, | Image provenance | A particular image digest satisfies your source and build policy. | | Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for inspecting image digests. Use a provenance verifier that validates the cryptographic evidence required by your policy. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for locating selected image digests in a configuration. Use a provenance verifier that validates the cryptographic evidence required by your policy. From f198efbfb2cdc8c23c5f12a707c07bb155e5c4bd Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 17:49:00 +0800 Subject: [PATCH 03/23] docs: split Cloud API and direct verification flows --- cloud/guides/e2ee-chat-completions.mdx | 11 +- cloud/private-inference.mdx | 22 ++-- cloud/verification/chat.mdx | 110 ++---------------- .../cloud-api/gateway-attestation.mdx | 47 ++++++++ .../cloud-api/image-provenance.mdx | 34 ++++++ cloud/verification/cloud-api/index.mdx | 71 +++++++++++ .../cloud-api/model-attestations.mdx | 48 ++++++++ .../cloud-api/response-signatures.mdx | 47 ++++++++ cloud/verification/cloud-api/tls.mdx | 38 ++++++ .../direct/deployment-control-evidence.mdx | 29 +++++ .../verification/direct/image-provenance.mdx | 28 +++++ cloud/verification/direct/index.mdx | 52 +++++++++ .../verification/direct/model-attestation.mdx | 52 +++++++++ .../direct/response-signatures.mdx | 46 ++++++++ cloud/verification/direct/tls.mdx | 38 ++++++ cloud/verification/gateway.mdx | 54 +-------- cloud/verification/index.mdx | 91 +++++---------- cloud/verification/model.mdx | 95 ++------------- cloud/verification/provenance.mdx | 63 ++-------- .../reference/quote-nonce-signer.mdx | 47 ++++++++ .../reference/verification-policy.mdx | 42 +++++++ cloud/verification/tls.mdx | 67 ++--------- docs.json | 34 +++++- 23 files changed, 743 insertions(+), 423 deletions(-) create mode 100644 cloud/verification/cloud-api/gateway-attestation.mdx create mode 100644 cloud/verification/cloud-api/image-provenance.mdx create mode 100644 cloud/verification/cloud-api/index.mdx create mode 100644 cloud/verification/cloud-api/model-attestations.mdx create mode 100644 cloud/verification/cloud-api/response-signatures.mdx create mode 100644 cloud/verification/cloud-api/tls.mdx create mode 100644 cloud/verification/direct/deployment-control-evidence.mdx create mode 100644 cloud/verification/direct/image-provenance.mdx create mode 100644 cloud/verification/direct/index.mdx create mode 100644 cloud/verification/direct/model-attestation.mdx create mode 100644 cloud/verification/direct/response-signatures.mdx create mode 100644 cloud/verification/direct/tls.mdx create mode 100644 cloud/verification/reference/quote-nonce-signer.mdx create mode 100644 cloud/verification/reference/verification-policy.mdx diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index bb668c5..59f0d89 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -222,7 +222,10 @@ response = requests.get( # ) attestation = response.json() +# Cloud API: model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] +# Direct completions use the root model report instead: +# model_public_key_hex = attestation["signing_public_key"] print(f"Model Ed25519 public key: {model_public_key_hex}") ``` @@ -251,7 +254,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -The response contains `model_attestations` with the model's `signing_public_key` in hex format. +For Cloud API, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. --- @@ -691,7 +694,7 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Verification -You can verify the model's public key is authentic by checking the attestation report against hardware attestation. See [Model Verification](/cloud/verification/model) for details. +You can verify the model's public key is authentic by checking the attestation report against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -810,5 +813,5 @@ You can verify the model's public key is authentic by checking the attestation r ## See Also - [Private Inference](/cloud/private-inference) β€” How TEE isolation protects your data -- [Model Verification](/cloud/verification/model) β€” Verify the model's TEE attestation -- [Chat Verification](/cloud/verification/chat) β€” Verify chat signatures +- [Verification](/cloud/verification) β€” Select Cloud API or direct model attestation verification +- [Verification Policy](/cloud/verification/reference/verification-policy) β€” Define the evidence your application requires diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index c215ee1..b752a17 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -54,7 +54,7 @@ NEAR AI Cloud combines Intel TDX and NVIDIA TEE technologies to create isolated, If you're using a standard OpenAI SDK or curl, your prompts are automatically protected by TLS encryptionβ€”no additional setup required. -NEAR AI Cloud supports two connection modes. Use [TLS attestation](/cloud/verification/tls) when your application requires evidence for the TLS endpoint of a particular connection: +NEAR AI Cloud supports two connection modes. Each has its own TLS-attestation flow when your application requires evidence for the TLS endpoint of a particular connection: **Gateway mode** routes through `cloud-api.near.ai`, which runs in its own TEE before forwarding to the model TEE: @@ -77,7 +77,7 @@ flowchart LR style C fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff ``` -**Key insight:** TLS protects traffic in transit. When the [TLS attestation flow](/cloud/verification/tls) passes for a particular connection, it binds that connection's endpoint to verified attestation evidence. +**Key insight:** TLS protects traffic in transit. When the route-specific TLS attestation flow passes for a particular connection, it binds that connection's endpoint to verified attestation evidence. Here's why this works: @@ -138,8 +138,8 @@ Gateway and direct completions responses include an `X-Request-Id` response head **Benefits of direct completions:** - **Fewer hops** β€” Your request reaches the model TEE directly, reducing latency -- **Simpler trust model** β€” Only the model TEE needs to be verified (no [gateway verification](/cloud/verification/gateway) required) -- **TLS binds to attestation** β€” The `include_tls_fingerprint=true` parameter on the attestation endpoint binds the TLS certificate to the attestation report +- **Simpler trust model** β€” Only the model TEE needs to be verified (no [Cloud API gateway attestation](/cloud/verification/cloud-api/gateway-attestation) required) +- **TLS binds to attestation** β€” The `include_tls_fingerprint=true` parameter can bind the direct endpoint TLS certificate to its attestation report when the [direct TLS flow](/cloud/verification/direct/tls) is completed ### The Inference Process @@ -148,7 +148,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Gateway (`cloud-api.near.ai`):** 1. **Request Initiation:** - You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [TLS attestation](/cloud/verification/tls) when your application needs to verify the endpoint for a particular connection. + You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls) when your application needs to verify the endpoint for a particular connection. 2. **Secure Request Routing:** The LLM Gateway routes your request to the appropriate Private LLM Node based on the requested model, availability, and load balancing requirements. @@ -168,7 +168,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [TLS attestation](/cloud/verification/tls) when endpoint verification is required. + You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [direct TLS connection binding](/cloud/verification/direct/tls) when endpoint verification is required. 2. **Secure Inference:** The model processes your request entirely within its TEE, where all data and model weights are protected by hardware-enforced isolation. @@ -177,7 +177,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel Attestation reports provide environment evidence; supported flows can make response signatures available for integrity verification. 4. **Verifiable Response:** - Use [model verification](/cloud/verification/model) for the direct endpoint. If response verification is required, also check that a signature is available and valid. + Use [direct model attestation](/cloud/verification/direct/model-attestation) for the direct endpoint. If response verification is required, also check that a signature is available and valid. --- @@ -215,13 +215,13 @@ NEAR AI Cloud's private inference implements multiple layers of security to prot TEEs create isolated execution environments enforced at the hardware level, preventing unauthorized access to memory and computation even from privileged system administrators or cloud providers. - **Secure Communication** : -All communication between your applications and the LLM infrastructure uses TLS encryption. Use [TLS attestation](/cloud/verification/tls) to verify endpoint binding for a particular connection when that property is required. +All communication between your applications and the LLM infrastructure uses TLS encryption. Use the route-specific [verification guide](/cloud/verification) to verify endpoint binding for a particular connection when that property is required. - **Cryptographic Attestation** : Every TEE environment generates cryptographic proofs that verify the integrity of the execution environment, allowing you to independently confirm your computations occurred in a genuine, unmodified TEE. - **Result Authentication** : -All AI outputs are cryptographically signed inside the TEE before leaving the secure environment, ensuring the authenticity and integrity of responses. +When a response signature is available, it can be verified against the attested signer to check response integrity. ### Threat Protection @@ -247,8 +247,8 @@ Cryptographic signatures generated inside the TEE ensure that responses cannot b Understand how to verify and validate secure interactions with AI models - - Verify TLS endpoint binding for a particular HTTPS connection using hardware-backed attestation + + Verify gateway, model, TLS, and response evidence for Cloud API requests Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 38fb96f..621a2e1 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -1,103 +1,15 @@ --- -title: "Chat Verification" -description: "Verify an available chat-completion signature against the exact request and response bytes." +title: "Choose a Response-Signature Flow" +description: "Select the response-signature guide that matches the endpoint your application calls." --- -Chat verification checks the integrity of a response for which a signature is available. It combines three pieces of evidence: +The signature payload and the attestation to match depend on the request path. -1. SHA-256 hashes of the exact request and response bytes; -2. a valid signature over the expected payload; and -3. a matching [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) attestation for the signer. - - - Not every response has an available signature. An unavailable signature is not a successful verification result. If response verification is required by your policy, treat that response as unverified. - - -## Preserve the exact bytes - -Build the request body once and retain the exact UTF-8 bytes that your HTTP client sends. Read and retain the raw response body before parsing it as JSON or Server-Sent Events. - -Calculate: - -```text -request_hash = SHA-256(exact request-body bytes) -response_hash = SHA-256(exact response-body bytes) -``` - -Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct the response from parsed events. Any of those changes can produce a different hash. - -For a streaming response, capture all bytes delivered by the HTTP response body, including framing such as the final SSE delimiter when present. - -## Fetch the signature - -After completion, use the completion ID as `chat_id` and request a signature with the algorithm you want to verify. - - - - -```bash -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```bash -curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -The returned object contains: - -| Field | Meaning | -| --- | --- | -| `text` | The payload that was signed. | -| `signature` | The cryptographic signature over `text`. | -| `signing_address` | The algorithm-specific identity that made the signature. | -| `signing_algo` | The algorithm required to verify the signature. | -| `signature_kind` | On a gateway response, identifies whether the model-serving signer or gateway signer produced the signature. | - -## Check the signed payload - -First calculate your `request_hash` and `response_hash`. Then construct the expected payload from `signature_kind`: - -| `signature_kind` | Expected `text` | Match signer to | -| --- | --- | --- | -| `provider_tee` | `::` | A model attestation with the same `signing_address` and `signing_algo`. | -| `gateway` | `:` | A gateway attestation with the same `signing_address` and `signing_algo`. | - -For a direct completion signature, use the model-attestation path. For a gateway response whose `signature_kind` is absent or unfamiliar, do not infer its classification from the shape of `text`; treat the signer classification as unavailable unless your endpoint contract supplies it. - -Reject the signature if `text` does not exactly equal the expected payload. Then verify `signature` locally with a cryptographic library that supports the reported `signing_algo`. - - - `ecdsa` signatures use an Ethereum-style signing address. `ed25519` signatures use an Ed25519 public key. Use the verification method appropriate to the reported algorithm; do not send prompts, responses, or signatures to a third-party verification website. - - -## Match the signature to attestation - -Fetch or select an attestation for the signer reported in the signature. Verify the quote, your client-generated nonce, the signer binding, and the workload policy as described in [model verification](/cloud/verification/model) or [gateway verification](/cloud/verification/gateway). - -Record the response time and attestation time, and enforce the maximum attestation age your policy requires. A matching report establishes the signer binding for that attestation; your policy determines how close in time it must be to the response. - -## Signature availability and retries - -The signature can be unavailable immediately after completion or when the response did not complete. Retry for a short, bounded interval, then make a decision according to your policy. - -| Result | Recommended handling | -| --- | --- | -| Signature returned | Complete the payload, cryptographic-signature, and attestation checks. | -| Temporary lookup failure | Retry briefly with the same completion ID and signing algorithm. | -| No signature after the retry window | Treat the response as unverified if a signature is required. | -| Signature or signer mismatch | Reject the verification result; do not substitute a different response or attestation. | - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for signature-related verification steps. + + + Use `signature_kind` when present to select a gateway or model attestation. + + + Match the signature to the direct model report. + + diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx new file mode 100644 index 0000000..13be109 --- /dev/null +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -0,0 +1,47 @@ +--- +title: "Gateway Attestation" +description: "Verify the Cloud API gateway attestation for requests sent to cloud-api.near.ai." +--- + +`gateway_attestation` is evidence for the Cloud API gateway. It is not evidence for a model environment; use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. + +## Request gateway evidence + +Generate the nonce in your client. It must be 32 random bytes encoded as 64 hexadecimal characters. + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). + +## Verify the gateway report + +For `gateway_attestation`: + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Read `report_data` and measurements from the **verified quote**, not only from fields echoed in the HTTP response. +3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. +4. When a TLS fingerprint was requested, verify its binding as part of the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +5. Apply your accepted measurement and image-provenance policy. + +`request_nonce` and `report_data` in the HTTP body are useful diagnostics. The verified quote is the cryptographic source of truth. + +## Field scope + +| Field path | What it describes | +| --- | --- | +| `gateway_attestation.intel_quote` | The gateway's Intel TDX quote. | +| `gateway_attestation.signing_address` and `.signing_algo` | The gateway signing identity and algorithm. | +| `gateway_attestation.tls_cert_fingerprint` | The gateway TLS binding when requested. | +| `model_attestations[]` | Separate model evidence; do not use it as a substitute for the gateway quote. | + +## Freshness and failure handling + +The nonce establishes freshness only when the client generated and retained it, and the verified quote contains that nonce. If the report is unavailable, the quote fails, the nonce does not match, or the identity does not match, treat the gateway property as unverified. Do not reuse evidence from a different request or instance as a substitute. diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx new file mode 100644 index 0000000..deb2319 --- /dev/null +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -0,0 +1,34 @@ +--- +title: "Image Provenance" +description: "Apply an image-provenance policy to configurations measured by Cloud API gateway and model attestations." +--- + +Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. + +For a Cloud API flow, decide separately whether your policy requires provenance for: + +- the configuration measured by `gateway_attestation`; +- each applicable model report in `model_attestations[]`; or +- both. + +## Verification flow + +1. Verify the relevant Intel TDX quote first. +2. Obtain the configuration text from that report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. +4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. +5. Apply your policy for repository, build identity, workflow, ref, and source revision. + + + A tag, search result, or HTTP `200` only locates a possible record. It does not cryptographically verify the provenance statement, its signer, or its subject digest. + + +## Provider boundaries + +Do not assume every Cloud API model report exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. + +## Missing evidence + +An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. + +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. When requesting help, include the endpoint, UTC time, requested model, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx new file mode 100644 index 0000000..8700478 --- /dev/null +++ b/cloud/verification/cloud-api/index.mdx @@ -0,0 +1,71 @@ +--- +title: "Verify Cloud API" +description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." +--- + +Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the Cloud API gateway and can include evidence for a selected model. + +The relevant verification fields in a Cloud API response are: + +```text +gateway_attestation +model_attestations[] # returned when model evidence is available +tls_certificate # optional +``` + +`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the Cloud API request path; verify a model attestation when you also need evidence for the model environment. + + + `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. + + +## Recommended flow + +1. Generate a fresh 32-byte nonce in the client for each verification attempt. +2. Request a Cloud API attestation report, including `model=` when model evidence is needed. +3. Verify `gateway_attestation` and apply your gateway measurement policy. +4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. +5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. +6. If you need response integrity, verify the response signature and match its signer to the appropriate gateway or model attestation. +7. Apply an image-provenance policy to each measured configuration your policy requires. + + + + Verify the Cloud API gateway quote and signer binding. + + + Verify provider model evidence returned in `model_attestations[]`. + + + Bind the live Cloud API TLS connection to `gateway_attestation`. + + + Match a response signature to its gateway or model signer. + + + Apply provenance policy to measured gateway and model configurations. + + + +## Get a fresh report + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). + +## Provider-specific model evidence + +Cloud API can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. + +This guide describes the Cloud API envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. + +The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx new file mode 100644 index 0000000..11e754a --- /dev/null +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -0,0 +1,48 @@ +--- +title: "Model Attestations" +description: "Verify model evidence returned in the Cloud API model_attestations array." +--- + +Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. + +Cloud API returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the Cloud API envelope and each provider report as separate contracts. + +## Request model evidence + +Include the model ID when requesting the report: + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +The response always has `gateway_attestation`; model evidence, when available, is carried in `model_attestations[]`. + +When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. + +Cloud API also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. + +## Verify the applicable entry + +1. Determine which provider's model evidence your request is using. +2. Verify the Intel TDX quote, nonce, signer binding, GPU evidence, and measurement according to that provider's verification rules. +3. When verifying a response signed by a model-serving TEE, request or select an entry whose `signing_address` **and** `signing_algo` match the response signature. +4. Apply your own allowlist and provenance policy to the verified measurements. + + + Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + + +## Relationship to Cloud API TLS + +The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint binding is verified against `gateway_attestation`, not against a TLS fingerprint carried by an upstream model report. See [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls). + +## Verify a signed response + +For a Cloud API response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [Cloud API response signatures](/cloud/verification/cloud-api/response-signatures). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx new file mode 100644 index 0000000..6540794 --- /dev/null +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -0,0 +1,47 @@ +--- +title: "Response Signatures" +description: "Verify available Cloud API response signatures against the exact bytes and the matching attestation." +--- + +Cloud API response signatures can be produced by either a model-serving TEE or the Cloud API gateway. When present, `signature_kind` determines which attestation must match the signer. + + + A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. + + +## Preserve exact request and response bytes + +Build the request body once and retain the exact bytes sent by the client. Retain the raw response bytes before parsing JSON or Server-Sent Events. + +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) +``` + +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. + +## Fetch and verify the signature + +```bash +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Verify `signature` locally using the returned `signing_algo`, then compare `text` with the expected payload: + +| `signature_kind` | Expected `text` | Required attestation match | +| --- | --- | --- | +| `provider_tee` | `::` | An applicable `model_attestations[]` item with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | + +If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. + +For `provider_tee`, `` is the canonical model identifier in the signed payload. Do not assume it is an alias originally supplied by the client. + +## Match the attestation + +For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. Include `model=` for model evidence. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. + +`ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx new file mode 100644 index 0000000..d80120d --- /dev/null +++ b/cloud/verification/cloud-api/tls.mdx @@ -0,0 +1,38 @@ +--- +title: "TLS Connection Binding" +description: "Bind a live Cloud API TLS connection to the Cloud API gateway attestation." +--- + +Use this flow to establish that the HTTPS connection to `cloud-api.near.ai` terminates at the endpoint bound to `gateway_attestation`. + + + The peer certificate and attestation report must be obtained on the **same TLS connection**. Two independent connections can reach different instances or observe different certificates, so they cannot establish this connection-specific conclusion. + + +## What this checks + +When you request `include_tls_fingerprint=true`, `gateway_attestation.tls_cert_fingerprint` is the SHA-256 hash of the gateway certificate's SubjectPublicKeyInfo (SPKI). The verified quote binds that fingerprint, the gateway signing identity, and the nonce. + +This is a gateway check. It does not bind the client connection to an upstream model endpoint. + +## Verification flow + +1. Generate a fresh 32-byte nonce in the client. +2. Open a TLS connection to `cloud-api.near.ai` using the trust configuration required by your application. +3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header; Cloud API report retrieval requires an API key. +5. Verify `gateway_attestation.intel_quote`, then check the nonce, gateway signer, and TLS-fingerprint binding in the verified quote. +6. Compare the calculated peer SPKI hash with `gateway_attestation.tls_cert_fingerprint`. + +The optional `tls_certificate` response field is not a substitute for the peer certificate observed on the connection. A command-line report request alone also cannot establish the same-connection property. + +## Required handling + +| Result | Action when TLS endpoint binding is required | +| --- | --- | +| `gateway_attestation.tls_cert_fingerprint` is absent | Treat TLS binding as unavailable. Request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signer binding fails | Reject the report and fetch fresh evidence. | +| Certificate and report came from different connections | Treat the result as inconclusive. | + +Do not use a cached fingerprint as evidence for a new connection. Fetch new evidence after a certificate, signer, measurement, or accepted-attestation-age change. diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx new file mode 100644 index 0000000..1c9f82f --- /dev/null +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -0,0 +1,29 @@ +--- +title: "Deployment Control Evidence" +description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." +--- + +A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the Cloud API attestation contract. + +The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. + +## What it can and cannot establish + +Use it as supplementary evidence about deployment-control activity. Do not use it alone to conclude that: + +- a particular project, model, or backend is currently running; +- every recorded action completed successfully; +- a later inference request was served by this same CVM; or +- the images involved satisfy your source and build-provenance policy. + +The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. + +## Use it with independent checks + +When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. + +When your policy requires software provenance, verify the image digests from the quote-bound configuration using the [direct image-provenance flow](/cloud/verification/direct/image-provenance). An action log or an image reference does not by itself verify image provenance. + + + Treat unavailable or incomplete deployment-control evidence as unavailable evidence for that optional check. Do not substitute it for a model quote, a response signature, or a verified provenance statement. + diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx new file mode 100644 index 0000000..7af46ef --- /dev/null +++ b/cloud/verification/direct/image-provenance.mdx @@ -0,0 +1,28 @@ +--- +title: "Image Provenance" +description: "Apply an image-provenance policy to the configuration measured by a direct model attestation." +--- + +Image provenance is separate from hardware attestation. A verified direct model quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. + +## Verification flow + +1. Verify the direct model's Intel TDX quote first. +2. Obtain the configuration text from the direct report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. +4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. +5. Apply your policy for repository, build identity, workflow, ref, and source revision. + + + An image tag, search result, or HTTP `200` only locates a possible record. It does not cryptographically verify the provenance statement, its signer, or its subject digest. + + +## Relationship to direct attestation + +The quote and configuration measurement establish that the configuration text you matched is bound to the verified model report. Provenance establishes that a specific digest satisfies your source and build policy. Use both checks when your application requires a source-to-runtime trust chain. + +## Missing evidence + +An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. + +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. When requesting help, include the endpoint, UTC time, model name, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx new file mode 100644 index 0000000..6261745 --- /dev/null +++ b/cloud/verification/direct/index.mdx @@ -0,0 +1,52 @@ +--- +title: "Verify Direct Model Endpoints" +description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." +--- + +Use this flow when your application sends requests directly to `https://.completions.near.ai`. The Cloud API gateway is not in this request path. + +The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. + + + `all_attestations[]` is not a fleet inventory. Do not infer that it enumerates every healthy CVM, or that a later completion will be served by the same instance as an earlier report. + + +## Recommended flow + +1. Generate a fresh 32-byte nonce in the client for each verification attempt. +2. Request the report from the same direct model endpoint used for inference. +3. Verify the model's quote, nonce, signer binding, GPU evidence when present, and accepted measurements. +4. If you need endpoint binding, verify the TLS connection to the direct model endpoint against that model report. +5. If you need response integrity, verify the response signature and match it to the model report's signer. +6. Apply image-provenance policy to the measured model configuration. + + + + Verify the direct endpoint's root model report and `all_attestations[]` semantics. + + + Bind the live direct-model TLS connection to its model quote. + + + Verify a direct-model response signature against the matching model signer. + + + Apply provenance policy to the direct model's measured configuration. + + + Interpret optional Compose Manager evidence without treating it as request-to-CVM binding. + + + +## Get a fresh report + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' +``` + +The endpoint hostname identifies the model, so this request does not use a `model` parameter. Add `include_tls_fingerprint=true` only when following the [direct TLS flow](/cloud/verification/direct/tls). If you are matching a response signature, use its `signing_address` and `signing_algo` when requesting the applicable report. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx new file mode 100644 index 0000000..98f40a1 --- /dev/null +++ b/cloud/verification/direct/model-attestation.mdx @@ -0,0 +1,52 @@ +--- +title: "Model Attestation Report" +description: "Verify the root model report returned by a direct completions endpoint." +--- + +Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the Cloud API `gateway_attestation` / `model_attestations[]` envelope. + +## Request a fresh report + +Generate the nonce in your client as 32 random bytes encoded as 64 hexadecimal characters. + +```bash +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'Accept: application/json' +``` + +To request evidence for a response signer, add `signing_address=` and use the response's `signing_algo`. Add `include_tls_fingerprint=true` only for [direct TLS connection binding](/cloud/verification/direct/tls). + +## Direct response shape + +The root response is the model report. Relevant fields include: + +| Field | Meaning | +| --- | --- | +| `model_name` | The model name configured for this direct endpoint. | +| `signing_address`, `signing_algo` | The model signing identity and algorithm. | +| `request_nonce` | The nonce echoed for diagnostics; verify it inside the quote. | +| `intel_quote` | The model's Intel TDX quote. | +| `nvidia_payload` | GPU evidence, when present. | +| `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | +| `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | +| `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | + +The direct response does not define a top-level `report_data` field. Read `report_data` and measurements from the verified TDX quote. + +## Verify the report + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Check the client-generated nonce, signer identity, and signing algorithm against the verified quote. +3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +4. Apply your accepted measurement policy to the verified configuration. +5. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). + +When a response signature is being verified, match both `signing_address` and `signing_algo`. A direct report for another signer is not a substitute, even when the hostname and model name are the same. + +## Load balancing and freshness + +The same direct model hostname can serve more than one instance. Do not assume two independent HTTP requests reached the same instance. Fetch fresh evidence when the signer, certificate, measured configuration, or accepted-attestation-age policy changes. diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx new file mode 100644 index 0000000..1cf88d3 --- /dev/null +++ b/cloud/verification/direct/response-signatures.mdx @@ -0,0 +1,46 @@ +--- +title: "Response Signatures" +description: "Verify an available direct-model response signature against exact bytes and the matching model report." +--- + +Use this flow for a completion sent directly to a model endpoint. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the Cloud API `signature_kind` routing field. + + + A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. + + +## Preserve exact request and response bytes + +Build the request body once and retain the exact bytes sent by the client. Retain the raw response bytes before parsing JSON or Server-Sent Events. + +```text +request_hash = SHA-256(exact request-body bytes) +response_hash = SHA-256(exact response-body bytes) +``` + +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. + +## Fetch and verify the signature + +```bash +curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ + --data-urlencode 'signing_algo=ecdsa' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' +``` + +Verify `signature` locally using the returned `signing_algo`. Then require that `text` is exactly: + +```text +:: +``` + +Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or Cloud API model ID, does not satisfy this check. + +## Match the model signer + +Request or select a direct model attestation whose `signing_address` and `signing_algo` exactly match the signature. Verify its quote, client-generated nonce, signer binding, and required workload policy as described in [direct model attestation](/cloud/verification/direct/model-attestation). + +Do not infer a gateway/model classification from the format of the signed text, and do not substitute a report for a different signer. The direct endpoint's `all_attestations[]` field is not proof that every instance behind the hostname has been attested. + +`ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx new file mode 100644 index 0000000..311cbe0 --- /dev/null +++ b/cloud/verification/direct/tls.mdx @@ -0,0 +1,38 @@ +--- +title: "TLS Connection Binding" +description: "Bind a live direct-model TLS connection to the direct model attestation report." +--- + +Use this flow to establish that the HTTPS connection to a direct model endpoint terminates at the endpoint bound to its model attestation. + + + Obtain the peer certificate and direct attestation report over the **same TLS connection**. Separate connections can reach different instances or observe a rotated certificate, so they cannot establish this connection-specific conclusion. + + +## What this checks + +With `include_tls_fingerprint=true`, the direct report includes `tls_cert_fingerprint`: the SHA-256 hash of the direct endpoint certificate's SubjectPublicKeyInfo (SPKI). The verified model quote binds that fingerprint, the model signing identity, and the nonce. + +This check applies to the direct model connection. It does not verify a connection to `cloud-api.near.ai`. + +## Verification flow + +1. Generate a fresh 32-byte nonce in the client. +2. Open a TLS connection to `https://.completions.near.ai` using the trust configuration required by your application. +3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. +5. Verify the direct report's Intel TDX quote, then check the nonce, model signer, and TLS-fingerprint binding in the verified quote. +6. Compare the calculated peer SPKI hash with `tls_cert_fingerprint`. + +A command-line request can retrieve a report, but cannot by itself demonstrate that the report and peer certificate came from one TLS connection. + +## Required handling + +| Result | Action when TLS endpoint binding is required | +| --- | --- | +| `tls_cert_fingerprint` is absent | Treat the TLS binding as unavailable. Request a new report with `include_tls_fingerprint=true`. | +| Peer SPKI hash differs | Reject the connection and repeat the flow with one TLS connection. | +| Quote, nonce, or signer binding fails | Reject the report and fetch fresh evidence. | +| Certificate and report came from different connections | Treat the result as inconclusive. | + +Do not use a cached fingerprint as evidence for a new connection. Fetch new evidence after a certificate, signer, measurement, or accepted-attestation-age change. diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index cfcd3de..791ee9c 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,52 +1,10 @@ --- -title: "Gateway Verification" -description: "Verify the attestation evidence for requests sent through the NEAR AI Cloud API gateway." +title: "Gateway Attestation" +description: "Verify gateway evidence for requests sent through the NEAR AI Cloud API." --- -Use gateway verification when your request is sent to `https://cloud-api.near.ai`. It verifies evidence for the gateway request path. It does not replace [model verification](/cloud/verification/model) when you also need to verify the environment that served the model. +This page has moved into the Cloud API verification flow. - - A [direct completions endpoint](/cloud/private-inference#direct-completions) connects to the model endpoint without the Cloud API gateway in the request path. Start with [model verification](/cloud/verification/model) for that flow. - - -## Request a fresh gateway report - -Generate a new 32-byte nonce in your client for each verification attempt. Encode it as 64 hexadecimal characters. - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - -Add `model=` when you also want the request to include model attestation evidence. Add `include_tls_fingerprint=true` when you need to verify the HTTPS connection; see [TLS attestation](/cloud/verification/tls). - -## Verify the gateway evidence - -Verify the Intel TDX quote before trusting report fields supplied over HTTP. Then check the following: - -1. The verified quote satisfies your Intel DCAP and advisory policy. -2. The quote's report data contains the nonce generated by your client and binds the reported gateway signing identity. -3. The signing identity is paired with its reported `signing_algo`. -4. The measured configuration satisfies the policy your application accepts. -5. If you require source and build provenance, verify it for each image digest in the measured configuration. See [image provenance](/cloud/verification/provenance). - -When the report includes `gateway_attestation`, use that entry for gateway verification. A request that includes a model can also include `model_attestations`; verify those separately when model evidence is required. - -## Verify responses signed by the gateway - -When a response signature reports `signature_kind: "gateway"`, verify its signature against the gateway attestation whose `signing_address` and `signing_algo` match the signature. Then verify that the signed payload contains the SHA-256 hashes of the exact request and response bytes you observed. - -Follow [chat verification](/cloud/verification/chat) for the full response-signature flow. - -## Freshness and failure handling - -The nonce proves freshness only when your client created and retained it. Do not accept a report merely because an HTTP field echoes a nonce; verify the binding inside the validated quote. - -An unavailable report, failed quote, nonce mismatch, identity mismatch, or policy mismatch is not an equivalent result. If your application requires that property, mark the verification as failed or unverified and fetch new evidence rather than reusing a report from a different instance. - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for individual verification steps. + + Open the route-specific guide for `gateway_attestation`. + diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 35ff5b5..4d84052 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,80 +1,43 @@ --- title: "Verification" -description: "Verify NEAR AI Cloud attestation reports, TLS endpoints, signed chat responses, and deployed image provenance." +description: "Choose the Cloud API or direct-model verification flow, then verify the evidence for that request path." --- -Verification lets you independently check evidence for a specific NEAR AI Cloud request path. Each check answers a different question: whether an attestation is fresh, whether a TLS connection reaches the attested endpoint, whether a response was signed by an attested signer, or whether deployed images have the provenance your policy requires. - -Start with the result you need to verify. +NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. - - Verify a model attestation, including its Intel TDX quote, nonce, signer, GPU evidence, and measured configuration. - - - Verify the Cloud API gateway when requests are sent through cloud-api.near.ai. - - - Verify that one HTTPS connection terminates at the endpoint named by its attestation. + + Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. - - Verify the integrity and signer of a chat completion. - - - Apply a source and build-provenance policy to every image pinned in an attested deployment. + + Verify requests sent directly to a model's completions.near.ai endpoint, without the Cloud API gateway in the request path. -## Choose the checks you need - -| Goal | Required evidence | -| --- | --- | -| Verify a model environment | A fresh model attestation, a verified Intel TDX quote, the request nonce, signer binding, and GPU evidence when present. | -| Verify the gateway | A fresh gateway attestation and a verified Intel TDX quote. Verify the model separately when you also need evidence for model execution. | -| Verify an HTTPS connection | A TLS-bound attestation and the live certificate's SPKI hash from the **same TLS connection**. | -| Verify a chat response | The exact request and response bytes, a valid signature, and an attestation for the signature's signer. | -| Verify deployed software provenance | A verified compose measurement plus a provenance decision for every image digest in that compose file. | - -## Recommended verification flow - -For a production integration, use the following order: - -1. Generate a fresh, random 32-byte nonce in your client. Send it as 64 hexadecimal characters with the attestation request. -2. Request the attestation for the endpoint and model you are using. When you need TLS endpoint verification, set `include_tls_fingerprint=true`. -3. Verify the Intel TDX quote with an Intel DCAP verifier. Read `report_data` and measurements from the **verified quote**, not from unverified response fields. -4. Check that the verified quote binds your nonce and the reported signing identity. Verify NVIDIA GPU evidence when the report contains it. -5. Apply your workload policy: inspect the measured compose configuration and, when required, verify provenance for every pinned image digest. -6. For a chat response, verify the signature over the exact bytes you sent and received, then match its signer to the appropriate attestation. - - - The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual checks. Treat any example as a starting point for your own policy: your application should fail verification when a check it requires is missing or does not pass. - +## Why the paths are separate -## Direct completions and the gateway +| | Cloud API | Direct model endpoint | +| --- | --- | --- | +| Request host | `cloud-api.near.ai` | `.completions.near.ai` | +| Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | +| Gateway evidence | Available as `gateway_attestation` | Not in the request path | +| Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | -Requests sent to a [direct completions endpoint](/cloud/private-inference#direct-completions) go directly to the model endpoint. In that path, start with [model verification](/cloud/verification/model); gateway verification is not required for the direct request path. +Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a Cloud API response field; it is not the direct endpoint's `all_attestations[]` field. -Requests sent to `https://cloud-api.near.ai` pass through the Cloud API gateway. Verify the gateway when you need evidence for that request path, and verify the model when you need evidence for the model environment. +## Shared requirements -## Important details for load-balanced endpoints +Both paths require a client-generated nonce, a verified Intel TDX quote, a signer binding, and an explicit acceptance policy. The path guides show where to find those values; the reference pages explain how to evaluate them. -An endpoint can be served by more than one attested instance. Do not assume that two independent requests reached the same instance. - -- When verifying a chat response, use the response's `signing_address` and `signing_algo` to select the matching attestation. -- When verifying TLS, obtain the live certificate and the attestation over the same TLS connection. Fetching them over separate connections can produce a false mismatch after load balancing or certificate rotation. -- When an instance is redeployed or its certificate changes, fetch a new attestation before accepting the new evidence. - -## What attestation does and does not establish - -Attestation verifies specific cryptographic evidence about a runtime and its measured configuration. It does not, by itself, validate the quality of a model response, secure a compromised client device, or establish source provenance for an image whose build evidence you have not verified. - -Use the individual guides in this section to decide which guarantees your application requires. - -## Common verification failures + + + Verify fresh evidence from the quote rather than trusting echoed HTTP fields. + + + Define required checks, handling for unavailable evidence, and image-provenance policy. + + -| Symptom | What to do | -| --- | --- | -| Nonce mismatch | Reject the report. Generate a new nonce in the client and request a new attestation. | -| TLS fingerprint mismatch | Reject the connection. Repeat the TLS flow using one connection for both the certificate and attestation request. | -| Signature is unavailable | Do not mark the response as verified. Retry briefly after completion; if it remains unavailable, retain the response ID and request ID for support. | -| Image provenance is unavailable | Treat the image as unverified under a provenance-required policy. Fetch a fresh attestation after a deployment change, then repeat the provenance check. | + + A report for one connection or one instance is not automatically evidence for another. For a TLS conclusion, obtain the peer certificate and report on the same TLS connection. For a signed response, match its signer and algorithm to the applicable attestation. + diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 44aaec1..1ed1dd7 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -1,88 +1,15 @@ --- -title: "Model Verification" -description: "Verify the attestation evidence for a NEAR AI Cloud model endpoint." +title: "Choose a Model Attestation Flow" +description: "Select the verification guide that matches the endpoint your application calls." --- -Use model verification to check the environment serving a model. It is the required attestation check for [direct completions](/cloud/private-inference#direct-completions), and it complements [gateway verification](/cloud/verification/gateway) when you call `cloud-api.near.ai`. +The report structure depends on the endpoint, so select the guide for the request path your application uses. -Model verification does not prove a particular response by itself. To verify a response, also follow [chat verification](/cloud/verification/chat). - -## Request a fresh report - -Generate the nonce in your client. Use 32 random bytes encoded as 64 hexadecimal characters, and retain it until verification has completed. - - - - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - - - - -Use the same `signing_algo` as the signature you plan to verify. Supported values are `ecdsa` and `ed25519`. If you need to bind the report to the HTTPS endpoint, add `include_tls_fingerprint=true` and follow the [TLS attestation guide](/cloud/verification/tls). - -The Cloud API request accepts a `model` parameter. A direct endpoint already identifies the model through its hostname. If you are verifying a response signature and the endpoint supports it, include its `signing_address` and `signing_algo` to request evidence for that signer. - - - A report can contain evidence from more than one serving instance. Do not assume a fixed number of entries. Select the entry that matches the signer and algorithm of the response you are verifying. - - -## Verify the report - -Verify the Intel TDX quote before trusting fields copied into the HTTP response. Your verifier should read the report data and measurements from the verified quote. - -For each applicable model-attestation entry, check the following: - -1. Verify the Intel TDX quote using an Intel DCAP verifier and apply your policy for its result and advisories. -2. Verify that the quote's report data contains the nonce your client generated and binds the reported signing identity. -3. Match `signing_address` and `signing_algo` to the identity that signed the response, when you are verifying a response. -4. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. -5. Check the measured configuration against the policy your application accepts. If you require source and build provenance, continue with [image provenance verification](/cloud/verification/provenance). - -The response's `request_nonce` and `report_data` fields are useful for diagnostics, but the verified quote is the cryptographic source of truth. - -## Signing identities - -`signing_address` is an algorithm-specific identity, not always a public key in the same format: - -| `signing_algo` | `signing_address` | -| --- | --- | -| `ecdsa` | An Ethereum-style address derived from the signing key. | -| `ed25519` | The Ed25519 public-key bytes, encoded as hexadecimal. | - -Always compare both the identity and `signing_algo`. Do not compare identities across algorithms. - -## Handling rotations and load balancing - -An endpoint may be served by several instances. A report fetched before or after a completion can therefore describe a different signer. - -- Fetch a new report when the signer, certificate, or measured configuration changes. -- For a signed response, request or select the attestation entry that matches its signer. -- Record the report time and enforce the maximum attestation age your policy permits. -- When a required check cannot be completed, treat that result as unverified rather than substituting evidence from a different instance. - -## Reference implementation - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) provides implementation examples for quote, GPU, TLS, signature, and image-digest inspection. Integrate the checks and acceptance policy appropriate for your application. + + + Use `model_attestations[]` returned by cloud-api.near.ai. + + + Use the model report returned by a direct completions endpoint. + + diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index c52bf6e..81b91b2 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -1,56 +1,15 @@ --- -title: "Image Provenance" -description: "Verify source and build provenance for images named in an attested deployment." +title: "Choose an Image-Provenance Flow" +description: "Select the provenance guide for the attestation envelope your application uses." --- -An attestation can establish that a measured deployment configuration was running. Image provenance answers a separate question: whether each image in that configuration was built from the source and build process your policy accepts. +The configuration to inspect is carried by different attestation envelopes for Cloud API and direct model endpoints. Select the matching path before evaluating image digests. -Use both checks when your application requires a source-to-runtime trust chain. - -## Verification flow - -1. Verify the Intel TDX quote. Obtain the compose configuration text from the report, calculate its hash, and compare it with the configuration measurement in the verified quote. Only then treat that text as the attested configuration. -2. Extract every container image pinned by immutable digest (`@sha256:…`) from the matched configuration. -3. For each digest, retrieve provenance evidence from the relevant build-provenance system. -4. Cryptographically verify the provenance statement and confirm that its subject digest exactly matches the image digest in the attested configuration. -5. Apply your policy to the verified statement, such as the expected repository, build identity, workflow, ref, and source revision. -6. Accept the deployment only when every image required by your policy passes the checks. - - - An image tag is not sufficient evidence. A search result or an HTTP `200` response only locates a possible record; it does not cryptographically verify the statement, its signer, or its subject digest. - - -## Compose Manager images - -When the attested configuration contains platform images such as `compose-manager` or `compose-manager-launcher`, evaluate their exact digests the same way as every other image in the configuration. First bind the configuration to the verified quote, then verify build provenance for each digest under your policy. - -An integrity check for a reported action list is a separate check. It does not establish the provenance of the image that ran the action, and it does not replace quote or image-provenance verification. - -## Define a provenance policy - -Your policy should specify the identities and inputs you accept before you evaluate a report. At minimum, define: - -- the expected source repository or repositories; -- the accepted build identity and workflow; -- the allowed source branch, tag, or commit policy; and -- whether provenance is required for every image or only selected images. - -Pin every evaluated image by digest. If a configuration references an image without an immutable digest, it cannot satisfy a digest-matching provenance check. - -## When a provenance record is missing - -A missing record, including an HTTP `404`, means provenance for the reported image digest could not be verified. It does not by itself identify a security incident. - -Deployments can change. Fetch a fresh attestation report and repeat the verification flow. If the current report still references a digest without the provenance required by your policy, treat that image as unverified. - -When requesting help, include the endpoint, time, report identifier, image name, and full digest. Do not include API keys, prompts, or response content. - -## Relationship to attestation - -| Check | What it establishes | -| --- | --- | -| Quote and configuration measurement | The configuration text you matched is bound to a measurement in the verified quote. | -| Image provenance | A particular image digest satisfies your source and build policy. | -| Both together | The measured runtime configuration uses images whose provenance satisfies your policy. | - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes reference code for locating selected image digests in a configuration. Use a provenance verifier that validates the cryptographic evidence required by your policy. + + + Evaluate configuration measurements from the gateway and any returned model attestations. + + + Evaluate the measured configuration from the direct model report. + + diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx new file mode 100644 index 0000000..70e6fae --- /dev/null +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -0,0 +1,47 @@ +--- +title: "Quote, Nonce, and Signer Bindings" +description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." +--- + +Every verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote; they are not a replacement for quote verification. + +## Use a client-generated nonce + +Generate 32 random bytes in the client and encode them as 64 hexadecimal characters. Retain the value until verification completes. + +```bash +NONCE="$(openssl rand -hex 32)" +``` + +Send this value with the attestation request. Reject a report unless the verified quote contains the same nonce in its request-freshness binding. + +## Find the quote for your path + +| Request path | Quote location | +| --- | --- | +| Cloud API gateway | `gateway_attestation.intel_quote` | +| Cloud API model evidence | The provider-specific item in `model_attestations[]` | +| Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | + +The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. + +## Verify the signer binding + +For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. + +`signing_address` is algorithm-specific: + +| `signing_algo` | `signing_address` | +| --- | --- | +| `ecdsa` | An Ethereum-style address derived from the signing key. | +| `ed25519` | Ed25519 public-key bytes encoded as hexadecimal. | + +Always compare both `signing_address` and `signing_algo`. Do not compare identities across algorithms. + +## Verify GPU evidence when present + +When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. + +## Reference code + +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual quote, GPU, TLS, signature, and configuration checks. Use it as an implementation reference, then enforce the acceptance policy required by your application. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx new file mode 100644 index 0000000..c76f4b7 --- /dev/null +++ b/cloud/verification/reference/verification-policy.mdx @@ -0,0 +1,42 @@ +--- +title: "Verification Policy" +description: "Define required evidence, acceptable measurements, provenance requirements, and handling for unavailable verification results." +--- + +Attestation verifies particular cryptographic statements. Your application must decide which statements it requires and what software, hardware, and source provenance it accepts. + +## Define acceptance before verification + +At minimum, specify: + +- acceptable Intel TDX verification status and advisory policy; +- required signer algorithm and identity matching rules; +- whether GPU evidence is required and the accepted verdict; +- accepted workload measurements and configuration rules; +- trusted image repositories, build identities, workflows, refs, and source revisions; +- maximum acceptable evidence age; and +- whether TLS endpoint binding and response signatures are required. + +## Treat required checks as fail-closed + +When a required check is missing, unavailable, malformed, mismatched, or fails verification, the result is unverified. Do not replace it with a report for a different signer, certificate, request, model instance, or provider. + +| Condition | Recommended result | +| --- | --- | +| Quote or nonce mismatch | Reject the evidence and request a fresh report. | +| TLS report or peer SPKI unavailable | Treat endpoint binding as unverified. | +| Response signature unavailable after a bounded retry | Treat the response as unverified when signatures are required. | +| Image-provenance record is missing or returns `404` | Treat that digest as unverified when provenance is required. | +| Provider report shape is not covered by your verifier | Treat the provider-specific property as unverified. | + +## Load balancing and rotations + +The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. + +- For TLS, obtain the peer certificate and report on the same connection. +- For a response signature, match the signer identity and algorithm to the applicable attestation. +- Fetch new evidence after a signer, certificate, configuration, deployment, or policy-age change. + +## What attestation does not establish by itself + +Attestation does not validate the quality of a model response, secure a compromised client device, or establish image provenance without a separate cryptographic provenance check. It also does not turn a reachable build record into a verified provenance statement. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index b8be96a..9d44252 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -1,60 +1,15 @@ --- -title: "TLS Attestation" -description: "Verify that a specific HTTPS connection terminates at the endpoint bound to an attestation report." +title: "Choose a TLS Attestation Flow" +description: "Select the TLS verification guide that matches the endpoint your application calls." --- -TLS attestation links a live HTTPS connection to the signing identity in a verified attestation. It is the check to use when your policy requires evidence that the TLS endpoint for a particular connection is the attested endpoint. +TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - - Obtain the peer certificate and the attestation report over the **same TLS connection**. Fetching them over separate connections can reach different instances or observe a rotated certificate, especially for load-balanced endpoints. - - -## What the check establishes - -When you request an attestation with `include_tls_fingerprint=true`, the report includes `tls_cert_fingerprint`: the SHA-256 hash of the certificate's SubjectPublicKeyInfo (SPKI). The verified TDX quote binds that fingerprint, the signing identity, and your nonce. - -After you compare the live peer certificate's SPKI hash with `tls_cert_fingerprint`, the result establishes that the TLS connection you observed is bound to that verified attestation. - -Your application must make an explicit certificate-validation decision. TLS attestation becomes a security conclusion only after the quote, binding, and same-connection checks pass; an unverified report is not a reason to relax TLS validation. - -## Verification flow - -1. Generate a fresh, random 32-byte nonce in your client and encode it as 64 hexadecimal characters. -2. Open a TLS connection using the trust configuration appropriate for your application. -3. From that connection, read the peer certificate and calculate `SHA-256(SPKI_DER)`. -4. Reuse the **same open connection** to request `/v1/attestation/report` with `include_tls_fingerprint=true`, your nonce, and the signing algorithm you require. -5. Verify the Intel TDX quote and verify that its report data binds the nonce, signing identity, and reported TLS fingerprint. -6. Compare the SPKI hash you calculated from the peer certificate with `tls_cert_fingerprint` from the report. -7. Apply the same measurement and provenance policy you use for [model](/cloud/verification/model) or [gateway](/cloud/verification/gateway) verification. - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) includes implementation examples for TLS-related verification steps. Independently ensure that your client keeps the certificate read and report request on one connection. - -## Request parameters - -Use the normal attestation endpoint for your request path, adding `include_tls_fingerprint=true`: - -```text -GET /v1/attestation/report?signing_algo=ecdsa&nonce=&include_tls_fingerprint=true -``` - -For the Cloud API gateway, include your Bearer API key and, when needed, `model=`. For a direct endpoint, call that endpoint's `/v1/attestation/report` route. A command-line request can retrieve a report, but it does not by itself prove the same-connection property unless your client also reads the peer certificate from that very connection. - -## Load balancing and certificate rotation - -Do not use a cached fingerprint as evidence for a new connection. It can be useful as an operational hint, but it is not a fresh attestation result. - -Request new evidence when any of the following changes: - -- the live certificate's public key; -- the report's signing identity or signing algorithm; -- the measured configuration; or -- your policy's allowed attestation age. - -## Failure handling - -| Result | Required action when TLS endpoint verification is required | -| --- | --- | -| No `tls_cert_fingerprint` | Treat the TLS binding as unavailable; request a new report with `include_tls_fingerprint=true`. | -| Peer SPKI hash differs from the report | Reject the connection and repeat the flow with one TLS connection. | -| Quote, nonce, or signing-identity check fails | Reject the report and fetch fresh evidence. | -| Certificate read and report fetch used different connections | Treat the result as inconclusive, not verified. | + + + Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. + + + Verify the TLS connection to a direct model endpoint against its model report. + + diff --git a/docs.json b/docs.json index e0b79c9..59ab7be 100644 --- a/docs.json +++ b/docs.json @@ -59,11 +59,35 @@ "icon": "file-check", "root": "cloud/verification/index", "pages": [ - "cloud/verification/model", - "cloud/verification/gateway", - "cloud/verification/tls", - "cloud/verification/chat", - "cloud/verification/provenance" + { + "group": "Cloud API", + "root": "cloud/verification/cloud-api/index", + "pages": [ + "cloud/verification/cloud-api/gateway-attestation", + "cloud/verification/cloud-api/model-attestations", + "cloud/verification/cloud-api/tls", + "cloud/verification/cloud-api/response-signatures", + "cloud/verification/cloud-api/image-provenance" + ] + }, + { + "group": "Direct Model Endpoints", + "root": "cloud/verification/direct/index", + "pages": [ + "cloud/verification/direct/model-attestation", + "cloud/verification/direct/tls", + "cloud/verification/direct/response-signatures", + "cloud/verification/direct/image-provenance", + "cloud/verification/direct/deployment-control-evidence" + ] + }, + { + "group": "Reference", + "pages": [ + "cloud/verification/reference/quote-nonce-signer", + "cloud/verification/reference/verification-policy" + ] + } ] } ] From e64b2f9cd29b65d916b4ab139d34e779a12373a8 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 20:48:50 +0800 Subject: [PATCH 04/23] docs: name the NEAR AI Cloud gateway --- cloud/guides/e2ee-chat-completions.mdx | 6 +++--- cloud/private-inference.mdx | 12 +++++------ cloud/verification/chat.mdx | 2 +- .../cloud-api/gateway-attestation.mdx | 8 ++++---- .../cloud-api/image-provenance.mdx | 6 +++--- cloud/verification/cloud-api/index.mdx | 20 +++++++++---------- .../cloud-api/model-attestations.mdx | 12 +++++------ .../cloud-api/response-signatures.mdx | 4 ++-- cloud/verification/cloud-api/tls.mdx | 4 ++-- .../direct/deployment-control-evidence.mdx | 2 +- cloud/verification/direct/index.mdx | 2 +- .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 4 ++-- cloud/verification/gateway.mdx | 6 +++--- cloud/verification/index.mdx | 10 +++++----- cloud/verification/model.mdx | 2 +- cloud/verification/provenance.mdx | 4 ++-- .../reference/quote-nonce-signer.mdx | 4 ++-- cloud/verification/tls.mdx | 2 +- docs.json | 2 +- 20 files changed, 57 insertions(+), 57 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 59f0d89..44dbbb1 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -222,7 +222,7 @@ response = requests.get( # ) attestation = response.json() -# Cloud API: +# NEAR AI Cloud gateway: model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] # Direct completions use the root model report instead: # model_public_key_hex = attestation["signing_public_key"] @@ -254,7 +254,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -For Cloud API, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. --- @@ -813,5 +813,5 @@ You can verify the model's public key is authentic by checking the attestation r ## See Also - [Private Inference](/cloud/private-inference) β€” How TEE isolation protects your data -- [Verification](/cloud/verification) β€” Select Cloud API or direct model attestation verification +- [Verification](/cloud/verification) β€” Select NEAR AI Cloud gateway or direct model attestation verification - [Verification Policy](/cloud/verification/reference/verification-policy) β€” Define the evidence your application requires diff --git a/cloud/private-inference.mdx b/cloud/private-inference.mdx index b752a17..467714e 100644 --- a/cloud/private-inference.mdx +++ b/cloud/private-inference.mdx @@ -60,7 +60,7 @@ NEAR AI Cloud supports two connection modes. Each has its own TLS-attestation fl ```mermaid flowchart LR - A["Your Machine"] -->|"πŸ”’ HTTPS"| B["Cloud API gateway"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ LLM"] + A["Your Machine"] -->|"πŸ”’ HTTPS"| B["NEAR AI Cloud gateway"] -->|"πŸ”’ HTTPS"| C["Model endpoint β†’ LLM"] style A fill:#1a1a2e,stroke:#4fc3f7,stroke-width:2px,color:#fff style B fill:#1b5e20,stroke:#66bb6a,stroke-width:2px,color:#fff @@ -138,7 +138,7 @@ Gateway and direct completions responses include an `X-Request-Id` response head **Benefits of direct completions:** - **Fewer hops** β€” Your request reaches the model TEE directly, reducing latency -- **Simpler trust model** β€” Only the model TEE needs to be verified (no [Cloud API gateway attestation](/cloud/verification/cloud-api/gateway-attestation) required) +- **Simpler trust model** β€” Only the model TEE needs to be verified (no [NEAR AI Cloud gateway attestation](/cloud/verification/cloud-api/gateway-attestation) required) - **TLS binds to attestation** β€” The `include_tls_fingerprint=true` parameter can bind the direct endpoint TLS certificate to its attestation report when the [direct TLS flow](/cloud/verification/direct/tls) is completed ### The Inference Process @@ -148,7 +148,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Gateway (`cloud-api.near.ai`):** 1. **Request Initiation:** - You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls) when your application needs to verify the endpoint for a particular connection. + You send chat completion requests via HTTPS to the LLM Gateway. TLS protects data in transit; use [NEAR AI Cloud gateway TLS connection binding](/cloud/verification/cloud-api/tls) when your application needs to verify the endpoint for a particular connection. 2. **Secure Request Routing:** The LLM Gateway routes your request to the appropriate Private LLM Node based on the requested model, availability, and load balancing requirements. @@ -168,7 +168,7 @@ When you make a request to NEAR AI Cloud, your data flows through a secure pipel **Via Direct Completions (`{slug}.completions.near.ai`):** 1. **Direct Request:** - You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate Cloud API gateway in the request path. Use [direct TLS connection binding](/cloud/verification/direct/tls) when endpoint verification is required. + You send chat completion requests via HTTPS directly to the model's subdomain, with no intermediate NEAR AI Cloud gateway in the request path. Use [direct TLS connection binding](/cloud/verification/direct/tls) when endpoint verification is required. 2. **Secure Inference:** The model processes your request entirely within its TEE, where all data and model weights are protected by hardware-enforced isolation. @@ -247,8 +247,8 @@ Cryptographic signatures generated inside the TEE ensure that responses cannot b Understand how to verify and validate secure interactions with AI models - - Verify gateway, model, TLS, and response evidence for Cloud API requests + + Verify gateway, model, TLS, and response evidence for requests sent through the NEAR AI Cloud gateway Add client-side encryption for defense-in-depth protection of your messages diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 621a2e1..2f94940 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -6,7 +6,7 @@ description: "Select the response-signature guide that matches the endpoint your The signature payload and the attestation to match depend on the request path. - + Use `signature_kind` when present to select a gateway or model attestation. diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 13be109..47a3750 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -1,9 +1,9 @@ --- title: "Gateway Attestation" -description: "Verify the Cloud API gateway attestation for requests sent to cloud-api.near.ai." +description: "Verify the NEAR AI Cloud gateway attestation for requests sent to cloud-api.near.ai." --- -`gateway_attestation` is evidence for the Cloud API gateway. It is not evidence for a model environment; use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. +`gateway_attestation` is evidence for the NEAR AI Cloud gateway. It is not evidence for a model environment; use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. ## Request gateway evidence @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Verify the gateway report @@ -28,7 +28,7 @@ For `gateway_attestation`: 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. 2. Read `report_data` and measurements from the **verified quote**, not only from fields echoed in the HTTP response. 3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. -4. When a TLS fingerprint was requested, verify its binding as part of the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +4. When a TLS fingerprint was requested, verify its binding as part of the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). 5. Apply your accepted measurement and image-provenance policy. `request_nonce` and `report_data` in the HTTP body are useful diagnostics. The verified quote is the cryptographic source of truth. diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index deb2319..ed60b72 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -1,11 +1,11 @@ --- title: "Image Provenance" -description: "Apply an image-provenance policy to configurations measured by Cloud API gateway and model attestations." +description: "Apply an image-provenance policy to configurations measured in NEAR AI Cloud gateway and model attestations." --- Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. -For a Cloud API flow, decide separately whether your policy requires provenance for: +For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; - each applicable model report in `model_attestations[]`; or @@ -25,7 +25,7 @@ For a Cloud API flow, decide separately whether your policy requires provenance ## Provider boundaries -Do not assume every Cloud API model report exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. +Do not assume every model report returned through the NEAR AI Cloud gateway exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. ## Missing evidence diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 8700478..09730f5 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,11 +1,11 @@ --- -title: "Verify Cloud API" +title: "Verify the NEAR AI Cloud Gateway" description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." --- -Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the Cloud API gateway and can include evidence for a selected model. +Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the NEAR AI Cloud gateway and can include evidence for a selected model. -The relevant verification fields in a Cloud API response are: +The relevant verification fields in a NEAR AI Cloud gateway response are: ```text gateway_attestation @@ -13,7 +13,7 @@ model_attestations[] # returned when model evidence is available tls_certificate # optional ``` -`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the Cloud API request path; verify a model attestation when you also need evidence for the model environment. +`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the NEAR AI Cloud gateway request path; verify a model attestation when you also need evidence for the model environment. `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. @@ -22,7 +22,7 @@ tls_certificate # optional ## Recommended flow 1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request a Cloud API attestation report, including `model=` when model evidence is needed. +2. Request a NEAR AI Cloud gateway attestation report, including `model=` when model evidence is needed. 3. Verify `gateway_attestation` and apply your gateway measurement policy. 4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. 5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. @@ -31,13 +31,13 @@ tls_certificate # optional - Verify the Cloud API gateway quote and signer binding. + Verify the gateway quote and signer binding. Verify provider model evidence returned in `model_attestations[]`. - Bind the live Cloud API TLS connection to `gateway_attestation`. + Bind the live gateway TLS connection to `gateway_attestation`. Match a response signature to its gateway or model signer. @@ -60,12 +60,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [Cloud API TLS flow](/cloud/verification/cloud-api/tls). +Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Provider-specific model evidence -Cloud API can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. +The NEAR AI Cloud gateway can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. -This guide describes the Cloud API envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. +This guide describes the NEAR AI Cloud gateway envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 11e754a..17cb2d1 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,11 +1,11 @@ --- title: "Model Attestations" -description: "Verify model evidence returned in the Cloud API model_attestations array." +description: "Verify model evidence returned in the NEAR AI Cloud gateway's model_attestations array." --- Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. -Cloud API returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the Cloud API envelope and each provider report as separate contracts. +The NEAR AI Cloud gateway returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the gateway envelope and each provider report as separate contracts. ## Request model evidence @@ -26,7 +26,7 @@ The response always has `gateway_attestation`; model evidence, when available, i When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -Cloud API also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. +The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. ## Verify the applicable entry @@ -39,10 +39,10 @@ Cloud API also accepts `provider=near` or `provider=chutes`. These provider tier Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. -## Relationship to Cloud API TLS +## Relationship to NEAR AI Cloud gateway TLS -The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint binding is verified against `gateway_attestation`, not against a TLS fingerprint carried by an upstream model report. See [Cloud API TLS connection binding](/cloud/verification/cloud-api/tls). +The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint binding is verified against `gateway_attestation`, not against a TLS fingerprint carried by an upstream model report. See [NEAR AI Cloud gateway TLS connection binding](/cloud/verification/cloud-api/tls). ## Verify a signed response -For a Cloud API response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [Cloud API response signatures](/cloud/verification/cloud-api/response-signatures). +For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 6540794..d00118f 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -1,9 +1,9 @@ --- title: "Response Signatures" -description: "Verify available Cloud API response signatures against the exact bytes and the matching attestation." +description: "Verify available response signatures from the NEAR AI Cloud gateway against the exact bytes and the matching attestation." --- -Cloud API response signatures can be produced by either a model-serving TEE or the Cloud API gateway. When present, `signature_kind` determines which attestation must match the signer. +Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. When present, `signature_kind` determines which attestation must match the signer. A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index d80120d..708d6e5 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -1,6 +1,6 @@ --- title: "TLS Connection Binding" -description: "Bind a live Cloud API TLS connection to the Cloud API gateway attestation." +description: "Bind a live NEAR AI Cloud gateway TLS connection to the gateway attestation." --- Use this flow to establish that the HTTPS connection to `cloud-api.near.ai` terminates at the endpoint bound to `gateway_attestation`. @@ -20,7 +20,7 @@ This is a gateway check. It does not bind the client connection to an upstream m 1. Generate a fresh 32-byte nonce in the client. 2. Open a TLS connection to `cloud-api.near.ai` using the trust configuration required by your application. 3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. -4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header; Cloud API report retrieval requires an API key. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header; NEAR AI Cloud gateway report retrieval requires an API key. 5. Verify `gateway_attestation.intel_quote`, then check the nonce, gateway signer, and TLS-fingerprint binding in the verified quote. 6. Compare the calculated peer SPKI hash with `gateway_attestation.tls_cert_fingerprint`. diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 1c9f82f..5013956 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -3,7 +3,7 @@ title: "Deployment Control Evidence" description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." --- -A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the Cloud API attestation contract. +A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the NEAR AI Cloud gateway attestation contract. The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 6261745..7106588 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -3,7 +3,7 @@ title: "Verify Direct Model Endpoints" description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." --- -Use this flow when your application sends requests directly to `https://.completions.near.ai`. The Cloud API gateway is not in this request path. +Use this flow when your application sends requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 98f40a1..f5fdf9f 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -3,7 +3,7 @@ title: "Model Attestation Report" description: "Verify the root model report returned by a direct completions endpoint." --- -Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the Cloud API `gateway_attestation` / `model_attestations[]` envelope. +Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the NEAR AI Cloud gateway's `gateway_attestation` / `model_attestations[]` envelope. ## Request a fresh report diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 1cf88d3..0ecc389 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -3,7 +3,7 @@ title: "Response Signatures" description: "Verify an available direct-model response signature against exact bytes and the matching model report." --- -Use this flow for a completion sent directly to a model endpoint. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the Cloud API `signature_kind` routing field. +Use this flow for a completion sent directly to a model endpoint. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the NEAR AI Cloud gateway's `signature_kind` routing field. A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. @@ -35,7 +35,7 @@ Verify `signature` locally using the returned `signing_algo`. Then require that :: ``` -Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or Cloud API model ID, does not satisfy this check. +Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or a NEAR AI Cloud gateway model ID, does not satisfy this check. ## Match the model signer diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index 791ee9c..681dc18 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -1,10 +1,10 @@ --- title: "Gateway Attestation" -description: "Verify gateway evidence for requests sent through the NEAR AI Cloud API." +description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." --- -This page has moved into the Cloud API verification flow. +This page has moved into the NEAR AI Cloud gateway verification flow. - + Open the route-specific guide for `gateway_attestation`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 4d84052..a7abc7d 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,29 +1,29 @@ --- title: "Verification" -description: "Choose the Cloud API or direct-model verification flow, then verify the evidence for that request path." +description: "Choose the NEAR AI Cloud gateway or direct-model verification flow, then verify the evidence for that request path." --- NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. - + Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. - Verify requests sent directly to a model's completions.near.ai endpoint, without the Cloud API gateway in the request path. + Verify requests sent directly to a model's completions.near.ai endpoint, without the NEAR AI Cloud gateway in the request path. ## Why the paths are separate -| | Cloud API | Direct model endpoint | +| | NEAR AI Cloud gateway | Direct model endpoint | | --- | --- | --- | | Request host | `cloud-api.near.ai` | `.completions.near.ai` | | Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | | Gateway evidence | Available as `gateway_attestation` | Not in the request path | | Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | -Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a Cloud API response field; it is not the direct endpoint's `all_attestations[]` field. +Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a NEAR AI Cloud gateway response field; it is not the direct endpoint's `all_attestations[]` field. ## Shared requirements diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 1ed1dd7..5a1b5d4 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -6,7 +6,7 @@ description: "Select the verification guide that matches the endpoint your appli The report structure depends on the endpoint, so select the guide for the request path your application uses. - + Use `model_attestations[]` returned by cloud-api.near.ai. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index 81b91b2..62768c0 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -3,10 +3,10 @@ title: "Choose an Image-Provenance Flow" description: "Select the provenance guide for the attestation envelope your application uses." --- -The configuration to inspect is carried by different attestation envelopes for Cloud API and direct model endpoints. Select the matching path before evaluating image digests. +The configuration to inspect is carried by different attestation envelopes for the NEAR AI Cloud gateway and direct model endpoints. Select the matching path before evaluating image digests. - + Evaluate configuration measurements from the gateway and any returned model attestations. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 70e6fae..f8a7bd8 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -19,8 +19,8 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | -| Cloud API gateway | `gateway_attestation.intel_quote` | -| Cloud API model evidence | The provider-specific item in `model_attestations[]` | +| NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | +| NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 9d44252..3828f2a 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -6,7 +6,7 @@ description: "Select the TLS verification guide that matches the endpoint your a TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - + Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. diff --git a/docs.json b/docs.json index 59ab7be..31f87f7 100644 --- a/docs.json +++ b/docs.json @@ -60,7 +60,7 @@ "root": "cloud/verification/index", "pages": [ { - "group": "Cloud API", + "group": "NEAR AI Cloud Gateway", "root": "cloud/verification/cloud-api/index", "pages": [ "cloud/verification/cloud-api/gateway-attestation", From f2e3d66bacac3807f2f4321509c762b032c3cc02 Mon Sep 17 00:00:00 2001 From: Coffee Date: Mon, 24 Aug 2026 20:57:10 +0800 Subject: [PATCH 05/23] docs: make verification guides task-oriented --- cloud/verification/chat.mdx | 6 +- cloud/verification/cloud-api/index.mdx | 64 ++++--------------- .../cloud-api/model-attestations.mdx | 12 ++-- .../direct/deployment-control-evidence.mdx | 6 +- cloud/verification/direct/index.mdx | 45 ++++--------- .../verification/direct/model-attestation.mdx | 4 +- .../direct/response-signatures.mdx | 4 +- cloud/verification/gateway.mdx | 4 +- cloud/verification/index.mdx | 35 ++++------ cloud/verification/model.mdx | 6 +- cloud/verification/provenance.mdx | 8 +-- cloud/verification/tls.mdx | 4 +- 12 files changed, 67 insertions(+), 131 deletions(-) diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx index 2f94940..fdfbe75 100644 --- a/cloud/verification/chat.mdx +++ b/cloud/verification/chat.mdx @@ -3,13 +3,13 @@ title: "Choose a Response-Signature Flow" description: "Select the response-signature guide that matches the endpoint your application calls." --- -The signature payload and the attestation to match depend on the request path. +Choose the guide from the URL your application used for the completion. - Use `signature_kind` when present to select a gateway or model attestation. + Use this when your application sent the completion through `cloud-api.near.ai`. - Match the signature to the direct model report. + Use this when your application sent the completion directly to a model endpoint. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 09730f5..be17aa8 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,71 +1,35 @@ --- title: "Verify the NEAR AI Cloud Gateway" -description: "Verify gateway, model, TLS, response-signature, and image-provenance evidence for requests sent through cloud-api.near.ai." +description: "Verify the gateway, selected model, connection, response, and software evidence for requests sent through cloud-api.near.ai." --- -Use this flow when your application sends requests to `https://cloud-api.near.ai`. The request path includes the NEAR AI Cloud gateway and can include evidence for a selected model. +Use this guide when your application sends inference requests to `https://cloud-api.near.ai`. If your application sends requests directly to a model's `completions.near.ai` endpoint, use the [direct model endpoint guide](/cloud/verification/direct) instead. -The relevant verification fields in a NEAR AI Cloud gateway response are: - -```text -gateway_attestation -model_attestations[] # returned when model evidence is available -tls_certificate # optional -``` - -`gateway_attestation` and `model_attestations[]` are separate pieces of evidence. Verify the gateway when you need evidence for the NEAR AI Cloud gateway request path; verify a model attestation when you also need evidence for the model environment. - - - `model_attestations[]` is not an inventory of every healthy model instance. Do not use its length, or the presence of one entry, as evidence that a later request will be served by that same instance. - - -## Recommended flow - -1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request a NEAR AI Cloud gateway attestation report, including `model=` when model evidence is needed. -3. Verify `gateway_attestation` and apply your gateway measurement policy. -4. Verify the applicable item in `model_attestations[]` using the verifier and policy for its provider. -5. If you need endpoint binding, verify the TLS connection to `cloud-api.near.ai` against the **gateway** attestation. -6. If you need response integrity, verify the response signature and match its signer to the appropriate gateway or model attestation. -7. Apply an image-provenance policy to each measured configuration your policy requires. +## What you can verify - Verify the gateway quote and signer binding. + Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - Verify provider model evidence returned in `model_attestations[]`. + When your policy requires it, verify model evidence returned with the gateway report. - Bind the live gateway TLS connection to `gateway_attestation`. + Verify that your live HTTPS connection terminates at the attested gateway. - Match a response signature to its gateway or model signer. + Verify that a signed response came from the attested gateway or model environment. - Apply provenance policy to measured gateway and model configurations. + Check that the software images in the attested environment meet your source and build policy. -## Get a fresh report - -```bash -NONCE="$(openssl rand -hex 32)" - -curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ - --data-urlencode 'model=' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' -``` - -Use the same `signing_algo` as the response signature you intend to verify. Add `include_tls_fingerprint=true` only when following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). - -## Provider-specific model evidence - -The NEAR AI Cloud gateway can return model evidence from different provider tiers. The outer `model_attestations[]` field does not make their internal report formats interchangeable. +## Start with gateway attestation -This guide describes the NEAR AI Cloud gateway envelope and the NEAR-operated model flow. When your integration selects a different provider, use that provider's report format and verification policy; do not apply a NEAR-specific signer or measurement rule to an unfamiliar report shape. +1. Generate a fresh nonce for this verification attempt. +2. Request a fresh gateway attestation report. +3. Verify the gateway's hardware attestation and apply your acceptance policy. +4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. -The report request accepts `provider=near` or `provider=chutes` when your integration needs to select a provider tier. Those choices can return different model-evidence formats, so make the selection part of your verification policy. +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 17cb2d1..bda34aa 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,11 +1,9 @@ --- title: "Model Attestations" -description: "Verify model evidence returned in the NEAR AI Cloud gateway's model_attestations array." +description: "Verify evidence for the model environment selected for a request through the NEAR AI Cloud gateway." --- -Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. - -The NEAR AI Cloud gateway returns model evidence in `model_attestations[]`; this is different from the direct endpoint's root model report and `all_attestations[]` field. Treat the gateway envelope and each provider report as separate contracts. +Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. Model evidence is returned with the gateway attestation report. Its format and verification requirements can vary by provider, so use the guidance for the provider your application selected. ## Request model evidence @@ -22,11 +20,11 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response always has `gateway_attestation`; model evidence, when available, is carried in `model_attestations[]`. +The response includes gateway evidence. When model evidence is available, it is carried in `model_attestations[]`. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. These provider tiers can use different model-report formats and verification rules, so select one only when it is covered by your policy. +Model providers can use different report formats and verification rules. Apply only the requirements documented for the provider your application uses; treat an unfamiliar report format as unverified under your policy. ## Verify the applicable entry @@ -36,7 +34,7 @@ The NEAR AI Cloud gateway also accepts `provider=near` or `provider=chutes`. The 4. Apply your own allowlist and provenance policy to the verified measurements. - Do not assume every `model_attestations[]` entry has the same fields, or that the array lists every model CVM behind a model URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + An entry in `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 5013956..c6f2404 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -3,13 +3,11 @@ title: "Deployment Control Evidence" description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." --- -A direct model attestation response can include an optional top-level `compose_manager_attestation`. This is separate deployment-control evidence; it is not a model-attestation entry and is not part of the NEAR AI Cloud gateway attestation contract. - -The current payload records deployment-control actions and related integrity data. It can be useful for deployment investigation, but it has a narrower purpose than a model quote or a response signature. +Some direct model reports include optional deployment-control evidence in `compose_manager_attestation`. It can help investigate deployment operations, but it does not verify an individual inference request. ## What it can and cannot establish -Use it as supplementary evidence about deployment-control activity. Do not use it alone to conclude that: +Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: - a particular project, model, or backend is currently running; - every recorded action completed successfully; diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 7106588..b7dee3f 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -1,52 +1,35 @@ --- title: "Verify Direct Model Endpoints" -description: "Verify model, TLS, response-signature, and image-provenance evidence for direct completions endpoints." +description: "Verify the model, connection, response, and software evidence for direct completions endpoints." --- -Use this flow when your application sends requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. +Use this guide when your application sends inference requests directly to `https://.completions.near.ai`. The NEAR AI Cloud gateway is not in this request path. If your application sends requests to `cloud-api.near.ai`, use the [NEAR AI Cloud gateway guide](/cloud/verification/cloud-api) instead. -The direct attestation response is a model report at the response root. It includes fields such as `model_name`, `signing_address`, `signing_algo`, `request_nonce`, and `intel_quote`, and also includes `all_attestations[]`. - - - `all_attestations[]` is not a fleet inventory. Do not infer that it enumerates every healthy CVM, or that a later completion will be served by the same instance as an earlier report. - - -## Recommended flow - -1. Generate a fresh 32-byte nonce in the client for each verification attempt. -2. Request the report from the same direct model endpoint used for inference. -3. Verify the model's quote, nonce, signer binding, GPU evidence when present, and accepted measurements. -4. If you need endpoint binding, verify the TLS connection to the direct model endpoint against that model report. -5. If you need response integrity, verify the response signature and match it to the model report's signer. -6. Apply image-provenance policy to the measured model configuration. +## What you can verify - Verify the direct endpoint's root model report and `all_attestations[]` semantics. + Verify the trusted model environment reported by the direct endpoint. - Bind the live direct-model TLS connection to its model quote. + Verify that your live HTTPS connection terminates at the attested model endpoint. - Verify a direct-model response signature against the matching model signer. + Verify that a signed response came from the attested model environment. - Apply provenance policy to the direct model's measured configuration. + Check that the software images in the attested environment meet your source and build policy. - Interpret optional Compose Manager evidence without treating it as request-to-CVM binding. + Review optional deployment-management evidence separately from verification of an individual request. -## Get a fresh report - -```bash -NONCE="$(openssl rand -hex 32)" +## Start with model attestation -curl --fail-with-body -G 'https://.completions.near.ai/v1/attestation/report' \ - --data-urlencode 'signing_algo=ecdsa' \ - --data-urlencode "nonce=${NONCE}" \ - -H 'Accept: application/json' -``` +1. Generate a fresh nonce for this verification attempt. +2. Request a fresh report from the same direct endpoint your application uses for inference. +3. Verify the model's hardware attestation and apply your acceptance policy. +4. Add TLS, response-signature, or image-provenance checks when your application requires them. -The endpoint hostname identifies the model, so this request does not use a `model` parameter. Add `include_tls_fingerprint=true` only when following the [direct TLS flow](/cloud/verification/direct/tls). If you are matching a response signature, use its `signing_address` and `signing_algo` when requesting the applicable report. +Start with [Model attestation report](/cloud/verification/direct/model-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index f5fdf9f..6186a88 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -3,7 +3,7 @@ title: "Model Attestation Report" description: "Verify the root model report returned by a direct completions endpoint." --- -Use this page for a direct `https://.completions.near.ai` endpoint. This response is not the NEAR AI Cloud gateway's `gateway_attestation` / `model_attestations[]` envelope. +Use this page to verify the model environment serving a direct `https://.completions.near.ai` endpoint. ## Request a fresh report @@ -26,7 +26,7 @@ The root response is the model report. Relevant fields include: | Field | Meaning | | --- | --- | -| `model_name` | The model name configured for this direct endpoint. | +| `model_name` | The model name stated in this report. | | `signing_address`, `signing_algo` | The model signing identity and algorithm. | | `request_nonce` | The nonce echoed for diagnostics; verify it inside the quote. | | `intel_quote` | The model's Intel TDX quote. | diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 0ecc389..76aa253 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -3,7 +3,7 @@ title: "Response Signatures" description: "Verify an available direct-model response signature against exact bytes and the matching model report." --- -Use this flow for a completion sent directly to a model endpoint. The direct signature response has `text`, `signature`, `signing_address`, and `signing_algo`; it does not use the NEAR AI Cloud gateway's `signature_kind` routing field. +Use this flow for a completion sent directly to a model endpoint. When a signature is available, it lets you verify that the exact response bytes came from the attested model environment. A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. @@ -41,6 +41,6 @@ Use the `model_name` reported by the direct model attestation. A different strin Request or select a direct model attestation whose `signing_address` and `signing_algo` exactly match the signature. Verify its quote, client-generated nonce, signer binding, and required workload policy as described in [direct model attestation](/cloud/verification/direct/model-attestation). -Do not infer a gateway/model classification from the format of the signed text, and do not substitute a report for a different signer. The direct endpoint's `all_attestations[]` field is not proof that every instance behind the hostname has been attested. +Do not infer a gateway or model classification from the format of the signed text, and do not substitute a report for a different signer. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx index 681dc18..5e789d8 100644 --- a/cloud/verification/gateway.mdx +++ b/cloud/verification/gateway.mdx @@ -3,8 +3,8 @@ title: "Gateway Attestation" description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." --- -This page has moved into the NEAR AI Cloud gateway verification flow. +This guide is now part of the NEAR AI Cloud gateway verification flow. - Open the route-specific guide for `gateway_attestation`. + Verify the trusted environment that receives requests through `cloud-api.near.ai`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index a7abc7d..7f549bc 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,43 +1,36 @@ --- title: "Verification" -description: "Choose the NEAR AI Cloud gateway or direct-model verification flow, then verify the evidence for that request path." +description: "Choose the verification guide that matches the URL your application uses for inference." --- -NEAR AI Cloud has two verification paths. They use different endpoints, response envelopes, and signer relationships. Select the path your application actually uses before interpreting an attestation report. +Use these guides to verify the evidence returned for your AI requests. Start with the URL your application sends requests to. + +## Choose your request path - Verify requests sent to cloud-api.near.ai, including its gateway attestation and any returned model attestations. + Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when available, model evidence returned with the report. - Verify requests sent directly to a model's completions.near.ai endpoint, without the NEAR AI Cloud gateway in the request path. + Use this when your application connects directly to a model's `completions.near.ai` endpoint. Verify the model environment reported by that endpoint. -## Why the paths are separate - -| | NEAR AI Cloud gateway | Direct model endpoint | -| --- | --- | --- | -| Request host | `cloud-api.near.ai` | `.completions.near.ai` | -| Attestation envelope | `gateway_attestation` and optional `model_attestations[]` | A model report at the response root, plus `all_attestations[]` | -| Gateway evidence | Available as `gateway_attestation` | Not in the request path | -| Response signature routing | `signature_kind` identifies a gateway or model-serving signer when present | Match the signature to the direct model report's signer | + + Choose the guide from the request URL in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the path it used. + -Do not rename or map these fields as if they were aliases. In particular, `model_attestations[]` is a NEAR AI Cloud gateway response field; it is not the direct endpoint's `all_attestations[]` field. +## What happens next -## Shared requirements +After you choose a path, its guide walks you through obtaining fresh attestation evidence and verifying the environment that handled the request. Depending on your application's policy, you can also verify the TLS connection, signed responses, and software-image provenance. -Both paths require a client-generated nonce, a verified Intel TDX quote, a signer binding, and an explicit acceptance policy. The path guides show where to find those values; the reference pages explain how to evaluate them. +## Common verification concepts - Verify fresh evidence from the quote rather than trusting echoed HTTP fields. + Learn how fresh attestation evidence is tied to your request and to a signing identity. - Define required checks, handling for unavailable evidence, and image-provenance policy. + Decide which checks, measurements, and source-provenance requirements your application accepts. - - - A report for one connection or one instance is not automatically evidence for another. For a TLS conclusion, obtain the peer certificate and report on the same TLS connection. For a signed response, match its signer and algorithm to the applicable attestation. - diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 5a1b5d4..a126551 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -3,13 +3,13 @@ title: "Choose a Model Attestation Flow" description: "Select the verification guide that matches the endpoint your application calls." --- -The report structure depends on the endpoint, so select the guide for the request path your application uses. +Choose the guide from the URL your application uses for inference. - Use `model_attestations[]` returned by cloud-api.near.ai. + Use this when your request goes through `cloud-api.near.ai` and you need to verify the selected model environment. - Use the model report returned by a direct completions endpoint. + Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx index 62768c0..700a62c 100644 --- a/cloud/verification/provenance.mdx +++ b/cloud/verification/provenance.mdx @@ -1,15 +1,15 @@ --- title: "Choose an Image-Provenance Flow" -description: "Select the provenance guide for the attestation envelope your application uses." +description: "Select the provenance guide that matches the endpoint your application uses." --- -The configuration to inspect is carried by different attestation envelopes for the NEAR AI Cloud gateway and direct model endpoints. Select the matching path before evaluating image digests. +Choose the guide from the URL your application uses for inference. - Evaluate configuration measurements from the gateway and any returned model attestations. + Use this when your application sends requests through `cloud-api.near.ai`. - Evaluate the measured configuration from the direct model report. + Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx index 3828f2a..b89278d 100644 --- a/cloud/verification/tls.mdx +++ b/cloud/verification/tls.mdx @@ -7,9 +7,9 @@ TLS evidence applies to a particular request path and connection. Select the end - Verify the TLS connection to cloud-api.near.ai against `gateway_attestation`. + Use this when your application connects to `cloud-api.near.ai`. - Verify the TLS connection to a direct model endpoint against its model report. + Use this when your application connects directly to a model's `completions.near.ai` endpoint. From 302d32568195d463a4a88e5c8faa2f69d98fdc93 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 10:35:06 +0800 Subject: [PATCH 06/23] docs: refer to inference endpoints --- cloud/verification/index.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 7f549bc..a24c85f 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -1,9 +1,9 @@ --- title: "Verification" -description: "Choose the verification guide that matches the URL your application uses for inference." +description: "Choose the verification guide that matches the inference endpoint your application calls." --- -Use these guides to verify the evidence returned for your AI requests. Start with the URL your application sends requests to. +Use these guides to verify the evidence returned for your AI requests. Start with the inference endpoint your application calls. ## Choose your request path @@ -17,7 +17,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Choose the guide from the request URL in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the path it used. + Choose the guide from the inference endpoint in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the endpoint it used. ## What happens next From 53cef116604b264b64d766e74edfa86f22166654 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 12:55:19 +0800 Subject: [PATCH 07/23] docs: remove redundant direct report caveat --- cloud/verification/direct/model-attestation.mdx | 2 -- cloud/verification/reference/quote-nonce-signer.mdx | 2 -- 2 files changed, 4 deletions(-) diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 6186a88..e66d141 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -35,8 +35,6 @@ The root response is the model report. Relevant fields include: | `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | | `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | -The direct response does not define a top-level `report_data` field. Read `report_data` and measurements from the verified TDX quote. - ## Verify the report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index f8a7bd8..33ee70f 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -23,8 +23,6 @@ Send this value with the attestation request. Reject a report unless the verifie | NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | -The direct report does not expose a top-level `report_data` field. Always obtain report data and measurements from the verified quote itself. - ## Verify the signer binding For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. From ee99e67afe5332ec639c21ae0afb4be828aada4f Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:01:17 +0800 Subject: [PATCH 08/23] docs: clarify safe provenance support details --- cloud/verification/cloud-api/image-provenance.mdx | 6 +++++- cloud/verification/direct/image-provenance.mdx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index ed60b72..b982276 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -31,4 +31,8 @@ Do not assume every model report returned through the NEAR AI Cloud gateway expo An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. -Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. When requesting help, include the endpoint, UTC time, requested model, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. + + + When requesting help, share only non-sensitive verification metadata: the endpoint, UTC time, requested model, signing address, `info.instance_id` when present, image name, and full digest. Never include API keys, prompts, or response content. + diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 7af46ef..26c8a6c 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -25,4 +25,8 @@ The quote and configuration measurement establish that the configuration text yo An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. -Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. When requesting help, include the endpoint, UTC time, model name, signing address and `info.instance_id` when present, image name, and full digestβ€”never API keys, prompts, or response content. +Fetch a fresh report after a deployment change. If the current evidence still lacks provenance required by your policy, treat that image as unverified. + + + When requesting help, share only non-sensitive verification metadata: the endpoint, UTC time, model name, signing address, `info.instance_id` when present, image name, and full digest. Never include API keys, prompts, or response content. + From 6f5fe2f169589e26381a97dee9dcad9c2de03b29 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:04:01 +0800 Subject: [PATCH 09/23] docs: remove unsupported provenance caveat --- cloud/verification/cloud-api/image-provenance.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index b982276..52aeeaa 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -23,10 +23,6 @@ For a NEAR AI Cloud gateway flow, decide separately whether your policy requires A tag, search result, or HTTP `200` only locates a possible record. It does not cryptographically verify the provenance statement, its signer, or its subject digest. -## Provider boundaries - -Do not assume every model report returned through the NEAR AI Cloud gateway exposes configuration evidence in the same format. Apply this flow only where the provider's report format and your verifier establish the configuration-to-quote binding. An unfamiliar or incomplete report is not equivalent to a verified configuration. - ## Missing evidence An unavailable provenance record, including an HTTP `404`, means the reported digest could not be verified under that policy. It does not by itself identify a security incident. From 2e6fc4c7b80ca90eb33453e42e7fa1e3cae4d9de Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 13:41:47 +0800 Subject: [PATCH 10/23] docs: scope gateway model verification to NEAR --- cloud/guides/e2ee-chat-completions.mdx | 16 +++---- .../cloud-api/gateway-attestation.mdx | 6 +-- .../cloud-api/image-provenance.mdx | 4 +- cloud/verification/cloud-api/index.mdx | 4 +- .../cloud-api/model-attestations.mdx | 42 ++++++++++++++----- .../cloud-api/response-signatures.mdx | 12 ++++-- .../verification/direct/image-provenance.mdx | 2 +- .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 6 ++- cloud/verification/direct/tls.mdx | 2 + cloud/verification/model.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 8 ++-- .../reference/verification-policy.mdx | 4 +- 13 files changed, 72 insertions(+), 38 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 44dbbb1..e036c35 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -129,7 +129,7 @@ def v2_decrypt(data: bytes, secret_x25519: bytes) -> bytes: attestation = requests.get( f"{ENDPOINT}/v1/attestation/report", headers={"Authorization": f"Bearer {API_KEY}"}, - params={"model": MODEL, "signing_algo": "ed25519"}, + params={"model": MODEL, "provider": "near", "signing_algo": "ed25519"}, ).json() model_ed25519_pub = bytes.fromhex(attestation["model_attestations"][0]["signing_public_key"]) model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) @@ -187,7 +187,7 @@ The gateway endpoint requires an API key (`Authorization: Bearer `); re ```bash -curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ed25519" \ +curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&provider=near&signing_algo=ed25519" \ -H 'Authorization: Bearer ' ``` @@ -211,6 +211,7 @@ response = requests.get( headers={"Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}"}, params={ "model": "zai-org/GLM-5.1-FP8", + "provider": "near", "signing_algo": "ed25519" } ) @@ -235,10 +236,11 @@ print(f"Model Ed25519 public key: {model_public_key_hex}") ```javascript const response = await fetch( 'https://cloud-api.near.ai/v1/attestation/report?' + - new URLSearchParams({ - model: 'zai-org/GLM-5.1-FP8', - signing_algo: 'ed25519' - }), + new URLSearchParams({ + model: 'zai-org/GLM-5.1-FP8', + provider: 'near', + signing_algo: 'ed25519' + }), { headers: { 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, @@ -254,7 +256,7 @@ console.log('Model Ed25519 public key:', modelPublicKeyHex); -For the NEAR AI Cloud gateway, the model key is in `model_attestations[0].signing_public_key`. For a direct completions endpoint, it is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, request `provider=near` before reading `model_attestations[0].signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 47a3750..584232c 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` only when you also need model evidence, and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. Add `model=` and `provider=near` only when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Verify the gateway report @@ -27,7 +27,7 @@ For `gateway_attestation`: 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. 2. Read `report_data` and measurements from the **verified quote**, not only from fields echoed in the HTTP response. -3. Check that the verified quote binds the nonce generated by your client, the reported `signing_address`, and its `signing_algo`. +3. Check that the verified quote binds the nonce generated by your client and the reported `signing_address`. Use `signing_algo` to interpret that identity and verify response signatures. 4. When a TLS fingerprint was requested, verify its binding as part of the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). 5. Apply your accepted measurement and image-provenance policy. @@ -40,7 +40,7 @@ For `gateway_attestation`: | `gateway_attestation.intel_quote` | The gateway's Intel TDX quote. | | `gateway_attestation.signing_address` and `.signing_algo` | The gateway signing identity and algorithm. | | `gateway_attestation.tls_cert_fingerprint` | The gateway TLS binding when requested. | -| `model_attestations[]` | Separate model evidence; do not use it as a substitute for the gateway quote. | +| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`; do not use it as a substitute for the gateway quote. | ## Freshness and failure handling diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 52aeeaa..5ce0048 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -8,13 +8,13 @@ Image provenance is separate from hardware attestation. A verified quote can bin For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; -- each applicable model report in `model_attestations[]`; or +- the NEAR model report in `model_attestations[]`, requested with `provider=near`; or - both. ## Verification flow 1. Verify the relevant Intel TDX quote first. -2. Obtain the configuration text from that report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +2. For a NEAR model report, use the raw `info.tcb_info.app_compose` string. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in the verified quote. 3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. 4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. 5. Apply your policy for repository, build identity, workflow, ref, and source revision. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index be17aa8..3b7ad58 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -11,8 +11,8 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - - When your policy requires it, verify model evidence returned with the gateway report. + + Request `provider=near` to verify the NEAR model report returned with the gateway report. Verify that your live HTTPS connection terminates at the attested gateway. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index bda34aa..e82ca8e 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -1,9 +1,9 @@ --- -title: "Model Attestations" -description: "Verify evidence for the model environment selected for a request through the NEAR AI Cloud gateway." +title: "NEAR Model Attestations" +description: "Verify the NEAR model-attestation report returned through the NEAR AI Cloud gateway." --- -Use this page when a request to `cloud-api.near.ai` needs evidence for a model environment in addition to the gateway. Model evidence is returned with the gateway attestation report. Its format and verification requirements can vary by provider, so use the guidance for the provider your application selected. +Use this page when a request to `cloud-api.near.ai` needs evidence for a NEAR-operated model environment in addition to the gateway. Request that report explicitly with `provider=near`. ## Request model evidence @@ -14,27 +14,43 @@ NONCE="$(openssl rand -hex 32)" curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ --data-urlencode 'model=' \ + --data-urlencode 'provider=near' \ --data-urlencode 'signing_algo=ecdsa' \ --data-urlencode "nonce=${NONCE}" \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence. When model evidence is available, it is carried in `model_attestations[]`. +The response includes gateway evidence and one NEAR model report in `model_attestations[]`. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. -Model providers can use different report formats and verification rules. Apply only the requirements documented for the provider your application uses; treat an unfamiliar report format as unverified under your policy. +## NEAR model report fields -## Verify the applicable entry +The NEAR report uses these fields: -1. Determine which provider's model evidence your request is using. -2. Verify the Intel TDX quote, nonce, signer binding, GPU evidence, and measurement according to that provider's verification rules. -3. When verifying a response signed by a model-serving TEE, request or select an entry whose `signing_address` **and** `signing_algo` match the response signature. -4. Apply your own allowlist and provenance policy to the verified measurements. +| Field | Purpose | +| --- | --- | +| `intel_quote` | Intel TDX evidence. | +| `event_log` | Runtime measurement log; replay it and match the result to the quote's RTMR3. | +| `request_nonce` | Echoed request nonce; confirm its binding from the verified quote. | +| `signing_address` and `signing_algo` | Model signing identity and its algorithm. | +| `signing_public_key` | Model public key for the E2EE flow; request `signing_algo=ed25519` for that flow. | +| `nvidia_payload` | NVIDIA GPU evidence, when present. | +| `info.tcb_info.app_compose` | Raw measured configuration string; hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | + +## Verify the NEAR model report + +1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. +2. Check the client-generated nonce and `signing_address` against the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. +4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. +5. When GPU evidence is present, verify `nvidia_payload` and require it to be bound to the same nonce. +6. When verifying a model-serving response signature, request a report whose `signing_address` and `signing_algo` match that signature. +7. Apply your own allowlist and provenance policy to the verified measurements. - An entry in `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS @@ -44,3 +60,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## Verify a signed response For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). + +## Other provider documentation + +Chutes documents its own evidence format in [TEE Evidence Verification](https://github.com/chutesai/chutes-api/blob/main/docs/tee-verification.md). diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index d00118f..302bceb 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -22,6 +22,8 @@ Do not parse and re-serialize JSON, normalize whitespace or line endings, omit s ## Fetch and verify the signature +Set `` to the `id` in the completion response whose exact bytes you retained. For the Responses API, use its returned `resp_…` ID. + ```bash curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ --data-urlencode 'signing_algo=ecdsa' \ @@ -29,11 +31,15 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ -H 'Authorization: Bearer ' ``` -Verify `signature` locally using the returned `signing_algo`, then compare `text` with the expected payload: +A `200` response containing `error_code` and `message` is an unavailable result, not a signature. + +For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. + +Then compare `text` with the expected payload: | `signature_kind` | Expected `text` | Required attestation match | | --- | --- | --- | -| `provider_tee` | `::` | An applicable `model_attestations[]` item with the same `signing_address` and `signing_algo`. | +| `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | | `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. @@ -42,6 +48,6 @@ For `provider_tee`, `` is the canonical model identifier in the signed ## Match the attestation -For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. Include `model=` for model evidence. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. For `provider_tee`, include `model=` and `provider=near` to obtain the NEAR model report. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 26c8a6c..3236385 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -8,7 +8,7 @@ Image provenance is separate from hardware attestation. A verified direct model ## Verification flow 1. Verify the direct model's Intel TDX quote first. -2. Obtain the configuration text from the direct report, calculate its configuration hash, and compare it with the configuration measurement in the verified quote. +2. Use the raw `info.tcb_info.app_compose` string from the direct report. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in the verified quote. 3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. 4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. 5. Apply your policy for repository, build identity, workflow, ref, and source revision. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index e66d141..902ecaa 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -38,7 +38,7 @@ The root response is the model report. Relevant fields include: ## Verify the report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce, signer identity, and signing algorithm against the verified quote. +2. Check the client-generated nonce and signer identity against the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. 3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. 4. Apply your accepted measurement policy to the verified configuration. 5. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 76aa253..caa657c 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -22,6 +22,8 @@ Do not parse and re-serialize JSON, normalize whitespace or line endings, omit s ## Fetch and verify the signature +Set `` to the `id` in the completion response whose exact bytes you retained. + ```bash curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ --data-urlencode 'signing_algo=ecdsa' \ @@ -29,7 +31,9 @@ curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/ -H 'Authorization: Bearer ' ``` -Verify `signature` locally using the returned `signing_algo`. Then require that `text` is exactly: +For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. + +Then require that `text` is exactly: ```text :: diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx index 311cbe0..eaacd68 100644 --- a/cloud/verification/direct/tls.mdx +++ b/cloud/verification/direct/tls.mdx @@ -26,6 +26,8 @@ This check applies to the direct model connection. It does not verify a connecti A command-line request can retrieve a report, but cannot by itself demonstrate that the report and peer certificate came from one TLS connection. +This result applies only to that open TLS connection. Send the inference request on it; if the client reconnects, repeat this flow before treating the new connection as bound. + ## Required handling | Result | Action when TLS endpoint binding is required | diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index a126551..5d2e7c4 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -7,7 +7,7 @@ Choose the guide from the URL your application uses for inference. - Use this when your request goes through `cloud-api.near.ai` and you need to verify the selected model environment. + Use this when your request goes through `cloud-api.near.ai`, then request `provider=near` to verify the NEAR model environment. Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 33ee70f..2ea0b6f 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -3,7 +3,7 @@ title: "Quote, Nonce, and Signer Bindings" description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." --- -Every verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote; they are not a replacement for quote verification. +Every NEAR verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote; they are not a replacement for quote verification. ## Use a client-generated nonce @@ -20,12 +20,12 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | | NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | -| NEAR AI Cloud gateway model evidence | The provider-specific item in `model_attestations[]` | +| NEAR AI Cloud gateway model evidence (`provider=near`) | The single model report in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | ## Verify the signer binding -For a NEAR-operated TLS-bound report, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. A provider can use a different report format, so verify the exact binding required by the selected endpoint and provider flow before accepting the report. +For a NEAR-operated report requested with `include_tls_fingerprint=true`, the verified quote binds the signer identity, TLS certificate fingerprint, and nonce. `signing_address` is algorithm-specific: @@ -42,4 +42,4 @@ When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for individual quote, GPU, TLS, signature, and configuration checks. Use it as an implementation reference, then enforce the acceptance policy required by your application. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides; the repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index c76f4b7..59733b8 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -19,7 +19,7 @@ At minimum, specify: ## Treat required checks as fail-closed -When a required check is missing, unavailable, malformed, mismatched, or fails verification, the result is unverified. Do not replace it with a report for a different signer, certificate, request, model instance, or provider. +When a required check is missing, unavailable, malformed, mismatched, or fails verification, the result is unverified. Do not replace it with a report for a different signer, certificate, request, or model instance. | Condition | Recommended result | | --- | --- | @@ -27,7 +27,7 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | TLS report or peer SPKI unavailable | Treat endpoint binding as unverified. | | Response signature unavailable after a bounded retry | Treat the response as unverified when signatures are required. | | Image-provenance record is missing or returns `404` | Treat that digest as unverified when provenance is required. | -| Provider report shape is not covered by your verifier | Treat the provider-specific property as unverified. | +| Required NEAR model report is unavailable or malformed | Treat the model property as unverified. | ## Load balancing and rotations From 6478d07a3d2ccf364bfbad30f16ad9cf95880a01 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 14:45:09 +0800 Subject: [PATCH 11/23] docs: clarify model-provider verification scope --- cloud/guides/e2ee-chat-completions.mdx | 2 ++ cloud/verification/cloud-api/image-provenance.mdx | 2 ++ cloud/verification/cloud-api/model-attestations.mdx | 4 ++++ cloud/verification/cloud-api/response-signatures.mdx | 2 ++ 4 files changed, 10 insertions(+) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index e036c35..247f6ff 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -8,6 +8,8 @@ icon: "message-square-lock" NEAR AI Cloud runs both the gateway and model inference inside Trusted Execution Environments (TEEs), providing strong hardware-level isolation. End-to-end encryption (E2EE) adds an additional layer of protection by encrypting your messages with the model's public key before they leave your machine. +This guide uses the NEAR model E2EE protocol. The gateway sends client-E2EE requests only to a compatible NEAR-serving path; if none is available, the request is unsupported rather than routed through a different E2EE protocol. + ## Why Use E2EE? With E2EE enabled, your messages are encrypted client-side using the model's public key, which is cryptographically bound to its TEE attestation. This provides: diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 5ce0048..80b9617 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -5,6 +5,8 @@ description: "Apply an image-provenance policy to configurations measured in NEA Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. +For model-level provenance through the gateway, this flow applies to the NEAR model report requested with `provider=near`. + For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation`; diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index e82ca8e..5f01e53 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -25,6 +25,10 @@ The response includes gateway evidence and one NEAR model report in `model_attes When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. + + `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. Treat a report as evidence for a completion only when its signer and algorithm match a `provider_tee` response signature. + + ## NEAR model report fields The NEAR report uses these fields: diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 302bceb..82a0627 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -42,6 +42,8 @@ Then compare `text` with the expected payload: | `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | | `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | +Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signature binds the response to the gateway, not to a model report. If a model signature is unavailable, model-level response binding is unavailable. + If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. For `provider_tee`, `` is the canonical model identifier in the signed payload. Do not assume it is an alias originally supplied by the client. From 1c53da6aed55795e1a1a5cc354dffd77b276a1c4 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 15:05:14 +0800 Subject: [PATCH 12/23] docs: simplify verification entry guidance --- cloud/verification/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index a24c85f..eea7381 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -17,7 +17,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Choose the guide from the inference endpoint in your application configuration, not from the model name. If your application uses both request paths, verify each request with the guide for the endpoint it used. + Choose the guide from the inference endpoint in your application configuration. If your application uses both request paths, verify each request with the guide for the endpoint it used. ## What happens next From dfb96d2109ea85ac5301a7191f26c3e0c93ebeb5 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 15:15:13 +0800 Subject: [PATCH 13/23] docs: simplify model attestation navigation --- cloud/verification/cloud-api/index.mdx | 4 ++-- cloud/verification/model.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 3b7ad58..56de90b 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -11,8 +11,8 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. - - Request `provider=near` to verify the NEAR model report returned with the gateway report. + + Verify the environment serving the selected model. Verify that your live HTTPS connection terminates at the attested gateway. diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx index 5d2e7c4..701b66a 100644 --- a/cloud/verification/model.mdx +++ b/cloud/verification/model.mdx @@ -6,8 +6,8 @@ description: "Select the verification guide that matches the endpoint your appli Choose the guide from the URL your application uses for inference. - - Use this when your request goes through `cloud-api.near.ai`, then request `provider=near` to verify the NEAR model environment. + + Use this when your request goes through `cloud-api.near.ai`. Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. From 12277df472566b04c669799adc7f9044c145aac0 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 25 Aug 2026 16:24:53 +0800 Subject: [PATCH 14/23] docs: simplify verification prose --- .../verification/cloud-api/gateway-attestation.mdx | 4 ++-- cloud/verification/cloud-api/image-provenance.mdx | 8 ++++---- cloud/verification/cloud-api/index.mdx | 2 +- .../verification/cloud-api/model-attestations.mdx | 10 +++++----- cloud/verification/cloud-api/tls.mdx | 2 +- .../direct/deployment-control-evidence.mdx | 8 ++++---- cloud/verification/direct/image-provenance.mdx | 2 +- cloud/verification/direct/index.mdx | 2 +- cloud/verification/direct/model-attestation.mdx | 2 +- cloud/verification/direct/tls.mdx | 2 +- .../verification/reference/quote-nonce-signer.mdx | 4 ++-- .../verification/reference/verification-policy.mdx | 14 +++++++------- 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 584232c..1af9871 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -3,7 +3,7 @@ title: "Gateway Attestation" description: "Verify the NEAR AI Cloud gateway attestation for requests sent to cloud-api.near.ai." --- -`gateway_attestation` is evidence for the NEAR AI Cloud gateway. It is not evidence for a model environment; use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. +`gateway_attestation` is evidence for the NEAR AI Cloud gateway. It is not evidence for a model environment. Use [model attestations](/cloud/verification/cloud-api/model-attestations) when your policy also requires model evidence. ## Request gateway evidence @@ -40,7 +40,7 @@ For `gateway_attestation`: | `gateway_attestation.intel_quote` | The gateway's Intel TDX quote. | | `gateway_attestation.signing_address` and `.signing_algo` | The gateway signing identity and algorithm. | | `gateway_attestation.tls_cert_fingerprint` | The gateway TLS binding when requested. | -| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`; do not use it as a substitute for the gateway quote. | +| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`. Do not use it as a substitute for the gateway quote. | ## Freshness and failure handling diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 80b9617..19572ce 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -3,15 +3,15 @@ title: "Image Provenance" description: "Apply an image-provenance policy to configurations measured in NEAR AI Cloud gateway and model attestations." --- -Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. +Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime. Provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. For model-level provenance through the gateway, this flow applies to the NEAR model report requested with `provider=near`. For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: -- the configuration measured by `gateway_attestation`; -- the NEAR model report in `model_attestations[]`, requested with `provider=near`; or -- both. +- the configuration measured by `gateway_attestation` +- the NEAR model report in `model_attestations[]`, requested with `provider=near` +- both ## Verification flow diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index 56de90b..b3dd6d3 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -32,4 +32,4 @@ Use this guide when your application sends inference requests to `https://cloud- 3. Verify the gateway's hardware attestation and apply your acceptance policy. 4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. -Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 5f01e53..b123cf6 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -36,12 +36,12 @@ The NEAR report uses these fields: | Field | Purpose | | --- | --- | | `intel_quote` | Intel TDX evidence. | -| `event_log` | Runtime measurement log; replay it and match the result to the quote's RTMR3. | -| `request_nonce` | Echoed request nonce; confirm its binding from the verified quote. | +| `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | +| `request_nonce` | Echoed request nonce. Confirm its binding from the verified quote. | | `signing_address` and `signing_algo` | Model signing identity and its algorithm. | -| `signing_public_key` | Model public key for the E2EE flow; request `signing_algo=ed25519` for that flow. | +| `signing_public_key` | Model public key for the E2EE flow. Request `signing_algo=ed25519` for that flow. | | `nvidia_payload` | NVIDIA GPU evidence, when present. | -| `info.tcb_info.app_compose` | Raw measured configuration string; hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | +| `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | ## Verify the NEAR model report @@ -54,7 +54,7 @@ The NEAR report uses these fields: 7. Apply your own allowlist and provenance policy to the verified measurements. - `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm; a report from a different signer is not an equivalent substitute. + `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm. A report from a different signer is not an equivalent substitute. ## Relationship to NEAR AI Cloud gateway TLS diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index 708d6e5..30b345b 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -20,7 +20,7 @@ This is a gateway check. It does not bind the client connection to an upstream m 1. Generate a fresh 32-byte nonce in the client. 2. Open a TLS connection to `cloud-api.near.ai` using the trust configuration required by your application. 3. Read the peer certificate from that connection and calculate `SHA-256(SPKI_DER)`. -4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header; NEAR AI Cloud gateway report retrieval requires an API key. +4. Reuse the same open connection to request `/v1/attestation/report` with `include_tls_fingerprint=true`, the nonce, and the signing algorithm you require. Include the normal `Authorization: Bearer ` header. NEAR AI Cloud gateway report retrieval requires an API key. 5. Verify `gateway_attestation.intel_quote`, then check the nonce, gateway signer, and TLS-fingerprint binding in the verified quote. 6. Compare the calculated peer SPKI hash with `gateway_attestation.tls_cert_fingerprint`. diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index c6f2404..8a14263 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -9,10 +9,10 @@ Some direct model reports include optional deployment-control evidence in `compo Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: -- a particular project, model, or backend is currently running; -- every recorded action completed successfully; -- a later inference request was served by this same CVM; or -- the images involved satisfy your source and build-provenance policy. +- a particular project, model, or backend is currently running +- every recorded action completed successfully +- a later inference request was served by this same CVM +- the images involved satisfy your source and build-provenance policy The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. diff --git a/cloud/verification/direct/image-provenance.mdx b/cloud/verification/direct/image-provenance.mdx index 3236385..4acf9f4 100644 --- a/cloud/verification/direct/image-provenance.mdx +++ b/cloud/verification/direct/image-provenance.mdx @@ -3,7 +3,7 @@ title: "Image Provenance" description: "Apply an image-provenance policy to the configuration measured by a direct model attestation." --- -Image provenance is separate from hardware attestation. A verified direct model quote can bind a configuration measurement to a runtime; provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. +Image provenance is separate from hardware attestation. A verified direct model quote can bind a configuration measurement to a runtime. Provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. ## Verification flow diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index b7dee3f..87c0239 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -32,4 +32,4 @@ Use this guide when your application sends inference requests directly to `https 3. Verify the model's hardware attestation and apply your acceptance policy. 4. Add TLS, response-signature, or image-provenance checks when your application requires them. -Start with [Model attestation report](/cloud/verification/direct/model-attestation). That guide shows how to request fresh evidence; the other guides explain the additional checks. +Start with [Model attestation report](/cloud/verification/direct/model-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 902ecaa..5c89919 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -28,7 +28,7 @@ The root response is the model report. Relevant fields include: | --- | --- | | `model_name` | The model name stated in this report. | | `signing_address`, `signing_algo` | The model signing identity and algorithm. | -| `request_nonce` | The nonce echoed for diagnostics; verify it inside the quote. | +| `request_nonce` | The nonce echoed for diagnostics. Verify it inside the quote. | | `intel_quote` | The model's Intel TDX quote. | | `nvidia_payload` | GPU evidence, when present. | | `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | diff --git a/cloud/verification/direct/tls.mdx b/cloud/verification/direct/tls.mdx index eaacd68..0af2ab6 100644 --- a/cloud/verification/direct/tls.mdx +++ b/cloud/verification/direct/tls.mdx @@ -26,7 +26,7 @@ This check applies to the direct model connection. It does not verify a connecti A command-line request can retrieve a report, but cannot by itself demonstrate that the report and peer certificate came from one TLS connection. -This result applies only to that open TLS connection. Send the inference request on it; if the client reconnects, repeat this flow before treating the new connection as bound. +This result applies only to that open TLS connection. Send the inference request on it. If the client reconnects, repeat this flow before treating the new connection as bound. ## Required handling diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 2ea0b6f..eaf378d 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -3,7 +3,7 @@ title: "Quote, Nonce, and Signer Bindings" description: "Shared rules for reading verified quotes, checking freshness, and matching signing identities." --- -Every NEAR verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote; they are not a replacement for quote verification. +Every NEAR verification flow starts with a client-generated nonce and a verified quote. HTTP fields are inputs to compare with the quote. They are not a replacement for quote verification. ## Use a client-generated nonce @@ -42,4 +42,4 @@ When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides; the repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides. The repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 59733b8..13ff4fd 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -9,13 +9,13 @@ Attestation verifies particular cryptographic statements. Your application must At minimum, specify: -- acceptable Intel TDX verification status and advisory policy; -- required signer algorithm and identity matching rules; -- whether GPU evidence is required and the accepted verdict; -- accepted workload measurements and configuration rules; -- trusted image repositories, build identities, workflows, refs, and source revisions; -- maximum acceptable evidence age; and -- whether TLS endpoint binding and response signatures are required. +- acceptable Intel TDX verification status and advisory policy +- required signer algorithm and identity matching rules +- whether GPU evidence is required and the accepted verdict +- accepted workload measurements and configuration rules +- trusted image repositories, build identities, workflows, refs, and source revisions +- maximum acceptable evidence age +- whether TLS endpoint binding and response signatures are required ## Treat required checks as fail-closed From 7e070442eafa01ecc7f47efc84ab32512fca6802 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 2 Sep 2026 13:53:15 +0800 Subject: [PATCH 15/23] docs: align verification guides with latest flows --- cloud/guides/e2ee-chat-completions.mdx | 127 +++++++++++++++--- .../cloud-api/gateway-attestation.mdx | 13 +- .../cloud-api/model-attestations.mdx | 9 +- .../cloud-api/response-signatures.mdx | 16 ++- cloud/verification/cloud-api/tls.mdx | 6 + .../direct/deployment-control-evidence.mdx | 12 ++ .../verification/direct/model-attestation.mdx | 13 +- .../direct/response-signatures.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 13 +- cloud/verification/reference/sdk.mdx | 26 ++++ .../reference/verification-policy.mdx | 2 + docs.json | 1 + 12 files changed, 200 insertions(+), 40 deletions(-) create mode 100644 cloud/verification/reference/sdk.mdx diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 247f6ff..1afe8bb 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -15,7 +15,7 @@ This guide uses the NEAR model E2EE protocol. The gateway sends client-E2EE requ With E2EE enabled, your messages are encrypted client-side using the model's public key, which is cryptographically bound to its TEE attestation. This provides: - **Defense in depth** β€” Multiple independent encryption layers on top of TLS -- **Model-specific encryption** β€” Only the specific attested model instance can decrypt your messages +- **Model-specific encryption** β€” Only model environments holding the attested model key can decrypt your messages - **Cryptographic binding** β€” Messages are tied to a verified TEE through attestation - **Forward secrecy** β€” Each request uses ephemeral keys @@ -84,7 +84,7 @@ Wire format for all encrypted fields: `[ephemeral_pubkey (32 bytes)][nonce (24 b ## Quick Start -A self-contained Python script that performs the full E2EE flow: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. +A Python script that demonstrates the E2EE encryption flow: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. Verify the model report before using its key; the endpoint-specific attestation guides describe that check. **Requirements:** `pip install requests PyNaCl cryptography` @@ -126,14 +126,32 @@ def v2_decrypt(data: bytes, secret_x25519: bytes) -> bytes: key = HKDF(hashes.SHA256(), 32, None, b"ed25519_encryption").derive(shared) return crypto_aead_xchacha20poly1305_ietf_decrypt(ct, None, nonce, key) -# ── 1. Fetch model's Ed25519 public key ───────────────────────────── +# ── 1. Fetch the model's Ed25519 public key ───────────────────────── -attestation = requests.get( +nonce = secrets.token_hex(32) +attestation_response = requests.get( f"{ENDPOINT}/v1/attestation/report", - headers={"Authorization": f"Bearer {API_KEY}"}, - params={"model": MODEL, "provider": "near", "signing_algo": "ed25519"}, -).json() -model_ed25519_pub = bytes.fromhex(attestation["model_attestations"][0]["signing_public_key"]) + headers={ + "Authorization": f"Bearer {API_KEY}", + "x-no-aliasing": "true", + }, + params={ + "model": MODEL, + "provider": "near", + "signing_algo": "ed25519", + "include_tls_fingerprint": "false", + "nonce": nonce, + }, +) +attestation_response.raise_for_status() +attestation = attestation_response.json() +model_reports = attestation.get("model_attestations", []) +if len(model_reports) != 1: + raise ValueError("Expected exactly one NEAR model attestation") + +# Verify this report and `nonce` before using its signing_public_key: +# https://docs.near.ai/cloud/verification/cloud-api/model-attestations +model_ed25519_pub = bytes.fromhex(model_reports[0]["signing_public_key"]) model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) # ── 2. Generate client Ed25519 key pair ────────────────────────────── @@ -155,7 +173,9 @@ resp = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_pub_hex, + "X-Model-Pub-Key": model_ed25519_pub.hex(), "X-Encryption-Version": "2", + "x-no-aliasing": "true", }, json={ "model": MODEL, @@ -171,6 +191,9 @@ msg = resp.json()["choices"][0]["message"] if msg.get("reasoning_content"): print("Reasoning:", v2_decrypt(bytes.fromhex(msg["reasoning_content"]), client_x25519_secret).decode()) +if msg.get("reasoning"): + print("Reasoning:", v2_decrypt(bytes.fromhex(msg["reasoning"]), client_x25519_secret).decode()) + if msg.get("content"): print("Content:", v2_decrypt(bytes.fromhex(msg["content"]), client_x25519_secret).decode()) ``` @@ -181,7 +204,7 @@ if msg.get("content"): ### Step 1: Get the Model's Public Key -Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. +Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. Verify the quote and fresh nonce before using the key; see [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) for gateway requests or [direct model attestation](/cloud/verification/direct/model-attestation) for direct requests. The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. The direct completions endpoint is served by the model TEE itself and needs no API key. @@ -189,7 +212,15 @@ The gateway endpoint requires an API key (`Authorization: Bearer `); re ```bash -curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&provider=near&signing_algo=ed25519" \ +NONCE="$(openssl rand -hex 32)" + +curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=zai-org/GLM-5.1-FP8' \ + --data-urlencode 'provider=near' \ + --data-urlencode 'signing_algo=ed25519' \ + --data-urlencode 'include_tls_fingerprint=false' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'x-no-aliasing: true' \ -H 'Authorization: Bearer ' ``` @@ -198,7 +229,11 @@ curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8& ```bash # Direct completions β€” no model parameter needed, the endpoint serves one model -curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo=ed25519" +NONCE="$(openssl rand -hex 32)" + +curl -G 'https://qwen35-122b.completions.near.ai/v1/attestation/report' \ + --data-urlencode 'signing_algo=ed25519' \ + --data-urlencode "nonce=${NONCE}" ``` @@ -206,27 +241,39 @@ curl "https://qwen35-122b.completions.near.ai/v1/attestation/report?signing_algo ```python import os +import secrets import requests +nonce = secrets.token_hex(32) response = requests.get( "https://cloud-api.near.ai/v1/attestation/report", - headers={"Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}"}, + headers={ + "Authorization": f"Bearer {os.environ['NEAR_AI_CLOUD_API_KEY']}", + "x-no-aliasing": "true", + }, params={ "model": "zai-org/GLM-5.1-FP8", "provider": "near", - "signing_algo": "ed25519" + "signing_algo": "ed25519", + "include_tls_fingerprint": "false", + "nonce": nonce, } ) +response.raise_for_status() # For direct completions: # response = requests.get( # "https://qwen35-122b.completions.near.ai/v1/attestation/report", -# params={"signing_algo": "ed25519"} +# params={"signing_algo": "ed25519", "nonce": nonce} # ) attestation = response.json() # NEAR AI Cloud gateway: -model_public_key_hex = attestation["model_attestations"][0]["signing_public_key"] +model_reports = attestation.get("model_attestations", []) +if len(model_reports) != 1: + raise ValueError("Expected exactly one NEAR model attestation") +# Verify `model_reports[0]` and `nonce` before using this key. +model_public_key_hex = model_reports[0]["signing_public_key"] # Direct completions use the root model report instead: # model_public_key_hex = attestation["signing_public_key"] print(f"Model Ed25519 public key: {model_public_key_hex}") @@ -236,29 +283,40 @@ print(f"Model Ed25519 public key: {model_public_key_hex}") ```javascript +const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => + byte.toString(16).padStart(2, '0') +).join(''); + const response = await fetch( 'https://cloud-api.near.ai/v1/attestation/report?' + new URLSearchParams({ model: 'zai-org/GLM-5.1-FP8', provider: 'near', - signing_algo: 'ed25519' + signing_algo: 'ed25519', + include_tls_fingerprint: 'false', + nonce, }), { headers: { 'Authorization': `Bearer ${process.env.NEAR_AI_CLOUD_API_KEY}`, + 'x-no-aliasing': 'true', }, } ); +if (!response.ok) throw new Error(`Attestation request failed: ${response.status}`); const attestation = await response.json(); -const modelPublicKeyHex = attestation.model_attestations[0].signing_public_key; +const modelReports = attestation.model_attestations ?? []; +if (modelReports.length !== 1) throw new Error('Expected exactly one NEAR model attestation'); +// Verify `modelReports[0]` and `nonce` before using this key. +const modelPublicKeyHex = modelReports[0].signing_public_key; console.log('Model Ed25519 public key:', modelPublicKeyHex); ``` -For the NEAR AI Cloud gateway, request `provider=near` before reading `model_attestations[0].signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Require exactly one returned model report, verify it with the nonce you generated, then read `signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- @@ -428,6 +486,7 @@ curl https://cloud-api.near.ai/v1/chat/completions \ -H "X-Client-Pub-Key: YOUR_ED25519_PUBLIC_KEY_HEX" \ -H "X-Model-Pub-Key: MODEL_ED25519_PUBLIC_KEY_HEX" \ -H "X-Encryption-Version: 2" \ + -H "x-no-aliasing: true" \ -d '{ "model": "zai-org/GLM-5.1-FP8", "messages": [{ @@ -472,7 +531,9 @@ response = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_public_key_hex, + "X-Model-Pub-Key": model_public_key_hex, "X-Encryption-Version": "2", + "x-no-aliasing": "true", }, json={ "model": "zai-org/GLM-5.1-FP8", @@ -498,7 +559,9 @@ const response = await fetch('https://cloud-api.near.ai/v1/chat/completions', { 'Content-Type': 'application/json', 'X-Signing-Algo': 'ed25519', 'X-Client-Pub-Key': clientPublicKeyHex, + 'X-Model-Pub-Key': modelPublicKeyHex, 'X-Encryption-Version': '2', + 'x-no-aliasing': 'true', }, body: JSON.stringify({ model: 'zai-org/GLM-5.1-FP8', @@ -524,13 +587,14 @@ const data = await response.json(); | `X-Client-Pub-Key` | Your Ed25519 public key in hex format (32 bytes / 64 hex chars) | | `X-Encryption-Version` | Set to `2` | | `X-Model-Pub-Key` | Model's Ed25519 public key from attestation (required when using the gateway for routing) | +| `x-no-aliasing` | Set to `true` for gateway requests so the model ID and attested model key cannot be silently redirected. | | `X-Encrypt-All-Fields` | Optional. Set to `true` to extend encryption beyond message content to tool definitions, tool calls, and other sensitive fields β€” see [Encrypting All Fields](#encrypting-all-fields-tool-calling) | --- ### Step 5: Decrypt the Response -The response `content` and `reasoning_content` fields will contain hex-encoded encrypted data. Decrypt using your private key. +The response `content`, `reasoning_content`, and `reasoning` fields can contain hex-encoded encrypted data. Decrypt each field that is present using your private key. @@ -579,6 +643,9 @@ if msg.get("content"): if msg.get("reasoning_content"): print("Reasoning:", decrypt_response(msg["reasoning_content"], client_x25519_secret)) + +if msg.get("reasoning"): + print("Reasoning:", decrypt_response(msg["reasoning"], client_x25519_secret)) ``` @@ -620,6 +687,9 @@ if (msg.content) { if (msg.reasoning_content) { console.log('Reasoning:', decryptResponse(msg.reasoning_content, clientX25519Secret)); } +if (msg.reasoning) { + console.log('Reasoning:', decryptResponse(msg.reasoning, clientX25519Secret)); +} ``` @@ -659,7 +729,9 @@ resp = requests.post( "Content-Type": "application/json", "X-Signing-Algo": "ed25519", "X-Client-Pub-Key": client_pub_hex, + "X-Model-Pub-Key": model_ed25519_pub.hex(), "X-Encryption-Version": "2", + "x-no-aliasing": "true", "X-Encrypt-All-Fields": "true", }, json={ @@ -693,12 +765,12 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Message Format - Encrypted message content must be hex-encoded -- The `content` and `reasoning_content` fields in responses will be hex-encoded encrypted data +- The `content`, `reasoning_content`, and `reasoning` fields in responses can be hex-encoded encrypted data - Each streaming chunk's content is independently encrypted ### Verification -You can verify the model's public key is authentic by checking the attestation report against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). +Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -707,8 +779,18 @@ You can verify the model's public key is authentic by checking the attestation r ### Get the Model's ECDSA Public Key + Verify the returned report and its fresh nonce before using the public key, as described in [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). + ```bash - curl "https://cloud-api.near.ai/v1/attestation/report?model=zai-org/GLM-5.1-FP8&signing_algo=ecdsa" \ + NONCE="$(openssl rand -hex 32)" + + curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ + --data-urlencode 'model=zai-org/GLM-5.1-FP8' \ + --data-urlencode 'provider=near' \ + --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode 'include_tls_fingerprint=false' \ + --data-urlencode "nonce=${NONCE}" \ + -H 'x-no-aliasing: true' \ -H 'Authorization: Bearer ' ``` @@ -777,6 +859,7 @@ You can verify the model's public key is authentic by checking the attestation r -H "X-Signing-Algo: ecdsa" \ -H "X-Client-Pub-Key: YOUR_ECDSA_PUBLIC_KEY_HEX" \ -H "X-Model-Pub-Key: MODEL_ECDSA_PUBLIC_KEY_HEX" \ + -H "x-no-aliasing: true" \ -d '{ "model": "zai-org/GLM-5.1-FP8", "messages": [{"role": "user", "content": "ENCRYPTED_CONTENT_HEX"}], diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 1af9871..a8c1738 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -26,20 +26,25 @@ The response contains the gateway report at `gateway_attestation`. Add `model=' \ --data-urlencode 'provider=near' \ + --data-urlencode 'include_tls_fingerprint=false' \ --data-urlencode 'signing_algo=ecdsa' \ --data-urlencode "nonce=${NONCE}" \ -H 'Accept: application/json' \ + -H 'x-no-aliasing: true' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence and one NEAR model report in `model_attestations[]`. +The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. @@ -38,6 +40,7 @@ The NEAR report uses these fields: | `intel_quote` | Intel TDX evidence. | | `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | | `request_nonce` | Echoed request nonce. Confirm its binding from the verified quote. | +| `report_data` | HTTP copy of quote report data. Compare it with the verified quote. | | `signing_address` and `signing_algo` | Model signing identity and its algorithm. | | `signing_public_key` | Model public key for the E2EE flow. Request `signing_algo=ed25519` for that flow. | | `nvidia_payload` | NVIDIA GPU evidence, when present. | @@ -46,7 +49,7 @@ The NEAR report uses these fields: ## Verify the NEAR model report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce and `signing_address` against the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +2. Check the client-generated nonce and `signing_address` against the verified quote. Require `request_nonce` and `report_data` to match the client nonce and verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. 3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. 4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. 5. When GPU evidence is present, verify `nvidia_payload` and require it to be bound to the same nonce. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 82a0627..ab80948 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -13,12 +13,14 @@ Response signatures returned through the NEAR AI Cloud gateway can be produced b Build the request body once and retain the exact bytes sent by the client. Retain the raw response bytes before parsing JSON or Server-Sent Events. +To verify a `provider_tee` signature, send the original completion with the canonical model ID and `x-no-aliasing: true`. Do not send an alias and replace it after receiving the response: the model ID in the original request is part of the signed text. + ```text request_hash = SHA-256(exact request-body bytes) response_hash = SHA-256(exact response-body bytes) ``` -Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. Configure the HTTP client to retain the entity bytes it received; requesting `Accept-Encoding: identity` is the simplest way to avoid transparent decompression changing those bytes. ## Fetch and verify the signature @@ -26,12 +28,12 @@ Set `` to the `id` in the completion response whose exact bytes you ret ```bash curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode 'signing_algo=' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` -A `200` response containing `error_code` and `message` is an unavailable result, not a signature. +A `200` response containing `error_code` and `message` is an unavailable result, not a signature. A `404` means no matching signature was found; it can be retried for a short, bounded interval when the signature may still be recorded. Treat either result as unverified if a signature is required. For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. @@ -46,10 +48,14 @@ Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signatu If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. -For `provider_tee`, `` is the canonical model identifier in the signed payload. Do not assume it is an alias originally supplied by the client. +For `provider_tee`, `` is the canonical model identifier in the signed payload. ## Match the attestation -For the selected signer, request a fresh report with a client-generated nonce and the signature's `signing_address` and `signing_algo`. For `provider_tee`, include `model=` and `provider=near` to obtain the NEAR model report. Then verify the quote and apply the required measurement and provenance policy. A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +For `provider_tee`, request fresh model evidence with a client-generated nonce, `model=`, `provider=near`, `signing_address=`, and the signature's `signing_algo`. Then verify the quote and apply the required measurement and provenance policy. + +For `gateway`, request fresh gateway evidence with a client-generated nonce and the signature's `signing_algo`, then compare the signer returned by the report locally. Gateway evidence does not need a model or provider selector. + +A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/cloud-api/tls.mdx b/cloud/verification/cloud-api/tls.mdx index 30b345b..d81bf4e 100644 --- a/cloud/verification/cloud-api/tls.mdx +++ b/cloud/verification/cloud-api/tls.mdx @@ -9,6 +9,10 @@ Use this flow to establish that the HTTPS connection to `cloud-api.near.ai` term The peer certificate and attestation report must be obtained on the **same TLS connection**. Two independent connections can reach different instances or observe different certificates, so they cannot establish this connection-specific conclusion. + + Browser Fetch APIs do not expose the peer certificate or its SPKI. A browser client can verify a gateway quote without TLS binding, but cannot establish this TLS connection-binding property. + + ## What this checks When you request `include_tls_fingerprint=true`, `gateway_attestation.tls_cert_fingerprint` is the SHA-256 hash of the gateway certificate's SubjectPublicKeyInfo (SPKI). The verified quote binds that fingerprint, the gateway signing identity, and the nonce. @@ -26,6 +30,8 @@ This is a gateway check. It does not bind the client connection to an upstream m The optional `tls_certificate` response field is not a substitute for the peer certificate observed on the connection. A command-line report request alone also cannot establish the same-connection property. +This result applies to the connection used for the evidence request. When it must also cover an inference request, send that request over the same open TLS connection. If the client reconnects, repeat the flow. + ## Required handling | Result | Action when TLS endpoint binding is required | diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx index 8a14263..49051e7 100644 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ b/cloud/verification/direct/deployment-control-evidence.mdx @@ -16,6 +16,18 @@ Use it as supplementary evidence about deployment activity. Do not use it alone The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. +## Verify recorded actions + +When this optional evidence is required, verify it as a separate attestation flow: + +1. Generate a fresh client nonce and require it to equal `compose_manager_attestation.nonce`. +2. Serialize `actions` as compact UTF-8 JSON with every object key sorted, then require its SHA-256 hash to equal `actions_hash`. Preserve the array order. +3. Verify the nested `quote` with an Intel DCAP verifier and replay its `event_log` against the verified quote. +4. Require the reported `report_data` to equal the verified quote's report data, and require that value to be `actions_hash || client_nonce`. +5. For an action that names a source `commit`, `file`, and `file_sha256`, obtain that exact file revision from the expected source and require its SHA-256 hash to match. + +These checks establish that the verified Compose Manager quote bound the returned action record to your nonce. They do not add a project or model identity, a successful-operation outcome, or a binding to a later inference request. + ## Use it with independent checks When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 5c89919..38076db 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -29,8 +29,11 @@ The root response is the model report. Relevant fields include: | `model_name` | The model name stated in this report. | | `signing_address`, `signing_algo` | The model signing identity and algorithm. | | `request_nonce` | The nonce echoed for diagnostics. Verify it inside the quote. | +| `report_data` | Optional HTTP copy of quote report data. When present, compare it with the verified quote. | | `intel_quote` | The model's Intel TDX quote. | +| `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | | `nvidia_payload` | GPU evidence, when present. | +| `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | | `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | | `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | | `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | @@ -38,10 +41,12 @@ The root response is the model report. Relevant fields include: ## Verify the report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce and signer identity against the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. -3. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. -4. Apply your accepted measurement policy to the verified configuration. -5. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). +2. Check the client-generated nonce and signer identity against the verified quote. Read report data from the verified quote; require `request_nonce` to match the client nonce and, when `report_data` is present, require it to match the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. +3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. +4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. +5. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +6. Apply your accepted measurement policy to the verified configuration. +7. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). When a response signature is being verified, match both `signing_address` and `signing_algo`. A direct report for another signer is not a substitute, even when the hostname and model name are the same. diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index caa657c..1587cec 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -18,7 +18,7 @@ request_hash = SHA-256(exact request-body bytes) response_hash = SHA-256(exact response-body bytes) ``` -Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. +Do not parse and re-serialize JSON, normalize whitespace or line endings, omit stream delimiters, or reconstruct a streamed response from parsed events. Configure the HTTP client to retain the entity bytes it received; requesting `Accept-Encoding: identity` is the simplest way to avoid transparent decompression changing those bytes. ## Fetch and verify the signature diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index eaf378d..5c6d865 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -36,10 +36,21 @@ For a NEAR-operated report requested with `include_tls_fingerprint=true`, the ve Always compare both `signing_address` and `signing_algo`. Do not compare identities across algorithms. +## Read NEAR report data + +For NEAR-operated reports, quote report data is 64 bytes. The final 32 bytes are the client nonce. The first 32 bytes bind the signing identity: + +- Without TLS binding, they are the decoded signing identity, zero-padded on the right to 32 bytes. An ECDSA address occupies 20 bytes; an Ed25519 public key occupies all 32 bytes. +- With TLS binding, they are `SHA-256(decoded signing identity || TLS SPKI SHA-256)`. Compare the quote-bound fingerprint with the peer certificate observed on the same TLS connection. + +This layout applies to NEAR-operated dstack reports only. Compose Manager evidence and other providers have their own report-data contracts. + ## Verify GPU evidence when present When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains implementation examples for NEAR quote, GPU, and configuration processing. Use the report-request examples in these guides. The repository is not a complete Cloud API client. Follow the endpoint-specific guides for TLS binding, response signatures, and image provenance. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, direct TLS, and Compose Manager evidence. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. + +For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx new file mode 100644 index 0000000..1226ebf --- /dev/null +++ b/cloud/verification/reference/sdk.mdx @@ -0,0 +1,26 @@ +--- +title: "Verification SDK" +description: "Use the NEAR AI verification SDK to verify attestation evidence in your application." +--- + +The NEAR AI verification SDK provides language-specific implementations for NEAR AI Cloud gateway attestation, NEAR model evidence returned through the gateway, and response signatures. + +The SDK supports TypeScript, Python, and Rust. + +## Installation + +Installation instructions will be added when the SDK packages are published. + +## Using the SDK + +The SDK follows the same verification model as these guides: generate a fresh nonce, obtain the gateway report, verify its quote and bindings, then apply your application's policy. + +For the protocol requirements and trust decisions behind each check, start with the gateway guide: + + + Verify gateway and NEAR model evidence returned through `cloud-api.near.ai`. + + + + Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added as they become available. + diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 13ff4fd..84c79ca 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -10,6 +10,7 @@ Attestation verifies particular cryptographic statements. Your application must At minimum, specify: - acceptable Intel TDX verification status and advisory policy +- rejection of quotes with TDX debug mode enabled - required signer algorithm and identity matching rules - whether GPU evidence is required and the accepted verdict - accepted workload measurements and configuration rules @@ -24,6 +25,7 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | Condition | Recommended result | | --- | --- | | Quote or nonce mismatch | Reject the evidence and request a fresh report. | +| Quote has TDX debug mode enabled | Reject the evidence. | | TLS report or peer SPKI unavailable | Treat endpoint binding as unverified. | | Response signature unavailable after a bounded retry | Treat the response as unverified when signatures are required. | | Image-provenance record is missing or returns `404` | Treat that digest as unverified when provenance is required. | diff --git a/docs.json b/docs.json index 31f87f7..07ce875 100644 --- a/docs.json +++ b/docs.json @@ -84,6 +84,7 @@ { "group": "Reference", "pages": [ + "cloud/verification/reference/sdk", "cloud/verification/reference/quote-nonce-signer", "cloud/verification/reference/verification-policy" ] From 965d0f5b081d8ccb9e9455443dcf116d358d4b68 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 14:10:03 +0800 Subject: [PATCH 16/23] docs: sync OpenAPI from staging --- api-reference/openapi.json | 13434 ++++++++++++++++++++++------------- docs.json | 39 +- 2 files changed, 8416 insertions(+), 5057 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 1361579..42c9519 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -211,28 +211,18 @@ ] } }, - "/v1/admin/feature-requests": { + "/v1/admin/aml/allowlist": { "get": { "tags": [ - "Feature Requests" + "Admin" ], - "summary": "List aggregated feature requests for admins.", - "operationId": "list_admin_feature_requests", + "summary": "List AML allowlist entries (Admin only)", + "operationId": "list_aml_allowlist", "parameters": [ - { - "name": "kind", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] - } - }, { "name": "limit", "in": "query", + "description": "Maximum number of allowlist entries to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -242,6 +232,7 @@ { "name": "offset", "in": "query", + "description": "Number of allowlist entries to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -251,17 +242,17 @@ ], "responses": { "200": { - "description": "Aggregated feature requests", + "description": "AML allowlist entries retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminFeatureRequestListResponse" + "$ref": "#/components/schemas/ListAdminAmlAllowlistResponse" } } } }, "400": { - "description": "Invalid parameters", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -280,6 +271,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -296,102 +297,30 @@ "session_token": [] } ] - } - }, - "/v1/admin/invitation-email-deliveries": { - "get": { + }, + "post": { "tags": [ "Admin" ], - "summary": "List organization invitation email deliveries (Admin only)", - "description": "Returns delivery metadata for organization invitation emails without exposing invitation tokens.", - "operationId": "list_invitation_email_deliveries", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number of deliveries to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of deliveries to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "organization_id", - "in": "query", - "description": "Filter by organization ID", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "recipient_email", - "in": "query", - "description": "Case-insensitive recipient email substring filter", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "email_status", - "in": "query", - "description": "Filter by email delivery status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationEmailStatus" - } - }, - { - "name": "invitation_status", - "in": "query", - "description": "Filter by invitation status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationStatus" - } - }, - { - "name": "created_after", - "in": "query", - "description": "Only invitations created at or after this timestamp", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "summary": "Add or update an AML allowlist entry (Admin only)", + "operationId": "upsert_aml_allowlist_entry", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertAmlAllowlistEntryRequest" + } } }, - { - "name": "created_before", - "in": "query", - "description": "Only invitations created at or before this timestamp", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - } - ], + "required": true + }, "responses": { "200": { - "description": "Invitation email deliveries retrieved successfully", + "description": "AML allowlist entry upserted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" + "$ref": "#/components/schemas/AdminAmlAllowlistEntryResponse" } } } @@ -416,6 +345,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -434,38 +373,40 @@ ] } }, - "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { - "post": { + "/v1/admin/aml/allowlist/{account_id}": { + "delete": { "tags": [ "Admin" ], - "summary": "Resend a single organization invitation email (Admin only)", - "operationId": "resend_invitation_email", + "summary": "Remove an AML allowlist entry (Admin only)", + "operationId": "delete_aml_allowlist_entry", "parameters": [ { - "name": "invitation_id", + "name": "account_id", "in": "path", - "description": "Invitation ID", + "description": "NEAR account ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "200": { - "description": "Invitation email resend attempted", + "204": { + "description": "AML allowlist entry removed successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invitation is not pending or has expired", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -474,8 +415,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -485,7 +426,7 @@ } }, "404": { - "description": "Invitation not found", + "description": "AML allowlist entry not found", "content": { "application/json": { "schema": { @@ -512,19 +453,18 @@ ] } }, - "/v1/admin/models": { + "/v1/admin/aml/reports": { "get": { "tags": [ "Admin" ], - "summary": "List all models (Admin only)", - "description": "Returns a paginated list of all models in the system. By default, only active models are returned.\nUse `include_inactive=true` to also include disabled models.", - "operationId": "list_models", + "summary": "List AML reports (Admin only)", + "operationId": "list_aml_reports", "parameters": [ { "name": "limit", "in": "query", - "description": "Maximum number of models to return (default: 100)", + "description": "Maximum number of reports to return (default: 100)", "required": false, "schema": { "type": "integer", @@ -534,30 +474,31 @@ { "name": "offset", "in": "query", - "description": "Number of models to skip (default: 0)", + "description": "Number of reports to skip (default: 0)", "required": false, "schema": { "type": "integer", "format": "int64" } - }, - { - "name": "include_inactive", - "in": "query", - "description": "Whether to include inactive (disabled) models (default: false)", - "required": false, - "schema": { - "type": "boolean" - } } ], "responses": { "200": { - "description": "Models retrieved successfully", + "description": "AML reports retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminModelListResponse" + "$ref": "#/components/schemas/ListAdminAmlReportsResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -572,6 +513,16 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -588,19 +539,31 @@ "session_token": [] } ] - }, + } + }, + "/v1/admin/aml/reports/{report_id}/status": { "patch": { "tags": [ "Admin" ], - "summary": "Batch upsert models metadata (Admin only)", - "description": "Upserts (inserts or updates) pricing and metadata for one or more models. Only authenticated admins can perform this operation.\nThe body should be an array of objects where each key is a model name and the value is the model data.", - "operationId": "batch_upsert_models", + "summary": "Update AML report active status (Admin only)", + "operationId": "update_aml_report_status", + "parameters": [ + { + "name": "report_id", + "in": "path", + "description": "AML report UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HashMap" + "$ref": "#/components/schemas/UpdateAmlReportStatusRequest" } } }, @@ -608,14 +571,11 @@ }, "responses": { "200": { - "description": "Models upserted successfully", + "description": "AML report status updated successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelWithPricing" - } + "$ref": "#/components/schemas/AdminAmlReportResponse" } } } @@ -640,6 +600,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "AML report not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -658,37 +638,57 @@ ] } }, - "/v1/admin/models/deprecate": { - "post": { + "/v1/admin/feature-requests": { + "get": { "tags": [ - "Admin" + "Feature Requests" + ], + "summary": "List aggregated feature requests for admins.", + "operationId": "list_admin_feature_requests", + "parameters": [ + { + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } ], - "summary": "Deprecate a model in favor of another (Admin only)", - "description": "Atomically marks `modelId` as deprecated and routes its traffic to\n`successorModelId`:\n1. Adds `modelId` as an alias of `successorModelId`, so existing clients\n sending `model: \"\"` keep working β€” the alias resolver rewrites\n the request-side `model` field to the successor before backend dispatch,\n and the response's `model` field reflects the canonical (successor) name.\n2. Re-points any pre-existing inbound aliases of `modelId` at the\n successor, so historical aliases keep resolving.\n3. Sets `modelId.isActive = false` so it is hidden from public\n `GET /v1/models` and from `GET /v1/admin/models` unless\n `include_inactive=true`.\n4. Records a `model_history` entry for audit purposes.\n\nAll steps run in a single DB transaction. If the successor is inactive or\neither model is missing, returns 404 without modifying state.", - "operationId": "deprecate_model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeprecateModelRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Model deprecated successfully", + "description": "Aggregated feature requests", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeprecateModelResponse" + "$ref": "#/components/schemas/AdminFeatureRequestListResponse" } } } }, "400": { - "description": "Invalid request (e.g. self-deprecation, empty model id)", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -707,16 +707,6 @@ } } }, - "404": { - "description": "Either model not found, or successor is not active", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -735,27 +725,29 @@ ] } }, - "/v1/admin/models/pricing-changes": { + "/v1/admin/invitation-email-deliveries": { "get": { "tags": [ "Admin" ], - "summary": "List scheduled pricing changes (Admin only).", - "operationId": "list_model_pricing_changes", + "summary": "List organization invitation email deliveries (Admin only)", + "description": "Returns delivery metadata for organization invitation emails without exposing invitation tokens.", + "operationId": "list_invitation_email_deliveries", "parameters": [ { - "name": "status", + "name": "limit", "in": "query", - "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", + "description": "Maximum number of deliveries to return (default: 100)", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "limit", + "name": "offset", "in": "query", - "description": "Maximum number of changes to return (default: 100)", + "description": "Number of deliveries to skip (default: 0)", "required": false, "schema": { "type": "integer", @@ -763,23 +755,70 @@ } }, { - "name": "offset", + "name": "organization_id", "in": "query", - "description": "Number of changes to skip (default: 0)", + "description": "Filter by organization ID", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" + } + }, + { + "name": "recipient_email", + "in": "query", + "description": "Case-insensitive recipient email substring filter", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "email_status", + "in": "query", + "description": "Filter by email delivery status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationEmailStatus" + } + }, + { + "name": "invitation_status", + "in": "query", + "description": "Filter by invitation status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationStatus" + } + }, + { + "name": "created_after", + "in": "query", + "description": "Only invitations created at or after this timestamp", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_before", + "in": "query", + "description": "Only invitations created at or before this timestamp", + "required": false, + "schema": { + "type": "string", + "format": "date-time" } } ], "responses": { "200": { - "description": "Scheduled pricing changes", + "description": "Invitation email deliveries retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListPricingChangesResponse" + "$ref": "#/components/schemas/ListAdminInvitationEmailDeliveriesResponse" } } } @@ -822,37 +861,38 @@ ] } }, - "/v1/admin/models/pricing-changes/confirm": { + "/v1/admin/invitation-email-deliveries/{invitation_id}/resend": { "post": { "tags": [ "Admin" ], - "summary": "Confirm a batch of scheduled pricing changes and notify affected admins (Admin only).", - "description": "Persists the schedule (the background scheduler applies each change at its\neffective date) and sends one consolidated email per affected recipient.", - "operationId": "confirm_model_pricing_changes", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" - } + "summary": "Resend a single organization invitation email (Admin only)", + "operationId": "resend_invitation_email", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Pricing changes scheduled and notifications attempted", + "description": "Invitation email resend attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeConfirmResponse" + "$ref": "#/components/schemas/AdminInvitationEmailResendResultResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invitation is not pending or has expired", "content": { "application/json": { "schema": { @@ -872,7 +912,7 @@ } }, "404": { - "description": "Model not found or inactive", + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -881,8 +921,76 @@ } } }, - "409": { - "description": "A pending pricing change already exists for a model", + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/admin/models": { + "get": { + "tags": [ + "Admin" + ], + "summary": "List all models (Admin only)", + "description": "Returns a paginated list of all models in the system. By default, only active models are returned.\nUse `include_inactive=true` to also include disabled models.", + "operationId": "list_models", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of models to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of models to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "include_inactive", + "in": "query", + "description": "Whether to include inactive (disabled) models (default: false)", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Models retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminModelListResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -907,20 +1015,19 @@ "session_token": [] } ] - } - }, - "/v1/admin/models/pricing-changes/preview": { - "post": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Preview a batch of scheduled pricing changes without mutating state (Admin only).", - "operationId": "preview_model_pricing_changes", + "summary": "Batch upsert models metadata (Admin only)", + "description": "Upserts (inserts or updates) pricing and metadata for one or more models. Only authenticated admins can perform this operation.\nThe body should be an array of objects where each key is a model name and the value is the model data.", + "operationId": "batch_upsert_models", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangeBatchRequest" + "$ref": "#/components/schemas/HashMap" } } }, @@ -928,11 +1035,14 @@ }, "responses": { "200": { - "description": "Pricing change notification preview", + "description": "Models upserted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PricingChangePreviewResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelWithPricing" + } } } } @@ -957,16 +1067,6 @@ } } }, - "404": { - "description": "Model not found or inactive", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -985,32 +1085,41 @@ ] } }, - "/v1/admin/models/pricing-changes/{id}": { - "delete": { + "/v1/admin/models/deprecate": { + "post": { "tags": [ "Admin" ], - "summary": "Cancel a pending scheduled pricing change (Admin only).", - "operationId": "cancel_model_pricing_change", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Scheduled pricing change ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Deprecate a model in favor of another (Admin only)", + "description": "Atomically marks `modelId` as deprecated and routes its traffic to\n`successorModelId`:\n1. Adds `modelId` as an alias of `successorModelId`, so existing clients\n sending `model: \"\"` keep working β€” the alias resolver rewrites\n the request-side `model` field to the successor before backend dispatch,\n and the response's `model` field reflects the canonical (successor) name.\n2. Re-points any pre-existing inbound aliases of `modelId` at the\n successor, so historical aliases keep resolving.\n3. Sets `modelId.isActive = false` so it is hidden from public\n `GET /v1/models` and from `GET /v1/admin/models` unless\n `include_inactive=true`.\n4. Records a `model_history` entry for audit purposes.\n\nAll steps run in a single DB transaction. If the successor is inactive or\neither model is missing, returns 404 without modifying state.", + "operationId": "deprecate_model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeprecateModelRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Pricing change cancelled", + "description": "Model deprecated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ScheduledPricingChangeDto" + "$ref": "#/components/schemas/DeprecateModelResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. self-deprecation, empty model id)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1026,7 +1135,7 @@ } }, "404": { - "description": "Pricing change not found or no longer pending", + "description": "Either model not found, or successor is not active", "content": { "application/json": { "schema": { @@ -1053,41 +1162,57 @@ ] } }, - "/v1/admin/models/{model_name}": { - "delete": { + "/v1/admin/models/pricing-changes": { + "get": { "tags": [ "Admin" ], - "summary": "Delete a model (Admin only)", - "description": "Soft deletes a model by setting is_active to false. This preserves historical usage records\nthat reference the model name while preventing it from being used in new requests.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", - "operationId": "delete_model", + "summary": "List scheduled pricing changes (Admin only).", + "operationId": "list_model_pricing_changes", "parameters": [ { - "name": "model_name", - "in": "path", - "description": "Model name to delete (URL-encode if it contains slashes)", - "required": true, + "name": "status", + "in": "query", + "description": "Filter by status: pending, applying, applied, cancelled, failed. Omit for all.", + "required": false, "schema": { "type": "string" } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of changes to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of changes to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteModelRequest" + "responses": { + "200": { + "description": "Scheduled pricing changes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListPricingChangesResponse" + } } } }, - "required": true - }, - "responses": { - "204": { - "description": "Model deleted successfully" - }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -1096,8 +1221,8 @@ } } }, - "404": { - "description": "Model not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -1124,28 +1249,19 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/confirm": { + "/v1/admin/models/pricing-changes/confirm": { "post": { "tags": [ "Admin" ], - "summary": "Confirm a planned model deprecation, update the catalog, and notify affected admins.", - "operationId": "confirm_model_deprecation", - "parameters": [ - { - "name": "model_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Confirm a batch of scheduled pricing changes and notify affected admins (Admin only).", + "description": "Persists the schedule (the background scheduler applies each change at its\neffective date) and sends one consolidated email per affected recipient.", + "operationId": "confirm_model_pricing_changes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/PricingChangeBatchRequest" } } }, @@ -1153,11 +1269,11 @@ }, "responses": { "200": { - "description": "Deprecation confirmed and notifications attempted", + "description": "Pricing changes scheduled and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" + "$ref": "#/components/schemas/PricingChangeConfirmResponse" } } } @@ -1183,7 +1299,17 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Model not found or inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "A pending pricing change already exists for a model", "content": { "application/json": { "schema": { @@ -1210,28 +1336,18 @@ ] } }, - "/v1/admin/models/{model_name}/deprecation/preview": { + "/v1/admin/models/pricing-changes/preview": { "post": { "tags": [ "Admin" ], - "summary": "Preview affected admins for a planned model deprecation (Admin only).", - "operationId": "preview_model_deprecation", - "parameters": [ - { - "name": "model_name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "summary": "Preview a batch of scheduled pricing changes without mutating state (Admin only).", + "operationId": "preview_model_pricing_changes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationRequest" + "$ref": "#/components/schemas/PricingChangeBatchRequest" } } }, @@ -1239,11 +1355,11 @@ }, "responses": { "200": { - "description": "Deprecation notification preview", + "description": "Pricing change notification preview", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" + "$ref": "#/components/schemas/PricingChangePreviewResponse" } } } @@ -1269,7 +1385,7 @@ } }, "404": { - "description": "Model or successor not found", + "description": "Model not found or inactive", "content": { "application/json": { "schema": { @@ -1296,52 +1412,32 @@ ] } }, - "/v1/admin/models/{model_name}/history": { - "get": { + "/v1/admin/models/pricing-changes/{id}": { + "delete": { "tags": [ "Admin" ], - "summary": "Get complete history for a model (Admin only)", - "description": "Returns the complete history for a specific model, showing all changes over time including pricing,\ncontext length, display name, and description.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", - "operationId": "get_model_history", + "summary": "Cancel a pending scheduled pricing change (Admin only).", + "operationId": "cancel_model_pricing_change", "parameters": [ { - "name": "model_name", + "name": "id", "in": "path", - "description": "Model name to get complete history for (URL-encode if it contains slashes)", + "description": "Scheduled pricing change ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of history entries to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of history entries to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Model history retrieved successfully", + "description": "Pricing change cancelled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelHistoryResponse" + "$ref": "#/components/schemas/ScheduledPricingChangeDto" } } } @@ -1357,7 +1453,7 @@ } }, "404": { - "description": "Model not found", + "description": "Pricing change not found or no longer pending", "content": { "application/json": { "schema": { @@ -1384,37 +1480,39 @@ ] } }, - "/v1/admin/organizations/{org_id}": { - "get": { + "/v1/admin/models/{model_name}": { + "delete": { "tags": [ "Admin" ], - "summary": "Get a single organization by id (Admin only)", - "description": "Returns one organization with its spend limit and usage. Only authenticated\nadmins can perform this operation. Returns 404 if the organization does not\nexist or is inactive (consistent with the admin organizations list, which\nhides inactive orgs).", - "operationId": "get_organization", + "summary": "Delete a model (Admin only)", + "description": "Soft deletes a model by setting is_active to false. This preserves historical usage records\nthat reference the model name while preventing it from being used in new requests.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", + "operationId": "delete_model", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID", + "description": "Model name to delete (URL-encode if it contains slashes)", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "responses": { - "200": { - "description": "Organization retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminOrganizationResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteModelRequest" } } }, + "required": true + }, + "responses": { + "204": { + "description": "Model deleted successfully" + }, "401": { "description": "Unauthorized", "content": { @@ -1426,7 +1524,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1453,32 +1551,50 @@ ] } }, - "/v1/admin/organizations/{org_id}/concurrent-limit": { - "get": { + "/v1/admin/models/{model_name}/deprecation/confirm": { + "post": { "tags": [ "Admin" ], - "summary": "Get organization concurrent request limit (Admin only)", - "description": "Returns the current concurrent request limit for an organization.\nIf no custom limit is set, returns null for concurrent_limit and the default (64) for effective_limit.", - "operationId": "get_organization_concurrent_limit", + "summary": "Confirm a planned model deprecation, update the catalog, and notify affected admins.", + "operationId": "confirm_model_deprecation", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelDeprecationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Concurrent limit retrieved successfully", + "description": "Deprecation confirmed and notifications attempted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" + "$ref": "#/components/schemas/ModelDeprecationConfirmResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1494,7 +1610,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1519,19 +1635,19 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/admin/models/{model_name}/deprecation/preview": { + "post": { "tags": [ "Admin" ], - "summary": "Update organization concurrent request limit (Admin only)", - "description": "Updates the maximum concurrent requests allowed per model for an organization.\nSet to null to use the default limit (64).\nChanges take effect within 5 minutes due to caching.", - "operationId": "update_organization_concurrent_limit", + "summary": "Preview affected admins for a planned model deprecation (Admin only).", + "operationId": "preview_model_deprecation", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" @@ -1542,7 +1658,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" + "$ref": "#/components/schemas/ModelDeprecationRequest" } } }, @@ -1550,11 +1666,11 @@ }, "responses": { "200": { - "description": "Concurrent limit updated successfully", + "description": "Deprecation notification preview", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" + "$ref": "#/components/schemas/ModelDeprecationPreviewResponse" } } } @@ -1580,7 +1696,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model or successor not found", "content": { "application/json": { "schema": { @@ -1607,52 +1723,52 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits": { - "patch": { + "/v1/admin/models/{model_name}/history": { + "get": { "tags": [ "Admin" ], - "summary": "Update organization limits (Admin only)", - "description": "Updates spending limits for a specific organization. This endpoint is typically called by\na billing service with an admin API key when a customer makes a purchase.", - "operationId": "update_organization_limits", + "summary": "Get complete history for a model (Admin only)", + "description": "Returns the complete history for a specific model, showing all changes over time including pricing,\ncontext length, display name, and description.\n\n**Note:** Model names containing forward slashes (e.g., \"Qwen/Qwen3-30B-A3B-Instruct-2507\") must be URL-encoded.\nFor example, use \"Qwen%2FQwen3-30B-A3B-Instruct-2507\" in the URL path.", + "operationId": "get_model_history", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID to update limits for", + "description": "Model name to get complete history for (URL-encode if it contains slashes)", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" - } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of history entries to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "required": true - }, + { + "name": "offset", + "in": "query", + "description": "Number of history entries to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], "responses": { "200": { - "description": "Organization limits updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Model history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ModelHistoryResponse" } } } @@ -1668,7 +1784,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -1695,52 +1811,33 @@ ] } }, - "/v1/admin/organizations/{org_id}/limits/history": { + "/v1/admin/organizations/{org_id}": { "get": { "tags": [ "Admin" ], - "summary": "Get limits history for an organization (Admin only)", - "description": "Returns the complete limits history for a specific organization, showing all limits changes over time.\nGet limits history for an organization (Admin only)\n\nReturns the complete limits history for a specific organization, showing all limits changes over time.", - "operationId": "get_organization_limits_history", + "summary": "Get a single organization by id (Admin only)", + "description": "Returns one organization with its spend limit and usage. Only authenticated\nadmins can perform this operation. Returns 404 if the organization does not\nexist or is inactive (consistent with the admin organizations list, which\nhides inactive orgs).", + "operationId": "get_organization", "parameters": [ { "name": "org_id", "in": "path", - "description": "The organization's ID (as a UUID)", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of history records to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of records to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Limits history retrieved successfully", + "description": "Organization retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgLimitsHistoryResponse" + "$ref": "#/components/schemas/AdminOrganizationResponse" } } } @@ -1783,63 +1880,32 @@ ] } }, - "/v1/admin/organizations/{org_id}/members": { + "/v1/admin/organizations/{org_id}/concurrent-limit": { "get": { "tags": [ "Admin" ], - "summary": "List members of a specific organization (Admin only)", - "description": "Returns the members of the given organization with full user details\n(email, last login, active status), consistent with `/v1/admin/users`.\nOnly authenticated admins can perform this operation.", - "operationId": "list_organization_members", + "summary": "Get organization concurrent request limit (Admin only)", + "description": "Returns the current concurrent request limit for an organization.\nIf no custom limit is set, returns null for concurrent_limit and the default (64) for effective_limit.", + "operationId": "get_organization_concurrent_limit", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum number of members to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of members to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "Organization members retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Concurrent limit retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/GetOrganizationConcurrentLimitResponse" } } } @@ -1880,48 +1946,45 @@ "session_token": [] } ] - } - }, - "/v1/admin/organizations/{org_id}/metrics": { - "get": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Get organization metrics (Admin only)", - "description": "Returns usage metrics for an organization including summary totals,\nand breakdowns by workspace, API key, and model.", - "operationId": "get_organization_metrics", + "summary": "Update organization concurrent request limit (Admin only)", + "description": "Updates the maximum concurrent requests allowed per model for an organization.\nSet to null to use the default limit (64).\nChanges take effect within 5 minutes due to caching.", + "operationId": "update_organization_concurrent_limit", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID to get metrics for", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } - }, - { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Organization metrics retrieved successfully" + "description": "Concurrent limit updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationConcurrentLimitResponse" + } + } + } }, "400": { "description": "Invalid request", @@ -1971,62 +2034,32 @@ ] } }, - "/v1/admin/organizations/{org_id}/metrics/timeseries": { + "/v1/admin/organizations/{org_id}/fallback": { "get": { "tags": [ "Admin" ], - "summary": "Get time series metrics for an organization (Admin only)", - "description": "Returns daily/weekly/hourly aggregations for charting:\nrequests, tokens, and cost per time period.", - "operationId": "get_organization_timeseries", + "summary": "Get an organization's effective fallback policy (Admin only).", + "operationId": "get_organization_fallback", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID to get metrics for", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "granularity", - "in": "query", - "description": "Time granularity: hour, day (default), or week", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Time series metrics retrieved successfully" - }, - "400": { - "description": "Invalid request", + "description": "Fallback policy retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationFallbackResponse" } } } @@ -2050,16 +2083,6 @@ } } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, "security": [ @@ -2067,16 +2090,13 @@ "session_token": [] } ] - } - }, - "/v1/admin/organizations/{org_id}/staking/farm": { - "get": { + }, + "patch": { "tags": [ "Admin" ], - "summary": "Get admin organization staking farm state", - "description": "Returns the staking farm source and last synced farm-credit state for any\norganization. Requires platform admin access.", - "operationId": "get_admin_organization_staking_farm", + "summary": "Update an organization's fallback policy (Admin only).", + "operationId": "update_organization_fallback", "parameters": [ { "name": "org_id", @@ -2084,27 +2104,28 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], - "responses": { - "200": { - "description": "Organization staking farm state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationFallbackRequest" } } }, - "400": { - "description": "Invalid organization ID", + "required": true + }, + "responses": { + "200": { + "description": "Fallback policy updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationFallbackResponse" } } } @@ -2119,28 +2140,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "404": { - "description": "No staking farm source found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2157,38 +2158,48 @@ ] } }, - "/v1/admin/organizations/{org_id}/staking/farm/sync": { - "post": { + "/v1/admin/organizations/{org_id}/limits": { + "patch": { "tags": [ "Admin" ], - "summary": "Sync admin organization staking farm credits", - "description": "Refreshes staking farm reward units and derived credits for any linked\norganization. Requires platform admin access.", - "operationId": "sync_admin_organization_staking_farm", + "summary": "Update organization limits (Admin only)", + "description": "Updates spending limits for a specific organization. This endpoint is typically called by\na billing service with an admin API key when a customer makes a purchase.", + "operationId": "update_organization_limits", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "Organization ID to update limits for", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationLimitsRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Synced organization staking farm state", + "description": "Organization limits updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/UpdateOrganizationLimitsResponse" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -2207,18 +2218,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "404": { - "description": "No staking farm source found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2245,42 +2246,52 @@ ] } }, - "/v1/admin/organizations/{org_id}/usage/balance": { + "/v1/admin/organizations/{org_id}/limits/history": { "get": { "tags": [ "Admin" ], - "summary": "Get organization balance (Admin only)", - "description": "Returns the current spending balance for an organization without requiring\nthe caller to be a member of that organization. Intended for trusted\nautomated billing services.", - "operationId": "get_admin_organization_balance", + "summary": "Get limits history for an organization (Admin only)", + "description": "Returns the complete limits history for a specific organization, showing all limits changes over time.\nGet limits history for an organization (Admin only)\n\nReturns the complete limits history for a specific organization, showing all limits changes over time.", + "operationId": "get_organization_limits_history", "parameters": [ { "name": "org_id", "in": "path", - "description": "Organization ID", + "description": "The organization's ID (as a UUID)", "required": true, "schema": { "type": "string" } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of history records to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of records to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } ], "responses": { "200": { - "description": "Organization balance", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Limits history retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrgLimitsHistoryResponse" } } } @@ -2296,7 +2307,7 @@ } }, "404": { - "description": "Not found", + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -2323,21 +2334,63 @@ ] } }, - "/v1/admin/platform/billing-summary": { + "/v1/admin/organizations/{org_id}/members": { "get": { "tags": [ "Admin" ], - "summary": "Get the platform billing summary (Admin only)", - "description": "Credit LIMITS (caps) and consumption β€” NOT payments/cash. Returns active paid/grant\ncredit limits, total consumed, paying/granted org counts, and a breakdown by funding\nsource. Real money-in lives in the billing service, not cloud-api.", - "operationId": "get_billing_summary", + "summary": "List members of a specific organization (Admin only)", + "description": "Returns the members of the given organization with full user details\n(email, last login, active status), consistent with `/v1/admin/users`.\nOnly authenticated admins can perform this operation.", + "operationId": "list_organization_members", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of members to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of members to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], "responses": { "200": { - "description": "Billing summary retrieved successfully", + "description": "Organization members retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingSummary" + "$ref": "#/components/schemas/ListAdminOrganizationMembersResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2352,6 +2405,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2370,21 +2433,53 @@ ] } }, - "/v1/admin/platform/infra-summary": { + "/v1/admin/organizations/{org_id}/metrics": { "get": { "tags": [ "Admin" ], - "summary": "Get the platform infrastructure / fleet burn summary (Admin only)", - "description": "Fetches the live host list, counts active/idle hosts, and computes the monthly/daily\nGPU burn rate from the configured cost-per-host. Degrades gracefully (stale=true) if\nthe host inventory is unreachable.", - "operationId": "get_infra_summary", + "summary": "Get organization metrics (Admin only)", + "description": "Returns usage metrics for an organization including summary totals,\nand breakdowns by workspace, API key, and model.", + "operationId": "get_organization_metrics", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID to get metrics for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Infra summary retrieved successfully", + "description": "Organization metrics retrieved successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InfraSummary" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2399,6 +2494,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2417,15 +2522,24 @@ ] } }, - "/v1/admin/platform/metrics": { + "/v1/admin/organizations/{org_id}/metrics/timeseries": { "get": { "tags": [ "Admin" ], - "summary": "Get platform-wide metrics for admin dashboards (Admin only)", - "description": "Returns aggregated metrics across all organizations including:\n- Total users and organizations\n- Total requests and revenue\n- Top models by usage\n- Top organizations by spend", - "operationId": "get_platform_metrics", + "summary": "Get time series metrics for an organization (Admin only)", + "description": "Returns daily/weekly/hourly aggregations for charting:\nrequests, tokens, and cost per time period.", + "operationId": "get_organization_timeseries", "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID to get metrics for", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "start", "in": "query", @@ -2443,15 +2557,27 @@ "schema": { "type": "string" } + }, + { + "name": "granularity", + "in": "query", + "description": "Time granularity: hour, day (default), or week", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { "200": { - "description": "Platform metrics retrieved successfully", + "description": "Time series metrics retrieved successfully" + }, + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlatformMetrics" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2466,6 +2592,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2484,38 +2620,20 @@ ] } }, - "/v1/admin/platform/metrics/timeseries": { + "/v1/admin/organizations/{org_id}/staking/farm": { "get": { "tags": [ "Admin" ], - "summary": "Get platform-wide time series for admin dashboards (Admin only)", - "description": "Returns per-bucket requests, tokens, cost (paid/granted + verifiable/external splits),\nactive organizations, and new signups for growth/mix trend charts.", - "operationId": "get_platform_timeseries", + "summary": "Get admin organization staking farm state", + "description": "Returns the staking farm source and last synced farm-credit state for any\norganization. Requires platform admin access.", + "operationId": "get_admin_organization_staking_farm", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "granularity", - "in": "query", - "description": "Time granularity: hour, day (default), week, or month", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2523,17 +2641,17 @@ ], "responses": { "200": { - "description": "Platform time series retrieved successfully", + "description": "Organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2552,6 +2670,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "No staking farm source found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2570,76 +2708,20 @@ ] } }, - "/v1/admin/platform/model-revenue": { - "get": { + "/v1/admin/organizations/{org_id}/staking/farm/sync": { + "post": { "tags": [ "Admin" ], - "summary": "Get the per-model consumption ranking (Admin only)", - "description": "Models for the selected period ranked by consumed cost, with requests, tokens,\nunique orgs, verifiable flag, provider type, and latency. Paginated and filterable.", - "operationId": "get_model_revenue", + "summary": "Sync admin organization staking farm credits", + "description": "Refreshes staking farm reward units and derived credits for any linked\norganization. Requires platform admin access.", + "operationId": "sync_admin_organization_staking_farm", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size (1-1000, default 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Page offset (default 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "verifiable", - "in": "query", - "description": "Filter to verifiable (true) or non-verifiable (false) models", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "provider_type", - "in": "query", - "description": "Filter by provider type (e.g. vllm, external, chutes)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "sort", - "in": "query", - "description": "Sort: revenue (default), requests, tokens", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2647,17 +2729,17 @@ ], "responses": { "200": { - "description": "Model revenue retrieved successfully", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelRevenueReport" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -2676,6 +2758,26 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "No staking farm source found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2694,67 +2796,20 @@ ] } }, - "/v1/admin/platform/org-revenue": { + "/v1/admin/organizations/{org_id}/usage/balance": { "get": { "tags": [ "Admin" ], - "summary": "Get the per-organization consumption ranking (Admin only)", - "description": "Organizations with usage in the selected period ranked by consumed cost, with the\nverifiable/external split, requests, tokens, models used, a current paying flag, and\nlast-usage timestamp. Paginated and filterable β€” full attribution of usage/spend per org.", - "operationId": "get_org_revenue", + "summary": "Get organization balance (Admin only)", + "description": "Returns the current spending balance for an organization without requiring\nthe caller to be a member of that organization. Intended for trusted\nautomated billing services.", + "operationId": "get_admin_organization_balance", "parameters": [ { - "name": "start", - "in": "query", - "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End of time range (ISO 8601). Defaults to now.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size (1-1000, default 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Page offset (default 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "paying", - "in": "query", - "description": "Filter to current paying (true) / non-paying (false) orgs", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "sort", - "in": "query", - "description": "Sort: revenue (default), requests, tokens", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { "type": "string" } @@ -2762,11 +2817,11 @@ ], "responses": { "200": { - "description": "Org revenue retrieved successfully", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgRevenueReport" + "$ref": "#/components/schemas/OrganizationBalanceResponse" } } } @@ -2791,6 +2846,16 @@ } } }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2809,40 +2874,21 @@ ] } }, - "/v1/admin/services": { - "post": { + "/v1/admin/platform/billing-summary": { + "get": { "tags": [ "Admin" ], - "summary": "Create platform service (Admin only)", - "operationId": "create_service", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateServiceRequest" - } - } - }, - "required": true - }, + "summary": "Get the platform billing summary (Admin only)", + "description": "Credit LIMITS (caps) and consumption β€” NOT payments/cash. Returns active paid/grant\ncredit limits, total consumed, paying/granted org counts, and a breakdown by funding\nsource. Real money-in lives in the billing service, not cloud-api.", + "operationId": "get_billing_summary", "responses": { "200": { - "description": "Service created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Billing summary retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/BillingSummary" } } } @@ -2875,52 +2921,21 @@ ] } }, - "/v1/admin/services/{id}": { - "patch": { + "/v1/admin/platform/infra-summary": { + "get": { "tags": [ "Admin" ], - "summary": "Update platform service (Admin only; display_name, description, cost_per_unit, is_active)", - "operationId": "update_service", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Service ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateServiceRequest" - } - } - }, - "required": true - }, + "summary": "Get the platform infrastructure / fleet burn summary (Admin only)", + "description": "Fetches the live host list, counts active/idle hosts, and computes the monthly/daily\nGPU burn rate from the configured cost-per-host. Degrades gracefully (stale=true) if\nthe host inventory is unreachable.", + "operationId": "get_infra_summary", "responses": { "200": { - "description": "Service updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminServiceResponse" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Infra summary retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/InfraSummary" } } } @@ -2935,16 +2950,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -2963,66 +2968,28 @@ ] } }, - "/v1/admin/users": { + "/v1/admin/platform/metrics": { "get": { "tags": [ "Admin" ], - "summary": "List all registered users with pagination (Admin only)", - "description": "Returns a paginated list of all users in the system. Only authenticated admins can perform this operation.", - "operationId": "list_users", + "summary": "Get platform-wide metrics for admin dashboards (Admin only)", + "description": "Returns aggregated metrics across all organizations including:\n- Total users and organizations\n- Total requests and revenue\n- Top models by usage\n- Top organizations by spend", + "operationId": "get_platform_metrics", "parameters": [ { - "name": "limit", - "in": "query", - "description": "Maximum number of users to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of users to skip (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "include_organizations", - "in": "query", - "description": "Whether to include organization information and spend limits for the first organization owned by each user (default: false)", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "search", + "name": "start", "in": "query", - "description": "Filter users by email, username, display name, user id, auth provider, or provider user id (case-insensitive partial match).", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { "type": "string" } }, { - "name": "is_active", - "in": "query", - "description": "Filter users by active status. Omit to include active and inactive users.", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "search_by_name", + "name": "end", "in": "query", - "description": "Filter users by organization name (case-insensitive match). Only effective when include_organizations=true; separate from user search.", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { "type": "string" @@ -3031,11 +2998,11 @@ ], "responses": { "200": { - "description": "Users retrieved successfully", + "description": "Platform metrics retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListUsersResponse" + "$ref": "#/components/schemas/PlatformMetrics" } } } @@ -3068,137 +3035,180 @@ ] } }, - "/v1/attestation/ita-token": { + "/v1/admin/platform/metrics/timeseries": { "get": { "tags": [ - "Attestation" + "Admin" ], - "summary": "Get Intel Trust Authority attestation token", - "description": "Get Intel Trust Authority signed attestation JWTs for the gateway and, when\nrequested, compatible model provider evidence. Public endpoint β€” this is an\nexplicit, documented decision (nearai/infra#193): the response carries only\nITA-signed TEE evidence (no customer or key-scoped data), public access lets\nthird parties verify the platform without an account, and upstream ITA rate\nlimits bound abuse (propagated as 429). See\n`build_public_attestation_routes` in `routes/attestation.rs`.", - "operationId": "get_ita_token", + "summary": "Get platform-wide time series for admin dashboards (Admin only)", + "description": "Returns per-bucket requests, tokens, cost (paid/granted + verifiable/external splits),\nactive organizations, and new signups for growth/mix trend charts.", + "operationId": "get_platform_timeseries", "parameters": [ { - "name": "model", + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "nonce", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "signing_algo", + "name": "granularity", "in": "query", + "description": "Time granularity: hour, day (default), week, or month", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Platform time series retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformTimeSeriesMetrics" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ { - "name": "signing_address", + "session_token": [] + } + ] + } + }, + "/v1/admin/platform/model-revenue": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get the per-model consumption ranking (Admin only)", + "description": "Models for the selected period ranked by consumed cost, with requests, tokens,\nunique orgs, verifiable flag, provider type, and latency. Paginated and filterable.", + "operationId": "get_model_revenue", + "parameters": [ + { + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "include_tls_fingerprint", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "policy_ids", + "name": "limit", "in": "query", + "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "policy_must_match", + "name": "offset", "in": "query", + "description": "Page offset (default 0)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "token_signing_alg", + "name": "verifiable", + "in": "query", + "description": "Filter to verifiable (true) or non-verifiable (false) models", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "provider_type", "in": "query", + "description": "Filter by provider type (e.g. vllm, external, chutes)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort: revenue (default), requests, tokens", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "ITA attestation token retrieved", + "description": "Model revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItaTokenResponse" + "$ref": "#/components/schemas/ModelRevenueReport" } } } }, "400": { - "description": "Invalid parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "ITA rate limit exceeded", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "502": { - "description": "Bad ITA upstream response", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3207,8 +3217,8 @@ } } }, - "503": { - "description": "ITA attestation unavailable", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3217,8 +3227,8 @@ } } }, - "504": { - "description": "ITA request timed out", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3229,101 +3239,91 @@ } }, "security": [ - {} + { + "session_token": [] + } ] } }, - "/v1/attestation/report": { + "/v1/admin/platform/org-revenue": { "get": { "tags": [ - "Attestation" + "Admin" ], - "summary": "Get attestation report", - "description": "Get hardware attestation report for TEE verification. Requires an API key\n(nearai/infra#193); report retrieval is non-billable β€” no usage or billing\nrecords are created.", - "operationId": "get_attestation_report", + "summary": "Get the per-organization consumption ranking (Admin only)", + "description": "Organizations with usage in the selected period ranked by consumed cost, with the\nverifiable/external split, requests, tokens, models used, a current paying flag, and\nlast-usage timestamp. Paginated and filterable β€” full attribution of usage/spend per org.", + "operationId": "get_org_revenue", "parameters": [ { - "name": "model", + "name": "start", "in": "query", + "description": "Start of time range (ISO 8601). Defaults to 30 days ago.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "signing_algo", + "name": "end", "in": "query", + "description": "End of time range (ISO 8601). Defaults to now.", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { - "name": "nonce", + "name": "limit", "in": "query", + "description": "Page size (1-1000, default 100)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "signing_address", + "name": "offset", "in": "query", + "description": "Page offset (default 0)", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "integer", + "format": "int64" } }, { - "name": "include_tls_fingerprint", + "name": "paying", "in": "query", - "description": "Include TLS certificate fingerprint in the report data.\nDefaults to false; when true, report_data[..32] = SHA256(signing_address || cert_fingerprint).", + "description": "Filter to current paying (true) / non-paying (false) orgs", "required": false, "schema": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, { - "name": "provider", + "name": "sort", "in": "query", - "description": "Restrict the report to a specific serving tier.\nAccepted values: `near` (NEAR AI's own TEE fleet) or `chutes` (attested Chutes fallback).\nWhen omitted, the first successfully responding provider is used.", + "description": "Sort: revenue (default), requests, tokens", "required": false, "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } } ], "responses": { "200": { - "description": "Attestation report retrieved", + "description": "Org revenue retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AttestationResponse" + "$ref": "#/components/schemas/OrgRevenueReport" } } } }, "400": { - "description": "Invalid nonce format", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3333,7 +3333,7 @@ } }, "401": { - "description": "Missing or invalid API key", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3342,8 +3342,8 @@ } } }, - "503": { - "description": "Service unavailable", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3355,24 +3355,23 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/audio/transcriptions": { + "/v1/admin/services": { "post": { "tags": [ - "Audio" + "Admin" ], - "summary": "Audio transcription endpoint", - "description": "Transcribe audio files using Whisper models. Accepts audio file uploads via multipart/form-data.\nSupports MP3, WAV, WEBM, FLAC, OGG, and M4A formats. Maximum file size: 25 MB.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", - "operationId": "audio_transcriptions", + "summary": "Create platform service (Admin only)", + "operationId": "create_service", "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" + "$ref": "#/components/schemas/CreateServiceRequest" } } }, @@ -3380,22 +3379,17 @@ }, "responses": { "200": { - "description": "Successful transcription", + "description": "Service created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AudioTranscriptionResponse" - } - }, - "text/plain": { - "schema": { - "type": "string" + "$ref": "#/components/schemas/AdminServiceResponse" } } } }, "400": { - "description": "Invalid request (empty file, unsupported format, file too large)", + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -3405,27 +3399,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Model not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3435,7 +3409,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3447,24 +3421,35 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/billing/costs": { - "post": { + "/v1/admin/services/{id}": { + "patch": { "tags": [ - "Billing" + "Admin" + ], + "summary": "Update platform service (Admin only; display_name, description, cost_per_unit, is_active)", + "operationId": "update_service", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Service ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Get costs by request IDs", - "description": "Returns the cost in nano-USD for each request ID provided.\nThis endpoint is designed for HuggingFace billing integration.\n\nRequest IDs that are not found will be returned with costNanoUsd: 0", - "operationId": "get_billing_costs", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingCostsRequest" + "$ref": "#/components/schemas/UpdateServiceRequest" } } }, @@ -3472,11 +3457,11 @@ }, "responses": { "200": { - "description": "Costs retrieved successfully", + "description": "Service updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BillingCostsResponse" + "$ref": "#/components/schemas/AdminServiceResponse" } } } @@ -3501,6 +3486,16 @@ } } }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3514,139 +3509,90 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/chat/completions": { - "post": { + "/v1/admin/users": { + "get": { "tags": [ - "Chat" + "Admin" ], - "summary": "Create chat completion", - "description": "Generate AI model responses for chat conversations. Supports both streaming and non-streaming modes.\nOpenAI-compatible endpoint.", - "operationId": "chat_completions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Completion generated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChatCompletionResponse" - } - } + "summary": "List all registered users with pagination (Admin only)", + "description": "Returns a paginated list of all users in the system. Only authenticated admins can perform this operation.", + "operationId": "list_users", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of users to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "400": { - "description": "Invalid request parameters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "offset", + "in": "query", + "description": "Number of users to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "include_organizations", + "in": "query", + "description": "Whether to include organization information and spend limits for the first organization owned by each user (default: false)", + "required": false, + "schema": { + "type": "boolean" } }, - "402": { - "description": "Insufficient credits", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "search", + "in": "query", + "description": "Filter users by email, username, display name, user id, auth provider, or provider user id (case-insensitive partial match).", + "required": false, + "schema": { + "type": "string" } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "is_active", + "in": "query", + "description": "Filter users by active status. Omit to include active and inactive users.", + "required": false, + "schema": { + "type": "boolean" } }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ { - "api_key": [] + "name": "search_by_name", + "in": "query", + "description": "Filter users by organization name (case-insensitive match). Only effective when include_organizations=true; separate from user search.", + "required": false, + "schema": { + "type": "string" + } } - ] - } - }, - "/v1/check_api_key": { - "post": { - "tags": [ - "Gateway" ], - "summary": "Check API key validity", - "description": "Validates the provided API key (via Bearer token), checks rate limits,\nand verifies the organization has sufficient credits.\n\nThis endpoint is designed for external model gateways to authenticate\nuser requests before forwarding to inference engines.", - "operationId": "check_api_key", "responses": { "200": { - "description": "API key is valid and has sufficient credits", + "description": "Users retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CheckApiKeyResponse" + "$ref": "#/components/schemas/ListUsersResponse" } } } }, "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "402": { - "description": "Insufficient credits or spend limit exceeded", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "429": { - "description": "Rate limit exceeded", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3668,42 +3614,122 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/completions": { - "post": { + "/v1/attestation/ita-token": { + "get": { "tags": [ - "Chat" + "Attestation" ], - "summary": "Create text completion", - "description": "Generate AI model responses for text prompts. OpenAI-compatible endpoint.", - "operationId": "completions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CompletionRequest" - } + "summary": "Get Intel Trust Authority attestation token", + "description": "Get Intel Trust Authority signed attestation JWTs for the gateway and, when\nrequested, compatible model provider evidence. Public endpoint β€” this is an\nexplicit, documented decision (nearai/infra#193): the response carries only\nITA-signed TEE evidence (no customer or key-scoped data), public access lets\nthird parties verify the platform without an account, and upstream ITA rate\nlimits bound abuse (propagated as 429). See\n`build_public_attestation_routes` in `routes/attestation.rs`.", + "operationId": "get_ita_token", + "parameters": [ + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } }, - "required": true - }, - "responses": { + { + "name": "nonce", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_address", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "include_tls_fingerprint", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "policy_ids", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "policy_must_match", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "token_signing_alg", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], + "responses": { "200": { - "description": "Completion generated successfully", + "description": "ITA attestation token retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CompletionResponse" + "$ref": "#/components/schemas/ItaTokenResponse" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid parameters", "content": { "application/json": { "schema": { @@ -3712,8 +3738,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "429": { + "description": "ITA rate limit exceeded", "content": { "application/json": { "schema": { @@ -3722,8 +3748,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "502": { + "description": "Bad ITA upstream response", "content": { "application/json": { "schema": { @@ -3732,8 +3758,18 @@ } } }, - "500": { - "description": "Server error", + "503": { + "description": "ITA attestation unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "504": { + "description": "ITA request timed out", "content": { "application/json": { "schema": { @@ -3744,42 +3780,101 @@ } }, "security": [ - { - "api_key": [] - } + {} ] } }, - "/v1/feature-requests": { - "post": { + "/v1/attestation/report": { + "get": { "tags": [ - "Feature Requests" + "Attestation" ], - "summary": "Submit or update the current user's interest in a feature request target.", - "operationId": "submit_feature_request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubmitFeatureRequest" - } + "summary": "Get attestation report", + "description": "Get hardware attestation report for TEE verification. Requires an API key\n(nearai/infra#193); report retrieval is non-billable β€” no usage or billing\nrecords are created.", + "operationId": "get_attestation_report", + "parameters": [ + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] } }, - "required": true - }, + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "nonce", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "signing_address", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "include_tls_fingerprint", + "in": "query", + "description": "Include the TLS certificate SPKI fingerprint in the report-data binding.\nDefaults to false; when true, report_data[..32] = SHA256(signing_address || tls_cert_fingerprint).", + "required": false, + "schema": { + "type": [ + "boolean", + "null" + ] + } + }, + { + "name": "provider", + "in": "query", + "description": "Restrict the report to a specific serving tier.\nAccepted values: `near` (NEAR AI's own TEE fleet) or `chutes` (attested Chutes fallback).\nWhen omitted, the first successfully responding provider is used.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + } + ], "responses": { "200": { - "description": "Feature request recorded", + "description": "Attestation report retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubmitFeatureRequestResponse" + "$ref": "#/components/schemas/AttestationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid nonce format", "content": { "application/json": { "schema": { @@ -3789,17 +3884,7 @@ } }, "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Organization membership required", + "description": "Missing or invalid API key", "content": { "application/json": { "schema": { @@ -3808,8 +3893,8 @@ } } }, - "500": { - "description": "Internal server error", + "503": { + "description": "Service unavailable", "content": { "application/json": { "schema": { @@ -3821,46 +3906,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/health": { - "get": { - "tags": [ - "Health" - ], - "summary": "Health check", - "description": "Check service health status. No authentication required.", - "operationId": "health_check", - "responses": { - "200": { - "description": "Service is healthy", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthResponse" - } - } - } - } - } - } - }, - "/v1/images/edits": { + "/v1/audio/transcriptions": { "post": { "tags": [ - "Images" + "Audio" ], - "summary": "Edit images from a text prompt and image", - "description": "Edit images using an AI model from an image and text description. OpenAI-compatible endpoint.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", - "operationId": "image_edits", + "summary": "Audio transcription endpoint", + "description": "Transcribe audio files using Whisper models. Accepts audio file uploads via multipart/form-data.\nSupports MP3, WAV, WEBM, FLAC, OGG, and M4A formats. Maximum file size: 25 MB.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", + "operationId": "audio_transcriptions", "requestBody": { "content": { "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ImageEditRequestSchema" + "$ref": "#/components/schemas/AudioTranscriptionRequestSchema" } } }, @@ -3868,17 +3931,22 @@ }, "responses": { "200": { - "description": "Image edited successfully", + "description": "Successful transcription", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/AudioTranscriptionResponse" + } + }, + "text/plain": { + "schema": { + "type": "string" } } } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid request (empty file, unsupported format, file too large)", "content": { "application/json": { "schema": { @@ -3888,7 +3956,7 @@ } }, "401": { - "description": "Invalid or missing API key", + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -3907,8 +3975,8 @@ } } }, - "413": { - "description": "Payload too large", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -3917,8 +3985,65 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v1/billing/costs": { + "post": { + "tags": [ + "Billing" + ], + "summary": "Get costs by request IDs", + "description": "Returns the cost in nano-USD for each request ID provided. A request ID is\nthe UUID from the `inference-id` response header of /v1/chat/completions\nand /v1/messages responses (equivalently: UUIDv5 of the response body `id`\nunder the DNS namespace). The `x-request-id` response header is a transport\ncorrelation ID and cannot be used here.\n\nRequest IDs that are not found are returned with costNanoUsd: 0\n(HuggingFace-compatible); the response then carries a `warning` field\npointing at the correct ID source. Usage for a just-finished request can\ntake a few seconds to become visible.", + "operationId": "get_billing_costs", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCostsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Costs retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BillingCostsResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -3928,7 +4053,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -3945,19 +4070,19 @@ ] } }, - "/v1/images/generations": { + "/v1/chat/completions": { "post": { "tags": [ - "Images" + "Chat" ], - "summary": "Generate images from text prompt", - "description": "Generate images using an AI model from a text description. OpenAI-compatible endpoint.", - "operationId": "image_generations", + "summary": "Create chat completion", + "description": "Generate AI model responses for chat conversations. Supports both streaming and non-streaming modes.\nOpenAI-compatible endpoint.", + "operationId": "chat_completions", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationRequest" + "$ref": "#/components/schemas/ChatCompletionRequest" } } }, @@ -3965,11 +4090,11 @@ }, "responses": { "200": { - "description": "Image generated successfully", + "description": "Completion generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageGenerationResponse" + "$ref": "#/components/schemas/ChatCompletionResponse" } } } @@ -3994,6 +4119,16 @@ } } }, + "402": { + "description": "Insufficient credits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "429": { "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { @@ -4022,38 +4157,27 @@ ] } }, - "/v1/invitations/{token}": { - "get": { + "/v1/check_api_key": { + "post": { "tags": [ - "Invitations" - ], - "summary": "Get invitation details by token (public endpoint)", - "description": "Returns invitation details for a specific token. This is a public endpoint\nthat allows users to view invitation details before logging in.", - "operationId": "get_invitation_by_token", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Invitation token", - "required": true, - "schema": { - "type": "string" - } - } + "Gateway" ], + "summary": "Check API key validity", + "description": "Validates the provided API key (via Bearer token), checks rate limits,\nand verifies the organization has sufficient credits.\n\nThis endpoint is designed for external model gateways to authenticate\nuser requests before forwarding to inference engines.", + "operationId": "check_api_key", "responses": { "200": { - "description": "Invitation details", + "description": "API key is valid and has sufficient credits", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" + "$ref": "#/components/schemas/CheckApiKeyResponse" } } } }, - "404": { - "description": "Invitation not found", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4062,8 +4186,18 @@ } } }, - "410": { - "description": "Invitation expired or no longer pending", + "402": { + "description": "Insufficient credits or spend limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", "content": { "application/json": { "schema": { @@ -4082,41 +4216,45 @@ } } } - } + }, + "security": [ + { + "api_key": [] + } + ] } }, - "/v1/invitations/{token}/accept": { + "/v1/completions": { "post": { "tags": [ - "Invitations" + "Chat" ], - "summary": "Accept invitation by token (requires authentication)", - "description": "Accepts an invitation using its token. The authenticated user's email\nmust match the invitation email.", - "operationId": "accept_invitation_by_token", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Invitation token", - "required": true, - "schema": { - "type": "string" + "summary": "Create text completion", + "description": "Generate AI model responses for text prompts. OpenAI-compatible endpoint.", + "operationId": "completions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompletionRequest" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "description": "Invitation accepted successfully", + "description": "Completion generated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" + "$ref": "#/components/schemas/CompletionResponse" } } } }, "400": { - "description": "Bad request - invitation expired or invalid", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -4126,7 +4264,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4135,8 +4273,8 @@ } } }, - "403": { - "description": "Forbidden - invitation belongs to another user", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -4145,8 +4283,8 @@ } } }, - "404": { - "description": "Invitation not found", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -4154,9 +4292,56 @@ } } } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/conversations": { + "post": { + "tags": [ + "Conversations" + ], + "summary": "Create conversation", + "description": "Create a new conversation to organize chat messages.", + "operationId": "create_conversation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConversationRequest" + } + } }, - "409": { - "description": "User is already a member", + "required": true + }, + "responses": { + "201": { + "description": "Conversation created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationObject" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -4166,7 +4351,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -4178,60 +4363,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/model/list": { + "/v1/conversations/{conversation_id}": { "get": { "tags": [ - "Models" + "Conversations" ], - "summary": "List models with pricing", - "description": "Get all available models with pricing information. Public endpoint.\n\nThe full model catalog (a few dozen entries) is loaded once and cached\nin-process for a short TTL. `limit` / `offset` slice the cached list\nin memory, so pagination is consistent across pages within a single\ncache window and adds essentially no DB load.", - "operationId": "list_models", + "summary": "Get conversation", + "description": "Retrieve conversation details by ID.", + "operationId": "get_conversation", "parameters": [ { - "name": "limit", - "in": "query", - "description": "Maximum number of models to return. Defaults to 100. Must be\nnon-negative; values are capped only by the catalog size.", - "required": false, - "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number of models to skip from the start of the catalog.\nDefaults to 0. Must be non-negative.", - "required": false, + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, "schema": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "List of models with pricing", + "description": "Conversation details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelListResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, "400": { - "description": "Invalid pagination parameters", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4240,8 +4408,8 @@ } } }, - "500": { - "description": "Server error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4249,42 +4417,9 @@ } } } - } - } - } - }, - "/v1/model/{model_name}": { - "get": { - "tags": [ - "Models" - ], - "summary": "Get model details", - "description": "Get pricing and metadata for a specific model. URL-encode model names containing slashes. Public endpoint.", - "operationId": "get_model_by_name", - "parameters": [ - { - "name": "model_name", - "in": "path", - "description": "Model name (URL-encode if it contains slashes)", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Model details with pricing", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelWithPricing" - } - } - } }, "404": { - "description": "Model not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4294,7 +4429,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -4303,30 +4438,54 @@ } } } - } - } - }, - "/v1/models": { - "get": { + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { "tags": [ - "Chat" + "Conversations" ], - "summary": "List available models", - "description": "Returns all AI models available for completions. OpenAI-compatible endpoint.", - "operationId": "models", + "summary": "Update conversation", + "description": "Update conversation metadata.", + "operationId": "update_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateConversationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of available models", + "description": "Conversation updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelsResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "500": { - "description": "Server error", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4334,74 +4493,19 @@ } } } - } - }, - "security": [ - {} - ] - } - }, - "/v1/organizations": { - "get": { - "tags": [ - "Organizations" - ], - "summary": "List organizations", - "description": "Get all organizations you belong to.", - "operationId": "list_organizations", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number to return", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Number to skip", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "order_by", - "in": "query", - "description": "Sort by field", - "required": false, - "schema": { - "$ref": "#/components/schemas/OrganizationOrderBy" - } }, - { - "name": "order_direction", - "in": "query", - "description": "Sort direction", - "required": false, - "schema": { - "$ref": "#/components/schemas/OrganizationOrderDirection" - } - } - ], - "responses": { - "200": { - "description": "List of organizations", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationsResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "401": { - "description": "Invalid or missing session token", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4411,7 +4515,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -4423,34 +4527,35 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, - "post": { + "delete": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Create a new organization", - "description": "Creates a new organization with the authenticated user as owner.", - "operationId": "create_organization", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOrganizationRequest" - } + "summary": "Delete conversation", + "description": "Delete a conversation and all its messages.", + "operationId": "delete_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "description": "Conversation ID", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Organization created successfully", + "description": "Conversation deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationDeleteResult" } } } @@ -4475,8 +4580,8 @@ } } }, - "409": { - "description": "Organization already exists", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4498,44 +4603,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}": { - "get": { + "/v1/conversations/{conversation_id}/archive": { + "post": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Get organization by ID", - "description": "Returns organization details for a specific organization ID.", - "operationId": "get_organization", + "summary": "Archive a conversation", + "description": "Archives a conversation to hide it from the main list.", + "operationId": "archive_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Organization details", + "description": "Conversation archived successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4544,8 +4648,8 @@ } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4555,7 +4659,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4577,46 +4681,35 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, - "put": { + "delete": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Update organization", - "description": "Updates organization details for a specific organization ID.", - "operationId": "update_organization", + "summary": "Unarchive a conversation", + "description": "Unarchives a conversation to show it in the main list again.", + "operationId": "unarchive_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Updated organization", + "description": "Conversation unarchived successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponse" + "$ref": "#/components/schemas/ConversationObject" } } } @@ -4641,18 +4734,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Organization not found", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4674,45 +4757,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, - "delete": { + } + }, + "/v1/conversations/{conversation_id}/clone": { + "post": { "tags": [ - "Organizations" + "Conversations" ], - "summary": "Delete organization (owner only)", - "description": "Deletes an organization. Only the organization owner can perform this action.", - "operationId": "delete_organization", + "summary": "Clone a conversation", + "description": "Creates a copy of an existing conversation with a new ID.", + "operationId": "clone_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "200": { - "description": "Organization deleted successfully" - }, - "401": { - "description": "Unauthorized", + "201": { + "description": "Conversation cloned successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "403": { - "description": "Forbidden", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4721,8 +4802,8 @@ } } }, - "404": { - "description": "Organization not found", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4731,8 +4812,8 @@ } } }, - "409": { - "description": "Organization is bound to a NEAR staking wallet", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4754,57 +4835,63 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/invitations/{invitation_id}": { - "delete": { + "/v1/conversations/{conversation_id}/items": { + "get": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "Cancel an organization invitation (short path)", - "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.\nThis endpoint is an alias for `DELETE /v1/organizations/{org_id}/members/invitations/{invitation_id}`.", - "operationId": "cancel_invitation", + "summary": "List conversation messages", + "description": "Get all messages and responses in a conversation, sorted by creation time.", + "operationId": "list_conversation_items", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } }, { - "name": "invitation_id", - "in": "path", - "description": "Invitation ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of items to return", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of items to skip", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } } ], "responses": { - "204": { - "description": "Invitation cancelled successfully" - }, - "400": { - "description": "Bad request - invitation is not pending", + "200": { + "description": "List of conversation items", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ConversationItemList" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4813,8 +4900,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -4824,7 +4911,7 @@ } }, "404": { - "description": "Organization or invitation not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4846,58 +4933,67 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - } - }, - "/v1/organizations/{org_id}/members": { - "get": { + }, + "post": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "List organization members with limited user information", - "description": "Returns limited user information for privacy and security:\n- All members: See only public user info (username, display name, avatar)\n- Sensitive data (email, last login, etc.) is not exposed to any organization members", - "operationId": "list_organization_members", + "summary": "Create items in a conversation (for backfilling)", + "description": "Adds items to a conversation, allowing API callers to backfill conversations.", + "operationId": "create_conversation_items", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of records to return (default: 100, max: 1000)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "include", "in": "query", - "description": "Offset for pagination (default: 0)", + "description": "Additional fields to include in the response", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "array", + "items": { + "type": "string" + } } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConversationItemsRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of organization members with public user information", + "description": "Items created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListOrganizationMembersResponse" + "$ref": "#/components/schemas/ConversationItemList" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4912,8 +5008,8 @@ } } }, - "403": { - "description": "Forbidden - not a member of the organization", + "404": { + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -4935,52 +5031,43 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, + } + }, + "/v1/conversations/{conversation_id}/pin": { "post": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "Add a member to an organization", - "description": "Adds a new member to the organization. The authenticated user must be an owner or admin.", - "operationId": "add_organization_member", + "summary": "Pin a conversation", + "description": "Pins a conversation to keep it at the top of the list.", + "operationId": "pin_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddOrganizationMemberRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Member added successfully", + "description": "Conversation pinned successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -4989,8 +5076,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5000,17 +5087,7 @@ } }, "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "409": { - "description": "User is already a member", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -5032,56 +5109,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - } - }, - "/v1/organizations/{org_id}/members/invitations": { - "get": { + }, + "delete": { "tags": [ - "Organization Members" + "Conversations" ], - "summary": "List organization invitations", - "description": "Returns invitations for the organization. Only accessible to owners and admins.", - "operationId": "list_organization_invitations", + "summary": "Unpin a conversation", + "description": "Unpins a conversation.", + "operationId": "unpin_conversation", "parameters": [ { - "name": "org_id", + "name": "conversation_id", "in": "path", - "description": "Organization ID", + "description": "Conversation ID", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter by status", - "required": false, - "schema": { - "$ref": "#/components/schemas/InvitationStatus" + "type": "string" } } ], "responses": { "200": { - "description": "List of organization invitations", + "description": "Conversation unpinned successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationInvitationResponse" - } + "$ref": "#/components/schemas/ConversationObject" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5090,8 +5152,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5101,7 +5163,7 @@ } }, "404": { - "description": "Organization not found", + "description": "Conversation not found", "content": { "application/json": { "schema": { @@ -5123,57 +5185,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { - "delete": { + "/v1/feature-requests": { + "post": { "tags": [ - "Organization Members" + "Feature Requests" ], - "summary": "Cancel an organization invitation", - "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.", - "operationId": "cancel_organization_invitation", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Submit or update the current user's interest in a feature request target.", + "operationId": "submit_feature_request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitFeatureRequest" + } } }, - { - "name": "invitation_id", - "in": "path", - "description": "Invitation ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], + "required": true + }, "responses": { - "204": { - "description": "Invitation cancelled successfully" - }, - "400": { - "description": "Bad request - invitation is not pending", + "200": { + "description": "Feature request recorded", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/SubmitFeatureRequestResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request", "content": { "application/json": { "schema": { @@ -5182,8 +5228,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5192,8 +5238,8 @@ } } }, - "404": { - "description": "Organization or invitation not found", + "403": { + "description": "Organization membership required", "content": { "application/json": { "schema": { @@ -5220,49 +5266,64 @@ ] } }, - "/v1/organizations/{org_id}/members/invite-by-email": { - "post": { + "/v1/files": { + "get": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Invite users to an organization by email", - "description": "Invites multiple users to the organization by their email addresses. The authenticated user must be an owner or admin.\nReturns results for each invitation attempt, including successes and failures.", - "operationId": "invite_organization_member_by_email", + "operationId": "list_files", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "name": "after", + "in": "query", + "description": "A cursor for pagination. Pass the file ID to fetch files after this one.", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InviteOrganizationMemberByEmailRequest" - } + }, + { + "name": "limit", + "in": "query", + "description": "Number of files to return (1-10000, default 10000)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "required": true - }, + { + "name": "order", + "in": "query", + "description": "Sort order by created_at timestamp: 'asc' or 'desc' (default 'desc')", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "purpose", + "in": "query", + "description": "Filter files by purpose", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Invitation results (may include partial failures)", + "description": "List of files retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" + "$ref": "#/components/schemas/FileListResponse" } } } }, "400": { - "description": "Bad request - empty invitation list", + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5280,9 +5341,37 @@ } } } + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Files" + ], + "operationId": "upload_file", + "requestBody": { + "content": { + "multipart/form-data": {} + } + }, + "responses": { + "201": { + "description": "File uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadResponse" + } + } + } }, - "403": { - "description": "Forbidden - not an admin or owner", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5291,8 +5380,18 @@ } } }, - "500": { - "description": "Internal server error", + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "File too large", "content": { "application/json": { "schema": { @@ -5304,64 +5403,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/members/{user_id}": { - "put": { + "/v1/files/{file_id}": { + "get": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Update an organization member's role", - "description": "Updates a member's role in the organization. The authenticated user must be an owner or admin.\nOnly owners can promote members to owner role.", - "operationId": "update_organization_member", + "operationId": "get_file", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "user_id", + "name": "file_id", "in": "path", - "description": "User ID of the member to update", + "description": "The ID of the file to retrieve", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Member updated successfully", + "description": "File information retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationMemberResponse" + "$ref": "#/components/schemas/FileUploadResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5370,8 +5446,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5380,8 +5456,8 @@ } } }, - "500": { - "description": "Internal server error", + "404": { + "description": "File not found", "content": { "application/json": { "schema": { @@ -5393,55 +5469,39 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] }, "delete": { "tags": [ - "Organization Members" + "Files" ], - "summary": "Remove a member from an organization", - "description": "Removes a member from the organization. The authenticated user must be an owner or admin,\nor the member can remove themselves. The last owner cannot be removed.", - "operationId": "remove_organization_member", + "operationId": "delete_file", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "user_id", + "name": "file_id", "in": "path", - "description": "User ID of the member to remove", + "description": "The ID of the file to delete", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { - "204": { - "description": "Member removed successfully" - }, - "400": { - "description": "Bad request - cannot remove last owner", + "200": { + "description": "File deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/FileDeleteResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -5450,8 +5510,8 @@ } } }, - "403": { - "description": "Forbidden - not an admin or owner", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -5461,17 +5521,7 @@ } }, "404": { - "description": "Member not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "description": "File not found", "content": { "application/json": { "schema": { @@ -5483,38 +5533,41 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/reporting-tokens": { + "/v1/files/{file_id}/content": { "get": { "tags": [ - "Reporting" + "Files" ], - "summary": "List active reporting tokens for an organization.", - "description": "The response includes non-secret prefixes and audit timestamps. It never\nincludes raw reporting tokens or stored hashes.", - "operationId": "list_reporting_tokens", + "operationId": "get_file_content", "parameters": [ { - "name": "org_id", + "name": "file_id", "in": "path", - "description": "Organization ID", + "description": "The ID of the file to retrieve content from", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Active reporting tokens", + "description": "File content retrieved successfully", + "content": { + "application/octet-stream": {} + } + }, + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListReportingTokensResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5529,18 +5582,8 @@ } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", + "404": { + "description": "File not found", "content": { "application/json": { "schema": { @@ -5552,52 +5595,64 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] - }, - "post": { + } + }, + "/v1/health": { + "get": { "tags": [ - "Reporting" + "Health" ], - "summary": "Create a read-only reporting token.", - "description": "Organization owners and admins can create reporting tokens for usage\nexport and summary endpoints. The raw `rpt-` token is returned once.", - "operationId": "create_reporting_token", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Health check", + "description": "Check service health status. No authentication required.", + "operationId": "health_check", + "responses": { + "200": { + "description": "Service is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } } } + } + } + }, + "/v1/images/edits": { + "post": { + "tags": [ + "Images" ], + "summary": "Edit images from a text prompt and image", + "description": "Edit images using an AI model from an image and text description. OpenAI-compatible endpoint.\n\n**Request Body (multipart/form-data):**\nAll fields should be provided as text values or files as indicated in the schema.", + "operationId": "image_edits", "requestBody": { "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/CreateReportingTokenRequest" + "$ref": "#/components/schemas/ImageEditRequestSchema" } } }, "required": true }, "responses": { - "201": { - "description": "Reporting token created", + "200": { + "description": "Image edited successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateReportingTokenResponse" + "$ref": "#/components/schemas/ImageGenerationResponse" } } } }, "400": { - "description": "Invalid request", + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -5607,7 +5662,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -5616,8 +5671,28 @@ } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Payload too large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -5627,7 +5702,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5639,47 +5714,42 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { - "delete": { + "/v1/images/generations": { + "post": { "tags": [ - "Reporting" + "Images" ], - "summary": "Revoke a reporting token.", - "description": "Revoked reporting tokens can no longer authenticate usage reporting\nrequests.", - "operationId": "revoke_reporting_token", - "parameters": [ - { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "Generate images from text prompt", + "description": "Generate images using an AI model from a text description. OpenAI-compatible endpoint.", + "operationId": "image_generations", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageGenerationRequest" + } } }, - { - "name": "token_id", - "in": "path", - "description": "Reporting token ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], + "required": true + }, "responses": { - "204": { - "description": "Reporting token revoked" + "200": { + "description": "Image generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageGenerationResponse" + } + } + } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid request parameters", "content": { "application/json": { "schema": { @@ -5688,8 +5758,8 @@ } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -5698,8 +5768,8 @@ } } }, - "404": { - "description": "Reporting token not found", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -5709,7 +5779,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5721,24 +5791,24 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/organizations/{org_id}/staking/farm": { + "/v1/invitations/{token}": { "get": { "tags": [ - "Staking Farm" + "Invitations" ], - "summary": "Get organization staking farm state", - "description": "Returns the staking farm source and last synced farm-credit state for an\norganization. The organization must be the NEAR-authenticated user's default\norganization.", - "operationId": "get_organization_staking_farm", + "summary": "Get invitation details by token (public endpoint)", + "description": "Returns invitation details for a specific token. This is a public endpoint\nthat allows users to view invitation details before logging in.", + "operationId": "get_invitation_by_token", "parameters": [ { - "name": "org_id", + "name": "token", "in": "path", - "description": "Organization ID", + "description": "Invitation token", "required": true, "schema": { "type": "string" @@ -5747,37 +5817,17 @@ ], "responses": { "200": { - "description": "Organization staking farm state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" - } - } - } - }, - "400": { - "description": "Invalid organization ID", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "description": "Invitation details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationInvitationResponse" } } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -5786,8 +5836,8 @@ } } }, - "404": { - "description": "No staking farm source found", + "410": { + "description": "Invitation expired or no longer pending", "content": { "application/json": { "schema": { @@ -5806,27 +5856,22 @@ } } } - }, - "security": [ - { - "session_token": [] - } - ] + } } }, - "/v1/organizations/{org_id}/staking/farm/sync": { + "/v1/invitations/{token}/accept": { "post": { "tags": [ - "Staking Farm" + "Invitations" ], - "summary": "Sync organization staking farm credits", - "description": "Links or refreshes the NEAR-authenticated user's staking farm source for the\norganization, then syncs reward units from the configured staking contract.", - "operationId": "sync_organization_staking_farm", + "summary": "Accept invitation by token (requires authentication)", + "description": "Accepts an invitation using its token. The authenticated user's email\nmust match the invitation email.", + "operationId": "accept_invitation_by_token", "parameters": [ { - "name": "org_id", + "name": "token", "in": "path", - "description": "Organization ID", + "description": "Invitation token", "required": true, "schema": { "type": "string" @@ -5835,17 +5880,17 @@ ], "responses": { "200": { - "description": "Synced organization staking farm state", + "description": "Invitation accepted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmStateResponse" + "$ref": "#/components/schemas/AcceptInvitationResponse" } } } }, "400": { - "description": "Invalid organization ID", + "description": "Bad request - invitation expired or invalid", "content": { "application/json": { "schema": { @@ -5865,7 +5910,17 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Invitation not found", "content": { "application/json": { "schema": { @@ -5875,7 +5930,7 @@ } }, "409": { - "description": "NEAR account conflict or organization inactive", + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -5902,58 +5957,55 @@ ] } }, - "/v1/organizations/{org_id}/usage/balance": { + "/v1/model/list": { "get": { "tags": [ - "Usage" + "Models" ], - "summary": "Get organization balance", - "description": "Returns the current spending balance for an organization", - "operationId": "get_organization_balance", - "parameters": [ + "summary": "List models with pricing", + "description": "Get all available models with pricing information. Public endpoint.\n\nThe full model catalog (a few dozen entries) is loaded once and cached\nin-process for a short TTL. `limit` / `offset` slice the cached list\nin memory, so pagination is consistent across pages within a single\ncache window and adds essentially no DB load.", + "operationId": "list_models", + "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of models to return. Defaults to 100. Must be\nnon-negative; values are capped only by the catalog size.", + "required": false, "schema": { - "type": "string" + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of models to skip from the start of the catalog.\nDefaults to 0. Must be non-negative.", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64" } } ], "responses": { "200": { - "description": "Organization balance", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrganizationBalanceResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Forbidden", + "description": "List of models with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ModelListResponse" } } } }, - "404": { - "description": "Not found", + "400": { + "description": "Invalid pagination parameters", "content": { "application/json": { "schema": { @@ -5963,7 +6015,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -5972,55 +6024,41 @@ } } } - }, - "security": [ - { - "session_token": [] - } - ] + } } }, - "/v1/organizations/{org_id}/usage/by-model": { + "/v1/model/{model_name}": { "get": { "tags": [ - "Usage" + "Models" ], - "summary": "Get organization usage broken down by model.", - "description": "Returns one row per model, summed over a rolling window ending now:\n`day` = last 24h, `week` = last 7 days, `month` = last 30 days (NOT calendar\nday/week/month-to-date). Used by the dashboard pie chart to show which models\ndrive spend.", - "operationId": "get_organization_usage_by_model", + "summary": "Get model details", + "description": "Get pricing and metadata for a specific model. URL-encode model names containing slashes. Public endpoint.", + "operationId": "get_model_by_name", "parameters": [ { - "name": "org_id", + "name": "model_name", "in": "path", - "description": "Organization ID", + "description": "Model name (URL-encode if it contains slashes)", "required": true, "schema": { "type": "string" } - }, - { - "name": "period", - "in": "query", - "description": "Rolling window: `day` (last 24h), `week` (last 7d), or `month` (last 30d). Default: `month`", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Per-model usage breakdown", + "description": "Model details with pricing", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageByModelResponse" + "$ref": "#/components/schemas/ModelWithPricing" } } } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -6029,8 +6067,8 @@ } } }, - "403": { - "description": "Forbidden", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -6038,9 +6076,31 @@ } } } + } + } + } + }, + "/v1/models": { + "get": { + "tags": [ + "Chat" + ], + "summary": "List available models", + "description": "Returns all AI models available for completions. OpenAI-compatible endpoint.", + "operationId": "models", + "responses": { + "200": { + "description": "List of available models", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelsResponse" + } + } + } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -6051,149 +6111,71 @@ } }, "security": [ - { - "session_token": [] - } + {} ] } }, - "/v1/organizations/{org_id}/usage/export": { + "/v1/organizations": { "get": { "tags": [ - "Reporting" + "Organizations" ], - "summary": "Export organization usage and cost rows.", - "description": "Requires a reporting token scoped to the organization in the path. Results\nare returned in descending `(created_at, source, id)` order with opaque\ncursor pagination.", - "operationId": "export_usage", + "summary": "List organizations", + "description": "Get all organizations you belong to.", + "operationId": "list_organizations", "parameters": [ { - "name": "org_id", - "in": "path", - "description": "Organization ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "source", - "in": "query", - "description": "Usage source to export. Defaults to all.", - "required": false, - "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "model", - "in": "query", - "description": "Filter inference rows by model name.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "inference_type", + "name": "limit", "in": "query", - "description": "Filter inference rows by inference type.", + "description": "Maximum number to return", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "service_name", + "name": "offset", "in": "query", - "description": "Filter service rows by platform service name.", + "description": "Number to skip", "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "limit", + "name": "order_by", "in": "query", - "description": "Maximum rows to return. Defaults to 100 and must not exceed 1000.", + "description": "Sort by field", "required": false, "schema": { - "type": "integer", - "format": "int32", - "minimum": 0 + "$ref": "#/components/schemas/OrganizationOrderBy" } }, { - "name": "cursor", + "name": "order_direction", "in": "query", - "description": "Opaque cursor returned by the previous export page.", + "description": "Sort direction", "required": false, "schema": { - "type": "string" + "$ref": "#/components/schemas/OrganizationOrderDirection" } } ], "responses": { "200": { - "description": "Usage export page", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReportingUsageExportResponse" - } - } - } - }, - "400": { - "description": "Invalid filters or cursor", + "description": "List of organizations", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListOrganizationsResponse" } } } }, "401": { - "description": "Unauthorized", + "description": "Invalid or missing session token", "content": { "application/json": { "schema": { @@ -6202,8 +6184,8 @@ } } }, - "403": { - "description": "Reporting token is not scoped to this organization", + "500": { + "description": "Server error", "content": { "application/json": { "schema": { @@ -6211,9 +6193,44 @@ } } } - }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { + "tags": [ + "Organizations" + ], + "summary": "Create a new organization", + "description": "Creates a new organization with the authenticated user as owner.", + "operationId": "create_organization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Organization created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResponse" + } + } + } + }, + "400": { + "description": "Bad request", "content": { "application/json": { "schema": { @@ -6222,8 +6239,8 @@ } } }, - "500": { - "description": "Internal server error", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6232,8 +6249,18 @@ } } }, - "504": { - "description": "Reporting request timed out", + "409": { + "description": "Organization already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -6245,100 +6272,25 @@ }, "security": [ { - "reporting_token": [] + "session_token": [] } ] } }, - "/v1/organizations/{org_id}/usage/history": { + "/v1/organizations/{org_id}": { "get": { "tags": [ - "Usage" + "Organizations" ], - "summary": "Get organization usage history", - "description": "Returns paginated usage history for an organization", - "operationId": "get_organization_usage_history", + "summary": "Get organization by ID", + "description": "Returns organization details for a specific organization ID.", + "operationId": "get_organization", "parameters": [ { "name": "org_id", "in": "path", "description": "Organization ID", "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of records to return (default: 100)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "description": "Offset for pagination (default: 0)", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "start_date", - "in": "query", - "description": "Inclusive UTC start date in YYYY-MM-DD or RFC3339 format.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_date", - "in": "query", - "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Takes precedence over start_date.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Takes precedence over end_date.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, "schema": { "type": "string", "format": "uuid" @@ -6347,11 +6299,11 @@ ], "responses": { "200": { - "description": "Usage history", + "description": "Organization details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageHistoryResponse" + "$ref": "#/components/schemas/OrganizationResponse" } } } @@ -6376,6 +6328,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -6392,14 +6354,14 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/metrics": { - "get": { + }, + "put": { "tags": [ - "Usage" + "Organizations" ], - "operationId": "get_user_organization_metrics", + "summary": "Update organization", + "description": "Updates organization details for a specific organization ID.", + "operationId": "update_organization", "parameters": [ { "name": "org_id", @@ -6407,35 +6369,38 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start date (ISO 8601, default: 30 days ago)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End date (ISO 8601, default: now)", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Organization usage metrics", + "description": "Updated organization", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserOrganizationMetrics" + "$ref": "#/components/schemas/OrganizationResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6460,6 +6425,16 @@ } } }, + "404": { + "description": "Organization not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -6476,16 +6451,14 @@ "session_token": [] } ] - } - }, - "/v1/organizations/{org_id}/usage/summary": { - "get": { + }, + "delete": { "tags": [ - "Reporting" + "Organizations" ], - "summary": "Summarize organization usage and costs.", - "description": "Requires a reporting token scoped to the organization in the path. Totals\ninclude inference and platform service usage by default.", - "operationId": "summary_usage", + "summary": "Delete organization (owner only)", + "description": "Deletes an organization. Only the organization owner can perform this action.", + "operationId": "delete_organization", "parameters": [ { "name": "org_id", @@ -6496,102 +6469,11 @@ "type": "string", "format": "uuid" } - }, - { - "name": "start_time", - "in": "query", - "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end_time", - "in": "query", - "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "source", - "in": "query", - "description": "Usage source to summarize. Defaults to all.", - "required": false, - "schema": { - "$ref": "#/components/schemas/ReportingUsageSource" - } - }, - { - "name": "workspace_id", - "in": "query", - "description": "Filter by workspace ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "api_key_id", - "in": "query", - "description": "Filter by API key ID.", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "model", - "in": "query", - "description": "Filter inference usage by model name.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "inference_type", - "in": "query", - "description": "Filter inference usage by inference type.", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "service_name", - "in": "query", - "description": "Filter service usage by platform service name.", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Usage summary", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReportingUsageSummaryResponse" - } - } - } - }, - "400": { - "description": "Invalid filters", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + } + ], + "responses": { + "200": { + "description": "Organization deleted successfully" }, "401": { "description": "Unauthorized", @@ -6604,7 +6486,7 @@ } }, "403": { - "description": "Reporting token is not scoped to this organization", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -6613,8 +6495,8 @@ } } }, - "429": { - "description": "Reporting rate or concurrency limit exceeded", + "404": { + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -6623,8 +6505,8 @@ } } }, - "500": { - "description": "Internal server error", + "409": { + "description": "Organization is bound to a NEAR staking wallet", "content": { "application/json": { "schema": { @@ -6633,8 +6515,8 @@ } } }, - "504": { - "description": "Reporting request timed out", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -6646,17 +6528,19 @@ }, "security": [ { - "reporting_token": [] + "session_token": [] } ] } }, - "/v1/organizations/{org_id}/usage/timeseries": { - "get": { + "/v1/organizations/{org_id}/invitations/{invitation_id}": { + "delete": { "tags": [ - "Usage" + "Organization Members" ], - "operationId": "get_user_organization_timeseries", + "summary": "Cancel an organization invitation (short path)", + "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.\nThis endpoint is an alias for `DELETE /v1/organizations/{org_id}/members/invitations/{invitation_id}`.", + "operationId": "cancel_invitation", "parameters": [ { "name": "org_id", @@ -6664,44 +6548,31 @@ "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "description": "Start date (ISO 8601, default: 30 days ago)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "description": "End date (ISO 8601, default: now)", - "required": false, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "granularity", - "in": "query", - "description": "Time bucket size: hour, day, week (default: day)", - "required": false, + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Organization usage timeseries", + "204": { + "description": "Invitation cancelled successfully" + }, + "400": { + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserTimeSeriesMetrics" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6717,7 +6588,17 @@ } }, "403": { - "description": "Forbidden", + "description": "Forbidden - not an admin or owner", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -6744,14 +6625,14 @@ ] } }, - "/v1/organizations/{org_id}/workspaces": { + "/v1/organizations/{org_id}/members": { "get": { "tags": [ - "Workspaces" + "Organization Members" ], - "summary": "List workspaces in an organization", - "description": "Lists all workspaces within the specified organization. The authenticated user must\nbe a member of the organization to list workspaces.", - "operationId": "list_organization_workspaces", + "summary": "List organization members with limited user information", + "description": "Returns limited user information for privacy and security:\n- All members: See only public user info (username, display name, avatar)\n- Sensitive data (email, last login, etc.) is not exposed to any organization members", + "operationId": "list_organization_members", "parameters": [ { "name": "org_id", @@ -6766,7 +6647,7 @@ { "name": "limit", "in": "query", - "description": "Maximum number of results", + "description": "Number of records to return (default: 100, max: 1000)", "required": false, "schema": { "type": "integer", @@ -6776,39 +6657,21 @@ { "name": "offset", "in": "query", - "description": "Number of results to skip", + "description": "Offset for pagination (default: 0)", "required": false, "schema": { "type": "integer", "format": "int64" } - }, - { - "name": "order_by", - "in": "query", - "description": "Order by", - "required": false, - "schema": { - "$ref": "#/components/schemas/WorkspaceOrderBy" - } - }, - { - "name": "order_direction", - "in": "query", - "description": "Order direction", - "required": false, - "schema": { - "$ref": "#/components/schemas/WorkspaceOrderDirection" - } } ], "responses": { "200": { - "description": "Paginated list of workspaces", + "description": "List of organization members with public user information", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWorkspacesResponse" + "$ref": "#/components/schemas/ListOrganizationMembersResponse" } } } @@ -6824,7 +6687,7 @@ } }, "403": { - "description": "Forbidden - not a member of organization", + "description": "Forbidden - not a member of the organization", "content": { "application/json": { "schema": { @@ -6852,11 +6715,11 @@ }, "post": { "tags": [ - "Workspaces" + "Organization Members" ], - "summary": "Create a new workspace in an organization", - "description": "Creates a new workspace within the specified organization. The authenticated user must\nbe a member of the organization to create workspaces.", - "operationId": "create_workspace", + "summary": "Add a member to an organization", + "description": "Adds a new member to the organization. The authenticated user must be an owner or admin.", + "operationId": "add_organization_member", "parameters": [ { "name": "org_id", @@ -6873,25 +6736,25 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" + "$ref": "#/components/schemas/AddOrganizationMemberRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Workspace created successfully", + "200": { + "description": "Member added successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, - "400": { - "description": "Bad request", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -6900,8 +6763,8 @@ } } }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -6910,8 +6773,8 @@ } } }, - "403": { - "description": "Forbidden - not a member of organization", + "404": { + "description": "User not found", "content": { "application/json": { "schema": { @@ -6921,7 +6784,7 @@ } }, "409": { - "description": "Workspace name already exists in organization", + "description": "User is already a member", "content": { "application/json": { "schema": { @@ -6948,39 +6811,45 @@ ] } }, - "/v1/privacy/classify": { - "post": { + "/v1/organizations/{org_id}/members/invitations": { + "get": { "tags": [ - "Privacy" + "Organization Members" ], - "operationId": "privacy_classify", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyClassifyRequestDoc" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Privacy classification completed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyClassifyResponseDoc" - } - } + "summary": "List organization invitations", + "description": "Returns invitations for the organization. Only accessible to owners and admins.", + "operationId": "list_organization_invitations", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "400": { - "description": "Invalid request", + { + "name": "status", + "in": "query", + "description": "Filter by status", + "required": false, + "schema": { + "$ref": "#/components/schemas/InvitationStatus" + } + } + ], + "responses": { + "200": { + "description": "List of organization invitations", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationInvitationResponse" + } } } } @@ -6995,8 +6864,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7005,8 +6874,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Organization not found", "content": { "application/json": { "schema": { @@ -7016,7 +6885,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7028,40 +6897,47 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/privacy/redact": { - "post": { + "/v1/organizations/{org_id}/members/invitations/{invitation_id}": { + "delete": { "tags": [ - "Privacy" + "Organization Members" ], - "operationId": "privacy_redact", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactRequestDoc" - } + "summary": "Cancel an organization invitation", + "description": "Cancels a pending invitation for the organization. Only owners and admins can cancel invitations.", + "operationId": "cancel_organization_invitation", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "responses": { - "200": { - "description": "Privacy redaction completed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PrivacyRedactResponseDoc" - } - } + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } + } + ], + "responses": { + "204": { + "description": "Invitation cancelled successfully" }, "400": { - "description": "Invalid request", + "description": "Bad request - invitation is not pending", "content": { "application/json": { "schema": { @@ -7080,8 +6956,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7090,8 +6966,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Organization or invitation not found", "content": { "application/json": { "schema": { @@ -7101,7 +6977,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7113,24 +6989,36 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/rerank": { + "/v1/organizations/{org_id}/members/invite-by-email": { "post": { "tags": [ - "Rerank" + "Organization Members" + ], + "summary": "Invite users to an organization by email", + "description": "Invites multiple users to the organization by their email addresses. The authenticated user must be an owner or admin.\nReturns results for each invitation attempt, including successes and failures.", + "operationId": "invite_organization_member_by_email", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Document reranking endpoint", - "description": "Ranks documents by relevance to a query using a reranker model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", - "operationId": "rerank", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RerankRequest" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailRequest" } } }, @@ -7138,17 +7026,17 @@ }, "responses": { "200": { - "description": "Successful rerank", + "description": "Invitation results (may include partial failures)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RerankResponse" + "$ref": "#/components/schemas/InviteOrganizationMemberByEmailResponse" } } } }, "400": { - "description": "Invalid request (empty documents, invalid model, etc.)", + "description": "Bad request - empty invitation list", "content": { "application/json": { "schema": { @@ -7158,17 +7046,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Model not found", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7177,8 +7055,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7188,7 +7066,7 @@ } }, "500": { - "description": "Server error (billing failure, provider error)", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7200,24 +7078,46 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/responses": { - "post": { + "/v1/organizations/{org_id}/members/{user_id}": { + "put": { "tags": [ - "Responses" + "Organization Members" + ], + "summary": "Update an organization member's role", + "description": "Updates a member's role in the organization. The authenticated user must be an owner or admin.\nOnly owners can promote members to owner role.", + "operationId": "update_organization_member", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "user_id", + "in": "path", + "description": "User ID of the member to update", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], - "summary": "Create response", - "description": "Generate a single-turn, stateless AI response with optional streaming.", - "operationId": "create_response", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StatelessCreateResponseRequestSchema" + "$ref": "#/components/schemas/UpdateOrganizationMemberRequest" } } }, @@ -7225,27 +7125,17 @@ }, "responses": { "200": { - "description": "Response created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseObject" - } - } - } - }, - "400": { - "description": "Invalid request", + "description": "Member updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationMemberResponse" } } } }, "401": { - "description": "Invalid or missing API key", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7254,8 +7144,8 @@ } } }, - "402": { - "description": "Insufficient credits", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7265,7 +7155,7 @@ } }, "500": { - "description": "Server error", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7277,42 +7167,45 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] - } - }, - "/v1/score": { - "post": { + }, + "delete": { "tags": [ - "Score" + "Organization Members" ], - "summary": "Text similarity scoring endpoint", - "description": "Scores the similarity between two texts using a scoring/ranking model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", - "operationId": "score", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScoreRequest" - } + "summary": "Remove a member from an organization", + "description": "Removes a member from the organization. The authenticated user must be an owner or admin,\nor the member can remove themselves. The last owner cannot be removed.", + "operationId": "remove_organization_member", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "responses": { - "200": { - "description": "Successful score", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScoreResponse" - } - } + { + "name": "user_id", + "in": "path", + "description": "User ID of the member to remove", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } + } + ], + "responses": { + "204": { + "description": "Member removed successfully" }, "400": { - "description": "Invalid request (empty texts, invalid model, etc.)", + "description": "Bad request - cannot remove last owner", "content": { "application/json": { "schema": { @@ -7322,7 +7215,7 @@ } }, "401": { - "description": "Unauthorized (missing or invalid API key)", + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -7331,8 +7224,8 @@ } } }, - "404": { - "description": "Model not found", + "403": { + "description": "Forbidden - not an admin or owner", "content": { "application/json": { "schema": { @@ -7341,8 +7234,8 @@ } } }, - "429": { - "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "404": { + "description": "Member not found", "content": { "application/json": { "schema": { @@ -7352,7 +7245,7 @@ } }, "500": { - "description": "Server error (billing failure, provider error)", + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7364,51 +7257,54 @@ }, "security": [ { - "ApiKeyAuth": [] + "session_token": [] } ] } }, - "/v1/services": { + "/v1/organizations/{org_id}/reporting-tokens": { "get": { "tags": [ - "Services" + "Reporting" ], - "summary": "List platform services (public, no auth)", - "operationId": "list_services", + "summary": "List active reporting tokens for an organization.", + "description": "The response includes non-secret prefixes and audit timestamps. It never\nincludes raw reporting tokens or stored hashes.", + "operationId": "list_reporting_tokens", "parameters": [ { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "offset", - "in": "query", - "required": false, + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Services list", + "description": "Active reporting tokens", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceListResponse" + "$ref": "#/components/schemas/ListReportingTokensResponse" } } } }, - "400": { - "description": "Invalid parameters", + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7427,40 +7323,75 @@ } } } - } - } - }, - "/v1/services/{service_name}": { - "get": { + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { "tags": [ - "Services" + "Reporting" ], - "summary": "Get platform service by name (public, no auth)", - "operationId": "get_service_by_name", + "summary": "Create a read-only reporting token.", + "description": "Organization owners and admins can create reporting tokens for usage\nexport and summary endpoints. The raw `rpt-` token is returned once.", + "operationId": "create_reporting_token", "parameters": [ { - "name": "service_name", + "name": "org_id", "in": "path", - "description": "Service name (e.g. web_search)", + "description": "Organization ID", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReportingTokenRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "Service details", + "201": { + "description": "Reporting token created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceResponse" + "$ref": "#/components/schemas/CreateReportingTokenResponse" } } } }, - "404": { - "description": "Service not found", + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7479,63 +7410,60 @@ } } } - } + }, + "security": [ + { + "session_token": [] + } + ] } }, - "/v1/signature/{chat_id}": { - "get": { + "/v1/organizations/{org_id}/reporting-tokens/{token_id}": { + "delete": { "tags": [ - "Attestation" + "Reporting" ], - "summary": "Get completion or Responses API signature", - "description": "Get a cryptographic signature for a chat completion or completed stateless\n`resp_*` response. Returns signature data on success, or an unavailable\nresponse if a chat-completion stream was disconnected.", - "operationId": "get_signature", + "summary": "Revoke a reporting token.", + "description": "Revoked reporting tokens can no longer authenticate usage reporting\nrequests.", + "operationId": "revoke_reporting_token", "parameters": [ { - "name": "chat_id", + "name": "org_id", "in": "path", - "description": "Chat completion ID or resp_* Responses API ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string" - } - }, - { - "name": "model", - "in": "query", - "required": false, - "schema": { - "type": [ - "string", - "null" - ] + "type": "string", + "format": "uuid" } }, { - "name": "signing_algo", - "in": "query", - "required": false, + "name": "token_id", + "in": "path", + "description": "Reporting token ID", + "required": true, "schema": { - "type": [ - "string", - "null" - ] + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "Signature retrieved or unavailable due to disconnect", + "204": { + "description": "Reporting token revoked" + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignatureResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invalid parameters", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7545,7 +7473,17 @@ } }, "404": { - "description": "Signature not found", + "description": "Reporting token not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -7557,26 +7495,47 @@ }, "security": [ { - "api_key": [] + "session_token": [] } ] } }, - "/v1/staking/farm/config": { + "/v1/organizations/{org_id}/staking/farm": { "get": { "tags": [ "Staking Farm" ], - "summary": "Get staking farm configuration", - "description": "Returns the active House of Stake farm configuration used to convert\non-chain staking reward units into NEAR AI Cloud credits.", - "operationId": "get_staking_farm_config", + "summary": "Get organization staking farm state", + "description": "Returns the staking farm source and last synced farm-credit state for an\norganization. The organization must be the NEAR-authenticated user's default\norganization.", + "operationId": "get_organization_staking_farm", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Staking farm configuration", + "description": "Organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StakingFarmConfigResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" + } + } + } + }, + "400": { + "description": "Invalid organization ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -7591,45 +7550,8 @@ } } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me": { - "get": { - "tags": [ - "Users" - ], - "summary": "Get current user", - "description": "Returns the profile of the currently authenticated user, including their organizations and workspaces.", - "operationId": "get_current_user", - "responses": { - "200": { - "description": "Current user profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7639,7 +7561,7 @@ } }, "404": { - "description": "User not found", + "description": "No staking farm source found", "content": { "application/json": { "schema": { @@ -7664,47 +7586,40 @@ "session_token": [] } ] - }, - "patch": { + } + }, + "/v1/organizations/{org_id}/staking/farm/sync": { + "post": { "tags": [ - "Users" + "Staking Farm" ], - "summary": "Update current user's profile", - "description": "Updates the profile information for the currently authenticated user.", - "operationId": "update_current_user_profile", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateUserProfileRequest" - } + "summary": "Sync organization staking farm credits", + "description": "Links or refreshes the NEAR-authenticated user's staking farm source for the\norganization, then syncs reward units from the configured staking contract.", + "operationId": "sync_organization_staking_farm", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Updated user profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - }, - "401": { - "description": "Unauthorized", + "description": "Synced organization staking farm state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/StakingFarmStateResponse" } } } }, - "500": { - "description": "Internal server error", + "400": { + "description": "Invalid organization ID", "content": { "application/json": { "schema": { @@ -7712,33 +7627,6 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me/access-tokens": { - "post": { - "tags": [ - "Users" - ], - "summary": "Create a new access token", - "description": "Creates a new short-lived access token using the current refresh token.", - "operationId": "create_access_token", - "responses": { - "200": { - "description": "Access token created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessAndRefreshTokenResponse" - } - } - } }, "401": { "description": "Unauthorized", @@ -7750,8 +7638,8 @@ } } }, - "500": { - "description": "Internal server error", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7759,39 +7647,9 @@ } } } - } - }, - "security": [ - { - "refresh_token": [] - } - ] - } - }, - "/v1/users/me/invitations": { - "get": { - "tags": [ - "Users" - ], - "summary": "List pending invitations for the current user", - "description": "Returns all pending organization invitations for the authenticated user's email.", - "operationId": "list_user_invitations", - "responses": { - "200": { - "description": "List of pending invitations", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationInvitationWithOrgResponse" - } - } - } - } }, - "401": { - "description": "Unauthorized", + "409": { + "description": "NEAR account conflict or organization inactive", "content": { "application/json": { "schema": { @@ -7818,43 +7676,32 @@ ] } }, - "/v1/users/me/invitations/{invitation_id}/accept": { - "post": { + "/v1/organizations/{org_id}/usage/balance": { + "get": { "tags": [ - "Users" + "Usage" ], - "summary": "Accept an organization invitation", - "description": "Accepts a pending invitation and adds the user as a member of the organization.", - "operationId": "accept_invitation", + "summary": "Get organization balance", + "description": "Returns the current spending balance for an organization", + "operationId": "get_organization_balance", "parameters": [ { - "name": "invitation_id", + "name": "org_id", "in": "path", - "description": "Invitation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], "responses": { "200": { - "description": "Invitation accepted successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptInvitationResponse" - } - } - } - }, - "400": { - "description": "Bad request - invitation expired or invalid", + "description": "Organization balance", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/OrganizationBalanceResponse" } } } @@ -7870,7 +7717,7 @@ } }, "403": { - "description": "Forbidden - invitation belongs to another user", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7880,17 +7727,7 @@ } }, "404": { - "description": "Invitation not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "409": { - "description": "User is already a member", + "description": "Not found", "content": { "application/json": { "schema": { @@ -7917,36 +7754,41 @@ ] } }, - "/v1/users/me/invitations/{invitation_id}/decline": { - "post": { + "/v1/organizations/{org_id}/usage/by-model": { + "get": { "tags": [ - "Users" + "Usage" ], - "summary": "Decline an organization invitation", - "description": "Declines a pending invitation to join an organization.", - "operationId": "decline_invitation", + "summary": "Get organization usage broken down by model.", + "description": "Returns one row per model, summed over a rolling window ending now:\n`day` = last 24h, `week` = last 7 days, `month` = last 30 days (NOT calendar\nday/week/month-to-date). Used by the dashboard pie chart to show which models\ndrive spend.", + "operationId": "get_organization_usage_by_model", "parameters": [ { - "name": "invitation_id", + "name": "org_id", "in": "path", - "description": "Invitation ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + }, + { + "name": "period", + "in": "query", + "description": "Rolling window: `day` (last 24h), `week` (last 7d), or `month` (last 30d). Default: `month`", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "Invitation declined successfully" - }, - "400": { - "description": "Bad request - invitation not pending", + "description": "Per-model usage breakdown", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UsageByModelResponse" } } } @@ -7962,17 +7804,7 @@ } }, "403": { - "description": "Forbidden - invitation belongs to another user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Invitation not found", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -7999,139 +7831,133 @@ ] } }, - "/v1/users/me/refresh-tokens": { + "/v1/organizations/{org_id}/usage/export": { "get": { "tags": [ - "Users" + "Reporting" ], - "summary": "Get user's refresh tokens", - "description": "Returns all active refresh tokens for the currently authenticated user.", - "operationId": "get_user_refresh_tokens", - "responses": { - "200": { - "description": "List of user refresh tokens", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefreshTokenResponse" - } - } - } + "summary": "Export organization usage and cost rows.", + "description": "Requires a reporting token scoped to the organization in the path. Results\nare returned in descending `(created_at, source, id)` order with opaque\ncursor pagination.", + "operationId": "export_usage", + "parameters": [ + { + "name": "org_id", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_time", + "in": "query", + "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "end_time", + "in": "query", + "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] - } - ] - } - }, - "/v1/users/me/refresh-tokens/{refresh_token_id}": { - "delete": { - "tags": [ - "Users" - ], - "summary": "Revoke a user refresh token", - "description": "Revokes a specific refresh token for the currently authenticated user.", - "operationId": "revoke_user_refresh_token", - "parameters": [ + "name": "source", + "in": "query", + "description": "Usage source to export. Defaults to all.", + "required": false, + "schema": { + "$ref": "#/components/schemas/ReportingUsageSource" + } + }, { - "name": "refresh_token_id", - "in": "path", - "description": "Refresh token ID to revoke", - "required": true, + "name": "workspace_id", + "in": "query", + "description": "Filter by workspace ID.", + "required": false, "schema": { "type": "string", "format": "uuid" } - } - ], - "responses": { - "204": { - "description": "Refresh token revoked successfully" }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" } }, - "404": { - "description": "Refresh token not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "model", + "in": "query", + "description": "Filter inference rows by model name.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "inference_type", + "in": "query", + "description": "Filter inference rows by inference type.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] + "name": "service_name", + "in": "query", + "description": "Filter service rows by platform service name.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum rows to return. Defaults to 100 and must not exceed 1000.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor returned by the previous export page.", + "required": false, + "schema": { + "type": "string" + } } - ] - } - }, - "/v1/users/me/status": { - "get": { - "tags": [ - "Users" ], - "summary": "Get current user's account eligibility status", - "description": "Returns a minimal eligibility response for frontend preflight checks. AML/KYT\nprovider details are never exposed in this public response.", - "operationId": "get_user_status", "responses": { "200": { - "description": "Current account is eligible", + "description": "Usage export page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserStatusResponse" + "$ref": "#/components/schemas/ReportingUsageExportResponse" } } } }, - "401": { - "description": "Unauthorized", + "400": { + "description": "Invalid filters or cursor", "content": { "application/json": { "schema": { @@ -8140,8 +7966,8 @@ } } }, - "403": { - "description": "Current account is not eligible", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8150,8 +7976,8 @@ } } }, - "500": { - "description": "Internal server error", + "403": { + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -8159,34 +7985,19 @@ } } } - } - }, - "security": [ - { - "session_token": [] - } - ] - } - }, - "/v1/users/me/tokens": { - "delete": { - "tags": [ - "Users" - ], - "summary": "Revoke all tokens for a user", - "description": "Revokes all tokens (both refresh tokens and access tokens) for the currently authenticated user.\nThis deletes all refresh tokens from the database and invalidates all JWT access tokens\nby updating the user's tokens_revoked_at timestamp.", - "operationId": "revoke_all_user_tokens", - "responses": { - "200": { - "description": "All tokens revoked successfully", + }, + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "401": { - "description": "Unauthorized", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -8195,8 +8006,8 @@ } } }, - "500": { - "description": "Internal server error", + "504": { + "description": "Reporting request timed out", "content": { "application/json": { "schema": { @@ -8208,135 +8019,113 @@ }, "security": [ { - "session_token": [] + "reporting_token": [] } ] } }, - "/v1/workspaces/{workspace_id}": { + "/v1/organizations/{org_id}/usage/history": { "get": { "tags": [ - "Workspaces" + "Usage" ], - "summary": "Get workspace by ID", - "description": "Returns workspace details for a specific workspace ID.", - "operationId": "get_workspace", + "summary": "Get organization usage history", + "description": "Returns paginated usage history for an organization", + "operationId": "get_organization_usage_history", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } - } - ], - "responses": { - "200": { - "description": "Workspace details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" - } - } + }, + { + "name": "limit", + "in": "query", + "description": "Number of records to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "offset", + "in": "query", + "description": "Offset for pagination (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_date", + "in": "query", + "description": "Inclusive UTC start date in YYYY-MM-DD or RFC3339 format.", + "required": false, + "schema": { + "type": "string" } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "end_date", + "in": "query", + "description": "Inclusive UTC end date in YYYY-MM-DD or RFC3339 format.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "start_time", + "in": "query", + "description": "Inclusive RFC3339 start timestamp. Takes precedence over start_date.", + "required": false, + "schema": { + "type": "string" } - } - }, - "security": [ + }, { - "session_token": [] - } - ] - }, - "put": { - "tags": [ - "Workspaces" - ], - "summary": "Update workspace", - "description": "Updates workspace details for a specific workspace ID.", - "operationId": "update_workspace", - "parameters": [ + "name": "end_time", + "in": "query", + "description": "Inclusive RFC3339 end timestamp. Takes precedence over end_date.", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, + "in": "query", + "description": "Filter by workspace ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, "schema": { "type": "string", "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWorkspaceRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Updated workspace", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceResponse" - } - } - } - }, - "400": { - "description": "Bad request", + "description": "Usage history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UsageHistoryResponse" } } } @@ -8361,16 +8150,6 @@ } } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -8387,42 +8166,56 @@ "session_token": [] } ] - }, - "delete": { + } + }, + "/v1/organizations/{org_id}/usage/metrics": { + "get": { "tags": [ - "Workspaces" + "Usage" ], - "summary": "Delete workspace", - "description": "Deletes (deactivates) a workspace. Only the workspace creator or organization admin/owner can delete.", - "operationId": "delete_workspace", + "operationId": "get_user_organization_metrics", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "description": "Start date (ISO 8601, default: 30 days ago)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "description": "End date (ISO 8601, default: now)", + "required": false, + "schema": { + "type": "string" } } ], "responses": { "200": { - "description": "Workspace deleted successfully" - }, - "401": { - "description": "Unauthorized", + "description": "Organization usage metrics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserOrganizationMetrics" } } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8431,8 +8224,8 @@ } } }, - "404": { - "description": "Workspace not found", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -8459,19 +8252,19 @@ ] } }, - "/v1/workspaces/{workspace_id}/api-keys": { + "/v1/organizations/{org_id}/usage/summary": { "get": { "tags": [ - "Workspaces" + "Reporting" ], - "summary": "List API keys for workspace", - "description": "Returns a paginated list of all API keys for a workspace with usage information.", - "operationId": "list_workspace_api_keys", + "summary": "Summarize organization usage and costs.", + "description": "Requires a reporting token scoped to the organization in the path. Totals\ninclude inference and platform service usage by default.", + "operationId": "summary_usage", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8479,144 +8272,93 @@ } }, { - "name": "limit", + "name": "start_time", "in": "query", - "description": "Maximum number of results (default: 20)", + "description": "Inclusive RFC3339 start timestamp. Defaults to 366 days before the effective end_time.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "end_time", "in": "query", - "description": "Number of results to skip (default: 0)", + "description": "Inclusive RFC3339 end timestamp. Defaults to the request time. The effective range must not exceed 366 days.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "order_by", + "name": "source", "in": "query", - "description": "Field to order by: created_at or usage", + "description": "Usage source to summarize. Defaults to all.", "required": false, "schema": { - "$ref": "#/components/schemas/ApiKeyOrderBy" + "$ref": "#/components/schemas/ReportingUsageSource" } }, { - "name": "order_direction", + "name": "workspace_id", "in": "query", - "description": "Sort direction: asc or desc", + "description": "Filter by workspace ID.", "required": false, "schema": { - "$ref": "#/components/schemas/ApiKeyOrderDirection" - } - } - ], - "responses": { - "200": { - "description": "Paginated list of workspace API keys", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListApiKeysResponse" - } - } + "type": "string", + "format": "uuid" } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "api_key_id", + "in": "query", + "description": "Filter by API key ID.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "model", + "in": "query", + "description": "Filter inference usage by model name.", + "required": false, + "schema": { + "type": "string" } }, - "404": { - "description": "Workspace not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + { + "name": "inference_type", + "in": "query", + "description": "Filter inference usage by inference type.", + "required": false, + "schema": { + "type": "string" } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "session_token": [] - } - ] - }, - "post": { - "tags": [ - "Workspaces" - ], - "summary": "Create API key for workspace", - "description": "Creates a new API key for a workspace. Note: The key may take up to 10 seconds\nto become active for authentication due to internal caching.", - "operationId": "create_workspace_api_key", - "parameters": [ { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, + "name": "service_name", + "in": "query", + "description": "Filter service usage by platform service name.", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyRequest" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "API key created successfully", + "200": { + "description": "Usage summary", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/ReportingUsageSummaryResponse" } } } }, "400": { - "description": "Bad request", + "description": "Invalid filters", "content": { "application/json": { "schema": { @@ -8636,7 +8378,7 @@ } }, "403": { - "description": "Forbidden", + "description": "Reporting token is not scoped to this organization", "content": { "application/json": { "schema": { @@ -8645,8 +8387,8 @@ } } }, - "404": { - "description": "Workspace not found", + "429": { + "description": "Reporting rate or concurrency limit exceeded", "content": { "application/json": { "schema": { @@ -8664,70 +8406,76 @@ } } } + }, + "504": { + "description": "Reporting request timed out", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, "security": [ { - "session_token": [] + "reporting_token": [] } ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{api_key_id}/usage/history": { + "/v1/organizations/{org_id}/usage/timeseries": { "get": { "tags": [ "Usage" ], - "summary": "Get API key usage history", - "description": "Returns paginated usage history for a specific API key", - "operationId": "get_api_key_usage_history", + "operationId": "get_user_organization_timeseries", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string" } }, { - "name": "api_key_id", - "in": "path", - "description": "API Key ID", - "required": true, + "name": "start", + "in": "query", + "description": "Start date (ISO 8601, default: 30 days ago)", + "required": false, "schema": { "type": "string" } }, { - "name": "limit", + "name": "end", "in": "query", - "description": "Number of records to return (default: 100)", + "description": "End date (ISO 8601, default: now)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "offset", + "name": "granularity", "in": "query", - "description": "Offset for pagination (default: 0)", + "description": "Time bucket size: hour, day, week (default: day)", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], "responses": { "200": { - "description": "Usage history", + "description": "Organization usage timeseries", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageHistoryResponse" + "$ref": "#/components/schemas/UserTimeSeriesMetrics" } } } @@ -8752,16 +8500,6 @@ } } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -8780,19 +8518,19 @@ ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{key_id}": { - "delete": { + "/v1/organizations/{org_id}/workspaces": { + "get": { "tags": [ "Workspaces" ], - "summary": "Revoke an API key", - "description": "Revokes a specific API key from a workspace.", - "operationId": "revoke_workspace_api_key", + "summary": "List workspaces in an organization", + "description": "Lists all workspaces within the specified organization. The authenticated user must\nbe a member of the organization to list workspaces.", + "operationId": "list_organization_workspaces", "parameters": [ { - "name": "workspace_id", + "name": "org_id", "in": "path", - "description": "Workspace ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8800,32 +8538,57 @@ } }, { - "name": "key_id", - "in": "path", - "description": "API Key ID", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of results", + "required": false, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of results to skip", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "order_by", + "in": "query", + "description": "Order by", + "required": false, + "schema": { + "$ref": "#/components/schemas/WorkspaceOrderBy" + } + }, + { + "name": "order_direction", + "in": "query", + "description": "Order direction", + "required": false, + "schema": { + "$ref": "#/components/schemas/WorkspaceOrderDirection" } } ], "responses": { - "204": { - "description": "API key revoked successfully" - }, - "401": { - "description": "Unauthorized", + "200": { + "description": "Paginated list of workspaces", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/ListWorkspacesResponse" } } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8834,8 +8597,8 @@ } } }, - "404": { - "description": "API key not found", + "403": { + "description": "Forbidden - not a member of organization", "content": { "application/json": { "schema": { @@ -8861,28 +8624,18 @@ } ] }, - "patch": { + "post": { "tags": [ "Workspaces" ], - "summary": "Update API key", - "description": "Updates an API key's name, expiration date, and/or spending limit. The user must be a member of the\norganization that owns the workspace. All fields are optional - only provided fields will be updated.", - "operationId": "update_workspace_api_key", + "summary": "Create a new workspace in an organization", + "description": "Creates a new workspace within the specified organization. The authenticated user must\nbe a member of the organization to create workspaces.", + "operationId": "create_workspace", "parameters": [ { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "key_id", + "name": "org_id", "in": "path", - "description": "API Key ID", + "description": "Organization ID", "required": true, "schema": { "type": "string", @@ -8894,19 +8647,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateApiKeyRequest" + "$ref": "#/components/schemas/CreateWorkspaceRequest" } } }, "required": true }, "responses": { - "200": { - "description": "API key updated successfully", + "201": { + "description": "Workspace created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/WorkspaceResponse" } } } @@ -8932,7 +8685,82 @@ } }, "403": { - "description": "Forbidden - not authorized to update this key", + "description": "Forbidden - not a member of organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Workspace name already exists in organization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/privacy/classify": { + "post": { + "tags": [ + "Privacy" + ], + "operationId": "privacy_classify", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyClassifyRequestDoc" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Privacy classification completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyClassifyResponseDoc" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "application/json": { "schema": { @@ -8942,7 +8770,7 @@ } }, "404": { - "description": "API key or workspace not found", + "description": "Model not found", "content": { "application/json": { "schema": { @@ -8951,8 +8779,8 @@ } } }, - "409": { - "description": "Conflict - API key with this name already exists", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -8962,7 +8790,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error", "content": { "application/json": { "schema": { @@ -8974,46 +8802,109 @@ }, "security": [ { - "session_token": [] + "api_key": [] } ] } }, - "/v1/workspaces/{workspace_id}/api-keys/{key_id}/spend-limit": { - "patch": { + "/v1/privacy/redact": { + "post": { "tags": [ - "Workspaces" + "Privacy" ], - "summary": "Update API key spend limit", - "description": "Updates the spending limit for a specific API key. The user must be a member of the\norganization that owns the workspace. Set spend_limit to null to remove the limit.", - "operationId": "update_api_key_spend_limit", - "parameters": [ - { - "name": "workspace_id", - "in": "path", - "description": "Workspace ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "operationId": "privacy_redact", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyRedactRequestDoc" + } } }, - { - "name": "key_id", - "in": "path", - "description": "API Key ID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "required": true + }, + "responses": { + "200": { + "description": "Privacy redaction completed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivacyRedactResponseDoc" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/rerank": { + "post": { + "tags": [ + "Rerank" ], + "summary": "Document reranking endpoint", + "description": "Ranks documents by relevance to a query using a reranker model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", + "operationId": "rerank", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateApiKeySpendLimitRequest" + "$ref": "#/components/schemas/RerankRequest" } } }, @@ -9021,17 +8912,17 @@ }, "responses": { "200": { - "description": "Spend limit updated successfully", + "description": "Successful rerank", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiKeyResponse" + "$ref": "#/components/schemas/RerankResponse" } } } }, "400": { - "description": "Bad request", + "description": "Invalid request (empty documents, invalid model, etc.)", "content": { "application/json": { "schema": { @@ -9041,7 +8932,7 @@ } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized (missing or invalid API key)", "content": { "application/json": { "schema": { @@ -9050,8 +8941,8 @@ } } }, - "403": { - "description": "Forbidden - not authorized to update this key", + "404": { + "description": "Model not found", "content": { "application/json": { "schema": { @@ -9060,8 +8951,8 @@ } } }, - "404": { - "description": "API key or workspace not found", + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", "content": { "application/json": { "schema": { @@ -9071,7 +8962,7 @@ } }, "500": { - "description": "Internal server error", + "description": "Server error (billing failure, provider error)", "content": { "application/json": { "schema": { @@ -9083,1392 +8974,4456 @@ }, "security": [ { - "session_token": [] + "ApiKeyAuth": [] } ] } - } - }, - "components": { - "schemas": { - "AcceptInvitationResponse": { - "type": "object", - "description": "Accept invitation response", - "required": [ - "organization_member", - "message" + }, + "/v1/responses": { + "post": { + "tags": [ + "Responses" ], - "properties": { - "message": { - "type": "string" + "summary": "Create response", + "description": "Generate an AI response for a conversation with tool calling and streaming support.", + "operationId": "create_response", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResponseRequest" + } + } }, - "organization_member": { - "$ref": "#/components/schemas/OrganizationMemberResponse" - } - } - }, - "AccessAndRefreshTokenResponse": { - "type": "object", - "description": "Access token + refresh token response model", - "required": [ - "access_token", - "refresh_token", - "refresh_token_expiration" - ], - "properties": { - "access_token": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "Response created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "refresh_token": { - "type": "string" + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "refresh_token_expiration": { - "type": "string", - "format": "date-time" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "402": { + "description": "Insufficient credits", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AddOrganizationMemberRequest": { - "type": "object", - "description": "Request to add an organization member", - "required": [ - "user_id", - "role" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}": { + "get": { + "tags": [ + "Responses" ], - "properties": { - "role": { - "$ref": "#/components/schemas/MemberRole" - }, - "user_id": { - "type": "string" + "summary": "Get a response by ID", + "description": "Retrieve details of a specific response.", + "operationId": "get_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "AdminAccessTokenResponse": { - "type": "object", - "description": "Admin access token response model", - "required": [ - "id", - "access_token", - "created_by_user_id", - "created_at", - "expires_at", - "name", - "reason" ], - "properties": { - "access_token": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "expires_at": { - "type": "string", - "format": "date-time" + "responses": { + "200": { + "description": "Response details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "id": { - "type": "string" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "name": { - "type": "string" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "reason": { - "type": "string" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } + }, + "security": [ + { + "api_key": [] + } + ] }, - "AdminFeatureRequestListResponse": { - "type": "object", - "required": [ - "requests", - "limit", - "offset", - "total" + "delete": { + "tags": [ + "Responses" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "offset": { - "type": "integer", - "format": "int64" - }, - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminFeatureRequestSummaryResponse" + "summary": "Delete a response", + "description": "Delete a specific response.", + "operationId": "delete_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" } - }, - "total": { - "type": "integer", - "format": "int64" } - } - }, - "AdminFeatureRequestSummaryResponse": { - "type": "object", - "required": [ - "target", - "uniqueUserCount", - "uniqueOrganizationCount", - "latestRequestedAt", - "recentVotes" ], - "properties": { - "latestRequestedAt": { - "type": "string", - "format": "date-time" + "responses": { + "200": { + "description": "Response deleted successfully" }, - "recentVotes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FeatureRequestVoteResponse" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "target": { - "$ref": "#/components/schemas/FeatureRequestTargetResponse" - }, - "uniqueOrganizationCount": { - "type": "integer", - "format": "int64" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "uniqueUserCount": { - "type": "integer", - "format": "int64" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminInvitationEmailDeliveryResponse": { - "type": "object", - "description": "Admin view of invitation email delivery metadata.", - "required": [ - "organization_id", - "organization_name", - "invitation_id", - "recipient_email", - "role", - "invitation_status", - "email_status", - "invited_by_user_id", - "created_at", - "expires_at" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}/cancel": { + "post": { + "tags": [ + "Responses" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "email_last_error": { - "type": [ - "string", - "null" - ] + "summary": "Cancel a response (for background responses)", + "description": "Cancel an in-progress background response.", + "operationId": "cancel_response", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Response cancelled successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseObject" + } + } + } }, - "email_message_id": { - "type": [ - "string", - "null" - ] + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "email_sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "email_status": { - "$ref": "#/components/schemas/InvitationEmailStatus" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/responses/{response_id}/input_items": { + "get": { + "tags": [ + "Responses" + ], + "summary": "List input items for a response", + "description": "Retrieve all input items (user messages and files) for a specific response.", + "operationId": "list_input_items", + "parameters": [ + { + "name": "response_id", + "in": "path", + "description": "Response ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of input items", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseInputItemList" + } + } + } }, - "expires_at": { - "type": "string", - "format": "date-time" + "400": { + "description": "Invalid response ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invitation_id": { - "type": "string" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invitation_status": { - "$ref": "#/components/schemas/InvitationStatus" + "404": { + "description": "Response not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "invited_by_display_name": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/score": { + "post": { + "tags": [ + "Score" + ], + "summary": "Text similarity scoring endpoint", + "description": "Scores the similarity between two texts using a scoring/ranking model.\n\n**Concurrent Request Limits:** Each organization has a per-model concurrent request limit (default: 64).\nWhen the limit is reached, new requests will fail with 429 status code. Wait for in-flight requests to complete before retrying.", + "operationId": "score", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScoreRequest" + } + } }, - "invited_by_email": { - "type": [ - "string", - "null" - ] + "required": true + }, + "responses": { + "200": { + "description": "Successful score", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScoreResponse" + } + } + } }, - "invited_by_user_id": { - "type": "string" + "400": { + "description": "Invalid request (empty texts, invalid model, etc.)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "organization_id": { - "type": "string" + "401": { + "description": "Unauthorized (missing or invalid API key)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "organization_name": { - "type": "string" + "404": { + "description": "Model not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "recipient_email": { - "type": "string" + "429": { + "description": "Rate limited or overloaded β€” retry with backoff. Check error.type: rate_limit_exceeded or service_overloaded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "responded_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "role": { - "$ref": "#/components/schemas/MemberRole" + "500": { + "description": "Server error (billing failure, provider error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminInvitationEmailResendResultResponse": { - "type": "object", - "description": "Admin invitation email resend result.", - "required": [ - "invitation_id", - "recipient_email", - "success", - "email_sent", - "email_status" + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v1/services": { + "get": { + "tags": [ + "Services" ], - "properties": { - "email_last_error": { - "type": [ - "string", - "null" - ] - }, - "email_message_id": { - "type": [ - "string", - "null" - ] - }, - "email_sent": { - "type": "boolean" - }, - "email_sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "email_status": { - "$ref": "#/components/schemas/InvitationEmailStatus" - }, - "error": { - "type": [ - "string", - "null" - ] - }, - "invitation_id": { - "type": "string" - }, - "recipient_email": { - "type": "string" + "summary": "List platform services (public, no auth)", + "operationId": "list_services", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } }, - "success": { - "type": "boolean" + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } } - } - }, - "AdminModelListResponse": { - "type": "object", - "description": "Response for admin model list endpoint", - "required": [ - "models", - "limit", - "offset", - "total" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "models": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminModelWithPricing" + "responses": { + "200": { + "description": "Services list", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceListResponse" + } + } } }, - "offset": { - "type": "integer", - "format": "int64" + "400": { + "description": "Invalid parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "total": { - "type": "integer", - "format": "int64" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - }, - "AdminModelWithPricing": { - "type": "object", - "description": "Model with pricing information for admin listing", - "required": [ - "modelId", - "inputCostPerToken", - "outputCostPerToken", - "costPerImage", - "metadata", - "isActive", - "createdAt", - "updatedAt" + } + }, + "/v1/services/{service_name}": { + "get": { + "tags": [ + "Services" ], - "properties": { - "cacheReadCostPerToken": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice", - "description": "Cost per cached input token. Omitted when cache pricing is disabled;\nan amount of 0 means cached tokens are genuinely free." + "summary": "Get platform service by name (public, no auth)", + "operationId": "get_service_by_name", + "parameters": [ + { + "name": "service_name", + "in": "path", + "description": "Service name (e.g. web_search)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Service details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceResponse" + } } - ] - }, - "costPerImage": { - "$ref": "#/components/schemas/DecimalPrice" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "inputCostPerToken": { - "$ref": "#/components/schemas/DecimalPrice" - }, - "isActive": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/ModelMetadata" - }, - "modelId": { - "type": "string" + } }, - "outputCostPerToken": { - "$ref": "#/components/schemas/DecimalPrice" + "404": { + "description": "Service not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "updatedAt": { - "type": "string", - "format": "date-time" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - }, - "AdminOrganizationMemberResponse": { - "type": "object", - "description": "Admin organization member response (for owners/admins)\nContains member info with full user details including sensitive data", - "required": [ - "id", - "organization_id", - "role", - "joined_at", - "user" - ], - "properties": { - "id": { - "type": "string" - }, - "invited_by": { - "type": [ - "string", - "null" - ] - }, - "joined_at": { - "type": "string", - "format": "date-time" - }, - "organization_id": { - "type": "string" + } + }, + "/v1/signature/{chat_id}": { + "get": { + "tags": [ + "Attestation" + ], + "summary": "Get completion signature", + "description": "Get cryptographic signature for a chat completion for verification.\nReturns signature data on success, or an unavailable response if the stream was disconnected.", + "operationId": "get_signature", + "parameters": [ + { + "name": "chat_id", + "in": "path", + "description": "Chat completion ID", + "required": true, + "schema": { + "type": "string" + } }, - "role": { - "$ref": "#/components/schemas/MemberRole" + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } }, - "user": { - "$ref": "#/components/schemas/AdminUserResponse" + { + "name": "signing_algo", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } } - } - }, - "AdminOrganizationResponse": { - "type": "object", - "description": "Organization details for admin organization listing", - "required": [ - "id", - "name", - "created_at" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "currentUsage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OrganizationUsage" + "responses": { + "200": { + "description": "Signature retrieved or unavailable due to disconnect", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignatureResponse" + } } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" + } }, - "name": { - "type": "string" + "400": { + "description": "Invalid parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spendLimit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SpendLimit" + "404": { + "description": "Signature not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } } - } - }, - "AdminServiceListResponse": { - "type": "object", - "description": "Response for admin service list endpoint", - "required": [ - "services", - "limit", - "offset", - "total" + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v1/staking/farm/config": { + "get": { + "tags": [ + "Staking Farm" ], - "properties": { - "limit": { - "type": "integer", - "format": "int64" - }, - "offset": { - "type": "integer", - "format": "int64" + "summary": "Get staking farm configuration", + "description": "Returns the active House of Stake farm configuration used to convert\non-chain staking reward units into NEAR AI Cloud credits.", + "operationId": "get_staking_farm_config", + "responses": { + "200": { + "description": "Staking farm configuration", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StakingFarmConfigResponse" + } + } + } }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdminServiceResponse" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "total": { - "type": "integer", - "format": "int64" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AdminServiceResponse": { - "type": "object", - "description": "Platform service (admin CRUD) β€” single item", - "required": [ - "id", - "serviceName", - "displayName", - "unit", - "costPerUnit", - "isActive", - "createdAt", - "updatedAt" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me": { + "get": { + "tags": [ + "Users" ], - "properties": { - "costPerUnit": { - "type": "integer", - "format": "int64", - "description": "Price per unit in nano-USD (scale 9)." + "summary": "Get current user", + "description": "Returns the profile of the currently authenticated user, including their organizations and workspaces.", + "operationId": "get_current_user", + "responses": { + "200": { + "description": "Current user profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } }, - "createdAt": { - "type": "string", - "format": "date-time" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "description": { - "type": [ - "string", - "null" - ] + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "displayName": { - "type": "string" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "isActive": { - "type": "boolean" - }, - "serviceName": { - "type": "string" - }, - "unit": { - "$ref": "#/components/schemas/ServiceUnit" - }, - "updatedAt": { - "type": "string", - "format": "date-time" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } + }, + "security": [ + { + "session_token": [] + } + ] }, - "AdminUserOrganizationDetails": { - "type": "object", - "description": "Organization details with spend limit and usage (for admin user listing)", - "required": [ - "id", - "name" + "patch": { + "tags": [ + "Users" ], - "properties": { - "currentUsage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OrganizationUsage" + "summary": "Update current user's profile", + "description": "Updates the profile information for the currently authenticated user.", + "operationId": "update_current_user_profile", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserProfileRequest" } - ] - }, - "description": { - "type": [ - "string", - "null" - ] + } }, - "id": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "Updated user profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } }, - "name": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spendLimit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SpendLimit" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } } - } - }, - "AdminUserResponse": { - "type": "object", - "description": "Admin user response model (for owners/admins)\nContains sensitive information only visible to organization owners/admins", - "required": [ - "id", - "email", - "created_at", - "is_active", - "auth_provider", - "provider_user_id" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/access-tokens": { + "post": { + "tags": [ + "Users" ], - "properties": { - "auth_provider": { - "type": "string" - }, - "avatar_url": { - "type": [ - "string", - "null" - ] - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "display_name": { - "type": [ - "string", - "null" - ] - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "last_login_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "organizations": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/AdminUserOrganizationDetails" + "summary": "Create a new access token", + "description": "Creates a new short-lived access token using the current refresh token.", + "operationId": "create_access_token", + "responses": { + "200": { + "description": "Access token created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessAndRefreshTokenResponse" + } + } } }, - "provider_user_id": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "username": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ApiKeyResponse": { - "type": "object", - "description": "API Key response model", - "required": [ - "id", - "key_prefix", - "workspace_id", - "created_by_user_id", - "created_at", - "is_active" + }, + "security": [ + { + "refresh_token": [] + } + ] + } + }, + "/v1/users/me/invitations": { + "get": { + "tags": [ + "Users" ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "deleted_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "expires_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "id": { - "type": "string" + "summary": "List pending invitations for the current user", + "description": "Returns all pending organization invitations for the authenticated user's email.", + "operationId": "list_user_invitations", + "responses": { + "200": { + "description": "List of pending invitations", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationInvitationWithOrgResponse" + } + } + } + } }, - "is_active": { - "type": "boolean" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "key": { - "type": [ - "string", - "null" - ] + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/invitations/{invitation_id}/accept": { + "post": { + "tags": [ + "Users" + ], + "summary": "Accept an organization invitation", + "description": "Accepts a pending invitation and adds the user as a member of the organization.", + "operationId": "accept_invitation", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Invitation accepted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcceptInvitationResponse" + } + } + } }, - "key_prefix": { - "type": "string" + "400": { + "description": "Bad request - invitation expired or invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "last_used_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "name": { - "type": [ - "string", - "null" - ] + "403": { + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "spend_limit": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice" + "404": { + "description": "Invitation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "usage": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/DecimalPrice" + "409": { + "description": "User is already a member", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "workspace_id": { - "type": "string" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AttestationResponse": { - "type": "object", - "required": [ - "gateway_attestation" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/invitations/{invitation_id}/decline": { + "post": { + "tags": [ + "Users" ], - "properties": { - "gateway_attestation": { - "$ref": "#/components/schemas/DstackCpuQuote" + "summary": "Decline an organization invitation", + "description": "Declines a pending invitation to join an organization.", + "operationId": "decline_invitation", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "description": "Invitation ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Invitation declined successfully" }, - "model_attestations": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {}, - "propertyNames": { - "type": "string" + "400": { + "description": "Bad request - invitation not pending", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "ohttp_attestation": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OhttpAttestation", - "description": "OHTTP key attestation payload. Includes an Ed25519 signature over the decoded\n`key_config` bytes so clients can verify the HPKE key is bound to the TEE." + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - ] + } }, - "ohttp_key_config": { - "type": [ - "string", - "null" - ], - "description": "Hex-encoded OHTTP key configuration (RFC 9458). Present only when OHTTP_ENABLED=true.\nLegacy flat field; mirrors `ohttp_attestation.key_config` when present." + "403": { + "description": "Forbidden - invitation belongs to another user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "tls_certificate": { - "type": [ - "string", - "null" - ], - "description": "TLS certificate file (PEM) from TLS_CERT_PATH; report_data binds via SHA256 of these exact bytes" - } - } - }, - "AudioTranscriptionRequestSchema": { - "type": "object", - "description": "Audio transcription request schema for OpenAPI documentation\nThis represents the multipart/form-data fields", - "required": [ - "file", - "model" - ], - "properties": { - "file": { - "type": "string", - "description": "Audio file (required) - binary audio data" - }, - "language": { - "type": [ - "string", - "null" - ], - "description": "Language code (optional) - e.g. \"en\", \"es\"" - }, - "model": { - "type": "string", - "description": "Model identifier (required) - e.g. \"openai/whisper-large-v3\"" - }, - "response_format": { - "type": [ - "string", - "null" - ], - "description": "Response format (optional) - one of: \"json\", \"text\", \"verbose_json\"" + "404": { + "description": "Invitation not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "timestamp_granularities": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "description": "Timestamp granularities (optional) - supports \"segment\" and \"word\" with verbose_json.\nMultipart requests may send repeated timestamp_granularities[] fields,\nrepeated timestamp_granularities fields, or comma-separated values." + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "AudioTranscriptionResponse": { - "type": "object", - "description": "Audio transcription response (with OpenAPI schema)", - "required": [ - "text" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/refresh-tokens": { + "get": { + "tags": [ + "Users" ], - "properties": { - "duration": { - "type": [ - "number", - "null" - ], - "format": "double", - "description": "Total audio duration in seconds" - }, - "language": { - "type": [ - "string", - "null" - ], - "description": "Detected language code" - }, - "segments": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/TranscriptionSegment" - }, - "description": "Transcription segments with timing" + "summary": "Get user's refresh tokens", + "description": "Returns all active refresh tokens for the currently authenticated user.", + "operationId": "get_user_refresh_tokens", + "responses": { + "200": { + "description": "List of user refresh tokens", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefreshTokenResponse" + } + } + } + } }, - "text": { - "type": "string", - "description": "Transcribed text" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "words": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/TranscriptionWord" - }, - "description": "Word-level timing information" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "BillingCostsRequest": { - "type": "object", - "description": "Billing costs request (HuggingFace compatible)", - "required": [ - "requestIds" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/refresh-tokens/{refresh_token_id}": { + "delete": { + "tags": [ + "Users" ], - "properties": { - "requestIds": { - "type": "array", - "items": { + "summary": "Revoke a user refresh token", + "description": "Revokes a specific refresh token for the currently authenticated user.", + "operationId": "revoke_user_refresh_token", + "parameters": [ + { + "name": "refresh_token_id", + "in": "path", + "description": "Refresh token ID to revoke", + "required": true, + "schema": { "type": "string", "format": "uuid" - }, - "description": "Array of request IDs (inference IDs) to get costs for" - } - } - }, - "BillingCostsResponse": { - "type": "object", - "description": "Billing costs response (HuggingFace compatible)", - "required": [ - "requests" - ], - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RequestCost" - }, - "description": "Array of request costs" + } } - } - }, - "BillingSourceBreakdown": { - "type": "object", - "description": "Active credit limit by funding source (caps, not payments).", - "required": [ - "source", - "paid_credit_limit_usd", - "org_count" ], - "properties": { - "org_count": { - "type": "integer", - "format": "int64" + "responses": { + "204": { + "description": "Refresh token revoked successfully" }, - "paid_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active payment-type spend limits (caps) for this source, USD" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "source": { - "type": "string" + "404": { + "description": "Refresh token not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "BillingSummary": { - "type": "object", - "description": "Platform billing summary β€” credit LIMITS (caps) and consumption.\n\nThese are spend-limit ceilings from `organization_limits_history`, NOT payments\nor cash received. Real money-in (Stripe top-ups) lives in the billing service\n(nearai-cloud-ui), not in cloud-api.", - "required": [ - "generated_at", - "active_paid_credit_limit_usd", - "active_grant_credit_limit_usd", - "total_consumed_usd", - "inference_consumed_usd", - "service_consumed_usd", - "paying_org_count", - "granted_org_count", - "by_source" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/status": { + "get": { + "tags": [ + "Users" ], - "properties": { - "active_grant_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active grant-type spend limits (caps), USD" - }, - "active_paid_credit_limit_usd": { - "type": "number", - "format": "double", - "description": "Sum of active payment-type spend limits (caps), USD" - }, - "by_source": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BillingSourceBreakdown" + "summary": "Get current user's account eligibility status", + "description": "Returns a minimal eligibility response for frontend preflight checks. AML/KYT\nprovider details are never exposed in this public response.", + "operationId": "get_user_status", + "responses": { + "200": { + "description": "Current account is eligible", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStatusResponse" + } + } } }, - "generated_at": { - "type": "string", - "format": "date-time" - }, - "granted_org_count": { - "type": "integer", - "format": "int64" - }, - "inference_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time inference consumed cost, USD (organization_usage_log)" - }, - "paying_org_count": { - "type": "integer", - "format": "int64" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "service_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time service consumed cost, USD (organization_service_usage_log, e.g. web_search)" + "403": { + "description": "Current account is not eligible", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "total_consumed_usd": { - "type": "number", - "format": "double", - "description": "All-time consumed cost across all orgs, USD β€” **all usage** (from\norganization_balance: inference + services). `inference_consumed_usd +\nservice_consumed_usd` reconcile to this." + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ChatChoice": { - "type": "object", - "required": [ - "index", - "message" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/users/me/tokens": { + "delete": { + "tags": [ + "Users" ], - "properties": { - "finish_reason": { - "type": [ - "string", - "null" - ] + "summary": "Revoke all tokens for a user", + "description": "Revokes all tokens (both refresh tokens and access tokens) for the currently authenticated user.\nThis deletes all refresh tokens from the database and invalidates all JWT access tokens\nby updating the user's tokens_revoked_at timestamp.", + "operationId": "revoke_all_user_tokens", + "responses": { + "200": { + "description": "All tokens revoked successfully", + "content": { + "application/json": { + "schema": {} + } + } }, - "index": { - "type": "integer", - "format": "int64" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "message": { - "$ref": "#/components/schemas/Message" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } - } - }, - "ChatCompletionRequest": { - "type": "object", - "required": [ - "model", - "messages" + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}": { + "get": { + "tags": [ + "Workspaces" ], - "properties": { - "frequency_penalty": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "max_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "summary": "Get workspace by ID", + "description": "Returns workspace details for a specific workspace ID.", + "operationId": "get_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Workspace details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } + } }, - "messages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - "model": { - "type": "string" + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "n": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "put": { + "tags": [ + "Workspaces" + ], + "summary": "Update workspace", + "description": "Updates workspace details for a specific workspace ID.", + "operationId": "update_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated workspace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "delete": { + "tags": [ + "Workspaces" + ], + "summary": "Delete workspace", + "description": "Deletes (deactivates) a workspace. Only the workspace creator or organization admin/owner can delete.", + "operationId": "delete_workspace", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Workspace deleted successfully" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys": { + "get": { + "tags": [ + "Workspaces" + ], + "summary": "List API keys for workspace", + "description": "Returns a paginated list of all API keys for a workspace with usage information.", + "operationId": "list_workspace_api_keys", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of results (default: 20)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of results to skip (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "order_by", + "in": "query", + "description": "Field to order by: created_at or usage", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiKeyOrderBy" + } + }, + { + "name": "order_direction", + "in": "query", + "description": "Sort direction: asc or desc", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiKeyOrderDirection" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of workspace API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListApiKeysResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "post": { + "tags": [ + "Workspaces" + ], + "summary": "Create API key for workspace", + "description": "Creates a new API key for a workspace. Note: The key may take up to 10 seconds\nto become active for authentication due to internal caching.", + "operationId": "create_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "API key created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{api_key_id}/usage/history": { + "get": { + "tags": [ + "Usage" + ], + "summary": "Get API key usage history", + "description": "Returns paginated usage history for a specific API key", + "operationId": "get_api_key_usage_history", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api_key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of records to return (default: 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for pagination (default: 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Usage history", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageHistoryResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{key_id}": { + "delete": { + "tags": [ + "Workspaces" + ], + "summary": "Revoke an API key", + "description": "Revokes a specific API key from a workspace.", + "operationId": "revoke_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "API key revoked successfully" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + }, + "patch": { + "tags": [ + "Workspaces" + ], + "summary": "Update API key", + "description": "Updates an API key's name, expiration date, and/or spending limit. The user must be a member of the\norganization that owns the workspace. All fields are optional - only provided fields will be updated.", + "operationId": "update_workspace_api_key", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "API key updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - not authorized to update this key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key or workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict - API key with this name already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + }, + "/v1/workspaces/{workspace_id}/api-keys/{key_id}/spend-limit": { + "patch": { + "tags": [ + "Workspaces" + ], + "summary": "Update API key spend limit", + "description": "Updates the spending limit for a specific API key. The user must be a member of the\norganization that owns the workspace. Set spend_limit to null to remove the limit.", + "operationId": "update_api_key_spend_limit", + "parameters": [ + { + "name": "workspace_id", + "in": "path", + "description": "Workspace ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "key_id", + "in": "path", + "description": "API Key ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeySpendLimitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Spend limit updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - not authorized to update this key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API key or workspace not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "session_token": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AcceptInvitationResponse": { + "type": "object", + "description": "Accept invitation response", + "required": [ + "organization_member", + "message" + ], + "properties": { + "message": { + "type": "string" + }, + "organization_member": { + "$ref": "#/components/schemas/OrganizationMemberResponse" + } + } + }, + "AccessAndRefreshTokenResponse": { + "type": "object", + "description": "Access token + refresh token response model", + "required": [ + "access_token", + "refresh_token", + "refresh_token_expiration" + ], + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "refresh_token_expiration": { + "type": "string", + "format": "date-time" + } + } + }, + "AddOrganizationMemberRequest": { + "type": "object", + "description": "Request to add an organization member", + "required": [ + "user_id", + "role" + ], + "properties": { + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user_id": { + "type": "string" + } + } + }, + "AdminAccessTokenResponse": { + "type": "object", + "description": "Admin access token response model", + "required": [ + "id", + "access_token", + "created_by_user_id", + "created_at", + "expires_at", + "name", + "reason" + ], + "properties": { + "access_token": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, + "AdminAmlAllowlistEntryResponse": { + "type": "object", + "description": "Admin AML allowlist entry response.", + "required": [ + "account_id", + "address_type", + "created_at" + ], + "properties": { + "account_id": { + "type": "string" + }, + "address_type": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + }, + "AdminAmlReportResponse": { + "type": "object", + "description": "Admin AML report audit entry.", + "required": [ + "id", + "flow", + "provider", + "account_id", + "address_type", + "risk_level", + "active", + "created_at", + "updated_at" + ], + "properties": { + "account_id": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "address_type": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "string" + }, + "id": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider_report_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "report_id": { + "type": [ + "string", + "null" + ] + }, + "risk_level": { + "type": "string" + }, + "score": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "user_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "AdminFeatureRequestListResponse": { + "type": "object", + "required": [ + "requests", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminFeatureRequestSummaryResponse" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminFeatureRequestSummaryResponse": { + "type": "object", + "required": [ + "target", + "uniqueUserCount", + "uniqueOrganizationCount", + "latestRequestedAt", + "recentVotes" + ], + "properties": { + "latestRequestedAt": { + "type": "string", + "format": "date-time" + }, + "recentVotes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FeatureRequestVoteResponse" + } + }, + "target": { + "$ref": "#/components/schemas/FeatureRequestTargetResponse" + }, + "uniqueOrganizationCount": { + "type": "integer", + "format": "int64" + }, + "uniqueUserCount": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminInvitationEmailDeliveryResponse": { + "type": "object", + "description": "Admin view of invitation email delivery metadata.", + "required": [ + "organization_id", + "organization_name", + "invitation_id", + "recipient_email", + "role", + "invitation_status", + "email_status", + "invited_by_user_id", + "created_at", + "expires_at" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "email_last_error": { + "type": [ + "string", + "null" + ] + }, + "email_message_id": { + "type": [ + "string", + "null" + ] + }, + "email_sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "email_status": { + "$ref": "#/components/schemas/InvitationEmailStatus" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "invitation_id": { + "type": "string" + }, + "invitation_status": { + "$ref": "#/components/schemas/InvitationStatus" + }, + "invited_by_display_name": { + "type": [ + "string", + "null" + ] + }, + "invited_by_email": { + "type": [ + "string", + "null" + ] + }, + "invited_by_user_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "organization_name": { + "type": "string" + }, + "recipient_email": { + "type": "string" + }, + "responded_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "role": { + "$ref": "#/components/schemas/MemberRole" + } + } + }, + "AdminInvitationEmailResendResultResponse": { + "type": "object", + "description": "Admin invitation email resend result.", + "required": [ + "invitation_id", + "recipient_email", + "success", + "email_sent", + "email_status" + ], + "properties": { + "email_last_error": { + "type": [ + "string", + "null" + ] + }, + "email_message_id": { + "type": [ + "string", + "null" + ] + }, + "email_sent": { + "type": "boolean" + }, + "email_sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "email_status": { + "$ref": "#/components/schemas/InvitationEmailStatus" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "invitation_id": { + "type": "string" + }, + "recipient_email": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "AdminModelListResponse": { + "type": "object", + "description": "Response for admin model list endpoint", + "required": [ + "models", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminModelWithPricing" + } + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminModelWithPricing": { + "type": "object", + "description": "Model with pricing information for admin listing", + "required": [ + "modelId", + "inputCostPerToken", + "outputCostPerToken", + "costPerImage", + "metadata", + "isActive", + "createdAt", + "updatedAt" + ], + "properties": { + "cacheReadCostPerToken": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice", + "description": "Cost per cached input token. Omitted when cache pricing is disabled;\nan amount of 0 means cached tokens are genuinely free." + } + ] + }, + "costPerImage": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "inputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "isActive": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/ModelMetadata" + }, + "modelId": { + "type": "string" + }, + "outputCostPerToken": { + "$ref": "#/components/schemas/DecimalPrice" + }, + "textPricing": {}, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AdminOrganizationMemberResponse": { + "type": "object", + "description": "Admin organization member response (for owners/admins)\nContains member info with full user details including sensitive data", + "required": [ + "id", + "organization_id", + "role", + "joined_at", + "user" + ], + "properties": { + "id": { + "type": "string" + }, + "invited_by": { + "type": [ + "string", + "null" + ] + }, + "joined_at": { + "type": "string", + "format": "date-time" + }, + "organization_id": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/MemberRole" + }, + "user": { + "$ref": "#/components/schemas/AdminUserResponse" + } + } + }, + "AdminOrganizationResponse": { + "type": "object", + "description": "Organization details for admin organization listing", + "required": [ + "id", + "name", + "created_at" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spendLimit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SpendLimit" + } + ] + } + } + }, + "AdminServiceListResponse": { + "type": "object", + "description": "Response for admin service list endpoint", + "required": [ + "services", + "limit", + "offset", + "total" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminServiceResponse" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "AdminServiceResponse": { + "type": "object", + "description": "Platform service (admin CRUD) β€” single item", + "required": [ + "id", + "serviceName", + "displayName", + "unit", + "costPerUnit", + "isActive", + "createdAt", + "updatedAt" + ], + "properties": { + "costPerUnit": { + "type": "integer", + "format": "int64", + "description": "Price per unit in nano-USD (scale 9)." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "isActive": { + "type": "boolean" + }, + "serviceName": { + "type": "string" + }, + "unit": { + "$ref": "#/components/schemas/ServiceUnit" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AdminUserOrganizationDetails": { + "type": "object", + "description": "Organization details with spend limit and usage (for admin user listing)", + "required": [ + "id", + "name" + ], + "properties": { + "currentUsage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OrganizationUsage" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spendLimit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SpendLimit" + } + ] + } + } + }, + "AdminUserResponse": { + "type": "object", + "description": "Admin user response model (for owners/admins)\nContains sensitive information only visible to organization owners/admins", + "required": [ + "id", + "email", + "created_at", + "is_active", + "auth_provider", + "provider_user_id" + ], + "properties": { + "auth_provider": { + "type": "string" + }, + "avatar_url": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "last_login_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "organizations": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AdminUserOrganizationDetails" + } + }, + "provider_user_id": { + "type": "string" + }, + "username": { + "type": [ + "string", + "null" + ] + } + } + }, + "ApiKeyResponse": { + "type": "object", + "description": "API Key response model", + "required": [ + "id", + "key_prefix", + "workspace_id", + "created_by_user_id", + "created_at", + "is_active" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string" + }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "key": { + "type": [ + "string", + "null" + ] + }, + "key_prefix": { + "type": "string" + }, + "last_used_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "spend_limit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice" + } + ] + }, + "usage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DecimalPrice" + } + ] + }, + "workspace_id": { + "type": "string" + } + } + }, + "AttestationResponse": { + "type": "object", + "required": [ + "gateway_attestation" + ], + "properties": { + "gateway_attestation": { + "$ref": "#/components/schemas/DstackCpuQuote" + }, + "model_attestations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + } + }, + "ohttp_attestation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OhttpAttestation", + "description": "OHTTP key attestation payload. Includes an Ed25519 signature over the decoded\n`key_config` bytes so clients can verify the HPKE key is bound to the TEE." + } + ] + }, + "ohttp_key_config": { + "type": [ + "string", + "null" + ], + "description": "Hex-encoded OHTTP key configuration (RFC 9458). Present only when OHTTP_ENABLED=true.\nLegacy flat field; mirrors `ohttp_attestation.key_config` when present." + }, + "tls_certificate": { + "type": [ + "string", + "null" + ], + "description": "PEM-encoded TLS certificate read from `TLS_CERT_PATH` when `include_tls_fingerprint=true`.\n`report_data` binds `tls_cert_fingerprint` (the SHA-256 hash of the TLS SPKI) together with\n`signing_address`, not the PEM bytes." + } + } + }, + "AudioTranscriptionRequestSchema": { + "type": "object", + "description": "Audio transcription request schema for OpenAPI documentation\nThis represents the multipart/form-data fields", + "required": [ + "file", + "model" + ], + "properties": { + "file": { + "type": "string", + "description": "Audio file (required) - binary audio data" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "Language code (optional) - e.g. \"en\", \"es\"" + }, + "model": { + "type": "string", + "description": "Model identifier (required) - e.g. \"openai/whisper-large-v3\"" + }, + "response_format": { + "type": [ + "string", + "null" + ], + "description": "Response format (optional) - one of: \"json\", \"text\", \"verbose_json\"" + }, + "timestamp_granularities": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "Timestamp granularities (optional) - supports \"segment\" and \"word\" with verbose_json.\nMultipart requests may send repeated timestamp_granularities[] fields,\nrepeated timestamp_granularities fields, or comma-separated values." + } + } + }, + "AudioTranscriptionResponse": { + "type": "object", + "description": "Audio transcription response (with OpenAPI schema)", + "required": [ + "text" + ], + "properties": { + "duration": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Total audio duration in seconds" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "Detected language code" + }, + "segments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/TranscriptionSegment" + }, + "description": "Transcription segments with timing" + }, + "text": { + "type": "string", + "description": "Transcribed text" + }, + "words": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/TranscriptionWord" + }, + "description": "Word-level timing information" + } + } + }, + "BillingCostsRequest": { + "type": "object", + "description": "Billing costs request (HuggingFace compatible)", + "required": [ + "requestIds" + ], + "properties": { + "requestIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Array of request IDs to get costs for: the UUID values of the\n`inference-id` response header returned by /v1/chat/completions and\n/v1/messages (not the `x-request-id` header)." + } + } + }, + "BillingCostsResponse": { + "type": "object", + "description": "Billing costs response (HuggingFace compatible)", + "required": [ + "requests" + ], + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequestCost" + }, + "description": "Array of request costs" + }, + "warning": { + "type": [ + "string", + "null" + ], + "description": "Present when some request IDs matched no usage record for this\norganization (those entries report costNanoUsd 0). Explains where the\ncorrect request IDs come from." + } + } + }, + "BillingSourceBreakdown": { + "type": "object", + "description": "Active credit limit by funding source (caps, not payments).", + "required": [ + "source", + "paid_credit_limit_usd", + "org_count" + ], + "properties": { + "org_count": { + "type": "integer", + "format": "int64" + }, + "paid_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active payment-type spend limits (caps) for this source, USD" + }, + "source": { + "type": "string" + } + } + }, + "BillingSummary": { + "type": "object", + "description": "Platform billing summary β€” credit LIMITS (caps) and consumption.\n\nThese are spend-limit ceilings from `organization_limits_history`, NOT payments\nor cash received. Real money-in (Stripe top-ups) lives in the billing service\n(nearai-cloud-ui), not in cloud-api.", + "required": [ + "generated_at", + "active_paid_credit_limit_usd", + "active_grant_credit_limit_usd", + "total_consumed_usd", + "inference_consumed_usd", + "service_consumed_usd", + "paying_org_count", + "granted_org_count", + "by_source" + ], + "properties": { + "active_grant_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active grant-type spend limits (caps), USD" + }, + "active_paid_credit_limit_usd": { + "type": "number", + "format": "double", + "description": "Sum of active payment-type spend limits (caps), USD" + }, + "by_source": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingSourceBreakdown" + } + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "granted_org_count": { + "type": "integer", + "format": "int64" + }, + "inference_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time inference consumed cost, USD (organization_usage_log)" + }, + "paying_org_count": { + "type": "integer", + "format": "int64" + }, + "service_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time service consumed cost, USD (organization_service_usage_log, e.g. web_search)" + }, + "total_consumed_usd": { + "type": "number", + "format": "double", + "description": "All-time consumed cost across all orgs, USD β€” **all usage** (from\norganization_balance: inference + services). `inference_consumed_usd +\nservice_consumed_usd` reconcile to this." + } + } + }, + "ChatChoice": { + "type": "object", + "required": [ + "index", + "message" + ], + "properties": { + "finish_reason": { + "type": [ + "string", + "null" + ] + }, + "index": { + "type": "integer", + "format": "int64" + }, + "message": { + "$ref": "#/components/schemas/Message" + } + } + }, + "ChatCompletionRequest": { + "type": "object", + "required": [ + "model", + "messages" + ], + "properties": { + "frequency_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "max_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + } + }, + "model": { + "type": "string" + }, + "n": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "presence_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "service_tier": { + "type": [ + "string", + "null" + ] + }, + "stop": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StopSequences", + "description": "OpenAI `stop` accepts either a single string or an array of strings." + } + ] + }, + "stream": { + "type": [ + "boolean", + "null" + ] + }, + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "additionalProperties": {} + }, + "ChatCompletionResponse": { + "type": "object", + "required": [ + "id", + "object", + "created", + "model", + "choices", + "usage" + ], + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatChoice" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" + } + }, + "additionalProperties": { + "description": "Unknown top-level fields from the provider (e.g. system_fingerprint, prompt_logprobs).\nRe-emitted when serializing so we do not drop them." + } + }, + "CheckApiKeyResponse": { + "type": "object", + "description": "Response from the check_api_key endpoint.\n\n`organization_id`, `workspace_id`, and `api_key_id` are returned so\ndownstream gateways (e.g. inference-proxy) have a server-side\nauthoritative subject identity and don't have to trust caller-supplied\nheaders when populating logs / usage records / billing reports. In\nparticular, this lets a trusted gateway report usage to cloud-api with\na shared service token instead of forwarding the user's `sk-…`.", + "required": [ + "valid", + "organization_id", + "workspace_id", + "api_key_id" + ], + "properties": { + "api_key_id": { + "type": "string", + "description": "UUID of the API key itself. Used by trusted gateways to attribute\nper-key usage rows without holding the raw `sk-…` value." + }, + "organization_id": { + "type": "string", + "description": "Organization the API key belongs to" + }, + "valid": { + "type": "boolean", + "description": "Whether the API key is valid and authorized" + }, + "workspace_id": { + "type": "string", + "description": "Workspace the API key belongs to" + } + } + }, + "CompletionChoice": { + "type": "object", + "required": [ + "index", + "text" + ], + "properties": { + "finish_reason": { + "type": [ + "string", + "null" + ] + }, + "index": { + "type": "integer", + "format": "int64" + }, + "logprobs": {}, + "text": { + "type": "string" + } + } + }, + "CompletionPrompt": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + ], + "description": "OpenAI `/v1/completions` `prompt`: a single string, a batch of strings, or\ntoken-ID array(s). This endpoint serves only the single-string form; the\nother shapes still deserialize (so the handler can return a clean 400 rather\nthan a framework deserialization error) and are rejected there." + }, + "CompletionRequest": { + "type": "object", + "required": [ + "model", + "prompt" + ], + "properties": { + "best_of": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "echo": { + "type": [ + "boolean", + "null" + ] + }, + "frequency_penalty": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "logprobs": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "max_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "model": { + "type": "string" + }, + "n": { + "type": [ + "integer", + "null" + ], + "format": "int64" }, "presence_penalty": { "type": [ "number", "null" ], - "format": "float" + "format": "float" + }, + "prompt": { + "$ref": "#/components/schemas/CompletionPrompt" + }, + "stop": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StopSequences" + } + ] + }, + "stream": { + "type": [ + "boolean", + "null" + ] + }, + "temperature": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "additionalProperties": {} + }, + "CompletionResponse": { + "type": "object", + "required": [ + "id", + "object", + "created", + "model", + "choices", + "usage" + ], + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CompletionChoice" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/CompletionUsage" + } + } + }, + "CompletionUsage": { + "type": "object", + "description": "Usage for chat/completions endpoints.\nSerializes as prompt_tokens, completion_tokens, prompt_tokens_details, completion_tokens_details, total_tokens.", + "required": [ + "prompt_tokens", + "completion_tokens", + "total_tokens" + ], + "properties": { + "completion_tokens": { + "type": "integer", + "format": "int32" + }, + "completion_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OutputTokensDetails" + } + ] + }, + "prompt_tokens": { + "type": "integer", + "format": "int32" + }, + "prompt_tokens_details": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/InputTokensDetails" + } + ] + }, + "total_tokens": { + "type": "integer", + "format": "int32" + } + } + }, + "ConversationContent": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationContentPart" + } + } + ], + "description": "Content for conversation items" + }, + "ConversationContentPart": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ] + } + } + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "image_url": { + "$ref": "#/components/schemas/ResponseImageUrl" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ] + } + } + }, + { + "type": "object", + "required": [ + "file_id", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "file_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_file" + ] + } + } + }, + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "annotations": { + "type": [ + "array", + "null" + ], + "items": {} + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "output_text" + ] + } + } + } + ], + "description": "Content part for conversations" + }, + "ConversationDeleteResult": { + "type": "object", + "description": "Deleted conversation result", + "required": [ + "id", + "object", + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, + "ConversationInputItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "role", + "content", + "type" + ], + "properties": { + "content": { + "$ref": "#/components/schemas/ConversationContent" + }, + "metadata": {}, + "model": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + ], + "description": "Input item for conversations" + }, + "ConversationItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "role", + "content", + "model", + "type" + ], + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationContentPart" + } + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "metadata": {}, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "type": { + "type": "string", + "enum": [ + "message" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "tool_type", + "function", + "model", + "type" + ], + "properties": { + "created_at": { + "type": "integer", + "format": "int64" + }, + "function": { + "$ref": "#/components/schemas/ConversationItemFunction" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "tool_type": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "tool_call" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "action", + "model", + "type" + ], + "properties": { + "action": { + "$ref": "#/components/schemas/ConversationItemWebSearchAction" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "type": { + "type": "string", + "enum": [ + "web_search_call" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "status", + "summary", + "content", + "model", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ResponseItemStatus" + }, + "summary": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "server_label", + "tools", + "type" + ], + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpDiscoveredTool" + } + }, + "type": { + "type": "string", + "enum": [ + "mcp_list_tools" + ] + } + } + }, + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "server_label", + "name", + "arguments", + "model", + "type" + ], + "properties": { + "approval_request_id": { + "type": [ + "string", + "null" + ] + }, + "arguments": { + "type": "string" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": [ + "string", + "null" + ] + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "mcp_call" + ] + } + } }, - "stop": { - "oneOf": [ - { - "type": "null" + { + "type": "object", + "required": [ + "id", + "response_id", + "created_at", + "server_label", + "name", + "arguments", + "model", + "type" + ], + "properties": { + "arguments": { + "type": "string" }, - { - "$ref": "#/components/schemas/StopSequences", - "description": "OpenAI `stop` accepts either a single string or an array of strings." + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "server_label": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mcp_approval_request" + ] + } + } + }, + { + "type": "object", + "description": "Function call requiring client execution", + "required": [ + "id", + "response_id", + "created_at", + "call_id", + "name", + "arguments", + "status", + "model", + "type" + ], + "properties": { + "arguments": { + "type": "string", + "description": "JSON-encoded arguments" + }, + "call_id": { + "type": "string", + "description": "The LLM's tool_call_id for correlation" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string", + "description": "Function name" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "status": { + "type": "string", + "description": "Status: \"in_progress\" when pending client execution" + }, + "type": { + "type": "string", + "enum": [ + "function_call" + ] + } + } + }, + { + "type": "object", + "description": "Result of a client-executed function call", + "required": [ + "id", + "response_id", + "created_at", + "call_id", + "output", + "type" + ], + "properties": { + "call_id": { + "type": "string", + "description": "The call_id from the FunctionCall this is a response to" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "next_response_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "string", + "description": "Result of the function execution" + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "response_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "function_call_output" + ] } - ] - }, - "stream": { - "type": [ - "boolean", - "null" - ] - }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + } } - }, - "additionalProperties": {} + ], + "description": "Conversation item (for responses)" }, - "ChatCompletionResponse": { + "ConversationItemFunction": { "type": "object", "required": [ - "id", - "object", - "created", - "model", - "choices", - "usage" + "name", + "arguments" ], "properties": { - "choices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChatChoice" - } - }, - "created": { - "type": "integer", - "format": "int64" - }, - "id": { - "type": "string" - }, - "model": { + "arguments": { "type": "string" }, - "object": { + "name": { "type": "string" - }, - "usage": { - "$ref": "#/components/schemas/CompletionUsage" } - }, - "additionalProperties": { - "description": "Unknown top-level fields from the provider (e.g. system_fingerprint, prompt_logprobs).\nRe-emitted when serializing so we do not drop them." } }, - "CheckApiKeyResponse": { + "ConversationItemList": { "type": "object", - "description": "Response from the check_api_key endpoint.\n\n`organization_id`, `workspace_id`, and `api_key_id` are returned so\ndownstream gateways (e.g. inference-proxy) have a server-side\nauthoritative subject identity and don't have to trust caller-supplied\nheaders when populating logs / usage records / billing reports. In\nparticular, this lets a trusted gateway report usage to cloud-api with\na shared service token instead of forwarding the user's `sk-…`.", + "description": "List of conversation items", "required": [ - "valid", - "organization_id", - "workspace_id", - "api_key_id" + "object", + "data", + "first_id", + "last_id", + "has_more" ], "properties": { - "api_key_id": { - "type": "string", - "description": "UUID of the API key itself. Used by trusted gateways to attribute\nper-key usage rows without holding the raw `sk-…` value." - }, - "organization_id": { - "type": "string", - "description": "Organization the API key belongs to" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationItem" + } }, - "valid": { - "type": "boolean", - "description": "Whether the API key is valid and authorized" + "first_id": { + "type": "string" }, - "workspace_id": { - "type": "string", - "description": "Workspace the API key belongs to" - } - } - }, - "CompletionChoice": { - "type": "object", - "required": [ - "index", - "text" - ], - "properties": { - "finish_reason": { - "type": [ - "string", - "null" - ] + "has_more": { + "type": "boolean" }, - "index": { - "type": "integer", - "format": "int64" + "last_id": { + "type": "string" }, - "logprobs": {}, - "text": { + "object": { "type": "string" } } }, - "CompletionPrompt": { + "ConversationItemWebSearchAction": { "oneOf": [ { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - }, - { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - } - } - ], - "description": "OpenAI `/v1/completions` `prompt`: a single string, a batch of strings, or\ntoken-ID array(s). This endpoint serves only the single-string form; the\nother shapes still deserialize (so the handler can return a clean 400 rather\nthan a framework deserialization error) and are rejected there." - }, - "CompletionRequest": { - "type": "object", - "required": [ - "model", - "prompt" - ], - "properties": { - "best_of": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "echo": { - "type": [ - "boolean", - "null" - ] - }, - "frequency_penalty": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "logprobs": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "max_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "model": { - "type": "string" - }, - "n": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "presence_penalty": { - "type": [ - "number", - "null" + "type": "object", + "required": [ + "query", + "type" ], - "format": "float" - }, - "prompt": { - "$ref": "#/components/schemas/CompletionPrompt" - }, - "stop": { - "oneOf": [ - { - "type": "null" + "properties": { + "query": { + "type": "string" }, - { - "$ref": "#/components/schemas/StopSequences" + "type": { + "type": "string", + "enum": [ + "search" + ] } - ] - }, - "stream": { - "type": [ - "boolean", - "null" - ] - }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" - }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + } } - }, - "additionalProperties": {} + ] }, - "CompletionResponse": { + "ConversationObject": { "type": "object", + "description": "Conversation object (follows OpenAI spec)", "required": [ "id", "object", - "created", - "model", - "choices", - "usage" + "created_at", + "metadata" ], - "properties": { - "choices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CompletionChoice" - } - }, - "created": { + "properties": { + "created_at": { "type": "integer", "format": "int64" }, "id": { "type": "string" }, - "model": { - "type": "string" - }, + "metadata": {}, "object": { "type": "string" - }, - "usage": { - "$ref": "#/components/schemas/CompletionUsage" } } }, - "CompletionUsage": { - "type": "object", - "description": "Usage for chat/completions endpoints.\nSerializes as prompt_tokens, completion_tokens, prompt_tokens_details, completion_tokens_details, total_tokens.", - "required": [ - "prompt_tokens", - "completion_tokens", - "total_tokens" - ], - "properties": { - "completion_tokens": { - "type": "integer", - "format": "int32" - }, - "completion_tokens_details": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/OutputTokensDetails" - } - ] - }, - "prompt_tokens": { - "type": "integer", - "format": "int32" + "ConversationReference": { + "oneOf": [ + { + "type": "string" }, - "prompt_tokens_details": { - "oneOf": [ - { - "type": "null" + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" }, - { - "$ref": "#/components/schemas/InputTokensDetails" - } - ] - }, - "total_tokens": { - "type": "integer", - "format": "int32" + "metadata": {} + } } - } + ], + "description": "Conversation reference" }, "ConversationResponseReference": { "type": "object", @@ -10534,6 +13489,28 @@ } } }, + "CreateConversationItemsRequest": { + "type": "object", + "description": "Request to create items in a conversation", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationInputItem" + } + } + } + }, + "CreateConversationRequest": { + "type": "object", + "description": "Request to create a conversation", + "properties": { + "metadata": {} + } + }, "CreateOrganizationRequest": { "type": "object", "description": "Request to create a new organization", @@ -10547,90 +13524,244 @@ "null" ] }, - "name": { - "type": "string" - } - }, - "additionalProperties": false - }, - "CreateReportingTokenRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "expires_at": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CreateReportingTokenRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Optional expiration time. Must be in the future." + }, + "name": { + "type": "string", + "description": "Human-readable name for the reporting token." + } + }, + "additionalProperties": false + }, + "CreateReportingTokenResponse": { + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "token", + "token_prefix", + "created_by_user_id", + "created_at", + "scope" + ], + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by_user_id": { + "type": "string", + "format": "uuid" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "last_used_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Approximate last-authentication timestamp, refreshed at most once every 15 minutes." + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "scope": { + "$ref": "#/components/schemas/ReportingTokenScope" + }, + "token": { + "type": "string", + "description": "Raw reporting token. Returned only once when the token is created." + }, + "token_prefix": { + "type": "string", + "description": "Non-secret token prefix for display and audit correlation." + } + } + }, + "CreateResponseRequest": { + "type": "object", + "description": "Request to create a response", + "required": [ + "model" + ], + "properties": { + "background": { + "type": [ + "boolean", + "null" + ] + }, + "conversation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ConversationReference" + } + ] + }, + "include": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "input": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseInput" + } + ] + }, + "instructions": { + "type": [ + "string", + "null" + ] + }, + "max_output_tokens": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "max_tool_calls": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "metadata": {}, + "model": { + "type": "string" + }, + "parallel_tool_calls": { + "type": [ + "boolean", + "null" + ] + }, + "previous_response_id": { + "type": [ + "string", + "null" + ] + }, + "prompt_cache_key": { + "type": [ + "string", + "null" + ] + }, + "reasoning": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseReasoningConfig" + } + ] + }, + "safety_identifier": { + "type": [ + "string", + "null" + ] + }, + "service_tier": { "type": [ "string", "null" ], - "format": "date-time", - "description": "Optional expiration time. Must be in the future." + "description": "Responses is Standard-only for now. `auto` is accepted but normalized\nto Standard before the internal Chat Completions call." }, - "name": { - "type": "string", - "description": "Human-readable name for the reporting token." - } - }, - "additionalProperties": false - }, - "CreateReportingTokenResponse": { - "type": "object", - "required": [ - "id", - "organization_id", - "name", - "token", - "token_prefix", - "created_by_user_id", - "created_at", - "scope" - ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" + "store": { + "type": [ + "boolean", + "null" + ] }, - "created_by_user_id": { - "type": "string", - "format": "uuid" + "stream": { + "type": [ + "boolean", + "null" + ] }, - "expires_at": { + "temperature": { "type": [ - "string", + "number", "null" ], - "format": "date-time" + "format": "float" }, - "id": { - "type": "string", - "format": "uuid" + "tool_choice": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResponseToolChoice" + } + ] }, - "last_used_at": { + "tools": { "type": [ - "string", + "array", "null" ], - "format": "date-time", - "description": "Approximate last-authentication timestamp, refreshed at most once every 15 minutes." - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "scope": { - "$ref": "#/components/schemas/ReportingTokenScope" - }, - "token": { - "type": "string", - "description": "Raw reporting token. Returned only once when the token is created." + "items": { + "$ref": "#/components/schemas/ResponseTool" + } }, - "token_prefix": { - "type": "string", - "description": "Non-secret token prefix for display and audit correlation." + "top_p": { + "type": [ + "number", + "null" + ], + "format": "float" } } }, @@ -10873,7 +14004,7 @@ }, "report_data": { "type": "string", - "description": "The report data that contains signing address and nonce" + "description": "Value bound into the quote. Its final 32 bytes contain the request nonce.\nWith `include_tls_fingerprint=true`, its first 32 bytes are\nSHA-256(signing_address || tls_cert_fingerprint)." }, "request_nonce": { "type": "string", @@ -10957,6 +14088,23 @@ } } }, + "ExpiresAfter": { + "type": "object", + "description": "Expires after configuration", + "required": [ + "anchor", + "seconds" + ], + "properties": { + "anchor": { + "type": "string" + }, + "seconds": { + "type": "integer", + "format": "int64" + } + } + }, "FeatureRequestKind": { "type": "string", "enum": [ @@ -11054,6 +14202,102 @@ } } }, + "FileDeleteResponse": { + "type": "object", + "description": "File delete response", + "required": [ + "id", + "object", + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, + "FileListResponse": { + "type": "object", + "description": "File list response", + "required": [ + "object", + "data", + "has_more" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileUploadResponse" + } + }, + "first_id": { + "type": [ + "string", + "null" + ] + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": [ + "string", + "null" + ] + }, + "object": { + "type": "string" + } + } + }, + "FileUploadResponse": { + "type": "object", + "description": "File upload response", + "required": [ + "id", + "object", + "bytes", + "created_at", + "filename", + "purpose" + ], + "properties": { + "bytes": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "expires_at": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "filename": { + "type": "string" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + }, + "purpose": { + "type": "string" + } + } + }, "FunctionCall": { "type": "object", "required": [ @@ -11327,6 +14571,13 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter vocabulary)." }, + "textPricing": { + "type": [ + "object", + "null" + ], + "description": "Exact, versioned USD-per-million text pricing profile.\nOmitted leaves it unchanged; null removes it." + }, "verifiable": { "type": [ "boolean", @@ -11533,6 +14784,11 @@ "cost_per_host_usd_month", "monthly_burn_usd", "daily_burn_usd", + "cost_per_gpu_hour_usd", + "total_allocated_gpus", + "hourly_gpu_burn_usd", + "model_gpu_allocations", + "gpu_data_stale", "fetched_at", "stale" ], @@ -11542,6 +14798,11 @@ "format": "int64", "description": "Hosts serving β‰₯1 model." }, + "cost_per_gpu_hour_usd": { + "type": "number", + "format": "double", + "description": "Planning rate for one allocated physical GPU-hour." + }, "cost_per_host_usd_month": { "type": "number", "format": "double" @@ -11554,11 +14815,27 @@ "type": "string", "format": "date-time" }, + "gpu_data_stale": { + "type": "boolean", + "description": "True when current GPU allocation is unavailable or served from cache." + }, + "hourly_gpu_burn_usd": { + "type": "number", + "format": "double", + "description": "Current projected fleet GPU burn per hour." + }, "idle_hosts": { "type": "integer", "format": "int64", "description": "Hosts serving no models (idle capacity we still pay for)." }, + "model_gpu_allocations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelGpuAllocation" + }, + "description": "Current allocation grouped by the model label emitted by DCGM. A GPU\ncarrying multiple labels can appear in multiple groups; the fleet total\nremains separately deduplicated by physical UUID." + }, "monthly_burn_usd": { "type": "number", "format": "double" @@ -11567,6 +14844,11 @@ "type": "boolean", "description": "True when this is last-known / fallback data because the live fetch failed." }, + "total_allocated_gpus": { + "type": "integer", + "format": "int64", + "description": "Current distinct physical GPUs allocated to a model workload." + }, "total_hosts": { "type": "integer", "format": "int64" @@ -11817,17 +15099,77 @@ "$ref": "#/components/schemas/ItaModelTokenItem" } }, - "nonce": { - "type": "string" + "nonce": { + "type": "string" + }, + "policy_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "policy_must_match": { + "type": "boolean" + } + } + }, + "ListAdminAmlAllowlistResponse": { + "type": "object", + "description": "Admin AML allowlist response.", + "required": [ + "accounts", + "total", + "limit", + "offset" + ], + "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminAmlAllowlistEntryResponse" + } + }, + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "ListAdminAmlReportsResponse": { + "type": "object", + "description": "Paginated admin AML reports response.", + "required": [ + "reports", + "total", + "limit", + "offset" + ], + "properties": { + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" }, - "policy_ids": { + "reports": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/AdminAmlReportResponse" } }, - "policy_must_match": { - "type": "boolean" + "total": { + "type": "integer", + "format": "int64" } } }, @@ -12084,8 +15426,7 @@ "enum": [ "always", "never" - ], - "description": "Simple MCP approval mode" + ] }, "McpApprovalRequirement": { "oneOf": [ @@ -12103,8 +15444,14 @@ } } } - ], - "description": "Approval requirement for MCP tool calls" + ] + }, + "McpApprovalResponseType": { + "type": "string", + "description": "Type marker for MCP approval response input", + "enum": [ + "mcp_approval_response" + ] }, "McpDiscoveredTool": { "type": "object", @@ -12125,6 +15472,13 @@ } } }, + "McpListToolsType": { + "type": "string", + "description": "Type marker for MCP list tools input", + "enum": [ + "mcp_list_tools" + ] + }, "McpToolNameFilter": { "type": "object", "required": [ @@ -12138,8 +15492,7 @@ }, "uniqueItems": true } - }, - "description": "Filter for tool names that don't require approval" + } }, "MemberRole": { "type": "string", @@ -12520,6 +15873,22 @@ } } }, + "ModelGpuAllocation": { + "type": "object", + "required": [ + "model_name", + "allocated_gpus" + ], + "properties": { + "allocated_gpus": { + "type": "integer", + "format": "int64" + }, + "model_name": { + "type": "string" + } + } + }, "ModelHistoryEntry": { "type": "object", "description": "Model history entry - includes pricing, context length, and other model attributes", @@ -12713,6 +16082,7 @@ "type": "string" } }, + "textPricing": {}, "verifiable": { "type": "boolean" } @@ -12911,6 +16281,9 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter `supported_sampling_parameters`)." }, + "textPricing": { + "description": "Exact, versioned USD-per-million text pricing. The legacy `pricing`\nprojection remains Standard-short for compatibility." + }, "top_provider": { "oneOf": [ { @@ -13330,7 +16703,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "ModelsResponse": { @@ -13682,6 +17056,23 @@ } } }, + "OrganizationFallbackResponse": { + "type": "object", + "description": "Effective fallback policy returned by admin organization endpoints.", + "required": [ + "organizationId", + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "organizationId": { + "type": "string", + "format": "uuid" + } + } + }, "OrganizationInvitationResponse": { "type": "object", "description": "Organization invitation response", @@ -14277,6 +17668,12 @@ "$ref": "#/components/schemas/DecimalPriceRequest" } ] + }, + "textPricing": { + "type": [ + "object", + "null" + ] } } }, @@ -14390,7 +17787,8 @@ "$ref": "#/components/schemas/DecimalPrice" } ] - } + }, + "textPricing": {} } }, "PricingFields": { @@ -14420,7 +17818,8 @@ }, "outputCostPerToken": { "$ref": "#/components/schemas/DecimalPrice" - } + }, + "textPricing": {} } }, "PrivacyClassifyRequestDoc": { @@ -14740,6 +18139,7 @@ "output_tokens", "total_tokens", "cache_read_tokens", + "cache_write_tokens", "input_cost", "output_cost", "total_cost", @@ -14753,6 +18153,11 @@ "format": "int32", "description": "Number of prompt tokens that were cache hits (subset of input_tokens)" }, + "cache_write_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of prompt tokens written to provider cache (subset of input_tokens)" + }, "created_at": { "type": "string", "description": "Timestamp of the recorded entry (RFC3339)" @@ -15620,6 +19025,20 @@ } } }, + "ResponseContent": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseContentPart" + } + } + ], + "description": "Content can be text or array of content parts" + }, "ResponseContentItem": { "oneOf": [ { @@ -15770,6 +19189,77 @@ ], "description": "Unified content item that can represent both user inputs and assistant outputs\nThis replaces ResponseOutputContent and correctly represents semantic types" }, + "ResponseContentPart": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ] + } + } + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "image_url": { + "$ref": "#/components/schemas/ResponseImageUrl" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ] + } + } + }, + { + "type": "object", + "required": [ + "file_id", + "type" + ], + "properties": { + "detail": { + "type": [ + "string", + "null" + ] + }, + "file_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_file" + ] + } + } + } + ], + "description": "Content part from user inputs (input-only variants).\n\nThis type is used for type-safe operations on user inputs only.\nIt cannot contain output variants, providing compile-time safety.\n\nUsed in:\n- ResponseContent::Parts (for input listing)\n- list_input_items endpoint\n- Input validation operations" + }, "ResponseError": { "type": "object", "required": [ @@ -15826,6 +19316,68 @@ } } }, + "ResponseInput": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseInputItem" + } + } + ], + "description": "Input for a response - can be text, array of items, or single item" + }, + "ResponseInputItem": { + "type": "object", + "description": "Single input item", + "required": [ + "role", + "content" + ], + "properties": { + "content": { + "$ref": "#/components/schemas/ResponseContent" + }, + "metadata": {}, + "role": { + "type": "string" + } + } + }, + "ResponseInputItemList": { + "type": "object", + "description": "Input item list for responses", + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseInputItem" + } + }, + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "object": { + "type": "string" + } + } + }, "ResponseItemStatus": { "type": "string", "enum": [ @@ -15989,7 +19541,7 @@ "tools": { "type": "array", "items": { - "$ref": "#/components/schemas/ClientManagedResponseTool" + "$ref": "#/components/schemas/ResponseTool" } }, "top_logprobs": { @@ -16516,13 +20068,6 @@ "type": "string", "description": "Status: \"in_progress\" when pending client execution" }, - "thought_signature": { - "type": [ - "string", - "null" - ], - "description": "Provider-specific metadata that clients must echo unchanged when\nreplaying the function call (for example Gemini's thought signature)." - }, "type": { "type": "string", "enum": [ @@ -16734,11 +20279,28 @@ "type" ], "properties": { + "filters": {}, + "search_context_size": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", "enum": [ "web_search" ] + }, + "user_location": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserLocation" + } + ] } } }, @@ -16800,6 +20362,7 @@ }, { "type": "object", + "description": "Remote MCP server tool", "required": [ "server_label", "server_url", @@ -16815,14 +20378,9 @@ "type": "string" } }, - "authorization": { - "type": [ - "string", - "null" - ] - }, "require_approval": { - "$ref": "#/components/schemas/McpApprovalRequirement" + "$ref": "#/components/schemas/McpApprovalRequirement", + "description": "Tool approval requirement (default: \"always\")" }, "server_description": { "type": [ @@ -16834,7 +20392,8 @@ "type": "string" }, "server_url": { - "type": "string" + "type": "string", + "description": "HTTPS endpoint for the remote MCP server" }, "type": { "type": "string", @@ -17777,6 +21336,18 @@ } } }, + "UpdateAmlReportStatusRequest": { + "type": "object", + "description": "Admin AML report status update request.", + "required": [ + "active" + ], + "properties": { + "active": { + "type": "boolean" + } + } + }, "UpdateApiKeyRequest": { "type": "object", "description": "Request to update API key (general update for name, expires_at, and/or spend_limit)", @@ -17828,6 +21399,13 @@ } } }, + "UpdateConversationRequest": { + "type": "object", + "description": "Request to update a conversation", + "properties": { + "metadata": {} + } + }, "UpdateModelApiRequest": { "type": "object", "description": "Request to update model pricing (admin endpoint)", @@ -18049,6 +21627,13 @@ }, "description": "Sampling parameters accepted by the model (OpenRouter vocabulary)." }, + "textPricing": { + "type": [ + "object", + "null" + ], + "description": "Exact, versioned USD-per-million text pricing profile.\nOmitted leaves it unchanged; null removes it." + }, "verifiable": { "type": [ "boolean", @@ -18097,6 +21682,18 @@ } } }, + "UpdateOrganizationFallbackRequest": { + "type": "object", + "description": "Admin request to update an organization's fallback policy.", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "UpdateOrganizationLimitsRequest": { "type": "object", "description": "Request to update organization limits (Admin only)", @@ -18269,6 +21866,24 @@ "settings": {} } }, + "UpsertAmlAllowlistEntryRequest": { + "type": "object", + "description": "Admin AML allowlist upsert request.", + "required": [ + "account_id" + ], + "properties": { + "account_id": { + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + }, "Usage": { "type": "object", "required": [ @@ -18386,6 +22001,7 @@ "input_tokens", "output_tokens", "cache_read_tokens", + "cache_write_tokens", "total_tokens", "total_cost", "total_cost_display", @@ -18396,11 +22012,23 @@ "api_key_id": { "type": "string" }, + "billingDetails": {}, "cache_read_tokens": { "type": "integer", "format": "int32", "description": "Number of prompt tokens that were cache hits" }, + "cache_write_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of prompt tokens written to provider cache" + }, + "contextBand": { + "type": [ + "string", + "null" + ] + }, "created_at": { "type": "string" }, @@ -18450,6 +22078,12 @@ ], "description": "Response ID when called from Responses API" }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, "stop_reason": { "type": [ "string", @@ -18503,6 +22137,42 @@ } } }, + "UserLocation": { + "type": "object", + "description": "User location for web search", + "required": [ + "type" + ], + "properties": { + "city": { + "type": [ + "string", + "null" + ] + }, + "country": { + "type": [ + "string", + "null" + ] + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "timezone": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string" + } + } + }, "UserMetricsSummary": { "type": "object", "required": [ @@ -18853,469 +22523,113 @@ "id": { "type": "string" }, - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string" - } - } - }, - "VerifyRequest": { - "type": "object", - "properties": { - "request_hash": { - "type": [ - "string", - "null" - ] - } - } - }, - "VpcInfo": { - "type": "object", - "description": "VPC information in attestation", - "properties": { - "vpc_hostname": { - "type": [ - "string", - "null" - ], - "description": "VPC hostname of this node" - }, - "vpc_server_app_id": { - "type": [ - "string", - "null" - ], - "description": "VPC server app ID" - } - } - }, - "WebSearchAction": { - "oneOf": [ - { - "type": "object", - "required": [ - "query", - "type" - ], - "properties": { - "query": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "search" - ] - } - } - } - ], - "description": "Web search action details" - }, - "WorkspaceResponse": { - "type": "object", - "description": "Workspace response model", - "required": [ - "id", - "name", - "organization_id", - "created_by_user_id", - "created_at", - "updated_at", - "is_active" - ], - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by_user_id": { - "type": "string" - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string" - }, - "settings": {}, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "ClientManagedResponseTool": { - "oneOf": [ - { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "parameters": {}, - "type": { - "type": "string", - "enum": [ - "function" - ] - } - } - } - ], - "description": "The only tool shape accepted from a public Responses request.\n\n`ResponseTool` retains legacy variants for dormant/internal code paths, but\nthe `CreateResponseRequest::tools` deserializer first parses this enum. That\nmakes unsupported builtin types ordinary request-deserialization failures,\nbefore the route or a provider can do any work." - }, - "StatelessResponseInputItemSchema": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "call_id", - "name", - "arguments" - ], - "properties": { - "arguments": { - "type": "string" - }, - "call_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "thought_signature": { - "type": [ - "string", - "null" - ] - }, - "type": { - "$ref": "#/components/schemas/FunctionCallType" - } - } - }, - { - "type": "object", - "required": [ - "type", - "call_id", - "output" - ], - "properties": { - "call_id": { - "type": "string" - }, - "output": { - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/FunctionCallOutputType" - } - } - }, - { - "type": "object", - "required": [ - "role", - "content" - ], - "properties": { - "content": { - "$ref": "#/components/schemas/StatelessResponseContentSchema" - }, - "metadata": {}, - "role": { - "type": "string" - } - } - } - ] - }, - "StatelessResponseContentSchema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/StatelessResponseContentPartSchema" - } - } - ] - }, - "StatelessResponseInputSchema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/StatelessResponseInputItemSchema" - } + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" } - ] + } }, - "FunctionCallType": { - "type": "string", - "description": "Type marker for a client-replayed function call input item.", - "enum": [ - "function_call" - ] + "VerifyRequest": { + "type": "object", + "properties": { + "request_hash": { + "type": [ + "string", + "null" + ] + } + } }, - "StatelessResponseToolSchema": { - "oneOf": [ - { - "type": "object", - "required": [ - "name", - "type" + "VpcInfo": { + "type": "object", + "description": "VPC information in attestation", + "properties": { + "vpc_hostname": { + "type": [ + "string", + "null" ], - "properties": { - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "parameters": {}, - "type": { - "type": "string", - "enum": [ - "function" - ] - } - } + "description": "VPC hostname of this node" + }, + "vpc_server_app_id": { + "type": [ + "string", + "null" + ], + "description": "VPC server app ID" } - ] + } }, - "StatelessResponseContentPartSchema": { + "WebSearchAction": { "oneOf": [ { "type": "object", "required": [ - "text", - "type" - ], - "properties": { - "text": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "input_text" - ] - } - } - }, - { - "type": "object", - "required": [ - "text", + "query", "type" ], "properties": { - "text": { + "query": { "type": "string" }, "type": { "type": "string", "enum": [ - "output_text" - ] - } - } - }, - { - "type": "object", - "required": [ - "image_url", - "type" - ], - "properties": { - "detail": { - "type": [ - "string", - "null" - ] - }, - "image_url": { - "$ref": "#/components/schemas/ResponseImageUrl" - }, - "type": { - "type": "string", - "enum": [ - "input_image" + "search" ] } } } - ] + ], + "description": "Web search action details" }, - "StatelessCreateResponseRequestSchema": { + "WorkspaceResponse": { "type": "object", - "description": "OpenAPI-only view of the stateless Responses request contract.\n\nThe runtime request type keeps legacy variants so it can return a precise\n`invalid_request_error` for them. The public endpoint schema should instead\nshow only the items accepted by the stateless implementation.", + "description": "Workspace response model", "required": [ - "model" + "id", + "name", + "organization_id", + "created_by_user_id", + "created_at", + "updated_at", + "is_active" ], "properties": { - "include": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "input": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/StatelessResponseInputSchema" - } - ] - }, - "instructions": { - "type": [ - "string", - "null" - ] - }, - "max_output_tokens": { - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "max_tool_calls": { - "type": [ - "integer", - "null" - ], - "format": "int64" + "created_at": { + "type": "string", + "format": "date-time" }, - "metadata": {}, - "model": { + "created_by_user_id": { "type": "string" }, - "parallel_tool_calls": { - "type": [ - "boolean", - "null" - ] - }, - "prompt_cache_key": { - "type": [ - "string", - "null" - ] - }, - "reasoning": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ResponseReasoningConfig" - } - ] - }, - "safety_identifier": { - "type": [ - "string", - "null" - ] - }, - "service_tier": { + "description": { "type": [ "string", "null" ] }, - "store": { - "type": "boolean", - "description": "Must be `false` when supplied; omitted is normalized to `false`.", - "default": false, - "example": false, - "const": false - }, - "stream": { - "type": [ - "boolean", - "null" - ] + "id": { + "type": "string" }, - "temperature": { - "type": [ - "number", - "null" - ], - "format": "float" + "is_active": { + "type": "boolean" }, - "tool_choice": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ResponseToolChoice" - } - ] + "name": { + "type": "string" }, - "tools": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/StatelessResponseToolSchema" - } + "organization_id": { + "type": "string" }, - "top_p": { - "type": [ - "number", - "null" - ], - "format": "float" + "settings": {}, + "updated_at": { + "type": "string", + "format": "date-time" } } } @@ -19384,9 +22698,13 @@ "name": "Models", "description": "Public model catalog and information" }, + { + "name": "Conversations", + "description": "Conversation management" + }, { "name": "Responses", - "description": "Stateless response inference (`store: false` only). Raw request/response content, response items, and history are not persisted. Clients must include any prior context in each request. Every successful Responses inference makes exactly one Chat Completions call. Only custom `function` tools are supported. They are client-managed: Cloud returns `function_call` items but never executes them; a later `store: false` request replays the individual call (the raw item from output is accepted) with its matching `function_call_output`, alongside caller-managed message history and the same function tool definitions. The minimal replay path also accepts assistant `message` text parts of type `output_text`, but not reasoning or arbitrary full `response.output` items. Server-executed tools (`web_search`, `web_context_search`, `file_search`, `code_interpreter`, `computer`, and remote `mcp`) and image-generation/editing models are rejected. The separate `POST /mcp` endpoint continues to expose its `web_search` tool independently of Responses; use `/v1/images/*` for image generation/editing. Existing completed-response gateway attestation is preserved best-effort: when the signature write succeeds, `GET /v1/signature/resp_*` retrieves signatures over SHA-256 request/response digests, never raw content. Interrupted streams create no `resp_*` attestation record or legacy disconnect fallback. Conversations, response history, and file input are rejected." + "description": "Response handling and streaming" }, { "name": "Organizations", @@ -19400,6 +22718,10 @@ "name": "Workspaces", "description": "Workspace and API key management" }, + { + "name": "Files", + "description": "File upload and management" + }, { "name": "Users", "description": "User profile and token management" diff --git a/docs.json b/docs.json index 07ce875..a2f6025 100644 --- a/docs.json +++ b/docs.json @@ -157,7 +157,11 @@ { "group": "Responses", "pages": [ - "POST /v1/responses" + "POST /v1/responses", + "GET /v1/responses/{response_id}", + "DELETE /v1/responses/{response_id}", + "POST /v1/responses/{response_id}/cancel", + "GET /v1/responses/{response_id}/input_items" ] }, { @@ -334,6 +338,11 @@ "GET /v1/admin/access-tokens", "POST /v1/admin/access-tokens", "DELETE /v1/admin/access-tokens/{token_id}", + "GET /v1/admin/aml/allowlist", + "POST /v1/admin/aml/allowlist", + "DELETE /v1/admin/aml/allowlist/{account_id}", + "GET /v1/admin/aml/reports", + "PATCH /v1/admin/aml/reports/{report_id}/status", "GET /v1/admin/invitation-email-deliveries", "POST /v1/admin/invitation-email-deliveries/{invitation_id}/resend", "GET /v1/admin/models", @@ -350,6 +359,8 @@ "GET /v1/admin/organizations/{org_id}", "GET /v1/admin/organizations/{org_id}/concurrent-limit", "PATCH /v1/admin/organizations/{org_id}/concurrent-limit", + "GET /v1/admin/organizations/{org_id}/fallback", + "PATCH /v1/admin/organizations/{org_id}/fallback", "PATCH /v1/admin/organizations/{org_id}/limits", "GET /v1/admin/organizations/{org_id}/limits/history", "GET /v1/admin/organizations/{org_id}/members", @@ -368,6 +379,32 @@ "PATCH /v1/admin/services/{id}", "GET /v1/admin/users" ] + }, + { + "group": "Conversations", + "pages": [ + "POST /v1/conversations", + "GET /v1/conversations/{conversation_id}", + "POST /v1/conversations/{conversation_id}", + "DELETE /v1/conversations/{conversation_id}", + "POST /v1/conversations/{conversation_id}/archive", + "DELETE /v1/conversations/{conversation_id}/archive", + "POST /v1/conversations/{conversation_id}/clone", + "GET /v1/conversations/{conversation_id}/items", + "POST /v1/conversations/{conversation_id}/items", + "POST /v1/conversations/{conversation_id}/pin", + "DELETE /v1/conversations/{conversation_id}/pin" + ] + }, + { + "group": "Files", + "pages": [ + "GET /v1/files", + "POST /v1/files", + "GET /v1/files/{file_id}", + "DELETE /v1/files/{file_id}", + "GET /v1/files/{file_id}/content" + ] } ] } From f18c4d9da8f55fc4c50e19e6dba36af0426bc5b8 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 14:15:49 +0800 Subject: [PATCH 17/23] docs: add verification SDK installation --- cloud/verification/reference/sdk.mdx | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 1226ebf..4159c4e 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -9,7 +9,37 @@ The SDK supports TypeScript, Python, and Rust. ## Installation -Installation instructions will be added when the SDK packages are published. + + + +Requires Node.js 24 or later. + +```bash +npm install verifiable-ai-sdk +``` + + + + +Requires Python 3.12 or later. + +```bash +pip install verifiable-ai-sdk +``` + +Import it as `verifiable_ai_sdk`. + + + + +```bash +cargo add verifiable-ai-sdk +``` + +Import it as `verifiable_ai_sdk`. + + + ## Using the SDK @@ -22,5 +52,5 @@ For the protocol requirements and trust decisions behind each check, start with - Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added as they become available. + Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added next. From 922b222f334c752f5a33ed6ce66b5397897d3d74 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 19:59:13 +0800 Subject: [PATCH 18/23] docs: align SDK verification flow --- .../cloud-api/model-attestations.mdx | 10 ++++----- .../cloud-api/response-signatures.mdx | 21 +++++++++++-------- cloud/verification/reference/sdk.mdx | 16 +++++++++----- .../reference/verification-policy.mdx | 4 ++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 38b6708..1e8efd2 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -23,12 +23,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. +The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. -When matching a response signature, request fresh evidence for that signer by adding both `signing_address=` and the signature's `signing_algo`. Do not use a report for a different signer as a substitute. +For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. Do not fetch another report after the completion merely to select a signer. - `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. Treat a report as evidence for a completion only when its signer and algorithm match a `provider_tee` response signature. + `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. ## NEAR model report fields @@ -53,7 +53,7 @@ The NEAR report uses these fields: 3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. 4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. 5. When GPU evidence is present, verify `nvidia_payload` and require it to be bound to the same nonce. -6. When verifying a model-serving response signature, request a report whose `signing_address` and `signing_algo` match that signature. +6. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. 7. Apply your own allowlist and provenance policy to the verified measurements. @@ -66,7 +66,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## Verify a signed response -For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the applicable model attestation. For a `gateway` signature, match the signer to `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). +For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the previously verified model attestation. For a `gateway` signature, match the signer to the previously verified `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). ## Other provider documentation diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index ab80948..5ce4b3c 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -1,9 +1,9 @@ --- title: "Response Signatures" -description: "Verify available response signatures from the NEAR AI Cloud gateway against the exact bytes and the matching attestation." +description: "Verify available response signatures from the NEAR AI Cloud gateway against exact bytes and matching preflight evidence." --- -Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. When present, `signature_kind` determines which attestation must match the signer. +Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. Before sending a completion, verify the gateway and selected NEAR model deployment your policy requires. When present, `signature_kind` determines which verified preflight result must match the signer. A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. @@ -39,10 +39,10 @@ For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, th Then compare `text` with the expected payload: -| `signature_kind` | Expected `text` | Required attestation match | +| `signature_kind` | Expected `text` | Required preflight match | | --- | --- | --- | -| `provider_tee` | `::` | A NEAR model report requested with `provider=near`, with the same `signing_address` and `signing_algo`. | -| `gateway` | `:` | `gateway_attestation` with the same `signing_address` and `signing_algo`. | +| `provider_tee` | `::` | A verified NEAR model report with the same `signing_address` and `signing_algo`. | +| `gateway` | `:` | A verified `gateway_attestation` with the same `signing_address` and `signing_algo`. | Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signature binds the response to the gateway, not to a model report. If a model signature is unavailable, model-level response binding is unavailable. @@ -50,12 +50,15 @@ If `signature_kind` is absent or unfamiliar, do not infer it from the shape of ` For `provider_tee`, `` is the canonical model identifier in the signed payload. -## Match the attestation +## Match preflight evidence -For `provider_tee`, request fresh model evidence with a client-generated nonce, `model=`, `provider=near`, `signing_address=`, and the signature's `signing_algo`. Then verify the quote and apply the required measurement and provenance policy. +Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. -For `gateway`, request fresh gateway evidence with a client-generated nonce and the signature's `signing_algo`, then compare the signer returned by the report locally. Gateway evidence does not need a model or provider selector. +After fetching the signature, use `signature_kind` to select the matching preflight result: -A signature made by a different signer is not evidence for the response, even if the model name or endpoint is the same. +- For `provider_tee`, require the signature signer and algorithm to match the verified NEAR model report. +- For `gateway`, require them to match the verified gateway report. + +Do not request a new report after the completion merely to make a signer match. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 4159c4e..7f1bf52 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -41,16 +41,22 @@ Import it as `verifiable_ai_sdk`. -## Using the SDK +## Verification flow -The SDK follows the same verification model as these guides: generate a fresh nonce, obtain the gateway report, verify its quote and bindings, then apply your application's policy. +For each completion, first use the SDK to verify the NEAR AI Cloud gateway and selected model deployments. Then send the completion with your HTTP client, retaining the exact request and response bytes. Finally, fetch the completion signature and verify it with the matching preflight result. -For the protocol requirements and trust decisions behind each check, start with the gateway guide: +The SDK creates a fresh nonce for every evidence request. It retrieves and verifies evidence, but does not send the completion request or retain its bytes for you. - Verify gateway and NEAR model evidence returned through `cloud-api.near.ai`. + Learn the protocol requirements and trust decisions for gateway and NEAR model evidence. +## Examples + +Runnable TypeScript, Python, and Rust examples follow this flow for non-streaming and streaming completions. + - Direct model endpoints, Compose Manager evidence, image provenance, and E2EE are not covered by the SDK yet. SDK examples and language-specific API documentation will be added next. + In TypeScript, import from `verifiable-ai-sdk/node` when a Node.js application requires Gateway TLS binding. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. + +The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, Compose Manager evidence, image provenance, and E2EE, use the corresponding endpoint guides. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 84c79ca..b37d75e 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -36,8 +36,8 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. - For TLS, obtain the peer certificate and report on the same connection. -- For a response signature, match the signer identity and algorithm to the applicable attestation. -- Fetch new evidence after a signer, certificate, configuration, deployment, or policy-age change. +- For a response signature, use only preflight evidence whose signer identity and algorithm match. +- Refresh preflight evidence before a later request after a signer, certificate, configuration, deployment, or policy-age change. ## What attestation does not establish by itself From 87680f3febdb86790a6007fd75cab5ebbf83dbc0 Mon Sep 17 00:00:00 2001 From: Coffee Date: Fri, 4 Sep 2026 20:16:26 +0800 Subject: [PATCH 19/23] docs: align manual verification flow --- .../cloud-api/gateway-attestation.mdx | 6 ++++-- cloud/verification/cloud-api/index.mdx | 19 ++++++++++--------- .../cloud-api/model-attestations.mdx | 2 +- .../cloud-api/response-signatures.mdx | 6 +++++- .../reference/verification-policy.mdx | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index a8c1738..538ebce 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. Add `model=` and `provider=near` only when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. For a deployment-first completion, verify and retain it before sending the completion. Add `model=` and `provider=near` when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Verify the gateway report @@ -49,4 +49,6 @@ The verified quote is the cryptographic source of truth. The HTTP fields are con ## Freshness and failure handling -The nonce establishes freshness only when the client generated and retained it, and the verified quote contains that nonce. If the report is unavailable, the quote fails, the nonce does not match, or the identity does not match, treat the gateway property as unverified. Do not reuse evidence from a different request or instance as a substitute. +The nonce establishes freshness only when the client generated and retained it, and the verified quote contains that nonce. If the report is unavailable, the quote fails, the nonce does not match, or the identity does not match, treat the gateway property as unverified. + +For a deployment-first response-signature flow, retain this verified report and require a later `gateway` signature to match its signer and algorithm. A report fetched after the completion cannot replace that preflight evidence, and a report from another signer is not a substitute. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index b3dd6d3..e4b9408 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -9,27 +9,28 @@ Use this guide when your application sends inference requests to `https://cloud- - Verify the trusted environment that receives your request at the NEAR AI Cloud gateway. + Verify the NEAR AI Cloud gateway deployment before an inference request. - Verify the environment serving the selected model. + Verify a selected NEAR model deployment before an inference request. Verify that your live HTTPS connection terminates at the attested gateway. - Verify that a signed response came from the attested gateway or model environment. + Verify exact response bytes against the matching preflight signer. Check that the software images in the attested environment meet your source and build policy. -## Start with gateway attestation +## Verify a completion -1. Generate a fresh nonce for this verification attempt. -2. Request a fresh gateway attestation report. -3. Verify the gateway's hardware attestation and apply your acceptance policy. -4. Add model, TLS, response-signature, or image-provenance checks when your application requires them. +1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request and verify the selected model report with its canonical ID and `provider=near` as well. For Gateway TLS binding, follow the same-connection [TLS flow](/cloud/verification/cloud-api/tls). +2. Send the completion with the same canonical model ID and `x-no-aliasing: true`. Retain the exact request and response bytes. +3. Fetch the completion signature. Its `signature_kind` selects the matching verified preflight result; it does not decide which deployment checks your policy required. -Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). That guide shows how to request fresh evidence. The other guides explain the additional checks. +Use one explicit `signing_algo` for the deployment reports and completion signature. Do not rely on endpoint defaults to choose a matching signer. + +Start with [Gateway attestation](/cloud/verification/cloud-api/gateway-attestation). The other guides provide the individual checks. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index 1e8efd2..f28e4dd 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -25,7 +25,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. -For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. Do not fetch another report after the completion merely to select a signer. +For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. A report fetched after the completion cannot replace this preflight evidence. `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 5ce4b3c..663a3d2 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -59,6 +59,10 @@ After fetching the signature, use `signature_kind` to select the matching prefli - For `provider_tee`, require the signature signer and algorithm to match the verified NEAR model report. - For `gateway`, require them to match the verified gateway report. -Do not request a new report after the completion merely to make a signer match. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified. +Do not use a report obtained after the completion as a substitute for preflight evidence. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified under this flow. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. + + + Preflight Gateway and model evidence plus one response signature do not yet form a complete model-to-Gateway-to-final-response chain. A `gateway` signature proves that the verified Gateway signed the final client-visible bytes, not that the verified model produced an upstream response. A `provider_tee` signature does not bind its model-signed bytes to the preflight Gateway or TLS evidence. + diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index b37d75e..142ca69 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -33,10 +33,10 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v ## Load balancing and rotations -The same hostname can be served by more than one attested instance. A report is not automatically evidence for a later request. +The same hostname can be served by more than one attested instance. A report is not evidence for a later request merely because it came from the same hostname. - For TLS, obtain the peer certificate and report on the same connection. -- For a response signature, use only preflight evidence whose signer identity and algorithm match. +- For a response signature, retain preflight evidence and use it only when its signer identity and algorithm match. A report fetched after the completion cannot replace that preflight check. - Refresh preflight evidence before a later request after a signer, certificate, configuration, deployment, or policy-age change. ## What attestation does not establish by itself From e808f8339b146ce45522f2e8f45e2bce44e5a4c7 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 11:59:38 +0800 Subject: [PATCH 20/23] docs: complete verification guidance --- cloud/verification/chat.mdx | 15 -- .../cloud-api/model-attestations.mdx | 10 +- .../cloud-api/response-signatures.mdx | 6 + .../verification/direct/model-attestation.mdx | 2 +- .../direct/response-signatures.mdx | 8 +- cloud/verification/gateway.mdx | 10 - cloud/verification/index.mdx | 2 +- cloud/verification/model.mdx | 15 -- cloud/verification/provenance.mdx | 15 -- .../reference/quote-nonce-signer.mdx | 17 +- .../response-signature-verification.mdx | 213 ++++++++++++++++++ cloud/verification/reference/sdk.mdx | 4 +- cloud/verification/tls.mdx | 15 -- docs.json | 21 ++ 14 files changed, 272 insertions(+), 81 deletions(-) delete mode 100644 cloud/verification/chat.mdx delete mode 100644 cloud/verification/gateway.mdx delete mode 100644 cloud/verification/model.mdx delete mode 100644 cloud/verification/provenance.mdx create mode 100644 cloud/verification/reference/response-signature-verification.mdx delete mode 100644 cloud/verification/tls.mdx diff --git a/cloud/verification/chat.mdx b/cloud/verification/chat.mdx deleted file mode 100644 index fdfbe75..0000000 --- a/cloud/verification/chat.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a Response-Signature Flow" -description: "Select the response-signature guide that matches the endpoint your application calls." ---- - -Choose the guide from the URL your application used for the completion. - - - - Use this when your application sent the completion through `cloud-api.near.ai`. - - - Use this when your application sent the completion directly to a model endpoint. - - diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index f28e4dd..cea0d43 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -39,20 +39,20 @@ The NEAR report uses these fields: | --- | --- | | `intel_quote` | Intel TDX evidence. | | `event_log` | Runtime measurement log. Replay it and match the result to the quote's RTMR3. | -| `request_nonce` | Echoed request nonce. Confirm its binding from the verified quote. | -| `report_data` | HTTP copy of quote report data. Compare it with the verified quote. | +| `request_nonce` | Echoed request nonce. Require it to match the client nonce, then confirm the nonce binding from the verified quote. | +| `report_data` | Optional HTTP copy of quote report data. The verified quote is the source of truth; when this field is present, compare it with the verified quote. | | `signing_address` and `signing_algo` | Model signing identity and its algorithm. | -| `signing_public_key` | Model public key for the E2EE flow. Request `signing_algo=ed25519` for that flow. | +| `signing_public_key` | Model public key needed by the E2EE flow. Other model-attestation flows do not require it. Request `signing_algo=ed25519` for E2EE. | | `nvidia_payload` | NVIDIA GPU evidence, when present. | | `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | ## Verify the NEAR model report 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Check the client-generated nonce and `signing_address` against the verified quote. Require `request_nonce` and `report_data` to match the client nonce and verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +2. Read report data from the verified quote. Require it to bind the client-generated nonce and `signing_address`; require `request_nonce` to match the client nonce. When `report_data` is present, require it to match the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. 3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. 4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. -5. When GPU evidence is present, verify `nvidia_payload` and require it to be bound to the same nonce. +5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). 6. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. 7. Apply your own allowlist and provenance policy to the verified measurements. diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 663a3d2..43493c9 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -35,6 +35,8 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ A `200` response containing `error_code` and `message` is an unavailable result, not a signature. A `404` means no matching signature was found; it can be retried for a short, bounded interval when the signature may still be recorded. Treat either result as unverified if a signature is required. +Use the same explicit `signing_algo` as both preflight attestation requests. These endpoints have different defaults. + For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. Then compare `text` with the expected payload: @@ -46,10 +48,14 @@ Then compare `text` with the expected payload: Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signature binds the response to the gateway, not to a model report. If a model signature is unavailable, model-level response binding is unavailable. +The Gateway can rewrite client-visible streaming bytes, so a `gateway` signature covers the exact bytes received from the Gateway, including stream framing and terminal events. + If `signature_kind` is absent or unfamiliar, do not infer it from the shape of `text`. Treat the signer classification as unavailable unless your endpoint contract gives a rule for that response. For `provider_tee`, `` is the canonical model identifier in the signed payload. +Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes. For `provider_tee`, pass the canonical model ID from the original request to build the expected text and use the verified model report's signer. For `gateway`, omit the model ID and use the verified Gateway report's signer. + ## Match preflight evidence Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 38076db..54aea0c 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -44,7 +44,7 @@ The root response is the model report. Relevant fields include: 2. Check the client-generated nonce and signer identity against the verified quote. Read report data from the verified quote; require `request_nonce` to match the client nonce and, when `report_data` is present, require it to match the verified quote. Use `signing_algo` to interpret that identity and verify response signatures. 3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. 4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. -5. When GPU evidence is present, verify it with the applicable NVIDIA verifier and ensure it is bound to the same nonce. +5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). 6. Apply your accepted measurement policy to the verified configuration. 7. If source and build provenance are required, continue with [direct image provenance](/cloud/verification/direct/image-provenance). diff --git a/cloud/verification/direct/response-signatures.mdx b/cloud/verification/direct/response-signatures.mdx index 1587cec..b22dee3 100644 --- a/cloud/verification/direct/response-signatures.mdx +++ b/cloud/verification/direct/response-signatures.mdx @@ -26,11 +26,13 @@ Set `` to the `id` in the completion response whose exact bytes you ret ```bash curl --fail-with-body -G 'https://.completions.near.ai/v1/signature/' \ - --data-urlencode 'signing_algo=ecdsa' \ + --data-urlencode 'signing_algo=' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` +Set `` to the explicit algorithm used for the matching model attestation. + For `ecdsa`, recover the Ethereum EIP-191 signer from `text` and `signature`, then compare it with `signing_address`. For `ed25519`, verify the UTF-8 bytes of `text` using `signing_address` as the public key. Reject unsupported algorithms. Then require that `text` is exactly: @@ -39,7 +41,9 @@ Then require that `text` is exactly: :: ``` -Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or a NEAR AI Cloud gateway model ID, does not satisfy this check. +Use the `model_name` reported by the direct model attestation. A different string, even if it resembles the hostname or a NEAR AI Cloud gateway model ID, does not satisfy this check. If the report has no non-empty `model_name`, this direct response-signature format cannot be verified. + +Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes, this `model_name`, and the signer from the verified direct model attestation. ## Match the model signer diff --git a/cloud/verification/gateway.mdx b/cloud/verification/gateway.mdx deleted file mode 100644 index 5e789d8..0000000 --- a/cloud/verification/gateway.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Gateway Attestation" -description: "Verify gateway evidence for requests sent through the NEAR AI Cloud gateway." ---- - -This guide is now part of the NEAR AI Cloud gateway verification flow. - - - Verify the trusted environment that receives requests through `cloud-api.near.ai`. - diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index eea7381..1858c3f 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -22,7 +22,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit ## What happens next -After you choose a path, its guide walks you through obtaining fresh attestation evidence and verifying the environment that handled the request. Depending on your application's policy, you can also verify the TLS connection, signed responses, and software-image provenance. +After you choose a path, its guide walks you through verifying fresh deployment evidence before a completion and, when present, a response signature against its reported signer. Depending on your application's policy, you can also verify the TLS connection and software-image provenance. ## Common verification concepts diff --git a/cloud/verification/model.mdx b/cloud/verification/model.mdx deleted file mode 100644 index 701b66a..0000000 --- a/cloud/verification/model.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a Model Attestation Flow" -description: "Select the verification guide that matches the endpoint your application calls." ---- - -Choose the guide from the URL your application uses for inference. - - - - Use this when your request goes through `cloud-api.near.ai`. - - - Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. - - diff --git a/cloud/verification/provenance.mdx b/cloud/verification/provenance.mdx deleted file mode 100644 index 700a62c..0000000 --- a/cloud/verification/provenance.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose an Image-Provenance Flow" -description: "Select the provenance guide that matches the endpoint your application uses." ---- - -Choose the guide from the URL your application uses for inference. - - - - Use this when your application sends requests through `cloud-api.near.ai`. - - - Use this when your application sends requests directly to a model's `completions.near.ai` endpoint. - - diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 5c6d865..e34015c 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -47,7 +47,22 @@ This layout applies to NEAR-operated dstack reports only. Compose Manager eviden ## Verify GPU evidence when present -When a report contains NVIDIA GPU evidence, verify it with the applicable NVIDIA verification service and require that it is bound to the same client nonce. Define in advance whether GPU evidence is mandatory for your workload policy. +`nvidia_payload`, when present, is a JSON string. Decode it and send the resulting JSON object unchanged to NVIDIA NRAS; do not wrap it in another JSON property. For a NEAR AI Cloud response, first save the selected `model_attestations[]` entry as `report.json`; for a direct response, save its root model report. + +```bash +GPU_PAYLOAD="$(jq -er '.nvidia_payload | fromjson' report.json)" + +printf '%s\n' "$GPU_PAYLOAD" | jq -e --arg nonce "$NONCE" \ + '(.nonce | type == "string") and ((.nonce | ascii_downcase) == ($nonce | ascii_downcase))' + +curl --fail-with-body -sS -X POST \ + 'https://nras.attestation.nvidia.com/v3/attest/gpu' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + --data-binary "$GPU_PAYLOAD" +``` + +NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that JWT's payload and require `x-nvidia-overall-att-result` to be boolean `true`. Reject missing or malformed GPU evidence, a nonce mismatch, or any other verdict. This minimal flow relies on the TLS-authenticated NRAS verdict; apply local JWT/EAT validation separately if your policy requires it. Define in advance whether missing GPU evidence is acceptable for your workload policy. ## Reference code diff --git a/cloud/verification/reference/response-signature-verification.mdx b/cloud/verification/reference/response-signature-verification.mdx new file mode 100644 index 0000000..ff11b48 --- /dev/null +++ b/cloud/verification/reference/response-signature-verification.mdx @@ -0,0 +1,213 @@ +--- +title: "Response Signature Verification" +description: "Verify a response signature over exact request and response bytes." +--- + +Use these helpers after you have fetched a response signature and verified the applicable preflight attestation. They verify only the signed response: exact-byte hashes, the signature, and the signer match. They do not verify a quote or choose which preflight evidence applies. + +Pass the signature JSON, the exact request and response bytes, the expected signed text for the endpoint, and the signer from the already verified preflight report: + +```text +preflight_signer = { + signing_algo: verified report signing_algo, + signing_address: verified report signing_address +} +``` + +The endpoint pages define the expected text and which preflight report to use: + +- [NEAR AI Cloud Gateway response signatures](/cloud/verification/cloud-api/response-signatures) +- [Direct model response signatures](/cloud/verification/direct/response-signatures) + + + + +Install [`ethers`](https://www.npmjs.com/package/ethers) and [`tweetnacl`](https://www.npmjs.com/package/tweetnacl): + +```bash +npm install ethers tweetnacl +``` + +```javascript +import { Buffer } from 'node:buffer'; +import { createHash, timingSafeEqual } from 'node:crypto'; +import { ethers } from 'ethers'; +import nacl from 'tweetnacl'; + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +// Omit modelName for a Gateway signature. +export function responseSignatureText(requestBytes, responseBytes, modelName) { + const hashes = `${sha256(requestBytes)}:${sha256(responseBytes)}`; + if (modelName === undefined) return hashes; + if (typeof modelName !== 'string' || !modelName) { + throw new Error('modelName must be a non-empty string'); + } + return `${modelName}:${hashes}`; +} + +function requireHex(value, label, expectedBytes) { + if (typeof value !== 'string') throw new Error(`${label} must be hex`); + const hex = value.replace(/^0x/i, ''); + if (!hex || !/^[0-9a-f]*$/i.test(hex) || hex.length % 2) { + throw new Error(`${label} must be hexadecimal`); + } + const bytes = Buffer.from(hex, 'hex'); + if (bytes.length !== expectedBytes) { + throw new Error(`${label} must be ${expectedBytes} bytes`); + } + return bytes; +} + +function signerIdentity(value, label) { + const algorithm = value?.signing_algo; + if (algorithm !== 'ecdsa' && algorithm !== 'ed25519') { + throw new Error(`${label}.signing_algo is unsupported`); + } + return { + algorithm, + address: requireHex( + value.signing_address, + `${label}.signing_address`, + algorithm === 'ecdsa' ? 20 : 32, + ), + }; +} + +function sameBytes(left, right) { + return left.length === right.length && timingSafeEqual(left, right); +} + +// signature is the JSON returned by GET /v1/signature/. +// preflightSigner comes from the already verified report selected by the endpoint. +export function verifyResponseSignature({ signature, expectedText, preflightSigner }) { + if (typeof signature?.text !== 'string' || signature.text !== expectedText) { + throw new Error('signature text does not match the exact request/response bytes'); + } + + const signer = signerIdentity(signature, 'signature'); + const signatureBytes = requireHex( + signature.signature, + 'signature.signature', + signer.algorithm === 'ecdsa' ? 65 : 64, + ); + + if (signer.algorithm === 'ecdsa') { + // ethers.verifyMessage applies the Ethereum EIP-191 personal-sign prefix. + const recovered = requireHex( + ethers.verifyMessage(Buffer.from(signature.text, 'utf8'), ethers.hexlify(signatureBytes)), + 'recovered signer', + 20, + ); + if (!sameBytes(recovered, signer.address)) { + throw new Error('ECDSA signature does not match signing_address'); + } + } else if (!nacl.sign.detached.verify( + Buffer.from(signature.text, 'utf8'), signatureBytes, signer.address, + )) { + throw new Error('Ed25519 signature does not match signing_address'); + } + + const preflight = signerIdentity(preflightSigner, 'preflightSigner'); + if (preflight.algorithm !== signer.algorithm || !sameBytes(preflight.address, signer.address)) { + throw new Error('signature signer does not match verified preflight evidence'); + } +} +``` + + + + +Install [`eth-account`](https://pypi.org/project/eth-account/) and [`PyNaCl`](https://pypi.org/project/PyNaCl/): + +```bash +pip install eth-account pynacl +``` + +```python +import re +from hashlib import sha256 +from hmac import compare_digest +from typing import Optional + +from eth_account import Account +from eth_account.messages import encode_defunct +from nacl.exceptions import BadSignatureError +from nacl.signing import VerifyKey + + +def response_signature_text( + request_bytes: bytes, response_bytes: bytes, model_name: Optional[str] = None +) -> str: + hashes = f'{sha256(request_bytes).hexdigest()}:{sha256(response_bytes).hexdigest()}' + if model_name is None: + return hashes + if not isinstance(model_name, str) or not model_name: + raise ValueError('model_name must be a non-empty string') + return f'{model_name}:{hashes}' + + +def _require_hex(value: object, label: str, expected_bytes: int) -> bytes: + if not isinstance(value, str): + raise ValueError(f'{label} must be hex') + value = value.removeprefix('0x').removeprefix('0X') + if not value or len(value) % 2 or not re.fullmatch(r'[0-9a-fA-F]*', value): + raise ValueError(f'{label} must be hexadecimal') + decoded = bytes.fromhex(value) + if len(decoded) != expected_bytes: + raise ValueError(f'{label} must be {expected_bytes} bytes') + return decoded + + +def _signer_identity(value: dict, label: str) -> tuple[str, bytes]: + algorithm = value.get('signing_algo') + if algorithm not in ('ecdsa', 'ed25519'): + raise ValueError(f'{label}.signing_algo is unsupported') + address = _require_hex( + value.get('signing_address'), + f'{label}.signing_address', + 20 if algorithm == 'ecdsa' else 32, + ) + return algorithm, address + + +# signature is the JSON returned by GET /v1/signature/. +# preflight_signer comes from the already verified report selected by the endpoint. +def verify_response_signature( + signature: dict, expected_text: str, preflight_signer: dict +) -> None: + if signature.get('text') != expected_text: + raise ValueError('signature text does not match the exact request/response bytes') + + algorithm, address = _signer_identity(signature, 'signature') + signature_bytes = _require_hex( + signature.get('signature'), + 'signature.signature', + 65 if algorithm == 'ecdsa' else 64, + ) + + if algorithm == 'ecdsa': + # encode_defunct applies the Ethereum EIP-191 personal-sign prefix. + recovered = Account.recover_message( + encode_defunct(text=signature['text']), + signature=f'0x{signature_bytes.hex()}', + ) + if not compare_digest(_require_hex(recovered, 'recovered signer', 20), address): + raise ValueError('ECDSA signature does not match signing_address') + else: + try: + VerifyKey(address).verify(signature['text'].encode('utf-8'), signature_bytes) + except (BadSignatureError, ValueError) as error: + raise ValueError('Ed25519 signature does not match signing_address') from error + + preflight_algorithm, preflight_address = _signer_identity( + preflight_signer, 'preflight_signer' + ) + if algorithm != preflight_algorithm or not compare_digest(address, preflight_address): + raise ValueError('signature signer does not match verified preflight evidence') +``` + + + diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 7f1bf52..3d09a49 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -47,6 +47,8 @@ For each completion, first use the SDK to verify the NEAR AI Cloud gateway and s The SDK creates a fresh nonce for every evidence request. It retrieves and verifies evidence, but does not send the completion request or retain its bytes for you. +Use one explicit `signing_algo` for the Gateway attestation request, model-attestation request, and completion-signature request in the same verification flow. Those Cloud API endpoints have different defaults. + Learn the protocol requirements and trust decisions for gateway and NEAR model evidence. @@ -56,7 +58,7 @@ The SDK creates a fresh nonce for every evidence request. It retrieves and verif Runnable TypeScript, Python, and Rust examples follow this flow for non-streaming and streaming completions. - In TypeScript, import from `verifiable-ai-sdk/node` when a Node.js application requires Gateway TLS binding. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. + In TypeScript, import from `verifiable-ai-sdk/node` to bind Gateway evidence to the TLS peer observed for the Gateway attestation request. This does not automatically cover a later inference request. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, Compose Manager evidence, image provenance, and E2EE, use the corresponding endpoint guides. diff --git a/cloud/verification/tls.mdx b/cloud/verification/tls.mdx deleted file mode 100644 index b89278d..0000000 --- a/cloud/verification/tls.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Choose a TLS Attestation Flow" -description: "Select the TLS verification guide that matches the endpoint your application calls." ---- - -TLS evidence applies to a particular request path and connection. Select the endpoint your application calls. - - - - Use this when your application connects to `cloud-api.near.ai`. - - - Use this when your application connects directly to a model's `completions.near.ai` endpoint. - - diff --git a/docs.json b/docs.json index a2f6025..53546ea 100644 --- a/docs.json +++ b/docs.json @@ -86,6 +86,7 @@ "pages": [ "cloud/verification/reference/sdk", "cloud/verification/reference/quote-nonce-signer", + "cloud/verification/reference/response-signature-verification", "cloud/verification/reference/verification-policy" ] } @@ -502,6 +503,26 @@ { "source": "/api", "destination": "/api-reference/introduction" + }, + { + "source": "/cloud/verification/chat", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/gateway", + "destination": "/cloud/verification/cloud-api/gateway-attestation" + }, + { + "source": "/cloud/verification/model", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/tls", + "destination": "/cloud/verification" + }, + { + "source": "/cloud/verification/provenance", + "destination": "/cloud/verification" } ] } From 86e552fa27d74abe662ddb0d08f6e54d1e6ea84e Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 15:13:14 +0800 Subject: [PATCH 21/23] docs: support multiple model attestations --- cloud/guides/e2ee-chat-completions.mdx | 77 ++++++++----------- .../cloud-api/gateway-attestation.mdx | 4 +- .../cloud-api/image-provenance.mdx | 8 +- cloud/verification/cloud-api/index.mdx | 8 +- .../cloud-api/model-attestations.mdx | 21 ++--- .../cloud-api/response-signatures.mdx | 14 ++-- cloud/verification/index.mdx | 2 +- .../reference/quote-nonce-signer.mdx | 4 +- cloud/verification/reference/sdk.mdx | 4 +- .../reference/verification-policy.mdx | 4 +- 10 files changed, 67 insertions(+), 79 deletions(-) diff --git a/cloud/guides/e2ee-chat-completions.mdx b/cloud/guides/e2ee-chat-completions.mdx index 1afe8bb..d1ad8dc 100644 --- a/cloud/guides/e2ee-chat-completions.mdx +++ b/cloud/guides/e2ee-chat-completions.mdx @@ -30,11 +30,12 @@ sequenceDiagram Client->>GatewayTEE: GET /attestation/report?model=X GatewayTEE->>ModelTEE: Forward attestation request Note over ModelTEE: Generate attestation report
with model's public key - ModelTEE->>GatewayTEE: Attestation report - GatewayTEE->>Client: Model public key (from Model TEE) + ModelTEE->>GatewayTEE: Attestation reports + GatewayTEE->>Client: Model attestation candidates + Note over Client: Verify candidates and select a key
under the application's acceptance policy Note over Client: Generate ephemeral key pair - Note over Client: Encrypt messages with model's public key + Note over Client: Encrypt messages with the selected model key Client->>GatewayTEE: POST /chat/completions
with encryption headers GatewayTEE->>ModelTEE: Forward encrypted request @@ -84,7 +85,7 @@ Wire format for all encrypted fields: `[ephemeral_pubkey (32 bytes)][nonce (24 b ## Quick Start -A Python script that demonstrates the E2EE encryption flow: fetch the model's key, generate a client key pair, encrypt, send, and decrypt. Verify the model report before using its key; the endpoint-specific attestation guides describe that check. +A Python script that demonstrates the E2EE encryption flow after you have obtained a verified model public key: generate a client key pair, encrypt, send, and decrypt. For a gateway report, verify every returned model-attestation candidate before applying your acceptance policy to select a key; the endpoint-specific attestation guides describe those checks. **Requirements:** `pip install requests PyNaCl cryptography` @@ -92,7 +93,7 @@ A Python script that demonstrates the E2EE encryption flow: fetch the model's ke - **cryptography** β€” X25519 ECDH key exchange and HKDF-SHA256 key derivation ```python -import secrets, requests +import os, secrets, requests from nacl.signing import SigningKey from nacl.bindings import ( crypto_sign_ed25519_pk_to_curve25519, @@ -106,8 +107,11 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes ENDPOINT = "https://cloud-api.near.ai" -API_KEY = "sk-..." -MODEL = "zai-org/GLM-5.1-FP8" +API_KEY = os.environ["NEAR_AI_CLOUD_API_KEY"] +MODEL = "zai-org/GLM-5.1-FP8" +# Obtain this from `signing_public_key` only after verifying the attestation +# candidates and selecting one report under your application's policy. +MODEL_ED25519_PUBLIC_KEY_HEX = os.environ["MODEL_ED25519_PUBLIC_KEY_HEX"] # ── Helpers ───────────────────────────────────────────────────────── @@ -126,32 +130,11 @@ def v2_decrypt(data: bytes, secret_x25519: bytes) -> bytes: key = HKDF(hashes.SHA256(), 32, None, b"ed25519_encryption").derive(shared) return crypto_aead_xchacha20poly1305_ietf_decrypt(ct, None, nonce, key) -# ── 1. Fetch the model's Ed25519 public key ───────────────────────── - -nonce = secrets.token_hex(32) -attestation_response = requests.get( - f"{ENDPOINT}/v1/attestation/report", - headers={ - "Authorization": f"Bearer {API_KEY}", - "x-no-aliasing": "true", - }, - params={ - "model": MODEL, - "provider": "near", - "signing_algo": "ed25519", - "include_tls_fingerprint": "false", - "nonce": nonce, - }, -) -attestation_response.raise_for_status() -attestation = attestation_response.json() -model_reports = attestation.get("model_attestations", []) -if len(model_reports) != 1: - raise ValueError("Expected exactly one NEAR model attestation") +# ── 1. Use the model's verified Ed25519 public key ────────────────── -# Verify this report and `nonce` before using its signing_public_key: -# https://docs.near.ai/cloud/verification/cloud-api/model-attestations -model_ed25519_pub = bytes.fromhex(model_reports[0]["signing_public_key"]) +model_ed25519_pub = bytes.fromhex(MODEL_ED25519_PUBLIC_KEY_HEX) +if len(model_ed25519_pub) != 32: + raise ValueError("MODEL_ED25519_PUBLIC_KEY_HEX must encode 32 bytes") model_x25519_pub = crypto_sign_ed25519_pk_to_curve25519(model_ed25519_pub) # ── 2. Generate client Ed25519 key pair ────────────────────────────── @@ -204,7 +187,7 @@ if msg.get("content"): ### Step 1: Get the Model's Public Key -Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. Verify the quote and fresh nonce before using the key; see [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) for gateway requests or [direct model attestation](/cloud/verification/direct/model-attestation) for direct requests. +Fetch the model's Ed25519 public key from its TEE attestation report. The public key is generated inside the Model TEE and cryptographically bound to its hardware attestation. For a gateway request, reject an empty `model_attestations[]` array, verify every returned candidate and its fresh nonce, then choose one verified report under your application's acceptance policy. Use that report's `signing_public_key` as the `X-Model-Pub-Key` routing pin. For a direct request, verify the root model report before using its key. See [NEAR model attestations](/cloud/verification/cloud-api/model-attestations) or [direct model attestation](/cloud/verification/direct/model-attestation). The gateway endpoint requires an API key (`Authorization: Bearer `); report retrieval is free and never counts against your usage. The direct completions endpoint is served by the model TEE itself and needs no API key. @@ -270,13 +253,14 @@ response.raise_for_status() attestation = response.json() # NEAR AI Cloud gateway: model_reports = attestation.get("model_attestations", []) -if len(model_reports) != 1: - raise ValueError("Expected exactly one NEAR model attestation") -# Verify `model_reports[0]` and `nonce` before using this key. -model_public_key_hex = model_reports[0]["signing_public_key"] +if not model_reports: + raise ValueError("No NEAR model attestations returned") +# Do not read a key from a raw candidate. Verify every candidate and `nonce` +# using the model-attestation flow, then select a verified report under your +# E2EE acceptance policy and read its `signing_public_key`. +print(f"Retrieved {len(model_reports)} NEAR model-attestation candidate(s)") # Direct completions use the root model report instead: # model_public_key_hex = attestation["signing_public_key"] -print(f"Model Ed25519 public key: {model_public_key_hex}") ``` @@ -307,16 +291,17 @@ const response = await fetch( if (!response.ok) throw new Error(`Attestation request failed: ${response.status}`); const attestation = await response.json(); const modelReports = attestation.model_attestations ?? []; -if (modelReports.length !== 1) throw new Error('Expected exactly one NEAR model attestation'); -// Verify `modelReports[0]` and `nonce` before using this key. -const modelPublicKeyHex = modelReports[0].signing_public_key; -console.log('Model Ed25519 public key:', modelPublicKeyHex); +if (modelReports.length === 0) throw new Error('No NEAR model attestations returned'); +// Do not read a key from a raw candidate. Verify every candidate and `nonce` +// using the model-attestation flow, then select a verified report under your +// E2EE acceptance policy and read its `signing_public_key`. +console.log(`Retrieved ${modelReports.length} NEAR model-attestation candidate(s)`); ``` -For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Require exactly one returned model report, verify it with the nonce you generated, then read `signing_public_key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. +For the NEAR AI Cloud gateway, request `provider=near` with a canonical model ID and `x-no-aliasing: true`. Reject an empty response, verify every returned model-report candidate with the nonce you generated, and retain only reports your application accepts. Select a key only from that verified set, then send its `signing_public_key` in `X-Model-Pub-Key`. For a direct completions endpoint, the key is in the root report's `signing_public_key`. --- @@ -586,7 +571,7 @@ const data = await response.json(); | `X-Signing-Algo` | Set to `ed25519` | | `X-Client-Pub-Key` | Your Ed25519 public key in hex format (32 bytes / 64 hex chars) | | `X-Encryption-Version` | Set to `2` | -| `X-Model-Pub-Key` | Model's Ed25519 public key from attestation (required when using the gateway for routing) | +| `X-Model-Pub-Key` | Ed25519 public key from the selected, verified model report; required as the gateway routing pin | | `x-no-aliasing` | Set to `true` for gateway requests so the model ID and attested model key cannot be silently redirected. | | `X-Encrypt-All-Fields` | Optional. Set to `true` to extend encryption beyond message content to tool definitions, tool calls, and other sensitive fields β€” see [Encrypting All Fields](#encrypting-all-fields-tool-calling) | @@ -770,7 +755,7 @@ E2EE is supported on the **Chat Completions API** (`/v1/chat/completions`), **Co ### Verification -Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. Choose the matching endpoint flow in [Verification](/cloud/verification). +Before using a model public key for encryption, verify the attestation report and its fresh client nonce against hardware attestation. For a gateway response, verify every returned model-attestation candidate and choose a key only from reports accepted by your policy. Choose the matching endpoint flow in [Verification](/cloud/verification). --- @@ -779,7 +764,7 @@ Before using a model public key for encryption, verify the attestation report an ### Get the Model's ECDSA Public Key - Verify the returned report and its fresh nonce before using the public key, as described in [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). + Verify every returned model-attestation candidate and its fresh nonce before applying your acceptance policy and using a selected public key, as described in [NEAR model attestations](/cloud/verification/cloud-api/model-attestations). ```bash NONCE="$(openssl rand -hex 32)" diff --git a/cloud/verification/cloud-api/gateway-attestation.mdx b/cloud/verification/cloud-api/gateway-attestation.mdx index 538ebce..061cda9 100644 --- a/cloud/verification/cloud-api/gateway-attestation.mdx +++ b/cloud/verification/cloud-api/gateway-attestation.mdx @@ -19,7 +19,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response contains the gateway report at `gateway_attestation`. For a deployment-first completion, verify and retain it before sending the completion. Add `model=` and `provider=near` when you also need the [NEAR model report](/cloud/verification/cloud-api/model-attestations), and add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). +The response contains the gateway report at `gateway_attestation`. For a deployment-first completion, verify and retain it before sending the completion. Add `model=` and `provider=near` when you also need [NEAR model evidence](/cloud/verification/cloud-api/model-attestations). That request can return multiple candidates in `model_attestations[]`; verify every candidate before sending the completion. Add `include_tls_fingerprint=true` only when you are following the [NEAR AI Cloud gateway TLS flow](/cloud/verification/cloud-api/tls). ## Verify the gateway report @@ -45,7 +45,7 @@ The verified quote is the cryptographic source of truth. The HTTP fields are con | `gateway_attestation.signing_address` and `.signing_algo` | The gateway signing identity and algorithm. | | `gateway_attestation.tls_cert_fingerprint` | The gateway TLS binding when requested. | | `gateway_attestation.info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. | -| `model_attestations[]` | Separate NEAR model evidence when requested with `model` and `provider=near`. Do not use it as a substitute for the gateway quote. | +| `model_attestations[]` | Separate NEAR model-evidence candidates when requested with `model` and `provider=near`. Verify every candidate separately; none is a substitute for the gateway quote. | ## Freshness and failure handling diff --git a/cloud/verification/cloud-api/image-provenance.mdx b/cloud/verification/cloud-api/image-provenance.mdx index 19572ce..5e0eacc 100644 --- a/cloud/verification/cloud-api/image-provenance.mdx +++ b/cloud/verification/cloud-api/image-provenance.mdx @@ -5,18 +5,18 @@ description: "Apply an image-provenance policy to configurations measured in NEA Image provenance is separate from hardware attestation. A verified quote can bind a configuration measurement to a runtime. Provenance determines whether the image digests in that configuration were built by a source and build process your policy accepts. -For model-level provenance through the gateway, this flow applies to the NEAR model report requested with `provider=near`. +For model-level provenance through the gateway, this flow applies to every NEAR model report returned by a request with `provider=near`. For a NEAR AI Cloud gateway flow, decide separately whether your policy requires provenance for: - the configuration measured by `gateway_attestation` -- the NEAR model report in `model_attestations[]`, requested with `provider=near` +- each NEAR model report in `model_attestations[]`, requested with `provider=near` - both ## Verification flow -1. Verify the relevant Intel TDX quote first. -2. For a NEAR model report, use the raw `info.tcb_info.app_compose` string. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in the verified quote. +1. Verify the relevant Intel TDX quote first. For `model_attestations[]`, verify each candidate report separately before treating it as eligible evidence. +2. For each eligible NEAR model report, use the raw `info.tcb_info.app_compose` string. Calculate its SHA-256 hash without parsing or reserializing it, and compare it with the configuration measurement in that report's verified quote. 3. Extract every immutable image digest (`@sha256:…`) from the matched configuration. 4. Cryptographically verify provenance for each required digest and confirm that the statement's subject equals the image digest exactly. 5. Apply your policy for repository, build identity, workflow, ref, and source revision. diff --git a/cloud/verification/cloud-api/index.mdx b/cloud/verification/cloud-api/index.mdx index e4b9408..028e1fb 100644 --- a/cloud/verification/cloud-api/index.mdx +++ b/cloud/verification/cloud-api/index.mdx @@ -1,6 +1,6 @@ --- title: "Verify the NEAR AI Cloud Gateway" -description: "Verify the gateway, selected model, connection, response, and software evidence for requests sent through cloud-api.near.ai." +description: "Verify the gateway, model evidence, connection, response, and software evidence for requests sent through cloud-api.near.ai." --- Use this guide when your application sends inference requests to `https://cloud-api.near.ai`. If your application sends requests directly to a model's `completions.near.ai` endpoint, use the [direct model endpoint guide](/cloud/verification/direct) instead. @@ -12,7 +12,7 @@ Use this guide when your application sends inference requests to `https://cloud- Verify the NEAR AI Cloud gateway deployment before an inference request.
- Verify a selected NEAR model deployment before an inference request. + Verify every returned NEAR model-attestation candidate before an inference request. Verify that your live HTTPS connection terminates at the attested gateway. @@ -27,9 +27,9 @@ Use this guide when your application sends inference requests to `https://cloud- ## Verify a completion -1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request and verify the selected model report with its canonical ID and `provider=near` as well. For Gateway TLS binding, follow the same-connection [TLS flow](/cloud/verification/cloud-api/tls). +1. Before the completion, request and verify fresh gateway evidence. If your policy requires NEAR model evidence, request it with the canonical model ID and `provider=near`, verify every returned candidate, and retain the verified results. For Gateway TLS binding, follow the same-connection [TLS flow](/cloud/verification/cloud-api/tls). 2. Send the completion with the same canonical model ID and `x-no-aliasing: true`. Retain the exact request and response bytes. -3. Fetch the completion signature. Its `signature_kind` selects the matching verified preflight result; it does not decide which deployment checks your policy required. +3. Fetch the completion signature. Its `signature_kind` selects the verified gateway report or the uniquely matching verified model report; it does not decide which deployment checks your policy required. Use one explicit `signing_algo` for the deployment reports and completion signature. Do not rely on endpoint defaults to choose a matching signer. diff --git a/cloud/verification/cloud-api/model-attestations.mdx b/cloud/verification/cloud-api/model-attestations.mdx index cea0d43..422cc54 100644 --- a/cloud/verification/cloud-api/model-attestations.mdx +++ b/cloud/verification/cloud-api/model-attestations.mdx @@ -23,12 +23,12 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/attestation/report' \ -H 'Authorization: Bearer ' ``` -The response includes gateway evidence and exactly one NEAR model report in `model_attestations[]`. Treat an empty or multi-item array as unavailable evidence for this flow; do not choose an entry yourself. Verify and retain the report before sending a completion. +The response includes gateway evidence and zero or more NEAR model-report candidates in `model_attestations[]`. If your policy requires NEAR model evidence, reject an empty array. Before sending a completion, verify every candidate and retain the verified results; do not choose a raw entry yourself. -For a later `provider_tee` response signature, require its `signing_address` and `signing_algo` to match this verified report. A report fetched after the completion cannot replace this preflight evidence. +For a later `provider_tee` response signature, select exactly one retained verified report whose `signing_address` and `signing_algo` match the signature. If no report or more than one report matches, the response is unverified. A report fetched after the completion cannot replace this preflight evidence. - `provider=near` selects the model report returned by this request. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm match the retained verified report. + `provider=near` requests NEAR model reports from this endpoint. It does not select the provider for a later completion. A `provider_tee` response is verified only when its signer and algorithm uniquely match a retained verified report. ## NEAR model report fields @@ -46,18 +46,21 @@ The NEAR report uses these fields: | `nvidia_payload` | NVIDIA GPU evidence, when present. | | `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | -## Verify the NEAR model report +## Verify NEAR model reports + +Perform these checks on every returned candidate before sending a completion: 1. Verify `intel_quote` with an Intel DCAP verifier and apply your TCB and advisory policy. -2. Read report data from the verified quote. Require it to bind the client-generated nonce and `signing_address`; require `request_nonce` to match the client nonce. When `report_data` is present, require it to match the verified quote. Interpret the address using `signing_algo`, and compare both values when matching a response signature. +2. Read report data from the verified quote. Require it to bind the client-generated nonce and `signing_address`; require `request_nonce` to match the client nonce. When `report_data` is present, require it to match the verified quote. Interpret the address using `signing_algo`. 3. Replay `event_log` and require the resulting RTMR3 to equal the RTMR3 in the verified quote. 4. If `tcb_info` is JSON text, decode it to obtain `app_compose`. Hash the raw `app_compose` string without parsing or reserializing it. Require the quote's MRCONFIGID to begin with `01` followed by that SHA-256 hash. 5. When GPU evidence is present, follow [GPU evidence verification](/cloud/verification/reference/quote-nonce-signer#verify-gpu-evidence-when-present). -6. When verifying a model-serving response signature, require its `signing_address` and `signing_algo` to match this verified report. -7. Apply your own allowlist and provenance policy to the verified measurements. +6. Apply your own allowlist and provenance policy to the verified measurements. + +After receiving a `provider_tee` response signature, select the one retained report whose signer identity and algorithm match the signature. If the match is absent or ambiguous, reject the response as unverified. - `model_attestations[]` is not an inventory of every model instance behind a URL. A response signature must be matched by signer identity and algorithm. A report from a different signer is not an equivalent substitute. + `model_attestations[]` is not an inventory of every model instance behind a URL. Candidates are not interchangeable: a response signature must be matched by signer identity and algorithm. ## Relationship to NEAR AI Cloud gateway TLS @@ -66,7 +69,7 @@ The browser or client connection is to `cloud-api.near.ai`. Its TLS endpoint bin ## Verify a signed response -For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to the previously verified model attestation. For a `gateway` signature, match the signer to the previously verified `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). +For a NEAR AI Cloud gateway response with `signature_kind: "provider_tee"`, verify the signature over the endpoint-defined payload and match its signer to exactly one previously verified model report. For a `gateway` signature, match the signer to the previously verified `gateway_attestation` instead. See [NEAR AI Cloud gateway response signatures](/cloud/verification/cloud-api/response-signatures). ## Other provider documentation diff --git a/cloud/verification/cloud-api/response-signatures.mdx b/cloud/verification/cloud-api/response-signatures.mdx index 43493c9..b85496e 100644 --- a/cloud/verification/cloud-api/response-signatures.mdx +++ b/cloud/verification/cloud-api/response-signatures.mdx @@ -3,7 +3,7 @@ title: "Response Signatures" description: "Verify available response signatures from the NEAR AI Cloud gateway against exact bytes and matching preflight evidence." --- -Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. Before sending a completion, verify the gateway and selected NEAR model deployment your policy requires. When present, `signature_kind` determines which verified preflight result must match the signer. +Response signatures returned through the NEAR AI Cloud gateway can be produced by either a model-serving TEE or the gateway itself. Before sending a completion, verify the gateway and, when your policy requires model evidence, every returned NEAR model-attestation candidate. When present, `signature_kind` determines which verified preflight result must match the signer. A response without an available signature is not a successfully verified response. If response verification is required by your policy, treat it as unverified after a short, bounded retry interval. @@ -33,7 +33,7 @@ curl --fail-with-body -G 'https://cloud-api.near.ai/v1/signature/' \ -H 'Authorization: Bearer ' ``` -A `200` response containing `error_code` and `message` is an unavailable result, not a signature. A `404` means no matching signature was found; it can be retried for a short, bounded interval when the signature may still be recorded. Treat either result as unverified if a signature is required. +A successful (`2xx`) response containing `error_code` and `message` is an unavailable result, not a signature. A `404` means no matching signature was found; it can be retried for a short, bounded interval when the signature may still be recorded. Treat either result as unverified if a signature is required. Use the same explicit `signing_algo` as both preflight attestation requests. These endpoints have different defaults. @@ -43,7 +43,7 @@ Then compare `text` with the expected payload: | `signature_kind` | Expected `text` | Required preflight match | | --- | --- | --- | -| `provider_tee` | `::` | A verified NEAR model report with the same `signing_address` and `signing_algo`. | +| `provider_tee` | `::` | Exactly one verified NEAR model report with the same `signing_address` and `signing_algo`. | | `gateway` | `:` | A verified `gateway_attestation` with the same `signing_address` and `signing_algo`. | Only `provider_tee` binds a response to a model-serving TEE. A `gateway` signature binds the response to the gateway, not to a model report. If a model signature is unavailable, model-level response binding is unavailable. @@ -54,18 +54,18 @@ If `signature_kind` is absent or unfamiliar, do not infer it from the shape of ` For `provider_tee`, `` is the canonical model identifier in the signed payload. -Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes. For `provider_tee`, pass the canonical model ID from the original request to build the expected text and use the verified model report's signer. For `gateway`, omit the model ID and use the verified Gateway report's signer. +Use the [manual response-signature helpers](/cloud/verification/reference/response-signature-verification) with the exact request and response bytes. For `provider_tee`, pass the canonical model ID from the original request to build the expected text and use the signer from the uniquely matching verified model report. For `gateway`, omit the model ID and use the verified Gateway report's signer. ## Match preflight evidence -Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support either signature kind, also request and verify the selected NEAR model report with `model=` and `provider=near`. Retain both verified results. +Before sending the completion, request and verify gateway evidence with a client-generated nonce. To support `provider_tee`, also request NEAR model evidence with `model=` and `provider=near`. Reject an empty `model_attestations[]` array; verify every returned candidate and retain the verified results. After fetching the signature, use `signature_kind` to select the matching preflight result: -- For `provider_tee`, require the signature signer and algorithm to match the verified NEAR model report. +- For `provider_tee`, require the signature signer and algorithm to match exactly one verified NEAR model report. If the match is absent or ambiguous, the response is unverified. - For `gateway`, require them to match the verified gateway report. -Do not use a report obtained after the completion as a substitute for preflight evidence. If the signature's signer or algorithm differs from the preflight evidence, the response is unverified under this flow. +Do not use a report obtained after the completion as a substitute for preflight evidence. If the signature's signer or algorithm differs from the required preflight evidence, the response is unverified under this flow. `ecdsa` identities are Ethereum-style signing addresses. `ed25519` identities are Ed25519 public-key bytes encoded as hexadecimal. Always compare the identity together with `signing_algo`. diff --git a/cloud/verification/index.mdx b/cloud/verification/index.mdx index 1858c3f..1a1f179 100644 --- a/cloud/verification/index.mdx +++ b/cloud/verification/index.mdx @@ -9,7 +9,7 @@ Use these guides to verify the evidence returned for your AI requests. Start wit - Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when available, model evidence returned with the report. + Use this when your application sends requests through `cloud-api.near.ai`. Verify the gateway and, when your policy requires it, every returned NEAR model-attestation candidate. Use this when your application connects directly to a model's `completions.near.ai` endpoint. Verify the model environment reported by that endpoint. diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index e34015c..572301e 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -20,7 +20,7 @@ Send this value with the attestation request. Reject a report unless the verifie | Request path | Quote location | | --- | --- | | NEAR AI Cloud gateway | `gateway_attestation.intel_quote` | -| NEAR AI Cloud gateway model evidence (`provider=near`) | The single model report in `model_attestations[]` | +| NEAR AI Cloud gateway model evidence (`provider=near`) | Each candidate report in `model_attestations[]` | | Direct model endpoint | Root `intel_quote` (and any applicable direct report entry) | ## Verify the signer binding @@ -47,7 +47,7 @@ This layout applies to NEAR-operated dstack reports only. Compose Manager eviden ## Verify GPU evidence when present -`nvidia_payload`, when present, is a JSON string. Decode it and send the resulting JSON object unchanged to NVIDIA NRAS; do not wrap it in another JSON property. For a NEAR AI Cloud response, first save the selected `model_attestations[]` entry as `report.json`; for a direct response, save its root model report. +`nvidia_payload`, when present, is a JSON string. Decode it and send the resulting JSON object unchanged to NVIDIA NRAS; do not wrap it in another JSON property. For a NEAR AI Cloud response, save each `model_attestations[]` candidate as a separate report file and run this check for every candidate before accepting it. For a direct response, save its root model report. ```bash GPU_PAYLOAD="$(jq -er '.nvidia_payload | fromjson' report.json)" diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 3d09a49..0714b24 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -43,11 +43,11 @@ Import it as `verifiable_ai_sdk`. ## Verification flow -For each completion, first use the SDK to verify the NEAR AI Cloud gateway and selected model deployments. Then send the completion with your HTTP client, retaining the exact request and response bytes. Finally, fetch the completion signature and verify it with the matching preflight result. +For each completion, first use the SDK to verify the NEAR AI Cloud gateway and, when your policy requires model evidence, every returned NEAR model-attestation candidate. Then send the completion with your HTTP client, retaining the exact request and response bytes. Finally, fetch the completion signature and verify it with the matching preflight result. For a `provider_tee` signature, select exactly one verified model result whose signer identity and algorithm match the signature. The SDK creates a fresh nonce for every evidence request. It retrieves and verifies evidence, but does not send the completion request or retain its bytes for you. -Use one explicit `signing_algo` for the Gateway attestation request, model-attestation request, and completion-signature request in the same verification flow. Those Cloud API endpoints have different defaults. +Use one explicit `signing_algo` for the Gateway attestation request, model-attestation request, and completion-signature request in the same verification flow. The Gateway attestation and signature endpoints have different defaults. Learn the protocol requirements and trust decisions for gateway and NEAR model evidence. diff --git a/cloud/verification/reference/verification-policy.mdx b/cloud/verification/reference/verification-policy.mdx index 142ca69..0e61dc8 100644 --- a/cloud/verification/reference/verification-policy.mdx +++ b/cloud/verification/reference/verification-policy.mdx @@ -29,14 +29,14 @@ When a required check is missing, unavailable, malformed, mismatched, or fails v | TLS report or peer SPKI unavailable | Treat endpoint binding as unverified. | | Response signature unavailable after a bounded retry | Treat the response as unverified when signatures are required. | | Image-provenance record is missing or returns `404` | Treat that digest as unverified when provenance is required. | -| Required NEAR model report is unavailable or malformed | Treat the model property as unverified. | +| A required NEAR model candidate is unavailable, malformed, or fails verification | Treat the model property as unverified. | ## Load balancing and rotations The same hostname can be served by more than one attested instance. A report is not evidence for a later request merely because it came from the same hostname. - For TLS, obtain the peer certificate and report on the same connection. -- For a response signature, retain preflight evidence and use it only when its signer identity and algorithm match. A report fetched after the completion cannot replace that preflight check. +- For a `provider_tee` response signature, verify and retain every returned model-attestation candidate before the completion. Accept the signature only when its signer identity and algorithm match exactly one verified candidate. A report fetched after the completion cannot replace that preflight check. - Refresh preflight evidence before a later request after a signer, certificate, configuration, deployment, or policy-age change. ## What attestation does not establish by itself From 37f2c9b556816483d38a934d7b6ffeefcc475ff5 Mon Sep 17 00:00:00 2001 From: Coffee Date: Wed, 9 Sep 2026 16:04:49 +0800 Subject: [PATCH 22/23] docs: remove deployment control evidence --- .../direct/deployment-control-evidence.mdx | 39 ------------------- cloud/verification/direct/index.mdx | 3 -- .../verification/direct/model-attestation.mdx | 1 - .../reference/quote-nonce-signer.mdx | 4 +- cloud/verification/reference/sdk.mdx | 2 +- docs.json | 3 +- 6 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 cloud/verification/direct/deployment-control-evidence.mdx diff --git a/cloud/verification/direct/deployment-control-evidence.mdx b/cloud/verification/direct/deployment-control-evidence.mdx deleted file mode 100644 index 49051e7..0000000 --- a/cloud/verification/direct/deployment-control-evidence.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Deployment Control Evidence" -description: "Interpret optional Compose Manager evidence returned by a direct model endpoint." ---- - -Some direct model reports include optional deployment-control evidence in `compose_manager_attestation`. It can help investigate deployment operations, but it does not verify an individual inference request. - -## What it can and cannot establish - -Use it as supplementary evidence about deployment activity. Do not use it alone to conclude that: - -- a particular project, model, or backend is currently running -- every recorded action completed successfully -- a later inference request was served by this same CVM -- the images involved satisfy your source and build-provenance policy - -The same model hostname can be load-balanced across instances. A direct report and a later inference request are therefore not automatically tied to the same CVM. - -## Verify recorded actions - -When this optional evidence is required, verify it as a separate attestation flow: - -1. Generate a fresh client nonce and require it to equal `compose_manager_attestation.nonce`. -2. Serialize `actions` as compact UTF-8 JSON with every object key sorted, then require its SHA-256 hash to equal `actions_hash`. Preserve the array order. -3. Verify the nested `quote` with an Intel DCAP verifier and replay its `event_log` against the verified quote. -4. Require the reported `report_data` to equal the verified quote's report data, and require that value to be `actions_hash || client_nonce`. -5. For an action that names a source `commit`, `file`, and `file_sha256`, obtain that exact file revision from the expected source and require its SHA-256 hash to match. - -These checks establish that the verified Compose Manager quote bound the returned action record to your nonce. They do not add a project or model identity, a successful-operation outcome, or a binding to a later inference request. - -## Use it with independent checks - -When your application requires evidence for an inference response, verify the direct [model attestation](/cloud/verification/direct/model-attestation), [TLS connection binding](/cloud/verification/direct/tls) when required, and [response signature](/cloud/verification/direct/response-signatures) when available. - -When your policy requires software provenance, verify the image digests from the quote-bound configuration using the [direct image-provenance flow](/cloud/verification/direct/image-provenance). An action log or an image reference does not by itself verify image provenance. - - - Treat unavailable or incomplete deployment-control evidence as unavailable evidence for that optional check. Do not substitute it for a model quote, a response signature, or a verified provenance statement. - diff --git a/cloud/verification/direct/index.mdx b/cloud/verification/direct/index.mdx index 87c0239..7f753bd 100644 --- a/cloud/verification/direct/index.mdx +++ b/cloud/verification/direct/index.mdx @@ -20,9 +20,6 @@ Use this guide when your application sends inference requests directly to `https Check that the software images in the attested environment meet your source and build policy. - - Review optional deployment-management evidence separately from verification of an individual request. - ## Start with model attestation diff --git a/cloud/verification/direct/model-attestation.mdx b/cloud/verification/direct/model-attestation.mdx index 54aea0c..51bc3e9 100644 --- a/cloud/verification/direct/model-attestation.mdx +++ b/cloud/verification/direct/model-attestation.mdx @@ -36,7 +36,6 @@ The root response is the model report. Relevant fields include: | `info.tcb_info.app_compose` | Raw measured configuration string. Hash it and match it to the quote's MRCONFIGID. `tcb_info` can itself be JSON text. | | `tls_cert_fingerprint` | The direct endpoint TLS binding, when requested. | | `all_attestations[]` | Model-report entries in this direct response envelope. It can include the root report and is not a fleet inventory. | -| `compose_manager_attestation` | Optional, separate deployment-control evidence. See [deployment control evidence](/cloud/verification/direct/deployment-control-evidence). | ## Verify the report diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 572301e..945ef11 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -43,7 +43,7 @@ For NEAR-operated reports, quote report data is 64 bytes. The final 32 bytes are - Without TLS binding, they are the decoded signing identity, zero-padded on the right to 32 bytes. An ECDSA address occupies 20 bytes; an Ed25519 public key occupies all 32 bytes. - With TLS binding, they are `SHA-256(decoded signing identity || TLS SPKI SHA-256)`. Compare the quote-bound fingerprint with the peer certificate observed on the same TLS connection. -This layout applies to NEAR-operated dstack reports only. Compose Manager evidence and other providers have their own report-data contracts. +This layout applies only to NEAR-operated dstack reports; other providers have their own report-data contracts. ## Verify GPU evidence when present @@ -66,6 +66,6 @@ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that ## Reference code -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, direct TLS, and Compose Manager evidence. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. +The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, and direct TLS. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk). diff --git a/cloud/verification/reference/sdk.mdx b/cloud/verification/reference/sdk.mdx index 0714b24..eb1d43c 100644 --- a/cloud/verification/reference/sdk.mdx +++ b/cloud/verification/reference/sdk.mdx @@ -61,4 +61,4 @@ Runnable TypeScript, Python, and Rust examples follow this flow for non-streamin In TypeScript, import from `verifiable-ai-sdk/node` to bind Gateway evidence to the TLS peer observed for the Gateway attestation request. This does not automatically cover a later inference request. The generic `verifiable-ai-sdk` entry point verifies Gateway evidence without a TLS peer binding. -The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, Compose Manager evidence, image provenance, and E2EE, use the corresponding endpoint guides. +The SDK currently covers the NEAR AI Cloud Gateway flow. For direct model endpoints, image provenance, and E2EE, use the corresponding endpoint guides. diff --git a/docs.json b/docs.json index 53546ea..a5bbca3 100644 --- a/docs.json +++ b/docs.json @@ -77,8 +77,7 @@ "cloud/verification/direct/model-attestation", "cloud/verification/direct/tls", "cloud/verification/direct/response-signatures", - "cloud/verification/direct/image-provenance", - "cloud/verification/direct/deployment-control-evidence" + "cloud/verification/direct/image-provenance" ] }, { From 93cf4cd8da8e17b5b3ec41110e4f39faf3171ab9 Mon Sep 17 00:00:00 2001 From: Coffee Date: Thu, 10 Sep 2026 10:45:36 +0800 Subject: [PATCH 23/23] docs: remove deprecated verifier reference --- cloud/verification/reference/quote-nonce-signer.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cloud/verification/reference/quote-nonce-signer.mdx b/cloud/verification/reference/quote-nonce-signer.mdx index 945ef11..01ccdfc 100644 --- a/cloud/verification/reference/quote-nonce-signer.mdx +++ b/cloud/verification/reference/quote-nonce-signer.mdx @@ -64,8 +64,6 @@ curl --fail-with-body -sS -X POST \ NRAS returns a JSON array whose first entry is `["JWT", ""]`. Decode that JWT's payload and require `x-nvidia-overall-att-result` to be boolean `true`. Reject missing or malformed GPU evidence, a nonce mismatch, or any other verdict. This minimal flow relies on the TLS-authenticated NRAS verdict; apply local JWT/EAT validation separately if your policy requires it. Define in advance whether missing GPU evidence is acceptable for your workload policy. -## Reference code - -The [NEAR AI Cloud Verifier](https://github.com/nearai/nearai-cloud-verifier) contains runnable TypeScript and Python reference flows for gateway and model evidence, response signatures, and direct TLS. It is reference code rather than a general-purpose client or SDK; use the endpoint guides here to choose the evidence and policy your application requires. +## Verification SDK For a language-specific library, see the [Verification SDK](/cloud/verification/reference/sdk).