From 2d6935705261b903f979d4a5f8d62f37d134c415 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:42:13 +0700 Subject: [PATCH] protocol: add canonical Phase 3A contract sources --- AGENTS.md | 38 +++ README.md | 25 ++ VERSION | 1 + buf.yaml | 9 + compatibility.json | 10 + docs/interface-inventory.md | 43 +++ frames/datagram-v1.md | 27 ++ frames/registry.json | 12 + go.mod | 3 + openapi/control-v1.yaml | 414 ++++++++++++++++++++++++ openspec/config.yaml | 20 ++ proto/versevdi/control/v1/control.proto | 34 ++ proto/versevdi/tunnel/v1/tunnel.proto | 51 +++ registries/datagrams.json | 9 + registries/features.json | 8 + schemas/control-v1.schema.json | 348 ++++++++++++++++++++ 16 files changed, 1052 insertions(+) create mode 100644 AGENTS.md create mode 100644 VERSION create mode 100644 buf.yaml create mode 100644 compatibility.json create mode 100644 docs/interface-inventory.md create mode 100644 frames/datagram-v1.md create mode 100644 frames/registry.json create mode 100644 go.mod create mode 100644 openapi/control-v1.yaml create mode 100644 openspec/config.yaml create mode 100644 proto/versevdi/control/v1/control.proto create mode 100644 proto/versevdi/tunnel/v1/tunnel.proto create mode 100644 registries/datagrams.json create mode 100644 registries/features.json create mode 100644 schemas/control-v1.schema.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f88da98 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# VerseVDI Protocol repository guidance + +This repository is the sole source of the VerseVDI Phase 3 wire contract. It +contains OpenAPI, JSON Schema, Protobuf, registries, fixtures, and generated +language bindings. It contains no Connection Server business policy, +database/auth implementation, provider credentials, or media implementation. + +## Source and generation rules + +- Edit `schemas/`, `openapi/`, `proto/`, `registries/`, and `fixtures/` only as + protocol sources. Never hand-edit `gen/`; run `python3 tools/generate.py`. +- `VERSION` and `compatibility.json` are the version authority. Current, N-1, + and N-2 fixtures must remain executable. +- `make verify` runs schema/fixture checks, Protobuf lint/descriptor checks, + the historical-descriptor breaking check, deterministic generation, Go tests, + and native conformance runners. +- Generated output must be byte-for-byte clean after a second generation. +- `gen/protobuf/phase3a-baseline.binpb` is a generated historical descriptor used + only by Buf breaking checks; regenerate it from an approved baseline tag, + never by hand. +- The Connection Server consumes generated Go types through adapters; it does + not own a competing wire schema. + +## Scope boundaries + +Phase 3A covers control, authorization, resources, broker session actions, +gateway-only grants/manifests, resumable control events, registered control +datagrams, and bounded text clipboard. It does not implement a gateway, +provider transport, video/audio forwarding, file transfer, or a second auth or +datastore path. + +## Git and release safety + +Keep private Gitea work and public-source boundaries explicit. Preserve dirty +work from other owners. Do not push, merge, publish, or change release +credentials unless the task separately authorizes it. A local immutable +candidate tag is allowed only after matching Go/Rust/Swift fixture hashes and +clean-checkout evidence; it is not a publication. diff --git a/README.md b/README.md index ba34ae1..0d67d07 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ # VerseVDI-Protocol +The Phase 3A Protocol repository is the sole source of VerseVDI wire contracts. +OpenAPI, JSON Schema, Protobuf, registries, and conformance fixtures live here; +the Connection Server consumes generated Go bindings and remains the authority +for authentication, authorization, persistence, licensing, and provider policy. + +## Scope + +Phase 3A defines versioned control requests/responses, native device contracts, +subject-scoped resources, broker actions, gateway-only manifests, resumable +control events, registered control datagrams, and bounded text clipboard. It +does not implement a gateway, provider transport, media forwarding, file +transfer, or a second authentication or datastore path. + +## Verification + +Run `make verify` after editing a source schema, registry, Protobuf definition, +or fixture. The command validates source bounds and scope, regenerates Go/Rust/ +Swift bindings and Protobuf descriptors, compiles generated bindings, runs the +shared Go/Rust/Swift corpus, checks datagram frames, and fails if regeneration +would dirty the checkout. + +Generated files under `gen/` are never edited by hand. `VERSION`, +`compatibility.json`, and the content-addressed fixture manifest identify the +release inputs. A Protocol release candidate is not valid until all consumers +report the same fixture hash and clean-checkout regeneration evidence. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c7e30e3 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/compatibility.json b/compatibility.json new file mode 100644 index 0000000..6c17fa9 --- /dev/null +++ b/compatibility.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "protocol": "versevdi-control", + "current": "1", + "n_minus_1": "0", + "n_minus_2": "-1", + "unsupported": ["-2", "2"], + "feature_registry": "registries/features.json", + "datagram_registry": "registries/datagrams.json" +} diff --git a/docs/interface-inventory.md b/docs/interface-inventory.md new file mode 100644 index 0000000..b00b09d --- /dev/null +++ b/docs/interface-inventory.md @@ -0,0 +1,43 @@ +# Phase 3A interface inventory + +This inventory records the wire boundary consumed by the closed Connection Server. +The Protocol repository owns only schemas, registries, fixtures, and generated bindings; +the Server remains the authority for authentication, authorization, persistence, and +provider selection. + +## Existing Server routes adapted by generated contracts + +| Route | Channel/guard | Contract boundary | Phase 2 behavior | +|---|---|---|---| +| `POST /api/v1/auth/refresh` | native credential body; rate limited | `RefreshRequest` -> `NativeCredential` | opaque refresh rotation and replay-family revocation remain PostgreSQL-owned | +| `POST /api/v1/auth/devices` | authenticated browser session + CSRF | `DeviceRegistrationRequest` -> `DeviceChallenge` | device challenge remains one-use and hash-backed | +| `POST /api/v1/auth/devices/:id/prove` | authenticated browser session + CSRF | `DeviceProofRequest` -> `NativeCredential` | canonical Ed25519 proof and native session issuance remain Server-owned | +| `DELETE /api/v1/auth/devices/:id` | authenticated browser session + CSRF | no body | principal-scoped device revocation remains Server-owned | +| `GET /api/v1/resources` | authenticated browser/native session | `ResourceList` | assigned desktop and entitled pool projections omit machine/provider data | +| `POST /api/v1/broker/sessions` | authenticated session + CSRF | `SessionRequest` -> `BrokerSession` | existing assignment, idempotency, queue, and PostgreSQL state machine remain authoritative | +| `GET /api/v1/broker/sessions/:id` | authenticated session | `BrokerSession` | owner-scoped lookup is preserved | +| `POST /api/v1/broker/sessions/:id/allocate` | authenticated session + CSRF | `ConnectionManifest` | gateway-only grant boundary; no provider/direct-host fields | +| `POST /api/v1/broker/sessions/:id/reconnect` | authenticated session + CSRF | `ReconnectRequest` -> `ConnectionManifest` | device proof, expected version, grant rotation, and monotonic reconnect sequence remain authoritative | +| `POST /api/v1/broker/sessions/:id/cancel` | authenticated session + CSRF | no body | existing compensation path remains authoritative | + +## Contract rules + +- Unknown fields are rejected for signed/security-sensitive requests and generated + decoders are strict. +- Responses are explicit projections; domain, sqlc, provider, VM, and credential + structures are never serialized directly. +- Resource projections contain only opaque assignment/pool identifiers and safe state; + machine identifiers and provider endpoints are not wire fields. +- A manifest contains only the versioned gateway/tunnel/profile/grant allow-list. The + opaque grant is stored as a hash in the Server database and is single-use. +- Events and QUIC/tunnel sources are defined here before any production media or gateway + implementation; the Phase 3A claim stops at control contracts and conformance. + +## Compatibility notes + +The existing routes and authentication channels remain unchanged. Phase 3A adds the +`page` member to the resource response and replaces the legacy flat manifest projection +with the versioned gateway-only manifest. Both changes are intentional contract changes: +the former is additive, and the latter is required to make provider/direct-host fields +unrepresentable. The Server integration tests retain the old safe resource members and +assert the new allow-list explicitly. diff --git a/frames/datagram-v1.md b/frames/datagram-v1.md new file mode 100644 index 0000000..bc4de6c --- /dev/null +++ b/frames/datagram-v1.md @@ -0,0 +1,27 @@ +# VerseVDI control datagram v1 + +Phase 3A reserves a bounded control-datagram envelope. It does not forward video, +audio, provider traffic, VM traffic, or arbitrary binary clipboard data. + +All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes: + +| Offset | Size | Field | Rule | +|---:|---:|---|---| +| 0 | 2 | magic | ASCII `VD` (`0x56 0x44`) | +| 2 | 1 | version | `1` only | +| 3 | 1 | channel | registered channel identifier | +| 4 | 1 | flags | registry-defined; unknown bits reject | +| 5 | 4 | sequence | session-local wrapping sequence | +| 9 | 8 | timestamp_ms | sender timestamp, bounded by transport skew policy | +| 17 | 1 | fragment_index | zero-based; `0` when not fragmented | +| 18 | 1 | fragment_count | `1` when not fragmented; index less than count | +| 19 | 2 | payload_length | exact payload byte count | + +The complete frame is at most 65,536 bytes and `payload_length` is at most 65,515. +Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and +length-mismatch frames are rejected before allocation proportional to the claimed +payload. Media/provider identifiers are not registered channels. + +Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and +`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain +subject to the 65,536-byte text limit and explicit authorization. diff --git a/frames/registry.json b/frames/registry.json new file mode 100644 index 0000000..f346426 --- /dev/null +++ b/frames/registry.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "version": "1", + "header_bytes": 21, + "maximum_frame_bytes": 65536, + "channels": [ + {"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024}, + {"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048}, + {"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515} + ], + "reserved_rejected": ["video", "audio", "provider", "vm", "file-transfer", "clipboard.binary"] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d339038 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sechmachine/VerseVDI-Protocol + +go 1.26 diff --git a/openapi/control-v1.yaml b/openapi/control-v1.yaml new file mode 100644 index 0000000..cd0c266 --- /dev/null +++ b/openapi/control-v1.yaml @@ -0,0 +1,414 @@ +openapi: 3.1.0 +info: + title: VerseVDI control contract + version: 1.0.0 + description: Bounded Phase 3A control-plane wire contract; no provider or media endpoints. +servers: + - url: / +tags: + - name: authentication + - name: resources + - name: broker + - name: events +paths: + /api/v1/auth/login: + post: + tags: [authentication] + operationId: loginBrowserSession + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/LoginRequest + responses: + '200': + description: Browser session established through secure cookies. + content: + application/json: + schema: + type: object + additionalProperties: false + required: [username, provider, roles, role] + properties: + username: {type: string, minLength: 1, maxLength: 256} + provider: {type: string, minLength: 1, maxLength: 64} + roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}} + role: {type: string, enum: [user, admin]} + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + '503': {$ref: '#/components/responses/Unavailable'} + /api/v1/auth/refresh: + post: + tags: [authentication] + operationId: rotateNativeCredential + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/RefreshRequest + responses: + '200': + description: Rotated opaque native credential. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/NativeCredential + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/session: + get: + tags: [authentication] + operationId: getAuthenticatedSession + security: + - browserSession: [] + - nativeBearer: [] + responses: + '200': + description: Authenticated subject summary. + content: + application/json: + schema: + type: object + additionalProperties: false + required: [username, provider, roles, role] + properties: + username: {type: string, minLength: 1, maxLength: 256} + provider: {type: string, minLength: 1, maxLength: 64} + roles: {type: array, maxItems: 16, items: {type: string, maxLength: 64}} + role: {type: string, enum: [user, admin]} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/reauth: + post: + tags: [authentication] + operationId: issueReauthenticationGrant + security: + - browserSession: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ReauthRequest + responses: + '200': + description: Short-lived opaque step-up grant. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ReauthGrant + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/logout: + post: + tags: [authentication] + operationId: logoutSession + security: + - browserSession: [] + - nativeBearer: [] + responses: + '204': {description: Session revoked and browser cookies cleared.} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/devices: + post: + tags: [authentication] + operationId: registerDevice + security: + - browserSession: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/DeviceRegistrationRequest + responses: + '201': + description: Single-use device challenge. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/DeviceChallenge + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/devices/{device_id}/prove: + post: + tags: [authentication] + operationId: proveDevice + security: + - browserSession: [] + parameters: + - $ref: '#/components/parameters/DeviceID' + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/DeviceProofRequest + responses: + '200': + description: Opaque native credential. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/NativeCredential + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/auth/devices/{device_id}: + delete: + tags: [authentication] + operationId: revokeDevice + security: + - browserSession: [] + parameters: + - $ref: '#/components/parameters/DeviceID' + responses: + '204': {description: Device and its native authority revoked.} + '401': {$ref: '#/components/responses/Unauthorized'} + '404': {$ref: '#/components/responses/NotFound'} + /api/v1/resources: + get: + tags: [resources] + operationId: listResources + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Cursor' + responses: + '200': + description: Subject-scoped bounded resources and entitlements. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ResourceList + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/broker/sessions: + post: + tags: [broker] + operationId: requestBrokerSession + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/SessionRequest + responses: + '201': + description: Durable broker session. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession + '202': + description: Durable session waiting for an eligible desktop or capacity. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/broker/sessions/{session_id}: + get: + tags: [broker] + operationId: getBrokerSession + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/SessionID' + responses: + '200': + description: Subject-scoped broker session projection. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession + '401': {$ref: '#/components/responses/Unauthorized'} + '404': {$ref: '#/components/responses/NotFound'} + /api/v1/broker/sessions/{session_id}/allocate: + post: + tags: [broker] + operationId: allocateBrokerSession + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/SessionID' + responses: + '200': + description: Gateway-only launch manifest with a single-use grant. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest + '202': + description: Session remains waiting for an eligible desktop or capacity. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/BrokerSession + '401': {$ref: '#/components/responses/Unauthorized'} + '404': {$ref: '#/components/responses/NotFound'} + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/broker/sessions/{session_id}/reconnect: + post: + tags: [broker] + operationId: reconnectBrokerSession + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/SessionID' + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ReconnectRequest + responses: + '200': + description: Gateway-only reconnect manifest with a monotonic sequence. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ConnectionManifest + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + '404': {$ref: '#/components/responses/NotFound'} + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/broker/sessions/{session_id}/cancel: + post: + tags: [broker] + operationId: cancelBrokerSession + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - $ref: '#/components/parameters/SessionID' + responses: + '204': {description: Cancellation accepted idempotently.} + '401': {$ref: '#/components/responses/Unauthorized'} + '404': {$ref: '#/components/responses/NotFound'} + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/events: + get: + tags: [events] + operationId: resumeUserEvents + security: + - browserSession: [] + - nativeBearer: [] + parameters: + - name: version + in: query + required: false + schema: {type: string, enum: ['1']} + - name: cursor + in: query + required: false + schema: {type: string, maxLength: 512} + - name: Upgrade + in: header + required: false + schema: {type: string, enum: [websocket]} + - name: Sec-WebSocket-Version + in: header + required: false + schema: {type: string, enum: ['13']} + responses: + '200': + description: Bounded resumable NDJSON event snapshot; WebSocket upgrades emit the same envelopes as text frames. + headers: + X-VerseVDI-Event-Cursor: + schema: {type: string, maxLength: 512} + content: + application/x-ndjson: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/EventEnvelope + '400': {$ref: '#/components/responses/InvalidRequest'} + '401': {$ref: '#/components/responses/Unauthorized'} + '409': {$ref: '#/components/responses/Conflict'} + '426': {description: WebSocket version 13 is required.} + '503': {$ref: '#/components/responses/Unavailable'} +components: + securitySchemes: + browserSession: + type: apiKey + in: cookie + name: versevdi_session + nativeBearer: + type: http + scheme: bearer + bearerFormat: opaque + parameters: + DeviceID: + name: device_id + in: path + required: true + schema: {type: string, minLength: 1, maxLength: 128} + SessionID: + name: session_id + in: path + required: true + schema: {type: string, minLength: 1, maxLength: 128} + Limit: + name: limit + in: query + required: false + schema: {type: integer, minimum: 1, maximum: 100, default: 100} + Cursor: + name: cursor + in: query + required: false + schema: {type: string, maxLength: 512} + IdempotencyKey: + name: Idempotency-Key + in: header + required: true + schema: {type: string, minLength: 1, maxLength: 256} + responses: + InvalidRequest: + description: The request is invalid. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope + Unauthorized: + description: Authentication or credential validation failed without revealing credential state. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope + Forbidden: + description: The authenticated subject is not entitled to the requested action. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope + NotFound: + description: The resource is unavailable to this subject. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope + Conflict: + description: The expected version, protocol version, idempotency, or event retention boundary conflicts. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope + Unavailable: + description: The authoritative service is temporarily unavailable. + content: + application/json: + schema: + $ref: ../schemas/control-v1.schema.json#/$defs/ErrorEnvelope diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/proto/versevdi/control/v1/control.proto b/proto/versevdi/control/v1/control.proto new file mode 100644 index 0000000..b48d45c --- /dev/null +++ b/proto/versevdi/control/v1/control.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package versevdi.control.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol"; + +message VersionNegotiation { + repeated string supported_versions = 1; + repeated string features = 2; +} + +message ResourceLink { + string type = 1; + string id = 2; + int64 version = 3; +} + +message EventEnvelope { + string event_id = 1; + int64 sequence = 2; + string type = 3; + int64 version = 4; + ResourceLink resource = 5; + google.protobuf.Timestamp occurred_at = 6; + bytes payload_json = 7; + string correlation_id = 8; +} + +message ClipboardText { + string text = 1; + string encoding = 2; +} diff --git a/proto/versevdi/tunnel/v1/tunnel.proto b/proto/versevdi/tunnel/v1/tunnel.proto new file mode 100644 index 0000000..5b2f66c --- /dev/null +++ b/proto/versevdi/tunnel/v1/tunnel.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package versevdi.tunnel.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/sechmachine/VerseVDI-Protocol/gen/go/tunnel"; + +message VersionNegotiation { + repeated string supported_versions = 1; + repeated string features = 2; +} + +message AdmissionRequest { + string session_id = 1; + string protocol_version = 2; + string audience = 3; + string purpose = 4; + string grant = 5; +} + +message AdmissionResult { + bool admitted = 1; + string session_id = 2; + string failure_code = 3; + int64 session_version = 4; +} + +message SessionClose { + string session_id = 1; + string code = 2; + string reason = 3; + google.protobuf.Timestamp occurred_at = 4; +} + +message Health { + string session_id = 1; + int64 sequence = 2; + google.protobuf.Timestamp observed_at = 3; +} + +message BoundedError { + string code = 1; + string message = 2; +} + +message ChannelOpen { + string channel_id = 1; + string direction = 2; + uint32 maximum_frame_bytes = 3; +} diff --git a/registries/datagrams.json b/registries/datagrams.json new file mode 100644 index 0000000..9110e68 --- /dev/null +++ b/registries/datagrams.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "max_frame_bytes": 65536, + "datagrams": [ + {"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024}, + {"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048}, + {"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536} + ] +} diff --git a/registries/features.json b/registries/features.json new file mode 100644 index 0000000..7601b41 --- /dev/null +++ b/registries/features.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "features": [ + {"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536}, + {"id": "events.resume.v1", "direction": "server-to-client", "max_frame_bytes": 65536}, + {"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536} + ] +} diff --git a/schemas/control-v1.schema.json b/schemas/control-v1.schema.json new file mode 100644 index 0000000..28fd2df --- /dev/null +++ b/schemas/control-v1.schema.json @@ -0,0 +1,348 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://protocol.versevdi.invalid/schemas/control-v1.schema.json", + "title": "VerseVDI Phase 3A control contract", + "type": "object", + "additionalProperties": false, + "$defs": { + "FieldViolation": { + "type": "object", + "additionalProperties": false, + "required": ["field", "code"], + "properties": { + "field": {"type": "string", "minLength": 1, "maxLength": 128}, + "code": {"type": "string", "minLength": 1, "maxLength": 64} + } + }, + "ErrorEnvelope": { + "type": "object", + "additionalProperties": false, + "required": ["status", "error", "code", "message", "resolution", "request_id", "violations"], + "properties": { + "status": {"type": "boolean"}, + "error": {"type": "string", "minLength": 1, "maxLength": 512}, + "code": {"type": "string", "minLength": 1, "maxLength": 128}, + "message": {"type": "string", "minLength": 1, "maxLength": 512}, + "resolution": {"type": "string", "minLength": 1, "maxLength": 128}, + "request_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "violations": {"type": "array", "maxItems": 16, "items": {"$ref": "#/$defs/FieldViolation"}} + } + }, + "PageInfo": { + "type": "object", + "additionalProperties": false, + "required": ["limit", "next_cursor"], + "properties": { + "limit": {"type": "integer", "minimum": 1, "maximum": 100}, + "next_cursor": {"type": "string", "maxLength": 512} + } + }, + "ResourceLink": { + "type": "object", + "additionalProperties": false, + "required": ["type", "id", "version"], + "properties": { + "type": {"type": "string", "minLength": 1, "maxLength": 64}, + "id": {"type": "string", "minLength": 1, "maxLength": 128}, + "version": {"type": "integer", "minimum": 1} + } + }, + "Resource": { + "type": "object", + "additionalProperties": false, + "required": ["id", "kind", "name", "state", "version", "links"], + "properties": { + "id": {"type": "string", "minLength": 1, "maxLength": 128}, + "kind": {"type": "string", "minLength": 1, "maxLength": 64}, + "name": {"type": "string", "minLength": 1, "maxLength": 256}, + "state": {"type": "string", "minLength": 1, "maxLength": 64}, + "assignment_state": {"type": "string", "maxLength": 64}, + "version": {"type": "integer", "minimum": 1}, + "links": {"type": "array", "maxItems": 16, "items": {"$ref": "#/$defs/ResourceLink"}} + } + }, + "AssignedDesktop": { + "type": "object", + "additionalProperties": false, + "required": ["assignment_id", "pool_id", "name", "availability"], + "properties": { + "assignment_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "name": {"type": "string", "minLength": 1, "maxLength": 256}, + "availability": {"type": "string", "minLength": 1, "maxLength": 64} + } + }, + "EntitledPool": { + "type": "object", + "additionalProperties": false, + "required": ["pool_id", "name", "assignment_state"], + "properties": { + "pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "name": {"type": "string", "minLength": 1, "maxLength": 256}, + "assignment_state": {"type": "string", "minLength": 1, "maxLength": 64} + } + }, + "ResourceList": { + "type": "object", + "additionalProperties": false, + "required": ["assigned_desktops", "entitled_pools", "page"], + "properties": { + "assigned_desktops": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/AssignedDesktop"}}, + "entitled_pools": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/EntitledPool"}}, + "page": {"$ref": "#/$defs/PageInfo"} + } + }, + "DeviceRegistrationRequest": { + "type": "object", + "additionalProperties": false, + "required": ["name", "platform", "device_subject", "algorithm", "public_key"], + "properties": { + "name": {"type": "string", "minLength": 1, "maxLength": 128}, + "platform": {"type": "string", "minLength": 1, "maxLength": 64}, + "device_subject": {"type": "string", "minLength": 1, "maxLength": 256}, + "algorithm": {"type": "string", "const": "ed25519"}, + "public_key": {"type": "string", "minLength": 1, "maxLength": 256} + } + }, + "DeviceChallenge": { + "type": "object", + "additionalProperties": false, + "required": ["device_id", "server_id", "principal_id", "challenge", "expires_at", "algorithm", "signature_format"], + "properties": { + "device_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "server_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "principal_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "challenge": {"type": "string", "minLength": 1, "maxLength": 256}, + "expires_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "algorithm": {"type": "string", "const": "ed25519"}, + "signature_format": {"type": "string", "const": "ed25519-domain-separated-v1"} + } + }, + "DeviceProofRequest": { + "type": "object", + "additionalProperties": false, + "required": ["challenge", "signature"], + "properties": { + "challenge": {"type": "string", "minLength": 1, "maxLength": 256}, + "signature": {"type": "string", "minLength": 1, "maxLength": 256} + } + }, + "LoginRequest": { + "type": "object", + "additionalProperties": false, + "required": ["username", "password"], + "properties": { + "provider": {"type": "string", "enum": ["ldap", "local"]}, + "username": {"type": "string", "minLength": 1, "maxLength": 256}, + "password": {"type": "string", "minLength": 1, "maxLength": 1024} + } + }, + "ReauthRequest": { + "type": "object", + "additionalProperties": false, + "required": ["password", "purpose"], + "properties": { + "password": {"type": "string", "minLength": 1, "maxLength": 1024}, + "purpose": {"type": "string", "enum": ["identity_change", "key_change", "backup_enable", "external_database_tls_disabled", "assignment_change"]} + } + }, + "ReauthGrant": { + "type": "object", + "additionalProperties": false, + "required": ["token", "purpose", "expires_at"], + "properties": { + "token": {"type": "string", "minLength": 1, "maxLength": 256}, + "purpose": {"type": "string", "minLength": 1, "maxLength": 64}, + "expires_at": {"type": "string", "format": "date-time", "maxLength": 64} + } + }, + "RefreshRequest": { + "type": "object", + "additionalProperties": false, + "required": ["family_id", "refresh_token"], + "properties": { + "family_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "refresh_token": {"type": "string", "minLength": 1, "maxLength": 256} + } + }, + "NativeCredential": { + "type": "object", + "additionalProperties": false, + "required": ["family_id", "access_token", "refresh_token", "expires_at"], + "properties": { + "device_id": {"type": "string", "maxLength": 128}, + "family_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "access_token": {"type": "string", "minLength": 1, "maxLength": 256}, + "refresh_token": {"type": "string", "minLength": 1, "maxLength": 256}, + "expires_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64} + } + }, + "SessionRequest": { + "type": "object", + "additionalProperties": false, + "required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "policy_snapshot"], + "properties": { + "client_device_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256}, + "policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"} + } + }, + "ReconnectRequest": { + "type": "object", + "additionalProperties": false, + "required": ["client_device_id", "device_key_id", "expected_version"], + "properties": { + "client_device_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "expected_version": {"type": "integer", "minimum": 1} + } + }, + "BrokerSession": { + "type": "object", + "additionalProperties": false, + "required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version"], + "properties": { + "id": {"type": "string", "minLength": 1, "maxLength": 128}, + "principal_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "assignment_id": {"type": "string", "maxLength": 128}, + "state": {"type": "string", "minLength": 1, "maxLength": 64}, + "policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"}, + "reconnect_deadline": {"type": "string", "format": "date-time", "maxLength": 64}, + "outcome": {"type": "string", "maxLength": 64}, + "failure_code": {"type": "string", "maxLength": 128}, + "cleanup_state": {"type": "string", "minLength": 1, "maxLength": 64}, + "idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256}, + "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "requested_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "ended_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "version": {"type": "integer", "minimum": 1} + } + }, + "AllocationPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["minimum_kbps", "target_kbps", "maximum_kbps", "tier", "audience", "protocol", "protocol_version", "grant_ttl_seconds", "reservation_lease_seconds"], + "properties": { + "minimum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}, + "target_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}, + "maximum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}, + "tier": {"type": "string", "enum": ["standard", "priority", "premium"]}, + "audience": {"type": "string", "minLength": 1, "maxLength": 256}, + "protocol": {"type": "string", "minLength": 1, "maxLength": 64}, + "protocol_version": {"type": "integer", "minimum": 1, "maximum": 100}, + "grant_ttl_seconds": {"type": "integer", "minimum": 5, "maximum": 300}, + "reservation_lease_seconds": {"type": "integer", "minimum": 5, "maximum": 3600} + } + }, + "ConnectionManifest": { + "type": "object", + "additionalProperties": false, + "required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id"], + "properties": { + "version": {"type": "string", "const": "1"}, + "purpose": {"type": "string", "enum": ["launch", "reconnect"]}, + "session_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "reconnect_sequence": {"type": "integer", "minimum": 0}, + "gateway": {"$ref": "#/$defs/ManifestGateway"}, + "tunnel": {"$ref": "#/$defs/ManifestTunnel"}, + "profile": {"$ref": "#/$defs/ManifestProfile"}, + "grant": {"$ref": "#/$defs/GrantReference"}, + "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128} + } + }, + "ManifestGateway": { + "type": "object", + "additionalProperties": false, + "required": ["id", "addresses", "public_identity"], + "properties": { + "id": {"type": "string", "minLength": 1, "maxLength": 128}, + "addresses": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 256}}, + "public_identity": {"type": "string", "minLength": 1, "maxLength": 256} + } + }, + "ManifestTunnel": { + "type": "object", + "additionalProperties": false, + "required": ["versions", "features"], + "properties": { + "versions": {"type": "array", "minItems": 1, "maxItems": 4, "items": {"type": "string", "minLength": 1, "maxLength": 64}}, + "features": {"type": "array", "maxItems": 32, "items": {"type": "string", "minLength": 1, "maxLength": 64}} + } + }, + "ManifestProfile": { + "type": "object", + "additionalProperties": false, + "required": ["id", "bounds"], + "properties": { + "id": {"type": "string", "minLength": 1, "maxLength": 128}, + "bounds": {"$ref": "#/$defs/ManifestBounds"} + } + }, + "ManifestBounds": { + "type": "object", + "additionalProperties": false, + "required": ["minimum_kbps", "target_kbps", "maximum_kbps"], + "properties": { + "minimum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}, + "target_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000}, + "maximum_kbps": {"type": "integer", "minimum": 1, "maximum": 100000000} + } + }, + "GrantReference": { + "type": "object", + "additionalProperties": false, + "required": ["opaque_value", "expires_at", "audience"], + "properties": { + "opaque_value": {"type": "string", "minLength": 43, "maxLength": 256}, + "expires_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "audience": {"type": "string", "minLength": 1, "maxLength": 128} + } + }, + "EventResume": { + "type": "object", + "additionalProperties": false, + "required": ["cursor", "last_sequence"], + "properties": { + "cursor": {"type": "string", "maxLength": 512}, + "last_sequence": {"type": "integer", "minimum": 0} + } + }, + "EventEnvelope": { + "type": "object", + "additionalProperties": false, + "required": ["event_id", "sequence", "type", "version", "resource", "occurred_at", "correlation_id", "payload"], + "properties": { + "event_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "sequence": {"type": "integer", "minimum": 1}, + "type": {"type": "string", "minLength": 1, "maxLength": 128}, + "version": {"type": "integer", "minimum": 1}, + "resource": {"$ref": "#/$defs/ResourceLink"}, + "occurred_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "payload": {"type": "object", "x-max-bytes": 16384} + } + }, + "ClipboardText": { + "type": "object", + "additionalProperties": false, + "required": ["text", "encoding"], + "properties": { + "text": {"type": "string", "maxLength": 65536}, + "encoding": {"type": "string", "const": "utf-8"} + } + }, + "VersionNegotiation": { + "type": "object", + "additionalProperties": false, + "required": ["supported_versions", "features"], + "properties": { + "supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}}, + "features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}} + } + } + } +}