syntax = "proto3";
package meshllm.node.v1;

// Stream 0x01 — Gossip
message GossipFrame {
  uint32 gen = 1;
  repeated PeerAnnouncement peers = 2;
  bytes sender_id = 3;          // must be exactly 32 bytes; must match QUIC peer identity
}

message PeerAnnouncement {
  bytes endpoint_id = 1;          // exactly 32 bytes
  NodeRole role = 2;
  optional uint32 http_port = 3;
  optional string version = 4;
  optional string gpu_name = 5;                      // Deprecated in v0.60.0: prefer hardware.gpus[].name
  optional string hostname = 6;                      // Deprecated in v0.60.0: prefer hardware.hostname
  optional bool is_soc = 7;                          // Deprecated in v0.60.0: prefer hardware.is_soc; true for system-on-chip / unified-memory hosts such as Apple Silicon and Jetson
  optional string gpu_vram = 8;                      // Deprecated in v0.60.0: prefer hardware.gpus[].vram_bytes
  repeated string available_models = 9;              // Deprecated in v0.55.0 for protobuf gossip; compatibility surface only
  repeated string serving_models = 10;               // Deprecated in v0.55.0: prefer served_model_identities and served_model_runtime
  repeated string requested_models = 11;
  repeated CompactModelMetadata available_model_metadata = 12;
  optional ExpertsSummary experts_summary = 13;
  optional uint32 rtt_ms = 14;
  repeated string catalog_models = 15;               // GGUF model names (mesh catalog contribution; was JSON `models`)
  uint64 vram_bytes = 16;                            // GPU VRAM in bytes
  optional string model_source = 17;                 // how to obtain this node's model
  optional string primary_serving = 18;              // primary serving model (backward compat; was JSON `serving`)
  optional string mesh_id = 19;                      // stable mesh identity (self entry only)
  repeated ModelDemandEntry demand = 20;             // demand map entries (self entry only)
  map<string, uint64> available_model_sizes = 21;   // file sizes (bytes) per GGUF model name
  bytes serialized_addr = 22;                        // JSON-serialized EndpointAddr (includes network addresses for peer discovery)
  repeated string hosted_models = 23;                // actually routable / healthy
  optional bool hosted_models_known = 24;            // false/absent when relayed from legacy JSON peers
  repeated ServedModelDescriptor served_model_descriptors = 25;
  repeated ServedModelIdentity served_model_identities = 26;
  repeated ModelRuntimeDescriptor served_model_runtime = 27;
  optional SignedNodeOwnership owner_attestation = 28;
  optional string gpu_mem_bandwidth_gbps = 29;      // Deprecated in v0.60.0: prefer hardware.gpus[].mem_bandwidth_gbps
  optional string gpu_compute_tflops_fp32 = 30;     // Deprecated in v0.60.0: prefer hardware.gpus[].compute_tflops_fp32
  optional string gpu_compute_tflops_fp16 = 31;     // Deprecated in v0.60.0: prefer hardware.gpus[].compute_tflops_fp16
  optional string gpu_reserved_bytes = 32;          // Deprecated in v0.60.0: prefer hardware.gpus[].reserved_bytes
  optional HardwareInfo hardware = 33;              // Introduced in v0.60.0; preferred structured hardware inventory
  optional uint64 first_joined_mesh_ts = 34;
  repeated string explicit_model_interests = 35;    // Advisory canonical refs this node wants the mesh to consider
  reserved 36;
  reserved "artifact_transfer_supported";
  repeated MeshSubprotocol subprotocols = 37;       // Additive feature discovery for admitted subsystem protocols
  optional uint32 latency_ms = 40;
  optional LatencySource latency_source = 41;
  optional uint32 latency_age_ms = 42;
  optional bytes latency_observer_id = 43;
  repeated AdvertisedModelThroughput advertised_model_throughput = 44;

enum LatencySource {
  LATENCY_SOURCE_UNSPECIFIED = 0;
  LATENCY_SOURCE_DIRECT = 1;       // Measured directly by this node
  LATENCY_SOURCE_ESTIMATED = 2;    // Estimated via transitive gossip
  LATENCY_SOURCE_UNKNOWN = 3;      // Unknown source (e.g. propagated with no origin)
}
}

message AdvertisedModelThroughput {
  string model_name = 1;
  uint64 avg_tokens_per_second_milli = 2;
  uint64 throughput_samples = 3;
}

message MeshSubprotocol {
  string name = 1;             // e.g. "skippy-stage"
  uint32 major = 2;            // subprotocol major version
  repeated string features = 3; // e.g. "artifact-transfer"
}

// Stream 0x0d — generic admitted subsystem stream.
// Mesh owns this open frame. The remaining bytes on the stream are opaque to
// mesh and are owned by the selected subsystem protocol.
message MeshSubprotocolOpen {
  uint32 gen = 1;
  string name = 2;             // e.g. "skippy-stage"
  uint32 major = 3;            // subprotocol major version
}

message HardwareInfo {
  optional bool is_soc = 1;                          // True for system-on-chip / unified-memory hosts such as Apple Silicon and Jetson
  optional string hostname = 2;
  repeated GpuInfo gpus = 3;
}

message GpuInfo {
  optional string name = 1;
  optional string vram_bytes = 2;
  optional string reserved_bytes = 3;
  optional string mem_bandwidth_gbps = 4;
  optional string compute_tflops_fp32 = 5;
  optional string compute_tflops_fp16 = 6;
}

message SignedNodeOwnership {
  uint32 version = 1;
  string cert_id = 2;
  string owner_id = 3;
  bytes owner_sign_public_key = 4;   // 32 bytes
  bytes node_endpoint_id = 5;        // 32 bytes
  uint64 issued_at_unix_ms = 6;
  uint64 expires_at_unix_ms = 7;
  optional string node_label = 8;
  optional string hostname_hint = 9;
  bytes signature = 10;              // Ed25519 signature over canonical claim bytes
}

message ServedModelDescriptor {
  ServedModelIdentity identity = 1;
  ModelCapabilities capabilities = 2;
  optional ModelTopology topology = 3;
  optional bool capabilities_known = 4;
}

message ServedModelIdentity {
  string model_name = 1;
  bool is_primary = 2;
  ModelSourceKind source_kind = 3;
  optional string canonical_ref = 4;
  optional string repository = 5;
  optional string revision = 6;
  optional string artifact = 7;
  optional string local_file_name = 8;
  optional string identity_hash = 9;
}

message ModelCapabilities {
  CapabilityLevel vision = 1;
  CapabilityLevel reasoning = 2;
  CapabilityLevel tool_use = 3;
  bool moe = 4;
  bool multimodal = 5;
  CapabilityLevel audio = 6;
}

enum CapabilityLevel {
  CAPABILITY_LEVEL_UNSPECIFIED = 0;
  CAPABILITY_LEVEL_NONE = 1;
  CAPABILITY_LEVEL_LIKELY = 2;
  CAPABILITY_LEVEL_SUPPORTED = 3;
}

message ModelTopology {
  optional ModelMoeInfo moe = 1;
}

message ModelRuntimeDescriptor {
  string model_name = 1;
  optional string identity_hash = 2;
  optional uint32 context_length = 3;
  bool ready = 4;
}

message ModelMoeInfo {
  uint32 expert_count = 1;
  uint32 used_expert_count = 2;
  optional uint32 min_experts_per_node = 3;
  optional string source = 4;
  optional string ranking_source = 5;
  repeated uint32 ranking = 6;
  optional uint32 ranking_prompt_count = 7;
  optional uint32 ranking_tokens = 8;
  optional string ranking_layer_scope = 9;
  optional string ranking_origin = 10;
}

message CompactModelMetadata {
  string model_key = 1;
  uint32 context_length = 2;
  uint32 vocab_size = 3;
  uint32 embedding_size = 4;
  uint32 head_count = 5;
  uint32 layer_count = 6;
  uint32 feed_forward_length = 7;
  uint32 key_length = 8;
  uint32 value_length = 9;
  string architecture = 10;
  string tokenizer_model_name = 11;
  repeated SpecialToken special_tokens = 12;
  float rope_scale = 13;
  float rope_freq_base = 14;
  bool is_moe = 15;
  uint32 expert_count = 16;
  uint32 used_expert_count = 17;
  string quantization_type = 18;                     // GGUF quantization type string (e.g. "Q4_K_M")
}

message SpecialToken {
  string name = 1;
  uint32 token_id = 2;
}

message ExpertsSummary {
  uint32 total_experts = 1;
  uint32 expert_count_used = 2;
  repeated uint32 top_expert_ids = 3;
}

enum ModelSourceKind {
  MODEL_SOURCE_KIND_UNSPECIFIED = 0;
  MODEL_SOURCE_KIND_CATALOG = 1;
  MODEL_SOURCE_KIND_HUGGING_FACE = 2;
  MODEL_SOURCE_KIND_LOCAL_GGUF = 3;
  MODEL_SOURCE_KIND_DIRECT_URL = 4;
  MODEL_SOURCE_KIND_UNKNOWN = 5;
}

message ModelDemandEntry {
  string model_name = 1;
  uint64 last_active = 2;
  uint64 request_count = 3;
}

// Stream 0x03 — Tunnel Map
message TunnelMap {
  bytes owner_peer_id = 1;
  repeated TunnelEntry entries = 2;
}

message TunnelEntry {
  bytes target_peer_id = 1;
  optional bytes relay_peer_id = 2;
  uint32 tunnel_port = 3;
}

// Stream 0x05 — Route Table
message RouteTableRequest {
  bytes requester_id = 1;  // 0 or exactly 32 bytes (passive callers may omit)
  uint32 gen = 2;          // must equal NODE_PROTOCOL_GENERATION; rejected otherwise
}

message RouteTable {
  repeated RouteEntry entries = 1;
  optional string mesh_id = 2;  // stable mesh identity — passive/client callers learn this here
  uint32 gen = 3;               // must equal NODE_PROTOCOL_GENERATION; caller rejects mismatches
}

message RouteEntry {
  bytes endpoint_id = 1;  // exactly 32 bytes (peer public key)
  string model = 2;       // model being served (empty if node is not actively serving)
}

// Stream 0x06 — Peer Down
message PeerDown {
  bytes peer_id = 1;   // exactly 32 bytes; the peer being reported as unreachable
  uint32 gen = 2;      // must equal NODE_PROTOCOL_GENERATION; rejected otherwise
}

// Stream 0x07 — Peer Leaving
message PeerLeaving {
  bytes peer_id = 1;   // exactly 32 bytes; must match the QUIC sender identity
  uint32 gen = 2;      // must equal NODE_PROTOCOL_GENERATION; rejected otherwise
}

// Shared enum
enum NodeRole {
  NODE_ROLE_UNSPECIFIED = 0;
  WORKER = 1;
  HOST = 2;
  CLIENT = 3;
}

message NodeConfigSnapshot {
  uint32 version = 1;                     // config schema version (currently 1)
  NodeGpuConfig gpu = 2;
  repeated NodeModelEntry models = 3;
  repeated NodePluginEntry plugins = 4;
  optional string config_toml = 5;        // canonical persisted config payload for additive owner-control roundtrip
}

enum GpuAssignment {
  GPU_ASSIGNMENT_UNSPECIFIED = 0;
  GPU_ASSIGNMENT_AUTO = 1;
  GPU_ASSIGNMENT_PINNED = 2;
}

message NodeGpuConfig {
  GpuAssignment assignment = 1;
}

message ConfiguredModelRef {
  string declared_ref = 1;
  optional string source_kind = 2;
  optional string revision = 3;
}

message NodeModelEntry {
  string model = 1;
  optional string mmproj = 2;
  optional uint32 ctx_size = 3;
  optional string gpu_id = 4;
  ConfiguredModelRef model_ref = 5;
  ConfiguredModelRef mmproj_ref = 6;
}

message NodePluginEntry {
  string name = 1;
  optional bool enabled = 2;
  optional string command = 3;
  repeated string args = 4;
}

enum ConfigApplyMode {
  CONFIG_APPLY_MODE_UNSPECIFIED = 0;
  CONFIG_APPLY_MODE_STAGED = 1;
  CONFIG_APPLY_MODE_LIVE = 2;
  CONFIG_APPLY_MODE_NOOP = 3;
}

message OwnerControlEnvelope {
  uint32 gen = 1;                         // must equal NODE_PROTOCOL_GENERATION
  OwnerControlHandshake handshake = 2;
  OwnerControlRequest request = 3;
  OwnerControlResponse response = 4;
  OwnerControlError error = 5;
}

message OwnerControlHandshake {
  SignedNodeOwnership ownership = 1;
}

message OwnerControlRequest {
  uint64 request_id = 1;
  OwnerControlGetConfigRequest get_config = 2;
  OwnerControlWatchConfigRequest watch_config = 3;
  OwnerControlApplyConfigRequest apply_config = 4;
  OwnerControlRefreshInventoryRequest refresh_inventory = 5;
}

message OwnerControlResponse {
  uint64 request_id = 1;
  OwnerControlGetConfigResponse get_config = 2;
  OwnerControlWatchConfigResponse watch_config = 3;
  OwnerControlApplyConfigResponse apply_config = 4;
  OwnerControlRefreshInventoryResponse refresh_inventory = 5;
}

message OwnerControlError {
  OwnerControlErrorCode code = 1;
  string message = 2;
  optional uint64 request_id = 3;
  optional uint64 current_revision = 4;
}

enum OwnerControlErrorCode {
  OWNER_CONTROL_ERROR_CODE_UNSPECIFIED = 0;
  OWNER_CONTROL_ERROR_CODE_BAD_REQUEST = 1;
  OWNER_CONTROL_ERROR_CODE_UNAUTHORIZED = 2;
  OWNER_CONTROL_ERROR_CODE_REVISION_CONFLICT = 3;
  OWNER_CONTROL_ERROR_CODE_CONTROL_UNSUPPORTED = 4;
  OWNER_CONTROL_ERROR_CODE_CONTROL_ENDPOINT_REQUIRED = 5;
  OWNER_CONTROL_ERROR_CODE_CONTROL_UNAVAILABLE = 6;
  OWNER_CONTROL_ERROR_CODE_UNKNOWN_COMMAND = 7;
  OWNER_CONTROL_ERROR_CODE_LEGACY_JSON_UNSUPPORTED = 8;
  OWNER_CONTROL_ERROR_CODE_INVALID_HANDSHAKE = 9;
  OWNER_CONTROL_ERROR_CODE_TARGET_NODE_MISMATCH = 10;
}

message OwnerControlGetConfigRequest {
  bytes requester_node_id = 1;            // exactly 32 bytes
  bytes target_node_id = 2;               // exactly 32 bytes
}

message OwnerControlGetConfigResponse {
  OwnerControlConfigSnapshot snapshot = 1;
}

message OwnerControlWatchConfigRequest {
  bytes requester_node_id = 1;            // exactly 32 bytes
  bytes target_node_id = 2;               // exactly 32 bytes
  bool include_snapshot = 3;
}

message OwnerControlWatchConfigResponse {
  OwnerControlWatchAccepted accepted = 1;
  OwnerControlConfigSnapshot snapshot = 2;
  OwnerControlConfigUpdate update = 3;
}

message OwnerControlWatchAccepted {
  bytes target_node_id = 1;               // exactly 32 bytes
}

message OwnerControlApplyConfigRequest {
  bytes requester_node_id = 1;            // exactly 32 bytes
  bytes target_node_id = 2;               // exactly 32 bytes
  uint64 expected_revision = 3;
  NodeConfigSnapshot config = 4;
}

message OwnerControlApplyConfigResponse {
  bool success = 1;
  uint64 current_revision = 2;
  bytes config_hash = 3;
  optional string error = 4;
  ConfigApplyMode apply_mode = 5;
}

message OwnerControlRefreshInventoryRequest {
  bytes requester_node_id = 1;            // exactly 32 bytes
  bytes target_node_id = 2;               // exactly 32 bytes
}

message OwnerControlRefreshInventoryResponse {
  OwnerControlConfigSnapshot snapshot = 1;
}

message OwnerControlConfigSnapshot {
  bytes node_id = 1;                      // exactly 32 bytes
  uint64 revision = 2;
  bytes config_hash = 3;                  // SHA-256 of canonical proto bytes (32 bytes)
  NodeConfigSnapshot config = 4;
  optional string hostname = 5;
}

message OwnerControlConfigUpdate {
  bytes node_id = 1;                      // exactly 32 bytes
  uint64 revision = 2;
  bytes config_hash = 3;                  // SHA-256 of canonical proto bytes (32 bytes)
  NodeConfigSnapshot config = 4;
}
