openvibers/openvibe.contracts Commits
Add node principals, node capabilities and per-kind resource offers (#6)
Changes
@@@ -16,6 +16,31 @@ listed by `manifests/services/billing.json`. `POST /api/v1/usage` takes one `pla`principal` and the `reading` itself; fixtures `recorded` (valid) and `reading-without-idempotency-key` (invalid).Network grants for the usage producers follow this release. Additive.++**Node principals, node capabilities and per-kind resource offers** (plan T1 lane A, the contracts brief of++OpenVibe.Network `docs/t2-cells-and-node-principal.md` §9.1):++- `lib/ids.js`: `PREFIX.node = 'nod'`; `principalSub({ type: 'node', id })` gives `node:nod_<ULID>`. A node is a++ principal only: `node` is not a `SUBJECT_TYPES` entry nor an `identity.subject-ref@1` type.++- `identity.service-token-claims`: `sub` also accepts `node:nod_<ULID>` and `actor_type` gains `node`, so++ `verifyServiceToken` accepts a node token and still rejects a malformed node id. Fixtures `node-token` (valid) and++ `node-bad-id` (invalid).++- New capabilities `network.node.manage` (a service mints pairing codes and reads or revokes the node principals it++ paired; `nodes:pair`, `nodes:read`, `nodes:revoke`), `network.node.self.manage` (a paired machine presents its own++ capabilities and rotates its own credential; `nodes:self`) and `network.resource.report` (a source reports its++ complete set of `platform.resource-offer@1`; `resources:write`), all active, internal and listed by++ `manifests/services/network.json`. Their bodies are Network-local, so they name no schemas and are recorded in++ `compatibility/capability-schema-gaps.json`. `network.node.report` is also implemented by++ `POST /internal/registry/instances/report`.++- `platform.resource-offer`: `kind` gains `storage`, `delivery`, `runtime`, `agent` and `harness`; the new++ optional `detail` is required for those kinds and validated by the kind's own `platform.<kind>-offer@1`, and is++ forbidden for `node` and `provider`. Fixtures `storage`, `delivery`, `runtime`, `agent`, `harness` (valid, beside++ `owned-node` and `provider`), `storage-without-detail` and `node-with-detail` (invalid).++- Optional fields: `platform.service-instance` `route_weight` (integer 0-1000); `platform.node-capabilities`++ `capabilities` (resource-offer capability form), `agent_version` (at most 40 characters) and `updated_at`++ (date-time); `network.node` `cell` (`^[a-z]{2,8}-[0-9]{1,3}$`). Fixtures `route-weight`, `agent-report` and++ `with-cell` (valid), `route-weight-over-1000`, `bad-capability` and `bad-cell` (invalid).++++Additive.++## 0.84.0 — 2026-10-02Add nine public `platform.*@1` schemas: usage and telemetry samples; node capabilities and@@@ -1,4 +1,26 @@{"_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",-- "gaps": []++ "gaps": [++ {++ "id": "network.node.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.node.self.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.resource.report",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ }++ ]}@@@ -2,7 +2,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json","title": "ServiceTokenClaims",-- "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",++ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.","type": "object","required": ["iss",@@@ -23,13 +23,14 @@},"sub": {"type": "string",-- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"++ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26})$"},"actor_type": {"enum": ["service","app",-- "mod"++ "mod",++ "node"]},"aud": {@@@ -36,6 +36,7 @@"checked_at": { "type": "string", "format": "date-time" }}},-- "updated_at": { "type": "string", "format": "date-time" }++ "updated_at": { "type": "string", "format": "date-time" },++ "cell": { "type": "string", "pattern": "^[a-z]{2,8}-[0-9]{1,3}$", "description": "Cell the node belongs to, e.g. wnam-1." }}}@@@ -136,6 +136,23 @@"required": ["per_hour_usd"]++ },++ "capabilities": {++ "type": "array",++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"++ },++ "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."++ },++ "agent_version": {++ "type": "string",++ "maxLength": 40,++ "description": "Version of the node agent that reported."++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"}},"examples": [@@@ -23,7 +23,12 @@"type": "string","enum": ["node",-- "provider"++ "provider",++ "storage",++ "delivery",++ "runtime",++ "agent",++ "harness"]},"node_id": {@@@ -255,6 +260,144 @@"updated_at": {"type": "string","format": "date-time"++ },++ "detail": {++ "type": "object",++ "description": "The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "kind": {++ "const": "storage"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "storage-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "delivery"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "delivery-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "runtime"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "runtime-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "agent"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "agent-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "harness"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "harness-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "enum": [++ "node",++ "provider"++ ]++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "not": {++ "required": [++ "detail"++ ]++ }++ }}-- }++ ]}@@@ -57,6 +57,12 @@"started_at": {"type": "string","format": "date-time"++ },++ "route_weight": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1000,++ "description": "Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight."}},"examples": [nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "iss": "https://openvibe.network",++ "sub": "node:nod_42",++ "actor_type": "node",++ "aud": [++ "openvibe.bot"++ ],++ "cap": [++ "network.node.self.manage"++ ],++ "iat": 1790000000,++ "exp": 1790000300,++ "jti": "tok_node0001"++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "iss": "https://openvibe.network",++ "sub": "node:nod_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "actor_type": "node",++ "aud": [++ "openvibe.bot"++ ],++ "cap": [++ "network.node.self.manage"++ ],++ "iat": 1790000000,++ "exp": 1790000300,++ "jti": "tok_node0001"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "oregon-1",++ "name": "Oregon 1",++ "roles": [++ "web",++ "app",++ "data",++ "ingest"++ ],++ "location": {++ "region": "us-west",++ "country": "US",++ "city": "Hillsboro",++ "lat": 45.52,++ "lon": -122.99++ },++ "provider": "ovh",++ "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",++ "health": {++ "status": "up",++ "checked_at": "2026-09-28T12:00:00Z"++ },++ "updated_at": "2026-09-28T12:00:00Z",++ "cell": "WNAM_1"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "oregon-1",++ "name": "Oregon 1",++ "roles": [++ "web",++ "app",++ "data",++ "ingest"++ ],++ "location": {++ "region": "us-west",++ "country": "US",++ "city": "Hillsboro",++ "lat": 45.52,++ "lon": -122.99++ },++ "provider": "ovh",++ "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",++ "health": {++ "status": "up",++ "checked_at": "2026-09-28T12:00:00Z"++ },++ "updated_at": "2026-09-28T12:00:00Z",++ "cell": "wnam-1"++}nnew file mode 100644@@@ -0,0 +1,37 @@++{++ "node_id": "node-1",++ "cpu": {++ "cores": 8,++ "threads": 16++ },++ "arch": "x86_64",++ "memory_mb": 16384,++ "gpu": {++ "model": "L4",++ "count": 1,++ "vram_mb": 24576++ },++ "storage": {++ "capacity_gb": 500,++ "available_gb": 300,++ "kind": "ssd"++ },++ "network": {++ "ingress_mbps": 1000,++ "egress_mbps": 1000++ },++ "regions": [++ "us-west"++ ],++ "tags": [++ "gpu",++ "edge"++ ],++ "costs": {++ "per_hour_usd": 0.2,++ "egress_per_gb_usd": 0.01++ },++ "capabilities": [++ "ffmpeg"++ ]++}nnew file mode 100644@@@ -0,0 +1,40 @@++{++ "node_id": "node-1",++ "cpu": {++ "cores": 8,++ "threads": 16++ },++ "arch": "x86_64",++ "memory_mb": 16384,++ "gpu": {++ "model": "L4",++ "count": 1,++ "vram_mb": 24576++ },++ "storage": {++ "capacity_gb": 500,++ "available_gb": 300,++ "kind": "ssd"++ },++ "network": {++ "ingress_mbps": 1000,++ "egress_mbps": 1000++ },++ "regions": [++ "us-west"++ ],++ "tags": [++ "gpu",++ "edge"++ ],++ "costs": {++ "per_hour_usd": 0.2,++ "egress_per_gb_usd": 0.01++ },++ "capabilities": [++ "node:http",++ "worker:ffmpeg"++ ],++ "agent_version": "0.9.0",++ "updated_at": "2026-10-02T00:00:00Z"++}nnew file mode 100644@@@ -0,0 +1,66 @@++{++ "offer_id": "off_oregon_1",++ "kind": "node",++ "node_id": "ovh-oregon-1",++ "provider": "ovh",++ "region": "us-west",++ "cell": "wnam-1",++ "trust": "first-party",++ "capabilities": [++ "node:http",++ "worker:browser",++ "worker:ffmpeg",++ "events:durable"++ ],++ "capacity": {++ "cpu": {++ "utilization": 0.31,++ "available_cores": 5.5++ },++ "memory": {++ "available_mb": 18300++ },++ "workers": {++ "browser": 6,++ "ffmpeg": 3++ },++ "queues": {++ "media-jobs": {++ "depth": 2,++ "oldest_ms": 40++ }++ }++ },++ "latency_ms": {++ "publish_p95": 3.1++ },++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "prepaid",++ "marginal_usd_per_unit": 0,++ "unit": "job"++ },++ "updated_at": "2026-09-28T23:00:00Z",++ "detail": {++ "id": "storage-1",++ "class": "warm",++ "capacity_gb": 1000,++ "price_per_gb_month_usd": 0.02,++ "price_per_operation_usd": 1e-05,++ "region": "us-west",++ "node": "node-1",++ "durability": {++ "replicas": 3,++ "target_nines": 9++ },++ "lifecycle_rules": [++ {++ "after_days": 90,++ "action": "transition",++ "target_class": "cold"++ }++ ]++ }++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "offer_id": "off_storage_1",++ "kind": "storage",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "object:r2"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z"++}nnew file mode 100644@@@ -0,0 +1,34 @@++{++ "offer_id": "off_agent_1",++ "kind": "agent",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "agent:model-1"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z",++ "detail": {++ "id": "agent-1",++ "harness": "harness-1",++ "provider": "example-ai",++ "model": "model-1",++ "context_limits": {++ "input_tokens": 128000,++ "output_tokens": 8192++ },++ "concurrency": 4,++ "price_per_1k_tokens": {++ "fresh_usd": 0.003,++ "cached_usd": 0.001,++ "output_usd": 0.01++ },++ "availability_windows": []++ }++}nnew file mode 100644@@@ -0,0 +1,36 @@++{++ "offer_id": "off_delivery_1",++ "kind": "delivery",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "delivery:hls"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z",++ "detail": {++ "id": "delivery-1",++ "transports": [++ "http",++ "hls"++ ],++ "regions": [++ "us-west"++ ],++ "edge": true,++ "node": "node-1",++ "price_per_gb_usd": 0.02,++ "price_per_request_usd": 1e-05,++ "cache_rules": {++ "enabled": true,++ "ttl_seconds": 300,++ "max_object_mb": 100++ }++ }++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "offer_id": "off_harness_1",++ "kind": "harness",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "harness:cli"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z",++ "detail": {++ "id": "harness-1",++ "name": "Example Agent",++ "provider": "example-ai",++ "capabilities": {++ "host_access": true,++ "mcp": true,++ "long_autonomy": true,++ "resume": true++ },++ "address": {++ "kind": "cli",++ "target": "example-agent"++ },++ "price": {++ "amount_usd": 0.1,++ "unit": "hour"++ },++ "limits": {++ "max_duration_seconds": 7200,++ "max_concurrent_runs": 4,++ "max_context_tokens": 128000++ }++ }++}nnew file mode 100644@@@ -0,0 +1,36 @@++{++ "offer_id": "off_runtime_1",++ "kind": "runtime",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "runtime:container"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z",++ "detail": {++ "id": "runtime-1",++ "kind": "container",++ "region": "us-west",++ "node": "node-1",++ "limits": {++ "cpu_cores": 2,++ "memory_mb": 4096,++ "duration_seconds": 3600++ },++ "price": {++ "amount_usd": 0.05,++ "unit": "hour"++ },++ "availability": "available",++ "constraints": [++ "first-party"++ ]++ }++}nnew file mode 100644@@@ -0,0 +1,37 @@++{++ "offer_id": "off_storage_1",++ "kind": "storage",++ "provider": "example",++ "region": "us-west",++ "trust": "first-party",++ "capabilities": [++ "object:r2"++ ],++ "health": {++ "status": "up"++ },++ "pricing": {++ "model": "per-operation"++ },++ "updated_at": "2026-10-02T00:00:00Z",++ "detail": {++ "id": "storage-1",++ "class": "warm",++ "capacity_gb": 1000,++ "price_per_gb_month_usd": 0.02,++ "price_per_operation_usd": 1e-05,++ "region": "us-west",++ "node": "node-1",++ "durability": {++ "replicas": 3,++ "target_nines": 9++ },++ "lifecycle_rules": [++ {++ "after_days": 90,++ "action": "transition",++ "target_class": "cold"++ }++ ]++ }++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "id": "inst-1",++ "service": "media",++ "version": "1.4.0",++ "cell": "cell-west",++ "node": "node-1",++ "region": "us-west",++ "endpoints": [++ "https://media.internal.example"++ ],++ "state": "ready",++ "started_at": "2026-09-30T10:00:00Z",++ "route_weight": 1001++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "id": "inst-1",++ "service": "media",++ "version": "1.4.0",++ "cell": "cell-west",++ "node": "node-1",++ "region": "us-west",++ "endpoints": [++ "https://media.internal.example"++ ],++ "state": "ready",++ "started_at": "2026-09-30T10:00:00Z",++ "route_weight": 250++}@@@ -187,7 +187,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json","title": "ServiceTokenClaims",-- "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",++ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.","type": "object","required": ["iss",@@@ -208,13 +208,14 @@},"sub": {"type": "string",-- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"++ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26})$"},"actor_type": {"enum": ["service","app",-- "mod"++ "mod",++ "node"]},"aud": {@@@ -57486,6 +57487,11 @@"updated_at": {"type": "string","format": "date-time"++ },++ "cell": {++ "type": "string",++ "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",++ "description": "Cell the node belongs to, e.g. wnam-1."}}}@@@ -57749,7 +57755,12 @@"type": "string","enum": ["node",-- "provider"++ "provider",++ "storage",++ "delivery",++ "runtime",++ "agent",++ "harness"]},"node_id": {@@@ -57981,8 +57992,146 @@"updated_at": {"type": "string","format": "date-time"++ },++ "detail": {++ "type": "object",++ "description": "The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider."}-- }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "kind": {++ "const": "storage"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "storage-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "delivery"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "delivery-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "runtime"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "runtime-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "agent"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "agent-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "harness"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "detail"++ ],++ "properties": {++ "detail": {++ "$ref": "harness-offer.v1.json"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "enum": [++ "node",++ "provider"++ ]++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "not": {++ "required": [++ "detail"++ ]++ }++ }++ }++ ]}},"platform.workload-requirements": {@@@ -59144,6 +59293,23 @@"required": ["per_hour_usd"]++ },++ "capabilities": {++ "type": "array",++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"++ },++ "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."++ },++ "agent_version": {++ "type": "string",++ "maxLength": 40,++ "description": "Version of the node agent that reported."++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"}},"examples": [@@@ -59247,6 +59413,12 @@"started_at": {"type": "string","format": "date-time"++ },++ "route_weight": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1000,++ "description": "Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight."}},"examples": [@@@ -96,8 +96,8 @@"service": "network","name": "OpenVibe.Network","origin": "https://openvibe.network",-- "operations": 29,-- "capabilities": 22++ "operations": 36,++ "capabilities": 25},{"service": "news",@@@ -136,6 +136,84 @@}}},++ "/api/v1/node/self/capabilities": {++ "put": {++ "operationId": "put_api_v1_node_self_capabilities",++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",++ "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's.",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.self.manage"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.self.manage"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/api/v1/node/self/credential": {++ "post": {++ "operationId": "post_api_v1_node_self_credential",++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",++ "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's.",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.self.manage"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.self.manage"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/api/v1/staff/moderators": {"get": {"operationId": "get_api_v1_staff_moderators",@@@ -1517,6 +1595,143 @@}}},++ "/internal/node-pairings": {++ "post": {++ "operationId": "post_internal_node_pairings",++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",++ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.manage"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.manage"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/node-principals/{id}": {++ "get": {++ "operationId": "get_internal_node_principals_id",++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",++ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.manage"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.manage"++ ]++ }++ ],++ "parameters": [++ {++ "name": "id",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/node-principals/{id}/revoke": {++ "post": {++ "operationId": "post_internal_node_principals_id_revoke",++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",++ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.manage"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.manage"++ ]++ }++ ],++ "parameters": [++ {++ "name": "id",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/internal/nodes/report": {"post": {"operationId": "post_internal_nodes_report",@@@ -1685,6 +1900,101 @@}}},++ "/internal/registry/instances/report": {++ "post": {++ "operationId": "post_internal_registry_instances_report",++ "summary": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",++ "description": "**network.node.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",++ "tags": [++ "network.node"++ ],++ "x-openvibe-capabilities": [++ "network.node.report"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.node.report"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/network.node-report-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/network.node-list-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/resources/report": {++ "post": {++ "operationId": "post_internal_resources_report",++ "summary": "Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",++ "description": "**network.resource.report** (internal): Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",++ "tags": [++ "network.resource"++ ],++ "x-openvibe-capabilities": [++ "network.resource.report"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "admin",++ "security": [++ {++ "openvibeToken": [++ "network.resource.report"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success"++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/internal/url-registry/resolved": {"get": {"operationId": "get_internal_url_registry_resolved",@@@ -3182,6 +3492,11 @@"updated_at": {"type": "string","format": "date-time"++ },++ "cell": {++ "type": "string",++ "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",++ "description": "Cell the node belongs to, e.g. wnam-1."}}},@@@ -53,7 +53,7 @@ export interface LegacyIdentityMapEntry {/** [email protected] (owner: network) *//**-- * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.++ * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.*/export type ServiceTokenClaims = {[k: string]: unknown | undefined;@@@ -63,7 +63,7 @@ export type ServiceTokenClaims = {*/iss: string;sub: string;-- actor_type: "service" | "app" | "mod";++ actor_type: "service" | "app" | "mod" | "node";/*** @minItems 1*/@@@ -50616,6 +50616,10 @@ export interface NetworkNode {checked_at: string;};updated_at: string;++ /**++ * Cell the node belongs to, e.g. wnam-1.++ */++ cell?: string;}/** [email protected] (owner: network) */@@@ -50712,9 +50716,11 @@ export interface NetworkCoinsReadResult {/*** What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.*/--export interface ResourceOffer {++export type ResourceOffer = {++ [k: string]: unknown | undefined;++} & {offer_id: string;-- kind: "node" | "provider";++ kind: "node" | "provider" | "storage" | "delivery" | "runtime" | "agent" | "harness";/*** network.node@1 id, for kind node*/@@@ -50798,7 +50804,11 @@ export interface ResourceOffer {rate_card?: string;};updated_at: string;--}++ /**++ * The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider.++ */++ detail?: {};++};/** [email protected] (owner: network) *//**@@@ -51195,6 +51205,15 @@ export interface NodeCapabilities {*/egress_per_gb_usd?: number;};++ /**++ * What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg).++ */++ capabilities?: string[];++ /**++ * Version of the node agent that reported.++ */++ agent_version?: string;++ updated_at?: string;}/** [email protected] (owner: network) */@@@ -51214,6 +51233,10 @@ export interface ServiceInstance {endpoints: [string, ...string[]];state: "starting" | "ready" | "degraded" | "draining" | "stopped";started_at: string;++ /**++ * Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight.++ */++ route_weight?: number;}/** [email protected] (owner: network) */@@@ -6,7 +6,7 @@const crypto = require('crypto');const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';--const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf' };++const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf' };const SLUG_TYPES = new Set(['service', 'system']);const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;@@@ -50,11 +50,11 @@ function formatSubject(ref) {return `${ref.type}:${ref.id}`;}--/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID>. */++/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID> | node:nod_<ULID>. */function principalSub(ref) {if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;-- if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;-- throw new TypeError('principals are service, app, mod or agent subjects');++ if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent' || ref.type === 'node') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;++ throw new TypeError('principals are service, app, mod, agent or node subjects');}/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "network.node.manage",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",++ "permissions": [++ "nodes:pair",++ "nodes:read",++ "nodes:revoke"++ ],++ "resourceConstraints": [++ "owner"++ ],++ "quotaClass": "admin",++ "events": [],++ "implementedBy": [++ "POST /internal/node-pairings",++ "GET /internal/node-principals/:id",++ "POST /internal/node-principals/:id/revoke"++ ]++}@@@ -14,7 +14,8 @@"quotaClass": "admin","events": [],"implementedBy": [-- "POST /internal/nodes/report"++ "POST /internal/nodes/report",++ "POST /internal/registry/instances/report"],"inputSchema": "network.node-report-request@1","outputSchema": "network.node-list-result@1"nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "id": "network.node.self.manage",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",++ "permissions": [++ "nodes:self"++ ],++ "resourceConstraints": [++ "owner"++ ],++ "quotaClass": "admin",++ "events": [],++ "implementedBy": [++ "PUT /api/v1/node/self/capabilities",++ "POST /api/v1/node/self/credential"++ ]++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "network.resource.report",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",++ "permissions": [++ "resources:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "admin",++ "events": [],++ "implementedBy": [++ "POST /internal/resources/report"++ ]++}@@@ -48,12 +48,15 @@"network.integration.github.read","network.modules.read","network.modules.write",++ "network.node.manage","network.node.report",++ "network.node.self.manage","network.notifications.push","network.operator.alert","network.project.manage","network.project.read","network.registry.read",++ "network.resource.report","network.staff.read","network.status.incident"],@@@ -500,6 +500,10 @@ for (const kind of ['user', 'guest', 'app', 'mod']) {ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');++const nodeId = ids.newId('node');++ok(nodeId.startsWith('nod_') && ids.principalSub({ type: 'node', id: nodeId }) === `node:${nodeId}`, 'node principal sub is node:nod_<ULID>');++assert.throws(() => ids.principalSub({ type: 'node', id: 'nod_42' }), /principals/);++ok(!ids.SUBJECT_TYPES.includes('node') && ids.parseSubject(`node:${nodeId}`) === null, 'a node is a principal, never a subject');assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');@@@ -563,6 +567,9 @@ ok(V(sandboxTok, { acceptSandbox: true }).ok, 'sandbox app token accepted when tok(V(serviceAuth.signServiceToken({ ...appClaims, env: 'production' }, kp.privateKey)).ok, 'production app token accepted');ok(V(serviceAuth.signServiceToken((({ project_id, ...rest }) => rest)(appClaims), kp.privateKey)).code === 'token.invalid_claims', 'an app token needs project_id');ok(contracts.ids.newId('project').startsWith('prj_'), 'project ids use the prj prefix');++const nodeClaims = { ...claims, sub: 'node:nod_01J0000000000000000000000Z', actor_type: 'node' };++ok(V(serviceAuth.signServiceToken(nodeClaims, kp.privateKey)).ok, 'node token accepted');++ok(V(serviceAuth.signServiceToken({ ...nodeClaims, sub: 'node:nod_42' }, kp.privateKey)).code === 'token.invalid_claims', 'a malformed node id is rejected');const run = (guard, headers) => new Promise((resolve) => {const req = { headers, body: {} };
@@ -16,6 +16,31 @@ listed by `manifests/services/billing.json`. `POST /api/v1/usage` takes one `pla
@@ -16,6 +16,31 @@ listed by `manifests/services/billing.json`. `POST /api/v1/usage` takes one `pla
`principal` and the `reading` itself; fixtures `recorded` (valid) and `reading-without-idempotency-key` (invalid).
`principal` and the `reading` itself; fixtures `recorded` (valid) and `reading-without-idempotency-key` (invalid).
Network grants for the usage producers follow this release. Additive.
Network grants for the usage producers follow this release. Additive.
+**Node principals, node capabilities and per-kind resource offers** (plan T1 lane A, the contracts brief of
+OpenVibe.Network `docs/t2-cells-and-node-principal.md` §9.1):
+- `lib/ids.js`: `PREFIX.node = 'nod'`; `principalSub({ type: 'node', id })` gives `node:nod_<ULID>`. A node is a
+ principal only: `node` is not a `SUBJECT_TYPES` entry nor an `identity.subject-ref@1` type.
+- `identity.service-token-claims`: `sub` also accepts `node:nod_<ULID>` and `actor_type` gains `node`, so
+ `verifyServiceToken` accepts a node token and still rejects a malformed node id. Fixtures `node-token` (valid) and
+ `node-bad-id` (invalid).
+- New capabilities `network.node.manage` (a service mints pairing codes and reads or revokes the node principals it
+ paired; `nodes:pair`, `nodes:read`, `nodes:revoke`), `network.node.self.manage` (a paired machine presents its own
+ capabilities and rotates its own credential; `nodes:self`) and `network.resource.report` (a source reports its
+ complete set of `platform.resource-offer@1`; `resources:write`), all active, internal and listed by
+ `manifests/services/network.json`. Their bodies are Network-local, so they name no schemas and are recorded in
+ `compatibility/capability-schema-gaps.json`. `network.node.report` is also implemented by
+ `POST /internal/registry/instances/report`.
+- `platform.resource-offer`: `kind` gains `storage`, `delivery`, `runtime`, `agent` and `harness`; the new
+ optional `detail` is required for those kinds and validated by the kind's own `platform.<kind>-offer@1`, and is
+ forbidden for `node` and `provider`. Fixtures `storage`, `delivery`, `runtime`, `agent`, `harness` (valid, beside
+ `owned-node` and `provider`), `storage-without-detail` and `node-with-detail` (invalid).
+- Optional fields: `platform.service-instance` `route_weight` (integer 0-1000); `platform.node-capabilities`
+ `capabilities` (resource-offer capability form), `agent_version` (at most 40 characters) and `updated_at`
+ (date-time); `network.node` `cell` (`^[a-z]{2,8}-[0-9]{1,3}$`). Fixtures `route-weight`, `agent-report` and
+ `with-cell` (valid), `route-weight-over-1000`, `bad-capability` and `bad-cell` (invalid).
+
+Additive.
+
## 0.84.0 — 2026-10-02
## 0.84.0 — 2026-10-02
Add nine public `platform.*@1` schemas: usage and telemetry samples; node capabilities and
Add nine public `platform.*@1` schemas: usage and telemetry samples; node capabilities and
@@ -1,4 +1,26 @@
@@ -1,4 +1,26 @@
{
{
"_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",
"_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",
- "gaps": []
+ "gaps": [
+ {
+ "id": "network.node.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.node.self.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.resource.report",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ }
+ ]
}
}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
"$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
"title": "ServiceTokenClaims",
"title": "ServiceTokenClaims",
- "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",
+ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",
"type": "object",
"type": "object",
"required": [
"required": [
"iss",
"iss",
@@ -23,13 +23,14 @@
@@ -23,13 +23,14 @@
},
},
"sub": {
"sub": {
"type": "string",
"type": "string",
- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"
+ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26})$"
},
},
"actor_type": {
"actor_type": {
"enum": [
"enum": [
"service",
"service",
"app",
"app",
- "mod"
+ "mod",
+ "node"
]
]
},
},
"aud": {
"aud": {
@@ -36,6 +36,7 @@
@@ -36,6 +36,7 @@
"checked_at": { "type": "string", "format": "date-time" }
"checked_at": { "type": "string", "format": "date-time" }
}
}
},
},
- "updated_at": { "type": "string", "format": "date-time" }
+ "updated_at": { "type": "string", "format": "date-time" },
+ "cell": { "type": "string", "pattern": "^[a-z]{2,8}-[0-9]{1,3}$", "description": "Cell the node belongs to, e.g. wnam-1." }
}
}
}
}
@@ -136,6 +136,23 @@
@@ -136,6 +136,23 @@
"required": [
"required": [
"per_hour_usd"
"per_hour_usd"
]
]
+ },
+ "capabilities": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
+ },
+ "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."
+ },
+ "agent_version": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "Version of the node agent that reported."
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
}
}
},
},
"examples": [
"examples": [
@@ -23,7 +23,12 @@
@@ -23,7 +23,12 @@
"type": "string",
"type": "string",
"enum": [
"enum": [
"node",
"node",
- "provider"
+ "provider",
+ "storage",
+ "delivery",
+ "runtime",
+ "agent",
+ "harness"
]
]
},
},
"node_id": {
"node_id": {
@@ -255,6 +260,144 @@
@@ -255,6 +260,144 @@
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "detail": {
+ "type": "object",
+ "description": "The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "storage"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "storage-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "delivery"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "delivery-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "runtime"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "runtime-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "agent"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "agent-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "harness"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "harness-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "enum": [
+ "node",
+ "provider"
+ ]
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "not": {
+ "required": [
+ "detail"
+ ]
+ }
+ }
}
}
- }
+ ]
}
}
@@ -57,6 +57,12 @@
@@ -57,6 +57,12 @@
"started_at": {
"started_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "route_weight": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight."
}
}
},
},
"examples": [
"examples": [
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "node:nod_42",
+ "actor_type": "node",
+ "aud": [
+ "openvibe.bot"
+ ],
+ "cap": [
+ "network.node.self.manage"
+ ],
+ "iat": 1790000000,
+ "exp": 1790000300,
+ "jti": "tok_node0001"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "node:nod_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "actor_type": "node",
+ "aud": [
+ "openvibe.bot"
+ ],
+ "cap": [
+ "network.node.self.manage"
+ ],
+ "iat": 1790000000,
+ "exp": 1790000300,
+ "jti": "tok_node0001"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "oregon-1",
+ "name": "Oregon 1",
+ "roles": [
+ "web",
+ "app",
+ "data",
+ "ingest"
+ ],
+ "location": {
+ "region": "us-west",
+ "country": "US",
+ "city": "Hillsboro",
+ "lat": 45.52,
+ "lon": -122.99
+ },
+ "provider": "ovh",
+ "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",
+ "health": {
+ "status": "up",
+ "checked_at": "2026-09-28T12:00:00Z"
+ },
+ "updated_at": "2026-09-28T12:00:00Z",
+ "cell": "WNAM_1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "oregon-1",
+ "name": "Oregon 1",
+ "roles": [
+ "web",
+ "app",
+ "data",
+ "ingest"
+ ],
+ "location": {
+ "region": "us-west",
+ "country": "US",
+ "city": "Hillsboro",
+ "lat": 45.52,
+ "lon": -122.99
+ },
+ "provider": "ovh",
+ "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",
+ "health": {
+ "status": "up",
+ "checked_at": "2026-09-28T12:00:00Z"
+ },
+ "updated_at": "2026-09-28T12:00:00Z",
+ "cell": "wnam-1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+{
+ "node_id": "node-1",
+ "cpu": {
+ "cores": 8,
+ "threads": 16
+ },
+ "arch": "x86_64",
+ "memory_mb": 16384,
+ "gpu": {
+ "model": "L4",
+ "count": 1,
+ "vram_mb": 24576
+ },
+ "storage": {
+ "capacity_gb": 500,
+ "available_gb": 300,
+ "kind": "ssd"
+ },
+ "network": {
+ "ingress_mbps": 1000,
+ "egress_mbps": 1000
+ },
+ "regions": [
+ "us-west"
+ ],
+ "tags": [
+ "gpu",
+ "edge"
+ ],
+ "costs": {
+ "per_hour_usd": 0.2,
+ "egress_per_gb_usd": 0.01
+ },
+ "capabilities": [
+ "ffmpeg"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+{
+ "node_id": "node-1",
+ "cpu": {
+ "cores": 8,
+ "threads": 16
+ },
+ "arch": "x86_64",
+ "memory_mb": 16384,
+ "gpu": {
+ "model": "L4",
+ "count": 1,
+ "vram_mb": 24576
+ },
+ "storage": {
+ "capacity_gb": 500,
+ "available_gb": 300,
+ "kind": "ssd"
+ },
+ "network": {
+ "ingress_mbps": 1000,
+ "egress_mbps": 1000
+ },
+ "regions": [
+ "us-west"
+ ],
+ "tags": [
+ "gpu",
+ "edge"
+ ],
+ "costs": {
+ "per_hour_usd": 0.2,
+ "egress_per_gb_usd": 0.01
+ },
+ "capabilities": [
+ "node:http",
+ "worker:ffmpeg"
+ ],
+ "agent_version": "0.9.0",
+ "updated_at": "2026-10-02T00:00:00Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
+{
+ "offer_id": "off_oregon_1",
+ "kind": "node",
+ "node_id": "ovh-oregon-1",
+ "provider": "ovh",
+ "region": "us-west",
+ "cell": "wnam-1",
+ "trust": "first-party",
+ "capabilities": [
+ "node:http",
+ "worker:browser",
+ "worker:ffmpeg",
+ "events:durable"
+ ],
+ "capacity": {
+ "cpu": {
+ "utilization": 0.31,
+ "available_cores": 5.5
+ },
+ "memory": {
+ "available_mb": 18300
+ },
+ "workers": {
+ "browser": 6,
+ "ffmpeg": 3
+ },
+ "queues": {
+ "media-jobs": {
+ "depth": 2,
+ "oldest_ms": 40
+ }
+ }
+ },
+ "latency_ms": {
+ "publish_p95": 3.1
+ },
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "prepaid",
+ "marginal_usd_per_unit": 0,
+ "unit": "job"
+ },
+ "updated_at": "2026-09-28T23:00:00Z",
+ "detail": {
+ "id": "storage-1",
+ "class": "warm",
+ "capacity_gb": 1000,
+ "price_per_gb_month_usd": 0.02,
+ "price_per_operation_usd": 1e-05,
+ "region": "us-west",
+ "node": "node-1",
+ "durability": {
+ "replicas": 3,
+ "target_nines": 9
+ },
+ "lifecycle_rules": [
+ {
+ "after_days": 90,
+ "action": "transition",
+ "target_class": "cold"
+ }
+ ]
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "offer_id": "off_storage_1",
+ "kind": "storage",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "object:r2"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+{
+ "offer_id": "off_agent_1",
+ "kind": "agent",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "agent:model-1"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z",
+ "detail": {
+ "id": "agent-1",
+ "harness": "harness-1",
+ "provider": "example-ai",
+ "model": "model-1",
+ "context_limits": {
+ "input_tokens": 128000,
+ "output_tokens": 8192
+ },
+ "concurrency": 4,
+ "price_per_1k_tokens": {
+ "fresh_usd": 0.003,
+ "cached_usd": 0.001,
+ "output_usd": 0.01
+ },
+ "availability_windows": []
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+{
+ "offer_id": "off_delivery_1",
+ "kind": "delivery",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "delivery:hls"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z",
+ "detail": {
+ "id": "delivery-1",
+ "transports": [
+ "http",
+ "hls"
+ ],
+ "regions": [
+ "us-west"
+ ],
+ "edge": true,
+ "node": "node-1",
+ "price_per_gb_usd": 0.02,
+ "price_per_request_usd": 1e-05,
+ "cache_rules": {
+ "enabled": true,
+ "ttl_seconds": 300,
+ "max_object_mb": 100
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "offer_id": "off_harness_1",
+ "kind": "harness",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "harness:cli"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z",
+ "detail": {
+ "id": "harness-1",
+ "name": "Example Agent",
+ "provider": "example-ai",
+ "capabilities": {
+ "host_access": true,
+ "mcp": true,
+ "long_autonomy": true,
+ "resume": true
+ },
+ "address": {
+ "kind": "cli",
+ "target": "example-agent"
+ },
+ "price": {
+ "amount_usd": 0.1,
+ "unit": "hour"
+ },
+ "limits": {
+ "max_duration_seconds": 7200,
+ "max_concurrent_runs": 4,
+ "max_context_tokens": 128000
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+{
+ "offer_id": "off_runtime_1",
+ "kind": "runtime",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "runtime:container"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z",
+ "detail": {
+ "id": "runtime-1",
+ "kind": "container",
+ "region": "us-west",
+ "node": "node-1",
+ "limits": {
+ "cpu_cores": 2,
+ "memory_mb": 4096,
+ "duration_seconds": 3600
+ },
+ "price": {
+ "amount_usd": 0.05,
+ "unit": "hour"
+ },
+ "availability": "available",
+ "constraints": [
+ "first-party"
+ ]
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+{
+ "offer_id": "off_storage_1",
+ "kind": "storage",
+ "provider": "example",
+ "region": "us-west",
+ "trust": "first-party",
+ "capabilities": [
+ "object:r2"
+ ],
+ "health": {
+ "status": "up"
+ },
+ "pricing": {
+ "model": "per-operation"
+ },
+ "updated_at": "2026-10-02T00:00:00Z",
+ "detail": {
+ "id": "storage-1",
+ "class": "warm",
+ "capacity_gb": 1000,
+ "price_per_gb_month_usd": 0.02,
+ "price_per_operation_usd": 1e-05,
+ "region": "us-west",
+ "node": "node-1",
+ "durability": {
+ "replicas": 3,
+ "target_nines": 9
+ },
+ "lifecycle_rules": [
+ {
+ "after_days": 90,
+ "action": "transition",
+ "target_class": "cold"
+ }
+ ]
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "id": "inst-1",
+ "service": "media",
+ "version": "1.4.0",
+ "cell": "cell-west",
+ "node": "node-1",
+ "region": "us-west",
+ "endpoints": [
+ "https://media.internal.example"
+ ],
+ "state": "ready",
+ "started_at": "2026-09-30T10:00:00Z",
+ "route_weight": 1001
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "id": "inst-1",
+ "service": "media",
+ "version": "1.4.0",
+ "cell": "cell-west",
+ "node": "node-1",
+ "region": "us-west",
+ "endpoints": [
+ "https://media.internal.example"
+ ],
+ "state": "ready",
+ "started_at": "2026-09-30T10:00:00Z",
+ "route_weight": 250
+}
@@ -187,7 +187,7 @@
@@ -187,7 +187,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
"$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
"title": "ServiceTokenClaims",
"title": "ServiceTokenClaims",
- "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",
+ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.",
"type": "object",
"type": "object",
"required": [
"required": [
"iss",
"iss",
@@ -208,13 +208,14 @@
@@ -208,13 +208,14 @@
},
},
"sub": {
"sub": {
"type": "string",
"type": "string",
- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"
+ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26})$"
},
},
"actor_type": {
"actor_type": {
"enum": [
"enum": [
"service",
"service",
"app",
"app",
- "mod"
+ "mod",
+ "node"
]
]
},
},
"aud": {
"aud": {
@@ -57486,6 +57487,11 @@
@@ -57486,6 +57487,11 @@
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "cell": {
+ "type": "string",
+ "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",
+ "description": "Cell the node belongs to, e.g. wnam-1."
}
}
}
}
}
}
@@ -57749,7 +57755,12 @@
@@ -57749,7 +57755,12 @@
"type": "string",
"type": "string",
"enum": [
"enum": [
"node",
"node",
- "provider"
+ "provider",
+ "storage",
+ "delivery",
+ "runtime",
+ "agent",
+ "harness"
]
]
},
},
"node_id": {
"node_id": {
@@ -57981,8 +57992,146 @@
@@ -57981,8 +57992,146 @@
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "detail": {
+ "type": "object",
+ "description": "The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider."
}
}
- }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "storage"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "storage-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "delivery"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "delivery-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "runtime"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "runtime-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "agent"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "agent-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "harness"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "detail"
+ ],
+ "properties": {
+ "detail": {
+ "$ref": "harness-offer.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "enum": [
+ "node",
+ "provider"
+ ]
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "not": {
+ "required": [
+ "detail"
+ ]
+ }
+ }
+ }
+ ]
}
}
},
},
"platform.workload-requirements": {
"platform.workload-requirements": {
@@ -59144,6 +59293,23 @@
@@ -59144,6 +59293,23 @@
"required": [
"required": [
"per_hour_usd"
"per_hour_usd"
]
]
+ },
+ "capabilities": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
+ },
+ "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."
+ },
+ "agent_version": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "Version of the node agent that reported."
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
}
}
},
},
"examples": [
"examples": [
@@ -59247,6 +59413,12 @@
@@ -59247,6 +59413,12 @@
"started_at": {
"started_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "route_weight": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight."
}
}
},
},
"examples": [
"examples": [
@@ -96,8 +96,8 @@
@@ -96,8 +96,8 @@
"service": "network",
"service": "network",
"name": "OpenVibe.Network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
"origin": "https://openvibe.network",
- "operations": 29,
- "capabilities": 22
+ "operations": 36,
+ "capabilities": 25
},
},
{
{
"service": "news",
"service": "news",
@@ -136,6 +136,84 @@
@@ -136,6 +136,84 @@
}
}
}
}
},
},
+ "/api/v1/node/self/capabilities": {
+ "put": {
+ "operationId": "put_api_v1_node_self_capabilities",
+ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
+ "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.self.manage"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.self.manage"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/node/self/credential": {
+ "post": {
+ "operationId": "post_api_v1_node_self_credential",
+ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
+ "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.self.manage"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.self.manage"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/staff/moderators": {
"/api/v1/staff/moderators": {
"get": {
"get": {
"operationId": "get_api_v1_staff_moderators",
"operationId": "get_api_v1_staff_moderators",
@@ -1517,6 +1595,143 @@
@@ -1517,6 +1595,143 @@
}
}
}
}
},
},
+ "/internal/node-pairings": {
+ "post": {
+ "operationId": "post_internal_node_pairings",
+ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
+ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.manage"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.manage"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/node-principals/{id}": {
+ "get": {
+ "operationId": "get_internal_node_principals_id",
+ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
+ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.manage"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.manage"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/node-principals/{id}/revoke": {
+ "post": {
+ "operationId": "post_internal_node_principals_id_revoke",
+ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
+ "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.manage"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.manage"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/internal/nodes/report": {
"/internal/nodes/report": {
"post": {
"post": {
"operationId": "post_internal_nodes_report",
"operationId": "post_internal_nodes_report",
@@ -1685,6 +1900,101 @@
@@ -1685,6 +1900,101 @@
}
}
}
}
},
},
+ "/internal/registry/instances/report": {
+ "post": {
+ "operationId": "post_internal_registry_instances_report",
+ "summary": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
+ "description": "**network.node.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
+ "tags": [
+ "network.node"
+ ],
+ "x-openvibe-capabilities": [
+ "network.node.report"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.node.report"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/network.node-report-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/network.node-list-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/resources/report": {
+ "post": {
+ "operationId": "post_internal_resources_report",
+ "summary": "Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",
+ "description": "**network.resource.report** (internal): Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",
+ "tags": [
+ "network.resource"
+ ],
+ "x-openvibe-capabilities": [
+ "network.resource.report"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "admin",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.resource.report"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success"
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/internal/url-registry/resolved": {
"/internal/url-registry/resolved": {
"get": {
"get": {
"operationId": "get_internal_url_registry_resolved",
"operationId": "get_internal_url_registry_resolved",
@@ -3182,6 +3492,11 @@
@@ -3182,6 +3492,11 @@
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
"format": "date-time"
"format": "date-time"
+ },
+ "cell": {
+ "type": "string",
+ "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",
+ "description": "Cell the node belongs to, e.g. wnam-1."
}
}
}
}
},
},
@@ -53,7 +53,7 @@ export interface LegacyIdentityMapEntry {
@@ -53,7 +53,7 @@ export interface LegacyIdentityMapEntry {
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
/**
/**
- * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.
+ * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod or node principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in.
*/
*/
export type ServiceTokenClaims = {
export type ServiceTokenClaims = {
[k: string]: unknown | undefined;
[k: string]: unknown | undefined;
@@ -63,7 +63,7 @@ export type ServiceTokenClaims = {
@@ -63,7 +63,7 @@ export type ServiceTokenClaims = {
*/
*/
iss: string;
iss: string;
sub: string;
sub: string;
- actor_type: "service" | "app" | "mod";
+ actor_type: "service" | "app" | "mod" | "node";
/**
/**
* @minItems 1
* @minItems 1
*/
*/
@@ -50616,6 +50616,10 @@ export interface NetworkNode {
@@ -50616,6 +50616,10 @@ export interface NetworkNode {
checked_at: string;
checked_at: string;
};
};
updated_at: string;
updated_at: string;
+ /**
+ * Cell the node belongs to, e.g. wnam-1.
+ */
+ cell?: string;
}
}
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
@@ -50712,9 +50716,11 @@ export interface NetworkCoinsReadResult {
@@ -50712,9 +50716,11 @@ export interface NetworkCoinsReadResult {
/**
/**
* What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.
* What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.
*/
*/
-export interface ResourceOffer {
+export type ResourceOffer = {
+ [k: string]: unknown | undefined;
+} & {
offer_id: string;
offer_id: string;
- kind: "node" | "provider";
+ kind: "node" | "provider" | "storage" | "delivery" | "runtime" | "agent" | "harness";
/**
/**
* network.node@1 id, for kind node
* network.node@1 id, for kind node
*/
*/
@@ -50798,7 +50804,11 @@ export interface ResourceOffer {
@@ -50798,7 +50804,11 @@ export interface ResourceOffer {
rate_card?: string;
rate_card?: string;
};
};
updated_at: string;
updated_at: string;
-}
+ /**
+ * The kind's own offer: platform.storage-offer@1, platform.delivery-offer@1, platform.runtime-offer@1, platform.agent-offer@1 or platform.harness-offer@1 for those kinds; absent for kind node and provider.
+ */
+ detail?: {};
+};
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
/**
/**
@@ -51195,6 +51205,15 @@ export interface NodeCapabilities {
@@ -51195,6 +51205,15 @@ export interface NodeCapabilities {
*/
*/
egress_per_gb_usd?: number;
egress_per_gb_usd?: number;
};
};
+ /**
+ * What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg).
+ */
+ capabilities?: string[];
+ /**
+ * Version of the node agent that reported.
+ */
+ agent_version?: string;
+ updated_at?: string;
}
}
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
@@ -51214,6 +51233,10 @@ export interface ServiceInstance {
@@ -51214,6 +51233,10 @@ export interface ServiceInstance {
endpoints: [string, ...string[]];
endpoints: [string, ...string[]];
state: "starting" | "ready" | "degraded" | "draining" | "stopped";
state: "starting" | "ready" | "degraded" | "draining" | "stopped";
started_at: string;
started_at: string;
+ /**
+ * Share of this service's traffic the router sends to this instance, relative to its siblings; absent means equal weight.
+ */
+ route_weight?: number;
}
}
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
const crypto = require('crypto');
const crypto = require('crypto');
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
-const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf' };
+const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf' };
const SLUG_TYPES = new Set(['service', 'system']);
const SLUG_TYPES = new Set(['service', 'system']);
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
@@ -50,11 +50,11 @@ function formatSubject(ref) {
@@ -50,11 +50,11 @@ function formatSubject(ref) {
return `${ref.type}:${ref.id}`;
return `${ref.type}:${ref.id}`;
}
}
-/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID>. */
+/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID> | node:nod_<ULID>. */
function principalSub(ref) {
function principalSub(ref) {
if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;
if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;
- if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;
- throw new TypeError('principals are service, app, mod or agent subjects');
+ if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent' || ref.type === 'node') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;
+ throw new TypeError('principals are service, app, mod, agent or node subjects');
}
}
/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */
/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "network.node.manage",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
+ "permissions": [
+ "nodes:pair",
+ "nodes:read",
+ "nodes:revoke"
+ ],
+ "resourceConstraints": [
+ "owner"
+ ],
+ "quotaClass": "admin",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/node-pairings",
+ "GET /internal/node-principals/:id",
+ "POST /internal/node-principals/:id/revoke"
+ ]
+}
@@ -14,7 +14,8 @@
@@ -14,7 +14,8 @@
"quotaClass": "admin",
"quotaClass": "admin",
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
- "POST /internal/nodes/report"
+ "POST /internal/nodes/report",
+ "POST /internal/registry/instances/report"
],
],
"inputSchema": "network.node-report-request@1",
"inputSchema": "network.node-report-request@1",
"outputSchema": "network.node-list-result@1"
"outputSchema": "network.node-list-result@1"
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "id": "network.node.self.manage",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
+ "permissions": [
+ "nodes:self"
+ ],
+ "resourceConstraints": [
+ "owner"
+ ],
+ "quotaClass": "admin",
+ "events": [],
+ "implementedBy": [
+ "PUT /api/v1/node/self/capabilities",
+ "POST /api/v1/node/self/credential"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "network.resource.report",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Report the complete set of a source's resource offers (platform.resource-offer@1) to the registry; offers absent from a later report of the same source are marked down.",
+ "permissions": [
+ "resources:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "admin",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/resources/report"
+ ]
+}
@@ -48,12 +48,15 @@
@@ -48,12 +48,15 @@
"network.integration.github.read",
"network.integration.github.read",
"network.modules.read",
"network.modules.read",
"network.modules.write",
"network.modules.write",
+ "network.node.manage",
"network.node.report",
"network.node.report",
+ "network.node.self.manage",
"network.notifications.push",
"network.notifications.push",
"network.operator.alert",
"network.operator.alert",
"network.project.manage",
"network.project.manage",
"network.project.read",
"network.project.read",
"network.registry.read",
"network.registry.read",
+ "network.resource.report",
"network.staff.read",
"network.staff.read",
"network.status.incident"
"network.status.incident"
],
],
@@ -500,6 +500,10 @@ for (const kind of ['user', 'guest', 'app', 'mod']) {
@@ -500,6 +500,10 @@ for (const kind of ['user', 'guest', 'app', 'mod']) {
ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');
ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');
ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');
ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');
ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');
ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');
+const nodeId = ids.newId('node');
+ok(nodeId.startsWith('nod_') && ids.principalSub({ type: 'node', id: nodeId }) === `node:${nodeId}`, 'node principal sub is node:nod_<ULID>');
+assert.throws(() => ids.principalSub({ type: 'node', id: 'nod_42' }), /principals/);
+ok(!ids.SUBJECT_TYPES.includes('node') && ids.parseSubject(`node:${nodeId}`) === null, 'a node is a principal, never a subject');
assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);
assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);
ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');
ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');
ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');
ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');
@@ -563,6 +567,9 @@ ok(V(sandboxTok, { acceptSandbox: true }).ok, 'sandbox app token accepted when t
@@ -563,6 +567,9 @@ ok(V(sandboxTok, { acceptSandbox: true }).ok, 'sandbox app token accepted when t
ok(V(serviceAuth.signServiceToken({ ...appClaims, env: 'production' }, kp.privateKey)).ok, 'production app token accepted');
ok(V(serviceAuth.signServiceToken({ ...appClaims, env: 'production' }, kp.privateKey)).ok, 'production app token accepted');
ok(V(serviceAuth.signServiceToken((({ project_id, ...rest }) => rest)(appClaims), kp.privateKey)).code === 'token.invalid_claims', 'an app token needs project_id');
ok(V(serviceAuth.signServiceToken((({ project_id, ...rest }) => rest)(appClaims), kp.privateKey)).code === 'token.invalid_claims', 'an app token needs project_id');
ok(contracts.ids.newId('project').startsWith('prj_'), 'project ids use the prj prefix');
ok(contracts.ids.newId('project').startsWith('prj_'), 'project ids use the prj prefix');
+const nodeClaims = { ...claims, sub: 'node:nod_01J0000000000000000000000Z', actor_type: 'node' };
+ok(V(serviceAuth.signServiceToken(nodeClaims, kp.privateKey)).ok, 'node token accepted');
+ok(V(serviceAuth.signServiceToken({ ...nodeClaims, sub: 'node:nod_42' }, kp.privateKey)).code === 'token.invalid_claims', 'a malformed node id is rejected');
const run = (guard, headers) => new Promise((resolve) => {
const run = (guard, headers) => new Promise((resolve) => {
const req = { headers, body: {} };
const req = { headers, body: {} };