Files
panel/proto/panel/v1/agent.proto
T
dbledeez 295eb22826 panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:26:41 -07:00

786 lines
27 KiB
Protocol Buffer

syntax = "proto3";
package panel.v1;
option go_package = "github.com/dbledeez/panel/proto/panel/v1;panelv1";
import "google/protobuf/timestamp.proto";
import "panel/v1/common.proto";
// Agent is the gRPC service a Target agent dials on the Controller.
// A single persistent bidirectional stream carries all control-plane
// traffic between Controller and Target: commands downstream, state
// and events upstream.
service Agent {
rpc Connect(stream AgentEnvelope) returns (stream ControllerEnvelope);
}
// ControllerEnvelope — messages from Controller to Target.
message ControllerEnvelope {
string correlation_id = 1;
google.protobuf.Timestamp sent_at = 2;
oneof payload {
ControllerHello hello = 10;
Ping ping = 11;
ExecRequest exec = 20;
InstanceCreate instance_create = 30;
InstanceStart instance_start = 31;
InstanceStop instance_stop = 32;
InstanceDelete instance_delete = 33;
RCONCommand rcon = 40;
FsListRequest fs_list = 50;
FsReadRequest fs_read = 51;
FsWriteRequest fs_write = 52;
FsDeleteRequest fs_delete = 53;
FsSymlinkRequest fs_symlink = 54;
FsExtractRequest fs_extract = 55;
FsCompressRequest fs_compress = 56;
FsRenameRequest fs_rename = 57;
FsWriteChunkRequest fs_write_chunk = 58;
ArkSaveRestoreRequest ark_save_restore = 59;
DayzModInstallRequest dayz_mod_install = 90;
DayzModUninstallRequest dayz_mod_uninstall = 91;
UpdateRequest update = 60;
BackupRequest backup = 70;
RestoreRequest restore = 71;
BackupListEntriesRequest backup_list_entries = 72;
BackupReadFileRequest backup_read_file = 73;
EmpyrionScenarioInstallRequest empyrion_scenario_install = 100;
EmpyrionDiscoveriesRequest empyrion_discoveries = 101;
EmpyrionPlayerSummaryRequest empyrion_player_summary = 102;
RegionScanRequest region_scan = 110;
RegionHealRequest region_heal = 111;
InstanceRenderConfigRequest instance_render_config = 112;
SeedModsRequest seed_mods = 113;
UpdateCheckRequest update_check = 114;
}
}
// AgentEnvelope — messages from Target to Controller.
message AgentEnvelope {
string correlation_id = 1;
google.protobuf.Timestamp sent_at = 2;
oneof payload {
AgentHello hello = 10;
Heartbeat heartbeat = 11;
Pong pong = 12;
CommandResult result = 20;
InstanceStateUpdate instance_state = 30;
AppStateUpdate app_state = 31;
LogLine log = 32;
PlayerEvent player = 33;
RCONResult rcon_result = 40;
FsListResult fs_list_result = 50;
FsReadResult fs_read_result = 51;
FsWriteResult fs_write_result = 52;
FsDeleteResult fs_delete_result = 53;
FsSymlinkResult fs_symlink_result = 54;
FsExtractResult fs_extract_result = 55;
FsCompressResult fs_compress_result = 56;
FsRenameResult fs_rename_result = 57;
FsWriteChunkResult fs_write_chunk_result = 58;
ArkSaveRestoreResult ark_save_restore_result = 59;
DayzModInstallResult dayz_mod_install_result = 90;
DayzModUninstallResult dayz_mod_uninstall_result = 91;
UpdateResult update_result = 60;
BackupResult backup_result = 70;
RestoreResult restore_result = 71;
BackupListEntriesResult backup_list_entries_result = 72;
BackupReadFileResult backup_read_file_result = 73;
InstanceStatsUpdate instance_stats = 80;
EmpyrionScenarioInstallResult empyrion_scenario_install_result = 100;
EmpyrionDiscoveriesResult empyrion_discoveries_result = 101;
EmpyrionPlayerSummaryResult empyrion_player_summary_result = 102;
RegionScanResult region_scan_result = 110;
RegionHealResult region_heal_result = 111;
InstanceRenderConfigResult instance_render_config_result = 112;
SeedModsResult seed_mods_result = 113;
UpdateCheckResult update_check_result = 114;
}
}
// ---- Handshake ----
message AgentHello {
string agent_id = 1;
string agent_version = 2;
string host_os = 3; // "linux", "windows"
string host_arch = 4; // "amd64", "arm64"
string hostname = 5;
repeated string supported_runtimes = 6; // "docker", "host"
repeated ModuleSummary modules = 7;
}
// ModuleSummary is what an agent advertises about one locally-loaded
// module at handshake time. The controller aggregates these across all
// agents so the dashboard can render a "known modules" picker without
// needing direct filesystem access to each agent's modules/ dir.
message ModuleSummary {
string id = 1;
string name = 2;
string version = 3;
repeated string supported_modes = 4;
repeated ModuleUpdateProviderSummary update_providers = 5;
bool has_rcon = 6;
repeated string authors = 7;
}
message ModuleUpdateProviderSummary {
string id = 1;
string kind = 2; // "steamcmd" | "github" | "direct"
// True when the provider sets `requires_steam_login: true` on the
// manifest (DayZ, Arma). Controller uses this to decide whether to
// pop the Steam-login modal before forwarding an UpdateRequest.
bool requires_steam_login = 3;
}
message ControllerHello {
string controller_version = 1;
google.protobuf.Timestamp server_time = 2;
}
message Heartbeat {
google.protobuf.Timestamp at = 1;
double cpu_percent = 2;
uint64 mem_used_bytes = 3;
uint64 mem_total_bytes = 4;
uint32 instance_count = 5;
}
message Ping { string nonce = 1; }
message Pong { string nonce = 1; }
// ---- Exec / commands ----
message ExecRequest {
string instance_id = 1; // empty = run on Target host
repeated string argv = 2;
map<string, string> env = 3;
string working_dir = 4;
uint32 timeout_seconds = 5;
}
message CommandResult {
int32 exit_code = 1;
bytes stdout = 2;
bytes stderr = 3;
Error error = 4;
}
// ---- Instance lifecycle ----
message InstanceCreate {
string instance_id = 1;
string module_id = 2;
string version = 3;
RunMode run_mode = 4;
ResourceLimits limits = 5;
map<string, string> config_values = 6; // flattened key/value, module schema-validated
repeated PortMap ports = 7;
string data_path = 8;
// mount_overrides lets the controller swap one of the module's
// manifest-defined volume mounts at create time. Keyed by container
// path (matching a volume entry's `container:` field in module.yaml).
// Value is either a Docker named volume name (no leading slash) or an
// absolute host path (leading slash = bind mount).
//
// Used by the ARK cluster feature to swap an instance's per-instance
// `panel-<id>-cluster` named volume for a shared `panel-ark-cluster-<id>`
// volume when the server joins a cluster. Generic enough for future
// features — any module can benefit from per-instance mount swaps
// without needing a manifest change.
map<string, string> mount_overrides = 9;
}
message InstanceStart { string instance_id = 1; }
message InstanceStop {
string instance_id = 1;
uint32 grace_seconds = 2;
bool force = 3;
}
// InstanceDelete tears down the instance record. If purge_volumes is
// true, every Docker volume labeled panel.instance_id=<instance_id> is
// removed too; otherwise volumes survive so a subsequent re-create can
// pick up the existing world/save data.
message InstanceDelete {
string instance_id = 1;
uint32 grace_seconds = 2; // if the container is still running, stop with this grace first
bool purge_volumes = 3;
}
// ---- State streams ----
message InstanceStateUpdate {
string instance_id = 1;
InstanceStatus status = 2;
int32 exit_code = 3;
google.protobuf.Timestamp at = 4;
string detail = 5;
}
message AppStateUpdate {
string instance_id = 1;
int32 players_online = 2;
int32 players_max = 3;
int64 uptime_seconds = 4;
map<string, string> module_fields = 10; // world_seed, day_number, tps, etc.
google.protobuf.Timestamp at = 20;
}
message LogLine {
string instance_id = 1;
string stream = 2; // "stdout", "stderr", or a named log file
google.protobuf.Timestamp at = 3;
string line = 4;
}
// InstanceStatsUpdate is emitted periodically (default every 10s) by the
// agent's per-instance stats poller. Values come from Docker's stats
// stream API. Kept sparse — enough for UI gauges + future scheduler
// expressions like "cpu_percent > 80 sustained 5m -> notify".
message InstanceStatsUpdate {
string instance_id = 1;
double cpu_percent = 2; // 100 = 1 vCPU saturated
uint64 mem_used_bytes = 3;
uint64 mem_limit_bytes = 4; // 0 if no explicit limit set (then use container's view)
uint64 net_rx_bytes = 5; // cumulative since container start
uint64 net_tx_bytes = 6;
uint32 pids = 7;
google.protobuf.Timestamp at = 8;
// Host core count (docker OnlineCPUs) so UIs can translate the
// docker-style cpu_percent (100 = 1 core) into percent-of-host.
uint32 host_cpus = 9;
}
// ---- Ad-hoc RCON ----
message RCONCommand {
string instance_id = 1;
string command = 2;
}
message RCONResult {
string output = 1;
string error = 2; // empty on success; short machine-readable on failure
}
// ---- File management (rooted at each instance's data_path) ----
message FsEntry {
string name = 1; // base name
string path = 2; // path relative to instance data_path
bool is_dir = 3;
int64 size = 4; // bytes (for files)
google.protobuf.Timestamp mod_time = 5;
}
message FsListRequest { string instance_id = 1; string path = 2; }
message FsListResult {
repeated FsEntry entries = 1;
string path = 2;
string error = 3;
}
message FsReadRequest { string instance_id = 1; string path = 2; }
message FsReadResult {
string path = 1;
bytes content = 2;
string error = 3;
}
message FsWriteRequest { string instance_id = 1; string path = 2; bytes content = 3; }
message FsWriteResult { int64 bytes_written = 1; string error = 2; }
message FsDeleteRequest { string instance_id = 1; string path = 2; bool recursive = 3; }
message FsDeleteResult { string error = 1; }
// FsSymlinkRequest creates (or replaces) a symlink inside the instance
// container. Used by the DayZ mod manager to link @ModName entries to
// the shared workshop tree at /game/steamapps/workshop/content/...
message FsSymlinkRequest { string instance_id = 1; string target = 2; string link_path = 3; }
message FsSymlinkResult { string error = 1; }
// FsExtractRequest expands an archive that already lives inside the
// instance's volume. Format is auto-detected from the archive bytes
// (zip / tar / tar.gz / tar.bz2). dest_dir is rooted in the instance's
// browseable root just like every other Fs op; "" means same dir as
// the archive.
message FsExtractRequest {
string instance_id = 1;
string archive_path = 2;
string dest_dir = 3;
bool overwrite = 4;
}
message FsExtractResult {
int64 entries_written = 1;
int64 bytes_written = 2;
string error = 3;
}
// FsCompressRequest produces a zip archive at dest_zip_path containing
// the contents of every path in `sources` (recursive for dirs). All
// paths must be inside the instance's browseable root.
message FsCompressRequest {
string instance_id = 1;
repeated string sources = 2;
string dest_zip_path = 3;
bool overwrite = 4;
}
message FsCompressResult {
int64 entries_written = 1;
int64 bytes_written = 2;
string error = 3;
}
// FsRenameRequest is a path-safe `mv` inside the instance's volume —
// supports rename + move-into-sibling-dir within the same root.
message FsRenameRequest {
string instance_id = 1;
string from_path = 2;
string to_path = 3;
}
message FsRenameResult { string error = 1; }
// FsWriteChunkRequest streams a single chunk of a larger upload. Chunks
// are uploaded in order; each one carries the same upload_id (browser-
// generated UUID) so the agent can append to the same temp file. On
// is_final = true the agent finalizes (closes the temp file, ships it
// to the container via CopyToContainer, then unlinks).
message FsWriteChunkRequest {
string instance_id = 1;
string upload_id = 2;
string path = 3;
int64 offset = 4;
bytes data = 5;
int64 total_size = 6; // optional; agent uses for sanity checks on final
bool is_final = 7;
}
message FsWriteChunkResult {
int64 bytes_received = 1;
int64 total_received = 2; // running total per upload_id
bool finalized = 3;
string error = 4;
}
// ArkSaveRestoreRequest swaps a timestamped rolling backup into the
// active save slot under SavedArks/<subdir>/<subdir>.ark. The previous
// active save is moved aside (NEVER deleted) by appending a "_replaced"
// timestamp suffix so even abandoned attempts are recoverable. Path is
// rooted at ShooterGame/Saved/SavedArks/<saved_arks_subdir>/ in the
// instance's browseable root.
//
// Caller (controller) is responsible for stopping the container before
// invoking this — the agent only performs file operations. If a backup
// rolling file (.arkrbf) is selected, it is renamed to .ark before
// installing as the active save.
message ArkSaveRestoreRequest {
string instance_id = 1;
string saved_arks_subdir = 2; // e.g. "TheIsland_WP"
string target_filename = 3; // basename within saved_arks_subdir (e.g. "TheIsland_WP_30.04.2026_22.13.00.ark")
}
message ArkSaveRestoreResult {
string aside_filename = 1; // what the previously-active save was renamed to
string installed_filename = 2; // the filename that's now active (always "<subdir>.ark")
string error = 3;
}
// DayzModInstallRequest asks the agent to (1) copy the mod's bikeys into
// /game/keys and (2) create a @ModName symlink at /game/<folder_name>
// pointing at the shared workshop content at
// /game/steamapps/workshop/content/221100/<workshop_id>. Agent-side so
// it can scan the workshop dir and preserve file ownership.
message DayzModInstallRequest {
string instance_id = 1;
string workshop_id = 2;
string folder_name = 3; // e.g. "@CommunityOnlineTools"
}
message DayzModInstallResult {
string error = 1;
repeated string bikeys_copied = 2; // filenames of bikeys dropped in /game/keys
string resolved_mod_path = 3; // /game/steamapps/workshop/content/221100/<id>
}
// DayzModUninstallRequest removes the @ModName symlink and the mod's
// bikeys from /game/keys (bikeys are matched by file hash — only keys
// whose content originated from this workshop item are removed).
message DayzModUninstallRequest {
string instance_id = 1;
string folder_name = 2;
}
message DayzModUninstallResult {
string error = 1;
repeated string bikeys_removed = 2;
}
// ---- Updates ----
//
// The agent runs the update asynchronously. The synchronous UpdateResult
// carries only the acknowledgement + ref-id for later correlation; real
// progress + final status flow through LogLine (stream="update") and the
// terminal "update:ok"/"update:error:<msg>" LogLine sentinel at the end.
message UpdateRequest {
string instance_id = 1;
string provider_id = 2; // empty = use the module's first update_provider
// Steam login credentials for apps whose SteamCMD refuses +login
// anonymous (DayZ, Arma, etc. — modules with `requires_steam_login`
// set on the provider). The controller pulls these from its
// encrypted steam_credentials store and forwards them via the
// mTLS-protected control stream. Agent never logs them.
string steam_username = 3;
string steam_password = 4;
}
message UpdateResult {
bool accepted = 1; // true = agent kicked off the update goroutine
string error = 2; // empty on accept; populated on immediate reject
string provider_id = 3;
string provider_kind = 4;
}
// ---- Backup + Restore ----
//
// Synchronous over the bidi stream. The sidecar tars the instance's
// volume into a .tar.gz on the agent's --backup-dir; the terminal
// BackupResult carries the path + size. Restore takes a backup id
// (path resolved by the controller via DB) and reverses.
message BackupRequest {
string instance_id = 1;
string description = 2;
}
message BackupResult {
string backup_id = 1; // bkp_<hex>
string path = 2;
int64 size_bytes = 3;
string error = 4;
}
message RestoreRequest {
string instance_id = 1;
string backup_id = 2;
string backup_path = 3; // absolute path on the agent
}
message RestoreResult {
string error = 1;
int64 bytes_restored = 2;
}
// BackupListEntriesRequest asks the agent to enumerate the contents of a
// backup tarball without extracting it. Used by the backup-browser UI so
// operators can drill into a snapshot to confirm what's inside before
// restoring (or to recover a single file).
message BackupListEntriesRequest {
string instance_id = 1;
string backup_id = 2;
string backup_path = 3; // absolute path on the agent (resolved controller-side)
}
// BackupTarEntry — one row from `tar -tvzf`. Size is bytes-on-disk, not
// compressed size. is_dir distinguishes directories so the UI can render
// a tree without a second pass.
message BackupTarEntry {
string path = 1; // path inside the tarball, no leading slash
int64 size = 2;
bool is_dir = 3;
string mode = 4; // permissions string, e.g. "-rw-r--r--"
google.protobuf.Timestamp mod_time = 5;
}
message BackupListEntriesResult {
repeated BackupTarEntry entries = 1;
int64 total_bytes = 2; // sum of file sizes (not compressed size of the tarball)
string error = 3;
}
// BackupReadFileRequest extracts a single entry's bytes to memory by
// streaming `tar -xzOf <archive> <path>` into a buffer. Caller-supplied
// max_bytes caps the read so a malicious / corrupt archive can't OOM the
// agent — the result includes truncated=true if max_bytes was hit.
message BackupReadFileRequest {
string instance_id = 1;
string backup_id = 2;
string backup_path = 3;
string entry_path = 4;
int64 max_bytes = 5; // 0 → controller-side default (8 MiB)
}
message BackupReadFileResult {
bytes content = 1;
int64 size = 2; // actual entry size from tar header (may be > len(content) if truncated)
bool truncated = 3;
bool is_binary = 4; // heuristic: NUL byte in first 8 KiB
string error = 5;
}
message PlayerEvent {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_JOIN = 1;
KIND_LEAVE = 2;
KIND_CHAT = 3;
KIND_DEATH = 4;
KIND_CUSTOM = 99;
}
string instance_id = 1;
Kind kind = 2;
string player_id = 3; // SteamID / UUID where available
string player_name = 4;
string detail = 5;
google.protobuf.Timestamp at = 6;
}
// EmpyrionScenarioInstallRequest is sent controller → agent to install
// (or refresh) a workshop scenario into a specific Empyrion instance's
// game volume. The agent runs SteamCMD on its OWN Docker daemon — the
// previous (single-host) implementation ran on the controller's daemon
// which has no view of an agent's volumes.
message EmpyrionScenarioInstallRequest {
string job_id = 1; // controller-generated; echoed in result + log lines
string instance_id = 2; // target empyrion instance (must live on this agent)
string workshop_id = 3; // Steam workshop item ID (e.g. "3041847672" for RE2)
string scenario_name = 4; // folder name under Content/Scenarios; sanitized by controller
string app_id = 5; // empyrion app id; defaults to 530870 if empty
// Steam credentials for the workshop_download_item call. Steam policy
// (~2023+) requires a logged-in account for most app workshops, even
// for public/free items — anonymous returns "Access Denied". Forwarded
// from the controller's encrypted steam_credentials store. Same shape
// as UpdateRequest's steam_username/steam_password fields.
string steam_username = 6;
string steam_password = 7;
}
message EmpyrionScenarioInstallResult {
string job_id = 1;
bool ok = 2;
string error = 3; // empty when ok
string scenario_name = 4;
}
// EmpyrionDiscoveriesRequest queries the empyrion server's global.db
// (SQLite) for what a specific player has discovered. The agent mounts
// the instance's saves volume into a tiny alpine+sqlite sidecar, runs
// a join query against DiscoveredPOIs / Entities / Playfields /
// SolarSystems / LoginLogoff, and returns the rows.
//
// `mode = "personal"` returns POIs the named player personally
// discovered. `mode = "faction"` returns POIs anyone in their faction
// has discovered (the in-game shared-map view). Default: personal.
message EmpyrionDiscoveriesRequest {
string instance_id = 1;
string steam_id = 2; // 17-digit SteamID64 of the player
string mode = 3; // "personal" | "faction"
}
message DiscoveredPOI {
string solar_system = 1;
string playfield = 2;
string poi_name = 3;
int32 poi_type = 4; // Entities.etype enum
int64 game_time = 5; // ticks since save start
string discoverer_name = 6; // who first discovered (when known)
string discoverer_steam_id = 7;
int32 faction_group = 8;
int32 faction_id = 9;
}
message EmpyrionDiscoveriesResult {
bool ok = 1;
string error = 2;
repeated DiscoveredPOI pois = 3;
string mode = 4; // echoed back from request
string steam_id = 5;
string player_name = 6; // last-known name from LoginLogoff
}
// EmpyrionPlayerSummaryRequest pulls everything we know about a single
// player from the empyrion savegame DB: stats, login sessions, current
// inventory snapshot. Used by the public /me page on AMP-Monitor.
message EmpyrionPlayerSummaryRequest {
string instance_id = 1;
string steam_id = 2; // 17-digit SteamID64
int32 max_login_rows = 3; // limit on session history; default 50
}
message PlayerStatsRow {
int64 killed_enemies = 1;
int64 killed_allied = 2;
int64 killed_animals = 3;
int64 killed_drones = 4;
int64 killed_players = 5;
int64 killed_allied_players = 6;
int64 died = 7;
int64 score = 8;
int64 blocks_placed = 9;
int64 blocks_digged = 10;
int64 walked_meters = 11;
int64 jetpack_meters = 12;
int64 hv_meters = 13;
int64 sv_meters = 14;
int64 cv_meters = 15;
double playtime = 16; // hours per the schema
double light_years = 17; // travly
double astronomical_units = 18; // travau
}
message PlayerLoginRow {
int64 login_ticks = 1;
int64 logoff_ticks = 2;
string player_name = 3;
int64 build_nr = 4;
string ip = 5;
string os = 6;
string gfx = 7;
string cpu = 8;
}
message PlayerInventoryItem {
int32 item_id = 1;
int32 count = 2;
}
message EmpyrionPlayerSummaryResult {
bool ok = 1;
string error = 2;
string steam_id = 3;
string player_name = 4;
int64 entity_id = 5;
int32 faction_group = 6;
int32 faction_id = 7;
// Aggregate stats. nil when player has no PlayerStatistics row yet
// (very new player).
PlayerStatsRow stats = 8;
// Recent login sessions, newest first.
repeated PlayerLoginRow sessions = 9;
// Most recent inventory snapshot. May be empty for players without
// a recorded snapshot.
int64 inventory_gametime = 10;
repeated PlayerInventoryItem inventory = 11;
// Last-known position.
string last_playfield = 12;
double last_x = 13;
double last_y = 14;
double last_z = 15;
int64 credits = 16;
}
// ---- Region Medic (7DTD .7rg region-corruption detect + heal) ----
// RegionScanRequest asks the agent to scan an instance's active-world Region
// directory for corruption evidence (error_backup salvage files + invalid
// region files).
message RegionScanRequest {
string instance_id = 1;
}
// AffectedRegionMsg is one region flagged by a scan.
message AffectedRegionMsg {
string region = 1; // canonical id, e.g. "r.-2.0"
int32 error_backups = 2; // salvage files whose chunks fall in this region
bool file_present = 3; // r.X.Z.7rg exists on disk
bool file_corrupt = 4; // region file failed structural validation
int32 bad_chunks = 5; // count of invalid chunks if validated
}
message RegionScanResult {
int32 error_backups = 1; // total salvage files found
repeated AffectedRegionMsg affected = 2;
string region_dir = 3; // resolved active Region dir (container path)
Error error = 4;
}
// RegionHealRequest asks the agent to heal one region from the newest clean
// backup. The agent stops the instance (graceful save first), swaps the region
// file in place, quarantines the region's error_backups, then restarts (if
// restart=true).
message RegionHealRequest {
string instance_id = 1;
string region = 2; // "r.-2.0"
bool restart = 3; // restart the instance after healing
}
message RegionHealResult {
string region = 1;
string source_backup = 2; // backup id the clean copy came from
int64 bytes_written = 3;
int32 quarantined = 4; // error_backup files moved aside
string snapshot_path = 5; // where the corrupt original was saved
bool healed = 6;
Error error = 7;
}
// ---- Durable config re-render (no container recreate) ----
// InstanceRenderConfigRequest asks the agent to re-render the module's
// declared config_files (templates) into the instance's host DataPath,
// WITHOUT recreating or restarting the container. For modules whose
// rendered output is bind-mounted into the container (7dtd's
// serverconfig.xml.rendered), the entrypoint picks the new content up on
// the instance's NEXT boot — this is the durable, no-recreate config path.
//
// config_values is the FULL desired value map (the controller sends its
// merged DB row, not a delta), mirroring what InstanceCreate would render.
message InstanceRenderConfigRequest {
string instance_id = 1;
map<string, string> config_values = 2;
}
message InstanceRenderConfigResult {
repeated string rendered_files = 1; // config_file paths written (manifest-relative)
string error = 2;
}
// SeedModsRequest asks an agent to clone the Mods/ folder of src_instance_id
// (the cluster master) onto instance_id (a member), live — no container
// recreate. Per-server state (RefugeBot homes.json) is excluded from the copy
// and the member's own homes.json is preserved. The member keeps running on
// its currently-loaded mods; the new files take effect on its NEXT restart.
// Both instances must live on the same agent (the sidecar mounts local volumes).
message SeedModsRequest {
string instance_id = 1; // destination member to receive the mods
string src_instance_id = 2; // source cluster master to copy mods from
}
message SeedModsResult {
string instance_id = 1;
bool ok = 2;
string error = 3;
int32 mod_count = 4; // number of mods present on the member after the copy
}
// ---- Update-available check (WI-14) ----
//
// CHECK-ONLY: this RPC never triggers an update or restart. The agent
// reads the installed buildid from the Steam appmanifest_<appid>.acf in
// the instance volume and fetches the latest buildid for the instance's
// branch via a one-shot `steamcmd +app_info_print` sidecar (cached per
// app_id for 15 minutes agent-side).
message UpdateCheckRequest {
string instance_id = 1;
// provider_id selects the module update_provider (steamcmd kind) whose
// app_id/beta define what "installed vs latest" means. Empty = same
// default the Update RPC uses (first provider in the manifest).
string provider_id = 2;
// refresh forces a fresh steamcmd app_info fetch, bypassing the
// agent-side 15-minute cache.
bool refresh = 3;
}
message UpdateCheckResult {
string instance_id = 1;
string app_id = 2;
string branch = 3; // Steam branch compared ("public" when no -beta)
string installed_buildid = 4; // from appmanifest ACF; "" if unreadable
string latest_buildid = 5; // from app_info branches map; "" if fetch failed
bool update_available = 6; // installed != latest (both non-empty)
bool cached = 7; // latest came from the agent-side cache
string error = 8; // non-empty on failure; other fields best-effort
}