diff --git a/README.md b/README.md index 0f9ee060..9fe1d45e 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,8 @@ engram/ |-------|-------------| | [docs/architecture.md](docs/architecture.md) | System design, data flows, Arweave integration | | [docs/miner.md](docs/miner.md) | Miner setup, configuration, systemd, Docker | +| [docs/openapi.json](docs/openapi.json) | OpenAPI 3.1 spec for miner HTTP endpoints | +| [docs/miner-openapi.html](docs/miner-openapi.html) | Redoc viewer for the miner HTTP API | | [docs/validator.md](docs/validator.md) | Validator setup and scoring loop | | [docs/sdk.md](docs/sdk.md) | Python SDK full reference | | [docs/cli.md](docs/cli.md) | CLI command reference | diff --git a/docs/miner-openapi.html b/docs/miner-openapi.html new file mode 100644 index 00000000..1c9d3cd5 --- /dev/null +++ b/docs/miner-openapi.html @@ -0,0 +1,12 @@ + + + + + + Engram Miner HTTP API + + + + + + diff --git a/docs/miner.md b/docs/miner.md index 138f24b9..de8d2a52 100644 --- a/docs/miner.md +++ b/docs/miner.md @@ -160,6 +160,23 @@ curl http://localhost:8091/health --- +## HTTP API Reference + +The miner HTTP API is documented as OpenAPI 3.1 in [`openapi.json`](openapi.json). +Browse it locally through the Redoc page at [`miner-openapi.html`](miner-openapi.html). + +Regenerate the spec after changing miner routes: + +```bash +python scripts/generate_openapi.py +python scripts/generate_openapi.py --check +``` + +The OpenAPI generator reads the same route registry used by `neurons/miner.py`, +so CI can catch stale docs when an endpoint is added, removed, or renamed. + +--- + ## systemd Service ```ini diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 00000000..6b9702e1 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,2687 @@ +{ + "components": { + "schemas": { + "AttestNamespaceRequest": { + "properties": { + "namespace": { + "type": "string" + }, + "owner_hotkey": { + "type": "string" + }, + "signature": { + "type": "string" + }, + "timestamp_ms": { + "type": "integer" + } + }, + "required": [ + "namespace", + "owner_hotkey", + "signature", + "timestamp_ms" + ], + "type": "object" + }, + "AttestNamespaceResponse": { + "properties": { + "namespace": { + "type": "string" + }, + "ok": { + "type": "boolean" + }, + "stake_tao": { + "type": "number" + }, + "trust_tier": { + "type": "string" + } + }, + "type": "object" + }, + "AttestationResponse": { + "additionalProperties": true, + "type": "object" + }, + "ChallengeRequest": { + "properties": { + "cid": { + "type": "string" + }, + "expires_at": { + "type": "integer" + }, + "hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "nonce_hex": { + "type": "string" + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "validator_hotkey_hex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "cid", + "nonce_hex", + "expires_at" + ], + "type": "object" + }, + "ChallengeResponse": { + "properties": { + "embedding_hash": { + "type": "string" + }, + "proof": { + "type": "string" + } + }, + "type": "object" + }, + "ChatHistoryResponse": { + "properties": { + "messages": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "ChatHistorySaveRequest": { + "properties": { + "conv_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "messages": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + "user_id": { + "type": "string" + } + }, + "required": [ + "user_id", + "messages" + ], + "type": "object" + }, + "CommitmentResponse": { + "properties": { + "built_at": { + "type": "number" + }, + "count": { + "type": "integer" + }, + "hotkey": { + "type": "string" + }, + "root_hex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ConversationCreateRequest": { + "properties": { + "conv_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "user_id": { + "type": "string" + } + }, + "required": [ + "user_id", + "conv_id" + ], + "type": "object" + }, + "ConversationRenameRequest": { + "properties": { + "title": { + "type": "string" + }, + "user_id": { + "type": "string" + } + }, + "required": [ + "user_id", + "title" + ], + "type": "object" + }, + "ConversationsResponse": { + "properties": { + "conversations": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "DeleteRequest": { + "properties": { + "hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "DeleteResponse": { + "properties": { + "cid": { + "type": "string" + }, + "deleted": { + "type": "boolean" + } + }, + "type": "object" + }, + "ErrorResponse": { + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "GenericObjectResponse": { + "additionalProperties": true, + "type": "object" + }, + "HealthResponse": { + "properties": { + "status": { + "type": "string" + } + }, + "type": "object" + }, + "IngestRequest": { + "properties": { + "hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "additionalProperties": true, + "type": "object" + }, + "model_version": { + "default": "v1", + "type": "string" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "raw_embedding": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "IngestResponse": { + "properties": { + "cid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "KeyShareRetrieveRequest": { + "properties": { + "namespace": { + "type": "string" + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "namespace" + ], + "type": "object" + }, + "KeyShareRetrieveResponse": { + "properties": { + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "share_hex": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "share_index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "threshold": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "total": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "KeyShareStoreRequest": { + "properties": { + "namespace": { + "type": "string" + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "share_hex": { + "type": "string" + }, + "share_index": { + "type": "integer" + }, + "threshold": { + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "required": [ + "namespace", + "share_index", + "share_hex", + "threshold", + "total" + ], + "type": "object" + }, + "KeyShareStoreResponse": { + "properties": { + "stored": { + "type": "boolean" + } + }, + "type": "object" + }, + "ListRequest": { + "properties": { + "filter": { + "additionalProperties": true, + "type": "object" + }, + "limit": { + "default": 50, + "maximum": 200, + "type": "integer" + }, + "namespace": { + "default": "__public__", + "type": "string" + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "offset": { + "default": 0, + "type": "integer" + } + }, + "type": "object" + }, + "ListResponse": { + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "records": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "MetagraphResponse": { + "properties": { + "block": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "neurons": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "NamespaceRequest": { + "properties": { + "action": { + "enum": [ + "create", + "delete", + "rotate", + "list" + ], + "type": "string" + }, + "key": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "new_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "NamespaceResponse": { + "additionalProperties": true, + "type": "object" + }, + "OkResponse": { + "properties": { + "ok": { + "type": "boolean" + } + }, + "type": "object" + }, + "OkSavedResponse": { + "properties": { + "ok": { + "type": "boolean" + }, + "saved": { + "type": "integer" + } + }, + "type": "object" + }, + "PrometheusMetricsResponse": { + "type": "string" + }, + "ProveMemoryRequest": { + "properties": { + "cid": { + "type": "string" + }, + "embedding_hash": { + "type": "string" + } + }, + "required": [ + "cid", + "embedding_hash" + ], + "type": "object" + }, + "ProveMemoryResponse": { + "properties": { + "cid": { + "type": "string" + }, + "proof": { + "additionalProperties": true, + "type": "object" + }, + "root_hex": { + "type": "string" + } + }, + "type": "object" + }, + "QueryRequest": { + "properties": { + "filter": { + "additionalProperties": true, + "type": "object" + }, + "hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_sig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "namespace_timestamp_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "query_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "query_vector": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "top_k": { + "default": 10, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "QueryResponse": { + "properties": { + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "latency_ms": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "results": { + "items": { + "$ref": "#/components/schemas/QueryResult" + }, + "type": "array" + } + }, + "type": "object" + }, + "QueryResult": { + "properties": { + "cid": { + "type": "string" + }, + "metadata": { + "additionalProperties": true, + "type": "object" + }, + "score": { + "type": "number" + } + }, + "required": [ + "cid", + "score" + ], + "type": "object" + }, + "RepairRequest": { + "properties": { + "cid": { + "type": "string" + }, + "hotkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "nonce": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "cid" + ], + "type": "object" + }, + "RepairResponse": { + "properties": { + "cid": { + "type": "string" + }, + "embedding": { + "items": { + "type": "number" + }, + "type": "array" + }, + "metadata": { + "additionalProperties": true, + "type": "object" + } + }, + "type": "object" + }, + "RetrieveResponse": { + "properties": { + "cid": { + "type": "string" + }, + "metadata": { + "additionalProperties": true, + "type": "object" + } + }, + "type": "object" + }, + "StatsResponse": { + "additionalProperties": true, + "type": "object" + } + }, + "securitySchemes": { + "Sr25519SignedBody": { + "description": "Documentation marker for Engram signed-body auth. Clients send hotkey, nonce, and signature in the JSON body; the miner verifies sr25519 signatures with engram.miner.auth.verify_request.", + "in": "header", + "name": "X-Engram-Signature-Fields-In-Body", + "type": "apiKey" + } + } + }, + "info": { + "description": "Machine-readable API contract for Engram miner endpoints. Signed requests use JSON body fields hotkey, nonce, and signature. The signature is sr25519 over '::', where payload_hash is the SHA-256 hash of the canonical JSON body excluding hotkey, nonce, and signature.", + "title": "Engram Miner HTTP API", + "version": "0.1.0" + }, + "openapi": "3.1.0", + "paths": { + "/AttestNamespace": { + "post": { + "operationId": "attest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttestNamespaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttestNamespaceResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Attest a namespace to a Bittensor hotkey.", + "tags": [ + "Namespaces" + ] + } + }, + "/ChallengeSynapse": { + "post": { + "operationId": "challenge", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChallengeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChallengeResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return a storage proof for a challenged CID.", + "tags": [ + "Proofs" + ] + } + }, + "/IngestSynapse": { + "post": { + "operationId": "ingest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Store text or an embedding and return a CID.", + "tags": [ + "Memory" + ] + } + }, + "/KeyShareRetrieve": { + "post": { + "operationId": "key_share_retrieve", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyShareRetrieveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyShareRetrieveResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Retrieve this miner's Shamir key share for a namespace.", + "tags": [ + "Namespaces" + ] + } + }, + "/KeyShareSynapse": { + "post": { + "operationId": "key_share_store", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyShareStoreRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyShareStoreResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Store a Shamir key share for a namespace.", + "tags": [ + "Namespaces" + ] + } + }, + "/QuerySynapse": { + "post": { + "operationId": "query", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Search stored vectors by text or embedding.", + "tags": [ + "Memory" + ] + } + }, + "/RepairSynapse": { + "post": { + "operationId": "repair_retrieve", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepairRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepairResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return a public embedding for validator repair replication.", + "tags": [ + "Repair" + ] + } + }, + "/attestation/{namespace}": { + "get": { + "operationId": "attestation_get", + "parameters": [ + { + "in": "path", + "name": "namespace", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttestationResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return attestation trust information for a namespace.", + "tags": [ + "Namespaces" + ] + } + }, + "/chat-history": { + "post": { + "operationId": "chat_history_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatHistorySaveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkSavedResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Save chat history for a user.", + "tags": [ + "Chat" + ] + } + }, + "/chat-history/{user_id}": { + "get": { + "operationId": "chat_history_get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatHistoryResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Load chat history for a user.", + "tags": [ + "Chat" + ] + } + }, + "/commitment": { + "get": { + "operationId": "commitment", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommitmentResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return the Merkle root for the miner's memory corpus.", + "tags": [ + "Proofs" + ] + } + }, + "/conversations": { + "post": { + "operationId": "conversations_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Create a conversation.", + "tags": [ + "Chat" + ] + } + }, + "/conversations/{conv_id}": { + "delete": { + "operationId": "conversations_delete", + "parameters": [ + { + "in": "path", + "name": "conv_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Delete a conversation.", + "tags": [ + "Chat" + ] + }, + "patch": { + "operationId": "conversations_patch", + "parameters": [ + { + "in": "path", + "name": "conv_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationRenameRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Rename a conversation.", + "tags": [ + "Chat" + ] + } + }, + "/conversations/{user_id}": { + "get": { + "operationId": "conversations_get", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "List conversations for a user.", + "tags": [ + "Chat" + ] + } + }, + "/health": { + "get": { + "operationId": "health", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return a minimal liveness status.", + "tags": [ + "Status" + ] + } + }, + "/list": { + "post": { + "operationId": "list", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "List stored memories with pagination and optional metadata filtering.", + "tags": [ + "Memory" + ] + } + }, + "/metagraph": { + "get": { + "operationId": "metagraph", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetagraphResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return a public metagraph snapshot.", + "tags": [ + "Status" + ] + } + }, + "/metrics": { + "get": { + "operationId": "metrics", + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return Prometheus metrics.", + "tags": [ + "Status" + ], + "x-engram-access": "Restricted to localhost or namespace owner depending on endpoint." + } + }, + "/namespace": { + "post": { + "operationId": "namespace", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamespaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamespaceResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Manage local private namespaces.", + "tags": [ + "Namespaces" + ], + "x-engram-access": "Restricted to localhost or namespace owner depending on endpoint." + } + }, + "/prove-memory": { + "post": { + "operationId": "prove_memory", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProveMemoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProveMemoryResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return a Merkle inclusion proof for a CID.", + "tags": [ + "Proofs" + ] + } + }, + "/retrieve/{cid}": { + "delete": { + "operationId": "delete", + "parameters": [ + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Delete a stored memory by CID.", + "tags": [ + "Memory" + ] + }, + "get": { + "operationId": "retrieve", + "parameters": [ + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Retrieve public metadata for a CID.", + "tags": [ + "Memory" + ] + } + }, + "/stats": { + "get": { + "operationId": "stats", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatsResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return public miner counters for dashboards.", + "tags": [ + "Status" + ] + } + }, + "/wallet-stats": { + "get": { + "operationId": "wallet_stats", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericObjectResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return aggregate local wallet activity stats.", + "tags": [ + "Status" + ], + "x-engram-access": "Restricted to localhost or namespace owner depending on endpoint." + } + }, + "/wallet-stats/{hotkey}": { + "get": { + "operationId": "wallet_stats", + "parameters": [ + { + "in": "path", + "name": "hotkey", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericObjectResponse" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request failed validation." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication failed." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found." + } + }, + "summary": "Return local activity stats for one hotkey.", + "tags": [ + "Status" + ], + "x-engram-access": "Restricted to localhost or namespace owner depending on endpoint." + } + } + }, + "servers": [ + { + "description": "Local miner", + "url": "http://127.0.0.1:8091" + } + ], + "tags": [ + { + "name": "Memory" + }, + { + "name": "Proofs" + }, + { + "name": "Namespaces" + }, + { + "name": "Repair" + }, + { + "name": "Chat" + }, + { + "name": "Status" + } + ], + "x-engram-route-count": 26 +} diff --git a/engram/miner/openapi.py b/engram/miner/openapi.py new file mode 100644 index 00000000..cc8323cf --- /dev/null +++ b/engram/miner/openapi.py @@ -0,0 +1,677 @@ +"""OpenAPI metadata for the Engram miner HTTP API. + +The miner runtime and the generated OpenAPI document both use +``MINER_HTTP_ROUTES`` so route drift is caught by tests and by the generation +script's ``--check`` mode. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class MinerRoute: + method: str + path: str + handler: str + summary: str + tag: str + request_schema: str | None = None + response_schema: str = "ErrorResponse" + private: bool = False + + +MINER_HTTP_ROUTES: tuple[MinerRoute, ...] = ( + MinerRoute( + "post", + "/IngestSynapse", + "handle_ingest", + "Store text or an embedding and return a CID.", + "Memory", + "IngestRequest", + "IngestResponse", + ), + MinerRoute( + "post", + "/QuerySynapse", + "handle_query", + "Search stored vectors by text or embedding.", + "Memory", + "QueryRequest", + "QueryResponse", + ), + MinerRoute( + "post", + "/ChallengeSynapse", + "handle_challenge", + "Return a storage proof for a challenged CID.", + "Proofs", + "ChallengeRequest", + "ChallengeResponse", + ), + MinerRoute( + "post", + "/namespace", + "handle_namespace", + "Manage local private namespaces.", + "Namespaces", + "NamespaceRequest", + "NamespaceResponse", + private=True, + ), + MinerRoute( + "post", + "/AttestNamespace", + "handle_attest", + "Attest a namespace to a Bittensor hotkey.", + "Namespaces", + "AttestNamespaceRequest", + "AttestNamespaceResponse", + ), + MinerRoute( + "get", + "/attestation/{namespace}", + "handle_attestation_get", + "Return attestation trust information for a namespace.", + "Namespaces", + None, + "AttestationResponse", + ), + MinerRoute( + "get", + "/chat-history/{user_id}", + "handle_chat_history_get", + "Load chat history for a user.", + "Chat", + None, + "ChatHistoryResponse", + ), + MinerRoute( + "post", + "/chat-history", + "handle_chat_history_post", + "Save chat history for a user.", + "Chat", + "ChatHistorySaveRequest", + "OkSavedResponse", + ), + MinerRoute( + "get", + "/conversations/{user_id}", + "handle_conversations_get", + "List conversations for a user.", + "Chat", + None, + "ConversationsResponse", + ), + MinerRoute( + "post", + "/conversations", + "handle_conversations_post", + "Create a conversation.", + "Chat", + "ConversationCreateRequest", + "OkResponse", + ), + MinerRoute( + "patch", + "/conversations/{conv_id}", + "handle_conversations_patch", + "Rename a conversation.", + "Chat", + "ConversationRenameRequest", + "OkResponse", + ), + MinerRoute( + "delete", + "/conversations/{conv_id}", + "handle_conversations_delete", + "Delete a conversation.", + "Chat", + None, + "OkResponse", + ), + MinerRoute( + "get", + "/retrieve/{cid}", + "handle_retrieve", + "Retrieve public metadata for a CID.", + "Memory", + None, + "RetrieveResponse", + ), + MinerRoute( + "delete", + "/retrieve/{cid}", + "handle_delete", + "Delete a stored memory by CID.", + "Memory", + "DeleteRequest", + "DeleteResponse", + ), + MinerRoute( + "post", + "/RepairSynapse", + "handle_repair_retrieve", + "Return a public embedding for validator repair replication.", + "Repair", + "RepairRequest", + "RepairResponse", + ), + MinerRoute( + "post", + "/KeyShareSynapse", + "handle_key_share_store", + "Store a Shamir key share for a namespace.", + "Namespaces", + "KeyShareStoreRequest", + "KeyShareStoreResponse", + ), + MinerRoute( + "post", + "/KeyShareRetrieve", + "handle_key_share_retrieve", + "Retrieve this miner's Shamir key share for a namespace.", + "Namespaces", + "KeyShareRetrieveRequest", + "KeyShareRetrieveResponse", + ), + MinerRoute( + "post", + "/list", + "handle_list", + "List stored memories with pagination and optional metadata filtering.", + "Memory", + "ListRequest", + "ListResponse", + ), + MinerRoute( + "get", + "/health", + "handle_health", + "Return a minimal liveness status.", + "Status", + None, + "HealthResponse", + ), + MinerRoute( + "get", + "/stats", + "handle_stats", + "Return public miner counters for dashboards.", + "Status", + None, + "StatsResponse", + ), + MinerRoute( + "get", + "/metagraph", + "handle_metagraph", + "Return a public metagraph snapshot.", + "Status", + None, + "MetagraphResponse", + ), + MinerRoute( + "get", + "/metrics", + "handle_metrics", + "Return Prometheus metrics.", + "Status", + None, + "PrometheusMetricsResponse", + private=True, + ), + MinerRoute( + "get", + "/wallet-stats", + "handle_wallet_stats", + "Return aggregate local wallet activity stats.", + "Status", + None, + "GenericObjectResponse", + private=True, + ), + MinerRoute( + "get", + "/wallet-stats/{hotkey}", + "handle_wallet_stats", + "Return local activity stats for one hotkey.", + "Status", + None, + "GenericObjectResponse", + private=True, + ), + MinerRoute( + "get", + "/commitment", + "handle_commitment", + "Return the Merkle root for the miner's memory corpus.", + "Proofs", + None, + "CommitmentResponse", + ), + MinerRoute( + "post", + "/prove-memory", + "handle_prove_memory", + "Return a Merkle inclusion proof for a CID.", + "Proofs", + "ProveMemoryRequest", + "ProveMemoryResponse", + ), +) + + +def build_openapi_spec() -> dict[str, Any]: + """Return the OpenAPI 3.1 document for the miner HTTP API.""" + paths: dict[str, dict[str, Any]] = {} + for route in MINER_HTTP_ROUTES: + operation: dict[str, Any] = { + "operationId": _operation_id(route), + "summary": route.summary, + "tags": [route.tag], + "responses": _responses(route.response_schema), + } + params = _path_parameters(route.path) + if params: + operation["parameters"] = params + if route.request_schema: + operation["requestBody"] = { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{route.request_schema}"} + } + }, + } + if route.private: + operation["x-engram-access"] = ( + "Restricted to localhost or namespace owner depending on endpoint." + ) + paths.setdefault(route.path, {})[route.method] = operation + + return { + "openapi": "3.1.0", + "info": { + "title": "Engram Miner HTTP API", + "version": "0.1.0", + "description": ( + "Machine-readable API contract for Engram miner endpoints. " + "Signed requests use JSON body fields hotkey, nonce, and signature. " + "The signature is sr25519 over '::', " + "where payload_hash is the SHA-256 hash of the canonical JSON body " + "excluding hotkey, nonce, and signature." + ), + }, + "servers": [{"url": "http://127.0.0.1:8091", "description": "Local miner"}], + "tags": [ + {"name": "Memory"}, + {"name": "Proofs"}, + {"name": "Namespaces"}, + {"name": "Repair"}, + {"name": "Chat"}, + {"name": "Status"}, + ], + "paths": paths, + "components": { + "schemas": _schemas(), + "securitySchemes": { + "Sr25519SignedBody": { + "type": "apiKey", + "in": "header", + "name": "X-Engram-Signature-Fields-In-Body", + "description": ( + "Documentation marker for Engram signed-body auth. " + "Clients send hotkey, nonce, and signature in the JSON body; " + "the miner verifies sr25519 signatures with " + "engram.miner.auth.verify_request." + ), + } + }, + }, + "x-engram-route-count": len(MINER_HTTP_ROUTES), + } + + +def _operation_id(route: MinerRoute) -> str: + return route.handler.removeprefix("handle_") + + +def _path_parameters(path: str) -> list[dict[str, Any]]: + params = [] + for name in ("namespace", "user_id", "conv_id", "cid", "hotkey"): + if "{" + name + "}" in path: + params.append({ + "name": name, + "in": "path", + "required": True, + "schema": {"type": "string"}, + }) + return params + + +def _responses(schema_name: str) -> dict[str, Any]: + content_type = ( + "text/plain" if schema_name == "PrometheusMetricsResponse" else "application/json" + ) + schema: dict[str, Any] + if schema_name == "PrometheusMetricsResponse": + schema = {"type": "string"} + else: + schema = {"$ref": f"#/components/schemas/{schema_name}"} + return { + "200": { + "description": "Success", + "content": {content_type: {"schema": schema}}, + }, + "400": { + "description": "Request failed validation.", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ErrorResponse"} + } + }, + }, + "401": { + "description": "Authentication failed.", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ErrorResponse"} + } + }, + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ErrorResponse"} + } + }, + }, + } + + +def _schemas() -> dict[str, Any]: + string_or_null = {"anyOf": [{"type": "string"}, {"type": "null"}]} + number_or_null = {"anyOf": [{"type": "number"}, {"type": "null"}]} + integer_or_null = {"anyOf": [{"type": "integer"}, {"type": "null"}]} + object_schema = {"type": "object", "additionalProperties": True} + signed_body = { + "hotkey": string_or_null, + "nonce": integer_or_null, + "signature": string_or_null, + } + namespace_sig = { + "namespace_hotkey": string_or_null, + "namespace_sig": string_or_null, + "namespace_timestamp_ms": integer_or_null, + "namespace_key": string_or_null, + } + + return { + "ErrorResponse": { + "type": "object", + "properties": {"error": {"type": "string"}}, + "required": ["error"], + }, + "IngestRequest": { + "type": "object", + "properties": { + **signed_body, + **namespace_sig, + "text": string_or_null, + "raw_embedding": { + "anyOf": [ + {"type": "array", "items": {"type": "number"}}, + {"type": "null"}, + ] + }, + "metadata": object_schema, + "model_version": {"type": "string", "default": "v1"}, + "namespace": string_or_null, + }, + }, + "IngestResponse": { + "type": "object", + "properties": {"cid": string_or_null, "error": string_or_null}, + }, + "QueryRequest": { + "type": "object", + "properties": { + **signed_body, + **namespace_sig, + "query_text": string_or_null, + "query_vector": { + "anyOf": [ + {"type": "array", "items": {"type": "number"}}, + {"type": "null"}, + ] + }, + "top_k": {"type": "integer", "minimum": 1, "maximum": 100, "default": 10}, + "namespace": string_or_null, + "filter": object_schema, + }, + }, + "QueryResult": { + "type": "object", + "properties": { + "cid": {"type": "string"}, + "score": {"type": "number"}, + "metadata": object_schema, + }, + "required": ["cid", "score"], + }, + "QueryResponse": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": {"$ref": "#/components/schemas/QueryResult"}, + }, + "latency_ms": number_or_null, + "error": string_or_null, + }, + }, + "ChallengeRequest": { + "type": "object", + "properties": { + **signed_body, + "cid": {"type": "string"}, + "nonce_hex": {"type": "string"}, + "expires_at": {"type": "integer"}, + "validator_hotkey_hex": string_or_null, + }, + "required": ["cid", "nonce_hex", "expires_at"], + }, + "ChallengeResponse": { + "type": "object", + "properties": { + "embedding_hash": {"type": "string"}, + "proof": {"type": "string"}, + }, + }, + "NamespaceRequest": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["create", "delete", "rotate", "list"], + }, + "namespace": {"type": "string"}, + "key": {"type": "string"}, + "new_key": string_or_null, + }, + "required": ["action"], + }, + "NamespaceResponse": {"type": "object", "additionalProperties": True}, + "AttestNamespaceRequest": { + "type": "object", + "properties": { + "namespace": {"type": "string"}, + "owner_hotkey": {"type": "string"}, + "signature": {"type": "string"}, + "timestamp_ms": {"type": "integer"}, + }, + "required": ["namespace", "owner_hotkey", "signature", "timestamp_ms"], + }, + "AttestNamespaceResponse": { + "type": "object", + "properties": { + "ok": {"type": "boolean"}, + "namespace": {"type": "string"}, + "trust_tier": {"type": "string"}, + "stake_tao": {"type": "number"}, + }, + }, + "AttestationResponse": {"type": "object", "additionalProperties": True}, + "ChatHistoryResponse": { + "type": "object", + "properties": {"messages": {"type": "array", "items": object_schema}}, + }, + "ChatHistorySaveRequest": { + "type": "object", + "properties": { + "user_id": {"type": "string"}, + "conv_id": string_or_null, + "messages": {"type": "array", "items": object_schema}, + }, + "required": ["user_id", "messages"], + }, + "OkSavedResponse": { + "type": "object", + "properties": {"ok": {"type": "boolean"}, "saved": {"type": "integer"}}, + }, + "ConversationsResponse": { + "type": "object", + "properties": {"conversations": {"type": "array", "items": object_schema}}, + }, + "ConversationCreateRequest": { + "type": "object", + "properties": { + "user_id": {"type": "string"}, + "conv_id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["user_id", "conv_id"], + }, + "ConversationRenameRequest": { + "type": "object", + "properties": {"user_id": {"type": "string"}, "title": {"type": "string"}}, + "required": ["user_id", "title"], + }, + "OkResponse": {"type": "object", "properties": {"ok": {"type": "boolean"}}}, + "RetrieveResponse": { + "type": "object", + "properties": {"cid": {"type": "string"}, "metadata": object_schema}, + }, + "DeleteRequest": { + "type": "object", + "properties": {**signed_body, **namespace_sig}, + }, + "DeleteResponse": { + "type": "object", + "properties": {"deleted": {"type": "boolean"}, "cid": {"type": "string"}}, + }, + "RepairRequest": { + "type": "object", + "properties": {**signed_body, "cid": {"type": "string"}}, + "required": ["cid"], + }, + "RepairResponse": { + "type": "object", + "properties": { + "cid": {"type": "string"}, + "embedding": {"type": "array", "items": {"type": "number"}}, + "metadata": object_schema, + }, + }, + "KeyShareStoreRequest": { + "type": "object", + "properties": { + **namespace_sig, + "namespace": {"type": "string"}, + "share_index": {"type": "integer"}, + "share_hex": {"type": "string"}, + "threshold": {"type": "integer"}, + "total": {"type": "integer"}, + }, + "required": ["namespace", "share_index", "share_hex", "threshold", "total"], + }, + "KeyShareStoreResponse": {"type": "object", "properties": {"stored": {"type": "boolean"}}}, + "KeyShareRetrieveRequest": { + "type": "object", + "properties": {**namespace_sig, "namespace": {"type": "string"}}, + "required": ["namespace"], + }, + "KeyShareRetrieveResponse": { + "type": "object", + "properties": { + "share_index": integer_or_null, + "share_hex": string_or_null, + "threshold": integer_or_null, + "total": integer_or_null, + "error": string_or_null, + }, + }, + "ListRequest": { + "type": "object", + "properties": { + **namespace_sig, + "filter": object_schema, + "limit": {"type": "integer", "default": 50, "maximum": 200}, + "offset": {"type": "integer", "default": 0}, + "namespace": {"type": "string", "default": "__public__"}, + }, + }, + "ListResponse": { + "type": "object", + "properties": { + "records": {"type": "array", "items": object_schema}, + "count": {"type": "integer"}, + "offset": {"type": "integer"}, + "limit": {"type": "integer"}, + }, + }, + "HealthResponse": { + "type": "object", + "properties": {"status": {"type": "string"}}, + }, + "StatsResponse": {"type": "object", "additionalProperties": True}, + "MetagraphResponse": { + "type": "object", + "properties": { + "neurons": {"type": "array", "items": object_schema}, + "block": integer_or_null, + }, + }, + "PrometheusMetricsResponse": {"type": "string"}, + "GenericObjectResponse": object_schema, + "CommitmentResponse": { + "type": "object", + "properties": { + "root_hex": string_or_null, + "count": {"type": "integer"}, + "built_at": {"type": "number"}, + "hotkey": {"type": "string"}, + }, + }, + "ProveMemoryRequest": { + "type": "object", + "properties": { + "cid": {"type": "string"}, + "embedding_hash": {"type": "string"}, + }, + "required": ["cid", "embedding_hash"], + }, + "ProveMemoryResponse": { + "type": "object", + "properties": { + "root_hex": {"type": "string"}, + "cid": {"type": "string"}, + "proof": object_schema, + }, + }, + } diff --git a/neurons/miner.py b/neurons/miner.py index a60c660d..7393dcc2 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -45,6 +45,7 @@ from engram.miner.store import build_store from engram.miner.http_synapses import ingest_synapse_from_body, query_synapse_from_body from engram.miner.key_share_store import KeyShareStore +from engram.miner.openapi import MINER_HTTP_ROUTES from engram.protocol import IngestSynapse, QuerySynapse from engram.storage.dht import DHTRouter, Peer from engram.storage.replication import ReplicationManager @@ -1276,32 +1277,35 @@ async def handle_metrics(req: web.Request) -> web.Response: # Prevents OOM from oversized request bodies. _MAX_BODY = int(os.getenv("MINER_MAX_BODY_BYTES", str(10 * 1024 * 1024))) app = web.Application(client_max_size=_MAX_BODY) - app.router.add_post("/IngestSynapse", handle_ingest) - app.router.add_post("/QuerySynapse", handle_query) - app.router.add_post("/ChallengeSynapse", handle_challenge) - app.router.add_post("/namespace", handle_namespace) - app.router.add_post("/AttestNamespace", handle_attest) - app.router.add_get("/attestation/{namespace}", handle_attestation_get) - app.router.add_get("/chat-history/{user_id}", handle_chat_history_get) - app.router.add_post("/chat-history", handle_chat_history_post) - app.router.add_get("/conversations/{user_id}", handle_conversations_get) - app.router.add_post("/conversations", handle_conversations_post) - app.router.add_patch("/conversations/{conv_id}", handle_conversations_patch) - app.router.add_delete("/conversations/{conv_id}", handle_conversations_delete) - app.router.add_get("/retrieve/{cid}", handle_retrieve) - app.router.add_delete("/retrieve/{cid}", handle_delete) - app.router.add_post("/RepairSynapse", handle_repair_retrieve) - app.router.add_post("/KeyShareSynapse", handle_key_share_store) - app.router.add_post("/KeyShareRetrieve", handle_key_share_retrieve) - app.router.add_post("/list", handle_list) - app.router.add_get("/health", handle_health) - app.router.add_get("/stats", handle_stats) - app.router.add_get("/metagraph", handle_metagraph) - app.router.add_get("/metrics", handle_metrics) - app.router.add_get("/wallet-stats", handle_wallet_stats) - app.router.add_get("/wallet-stats/{hotkey}", handle_wallet_stats) - app.router.add_get("/commitment", handle_commitment) - app.router.add_post("/prove-memory", handle_prove_memory) + handlers = { + "handle_ingest": handle_ingest, + "handle_query": handle_query, + "handle_challenge": handle_challenge, + "handle_namespace": handle_namespace, + "handle_attest": handle_attest, + "handle_attestation_get": handle_attestation_get, + "handle_chat_history_get": handle_chat_history_get, + "handle_chat_history_post": handle_chat_history_post, + "handle_conversations_get": handle_conversations_get, + "handle_conversations_post": handle_conversations_post, + "handle_conversations_patch": handle_conversations_patch, + "handle_conversations_delete": handle_conversations_delete, + "handle_retrieve": handle_retrieve, + "handle_delete": handle_delete, + "handle_repair_retrieve": handle_repair_retrieve, + "handle_key_share_store": handle_key_share_store, + "handle_key_share_retrieve": handle_key_share_retrieve, + "handle_list": handle_list, + "handle_health": handle_health, + "handle_stats": handle_stats, + "handle_metagraph": handle_metagraph, + "handle_metrics": handle_metrics, + "handle_wallet_stats": handle_wallet_stats, + "handle_commitment": handle_commitment, + "handle_prove_memory": handle_prove_memory, + } + for route in MINER_HTTP_ROUTES: + getattr(app.router, f"add_{route.method}")(route.path, handlers[route.handler]) runner = web.AppRunner(app, keepalive_timeout=15) await runner.setup() diff --git a/scripts/generate_openapi.py b/scripts/generate_openapi.py new file mode 100644 index 00000000..ff31bea1 --- /dev/null +++ b/scripts/generate_openapi.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Generate and verify the Engram miner OpenAPI document.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from engram.miner.openapi import build_openapi_spec + + +OPENAPI_PATH = ROOT / "docs" / "openapi.json" +REDOC_PATH = ROOT / "docs" / "miner-openapi.html" + + +def _render_json() -> str: + return json.dumps(build_openapi_spec(), indent=2, sort_keys=True) + "\n" + + +def _render_redoc() -> str: + return """ + + + + + Engram Miner HTTP API + + + + + + +""" + + +def write_files() -> None: + OPENAPI_PATH.parent.mkdir(parents=True, exist_ok=True) + OPENAPI_PATH.write_text(_render_json(), encoding="utf-8") + REDOC_PATH.write_text(_render_redoc(), encoding="utf-8") + + +def check_files() -> list[str]: + expected = { + OPENAPI_PATH: _render_json(), + REDOC_PATH: _render_redoc(), + } + stale = [] + for path, content in expected.items(): + if not path.exists() or path.read_text(encoding="utf-8") != content: + stale.append(str(path.relative_to(ROOT))) + return stale + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="fail if generated files are stale") + args = parser.parse_args() + + if args.check: + stale = check_files() + if stale: + print("OpenAPI files are stale:") + for path in stale: + print(f"- {path}") + print("Run: python scripts/generate_openapi.py") + return 1 + print("OpenAPI files are up to date") + return 0 + + write_files() + print(f"Wrote {OPENAPI_PATH.relative_to(ROOT)}") + print(f"Wrote {REDOC_PATH.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 00000000..a7c737ad --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,52 @@ +"""Tests for generated miner OpenAPI metadata.""" + +import json +import subprocess +import sys +from pathlib import Path + +from engram.miner.openapi import MINER_HTTP_ROUTES, build_openapi_spec + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_route_registry_has_unique_method_path_pairs(): + pairs = [(route.method, route.path) for route in MINER_HTTP_ROUTES] + assert len(pairs) == len(set(pairs)) + + +def test_openapi_paths_match_route_registry(): + spec = build_openapi_spec() + spec_pairs = { + (method, path) + for path, methods in spec["paths"].items() + for method in methods + } + route_pairs = {(route.method, route.path) for route in MINER_HTTP_ROUTES} + assert spec_pairs == route_pairs + assert spec["x-engram-route-count"] == len(MINER_HTTP_ROUTES) + + +def test_openapi_documents_signed_body_auth(): + spec = build_openapi_spec() + auth = spec["components"]["securitySchemes"]["Sr25519SignedBody"] + assert "sr25519" in auth["description"] + ingest_fields = spec["components"]["schemas"]["IngestRequest"]["properties"] + assert {"hotkey", "nonce", "signature"}.issubset(ingest_fields) + + +def test_generated_openapi_file_is_current(): + expected = json.dumps(build_openapi_spec(), indent=2, sort_keys=True) + "\n" + assert (ROOT / "docs" / "openapi.json").read_text(encoding="utf-8") == expected + + +def test_generate_openapi_check_mode_passes(): + result = subprocess.run( + [sys.executable, "scripts/generate_openapi.py", "--check"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr