diff --git a/proto/vantage/v1/vantage.proto b/proto/vantage/v1/vantage.proto new file mode 100644 index 0000000..c0a2822 --- /dev/null +++ b/proto/vantage/v1/vantage.proto @@ -0,0 +1,392 @@ +syntax = "proto3"; + +package vantage.v1; + +option go_package="gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"; + +service Vantage { + rpc Register(RegisterRequest) returns (RegisterResponse); + rpc SyncKeys(SyncRequest) returns (SyncResponse); + rpc UploadGeneratedKey(UploadKeyRequest) returns (UploadKeyResponse); + rpc ReportUpdates(ReportUpdatesRequest) returns (ReportUpdatesResponse); + rpc ReportPackages(ReportPackagesRequest) returns (ReportPackagesResponse); + rpc ReportWorkloads(ReportWorkloadsRequest) returns (ReportWorkloadsResponse); + rpc ReportInventory(InventoryReport) returns (InventoryReportResponse); + rpc SyncMonitors(SyncMonitorsRequest) returns (SyncMonitorsResponse); + rpc ReportChecks(ReportChecksRequest) returns (ReportChecksResponse); + // Bidirectional stream: agent sends auth once, server pushes commands. + rpc CommandStream(stream AgentMessage) returns (stream ServerCommand); + rpc ProxyStream(stream ProxyClientMsg) returns (stream ProxyServerMsg); +} + +message RegisterRequest { + string server_id = 1; + string pre_reg_token = 2; + string hostname = 3; + string ip_address = 4; + string os_info = 5; +} + +message RegisterResponse { + string agent_token = 1; +} + +message SyncRequest { + string server_id = 1; + string agent_token = 2; + string agent_version = 3; +} + +message SyncResponse { + repeated string public_keys = 1; + + // collect_packages tells the agent whether this instance's licence grants + // vulnerability scanning. False means do not collect at all: no gRPC body, + // no document, no storage. The server re-checks on ReportPackages — this + // flag is the optimisation, the server check is the boundary. + // + // Absent reads as false, which is the safe direction: an old server that + // does not send it leaves agents collecting nothing. + bool collect_packages = 2; +} + +// ReportPackages carries a server's installed package set. +// +// The agent calls twice at most. The first call sends only the hash; if the +// server already holds that hash it answers need_full = false and the ~150KB +// body is never sent. A machine's package set changes rarely, so almost every +// hour costs one small message. +message ReportPackagesRequest { + string server_id = 1; + string agent_token = 2; + string hash = 3; + OSRelease os = 4; + repeated InstalledPackage packages = 5; // empty on the offer call +} + +message ReportPackagesResponse { + bool need_full = 1; +} + +message OSRelease { + string family = 1; + // version_id is not optional: Ubuntu 22.04 and 24.04 publish different fixed + // versions for the same CVE, so a scan without it is guesswork. + string version_id = 2; + string arch = 3; +} + +message InstalledPackage { + string name = 1; + string version = 2; + int32 epoch = 3; + string arch = 4; + // source_name is what the Debian and Ubuntu feeds are keyed on: one advisory + // against "openssl" covers libssl3, openssl and libssl-dev. + string source_name = 5; +} + +message UploadKeyRequest { + string server_id = 1; + string agent_token = 2; + string public_key = 3; + string label = 4; + string private_key = 5; +} + +message UploadKeyResponse { + string key_id = 1; +} + +// CommandStream messages + +message AgentMessage { + string server_id = 1; + string agent_token = 2; + oneof payload { + AgentReady ready = 3; + CommandResult result = 4; + StepResult step_result = 5; + StepOutputChunk step_output = 6; + WorkloadLogsResult workload_logs_result = 7; + } +} + +message AgentReady { + +} + +message CommandResult { + string command_id = 1; + bool success = 2; + string message = 3; +} + +message PackageUpdate { + string name = 1; + string current_version = 2; + string new_version = 3; +} + +message ReportUpdatesRequest { + string server_id = 1; + string agent_token = 2; + repeated PackageUpdate updates = 3; +} + +message ReportUpdatesResponse { + +} + +message CPUReport { + string model = 1; + int32 cores = 2; + double usage_pct = 3; + double load1 = 4; +} + +message MemReport { + uint64 total_bytes = 1; + uint64 used_bytes = 2; +} + +message PartitionReport { + string device = 1; + string mountpoint = 2; + string fstype = 3; + uint64 total_bytes = 4; + uint64 used_bytes = 5; +} + +message InventoryReport { + string server_id = 1; + string agent_token = 2; + bool include_static = 3; + CPUReport cpu = 4; + MemReport memory = 5; + uint64 swap_total = 6; + uint64 swap_used = 7; + repeated PartitionReport partitions = 8; + string kernel = 9; + // Set on static snapshots only. The agent never reboots; it reports that one + // is owed and leaves the decision to a person or a workflow. + bool reboot_required = 10; +} + +message InventoryReportResponse { + +} + +message MonitorSpec { + string monitor_id = 1; + string type = 2; + string url = 3; + string host = 4; + int32 port = 5; + string method = 6; + int32 expected_status = 7; + string keyword = 8; + int32 tls_warn_days = 9; + int32 interval_sec = 10; + int32 retries = 11; + bool insecure = 12; +} + +message SyncMonitorsRequest { + string server_id = 1; + string agent_token = 2; +} + +message SyncMonitorsResponse { + repeated MonitorSpec monitors = 1; +} + +message CheckResult { + string monitor_id = 1; + bool up = 2; + int32 latency_ms = 3; + string message = 4; + int64 cert_expiry_unix = 5; +} + +message ReportChecksRequest { + string server_id = 1; + string agent_token = 2; + repeated CheckResult results = 3; +} + +message ReportChecksResponse { + +} + +message ApplyUpdatesCmd { + +} + +message ServerCommand { + string command_id = 1; + oneof command { + GenerateKeyCmd generate_key = 2; + DeleteKeyCmd delete_key = 3; + UpdateAgentCmd update_agent = 4; + ApplyUpdatesCmd apply_updates = 5; + RunStepCmd run_step = 6; + CleanupWorkspaceCmd cleanup_workspace = 7; + OpenProxyCmd open_proxy = 8; + PingCmd ping = 9; + RefreshWorkloadsCmd refresh_workloads = 10; + ControlWorkloadCmd control_workload = 11; + WorkloadLogsCmd workload_logs = 12; + } +} + +// PingCmd is a liveness beat, carrying nothing and requiring no reply. +// +// It exists because gRPC keepalive cannot prove what the agent needs to know. +// Behind an L7 proxy the agent's HTTP/2 connection terminates at the proxy, so +// keepalive pings are answered by the proxy whether or not the server behind it +// is still there. A pod that dies leaves the agent blocked in Recv on a stream +// that will never produce another message and never error — commands are +// dispatched into it and silently lost. Only traffic that originates at the +// server itself distinguishes a live stream from an orphaned one. +message PingCmd { +} + +// CleanupWorkspaceCmd tells the agent to recursively remove the run's working +// directory once all steps on that server have finished. +message CleanupWorkspaceCmd { + string workspace_id = 1; +} + +message DeleteKeyCmd { + string label = 1; +} + +message UpdateAgentCmd { + string version = 1; // e.g. "1.2.3" + string gitea_base_url = 2; // e.g. "https://gitea.example.com" +} + +message GenerateKeyCmd { + string label = 1; + string key_type = 2; // ed25519 | rsa | ecdsa (default: ed25519) + int32 key_size = 3; // bits; used for rsa and ecdsa + string passphrase = 4; // empty = no passphrase + string comment = 5; // embedded in public key +} + +message RunStepCmd { + string interpreter = 1; // "bash" | "powershell" + string script = 2; + map env = 3; + int32 timeout_seconds = 4; + string workspace_id = 5; +} + +message StepResult { + string command_id = 1; + int32 exit_code = 2; + string stdout = 3; + string stderr = 4; + map output_env = 5; +} + +message StepOutputChunk { + string command_id = 1; + uint64 seq = 2; + bytes data = 3; + bool eof = 4; +} + +// OpenProxyCmd tells the agent to dial 127.0.0.1:port locally and relay that +// connection back over a fresh ProxyStream identified by proxy_id. +message OpenProxyCmd { + string proxy_id = 1; + uint32 port = 2; +} + +message ProxyOpen { + string server_id = 1; + string agent_token = 2; + string proxy_id = 3; +} + +message ProxyClose { string reason = 1; } + +message ProxyClientMsg { + oneof payload { + ProxyOpen open = 1; // first message only + bytes data = 2; + ProxyClose close = 3; + } +} + +message ProxyServerMsg { + oneof payload { + bytes data = 1; + ProxyClose close = 2; + } +} + +// --------------------------------------------------------------------------- +// Workload registry + +// ReportWorkloads carries what a server is running. +// +// Offer-then-send, the same handshake as ReportPackages: the agent calls once +// with workloads empty, and resends with the body only if need_full is set. +message ReportWorkloadsRequest { + string server_id = 1; + string agent_token = 2; + string hash = 3; + bool docker_ok = 4; + string docker_error = 5; + bool systemd_ok = 6; + string systemd_error = 7; + repeated Workload workloads = 8; // empty on the offer call + // full marks the second call. It is not inferred from an empty workloads + // list: a host running nothing sends an empty list as its full report. + bool full = 9; +} + +message ReportWorkloadsResponse { + bool need_full = 1; +} + +message Workload { + string kind = 1; // "container" | "unit" + string id = 2; + string name = 3; + string state = 4; + string health = 5; + string image = 6; + string stack = 7; + repeated string ports = 8; + int32 restarts = 9; + string started_at = 10; // RFC3339, empty when not running + bool protected = 11; +} + +// RefreshWorkloadsCmd carries no payload back. It makes the agent report +// immediately through ReportWorkloads, so there is exactly one writer for the +// server_workloads collection rather than two arriving by different routes. +message RefreshWorkloadsCmd {} + +message ControlWorkloadCmd { + string kind = 1; + string id = 2; + string action = 3; // "start" | "stop" | "restart" +} + +message WorkloadLogsCmd { + string kind = 1; + string id = 2; + int32 tail = 3; +} + +message WorkloadLogsResult { + string command_id = 1; + string text = 2; + bool truncated = 3; + string error = 4; +}