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;
}

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<string, string> 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<string, string> 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;
}
