feat(server): real agent credentials + agent.toml setup; per-game config honesty
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 17s
CI / agent-tests (push) Successful in 45s
CI / integration (push) Successful in 22s

Server page Host-agent panel now fetches GET /api/servers/agent-
credentials and renders the real agent.toml (license UUID, nats_user,
nats_password) instead of the broken LICENSE_ID=license_key env
commands that would never connect. Password masked by default with a
reveal toggle; copy-to-clipboard uses the real value. Setup commands
point at --config /etc/corrosion/agent.toml.

Configuration panel: World size / Current seed (Rust-only Facepunch
concepts) gated behind isRust; Conan/Soulmask/Dune get an honest note
pointing to the File Manager for their real config files instead of
fake Rust fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 13:23:47 -04:00
parent 6f31c41dc3
commit 009ceb86ad

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useServerStore } from '@/stores/server' import { useServerStore } from '@/stores/server'
import { useAuthStore } from '@/stores/auth'
import { useToastStore } from '@/stores/toast' import { useToastStore } from '@/stores/toast'
import { useThemeGame } from '@/composables/useThemeGame' import { useThemeGame } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles' import { useGameProfile } from '@/config/gameProfiles'
import type { DeploymentConfig, DeploymentStatus } from '@/types' import type { DeploymentConfig, DeploymentStatus } from '@/types'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { useApi } from '@/composables/useApi'
import Panel from '@/components/ds/data/Panel.vue' import Panel from '@/components/ds/data/Panel.vue'
import Button from '@/components/ds/core/Button.vue' import Button from '@/components/ds/core/Button.vue'
import Badge from '@/components/ds/core/Badge.vue' import Badge from '@/components/ds/core/Badge.vue'
@@ -19,7 +19,6 @@ import Switch from '@/components/ds/forms/Switch.vue'
import Tabs from '@/components/ds/navigation/Tabs.vue' import Tabs from '@/components/ds/navigation/Tabs.vue'
const server = useServerStore() const server = useServerStore()
const auth = useAuthStore()
const toast = useToastStore() const toast = useToastStore()
const { activeGame } = useThemeGame() const { activeGame } = useThemeGame()
@@ -66,6 +65,18 @@ const deployLoading = ref(false)
const oxideStatus = ref<{ stage: string; progress: number; message: string; error?: string } | null>(null) const oxideStatus = ref<{ stage: string; progress: number; message: string; error?: string } | null>(null)
const isInstallingOxide = ref(false) const isInstallingOxide = ref(false)
// Agent credentials (fetched from /api/servers/agent-credentials on mount)
interface AgentCreds {
license_id: string
nats_user: string
nats_password: string
nats_url: string
}
const agentCreds = ref<AgentCreds | null>(null)
const showCreds = ref(false)
// Ref for the TOML block copy button
const tomlCopied = ref(false)
const deployForm = ref<DeploymentConfig>({ const deployForm = ref<DeploymentConfig>({
server_name: 'My Rust Server', server_name: 'My Rust Server',
max_players: 100, max_players: 100,
@@ -97,25 +108,62 @@ const agentLastSeenLabel = computed(() => {
return d.toLocaleDateString() return d.toLocaleDateString()
}) })
const licenseKey = computed(() => auth.license?.license_key || 'YOUR-LICENSE-KEY')
const linuxCommands = computed(() => `# Download the agent const linuxCommands = computed(() => `# Download the agent
curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64 curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64
chmod +x corrosion-host-agent-linux-amd64 chmod +x corrosion-host-agent-linux-amd64
# Start with your license key # Write /etc/corrosion/agent.toml (see config block below), then run:
export LICENSE_ID="${licenseKey.value}" sudo mkdir -p /etc/corrosion
export NATS_URL="nats://nats.corrosionmgmt.com:4222" sudo ./corrosion-host-agent-linux-amd64 --config /etc/corrosion/agent.toml`)
./corrosion-host-agent-linux-amd64`)
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt) const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
# Download the agent # Download the agent
Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe" -OutFile "corrosion-host-agent-windows-amd64.exe" Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe" -OutFile "corrosion-host-agent-windows-amd64.exe"
# Start with your license key # Write C:\\ProgramData\\Corrosion\\agent.toml (see config block below), then run:
$env:LICENSE_ID="${licenseKey.value}" New-Item -ItemType Directory -Force -Path "C:\\ProgramData\\Corrosion"
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222" .\\corrosion-host-agent-windows-amd64.exe --config "C:\\ProgramData\\Corrosion\\agent.toml"`)
.\\corrosion-host-agent-windows-amd64.exe`)
const agentTomlConfig = computed(() => {
const c = agentCreds.value
const licenseId = c?.license_id ?? 'YOUR-LICENSE-ID'
const natsUrl = c?.nats_url ?? 'nats://nats.corrosionmgmt.com:4222'
const natsUser = c?.nats_user ?? 'YOUR-LICENSE-ID'
const natsPassword = c ? (showCreds.value ? c.nats_password : '••••••••') : 'YOUR-AGENT-TOKEN'
return `[agent]
license_id = "${licenseId}"
nats_url = "${natsUrl}"
nats_user = "${natsUser}"
nats_password = "${natsPassword}"
heartbeat_seconds = 60
[[instance]]
id = "rust-main"
game = "rust"
root = "/opt/rustserver"
label = "My Server"`
})
// Returns the raw (unmasked) TOML for clipboard — always use actual password if available
const agentTomlConfigRaw = computed(() => {
const c = agentCreds.value
const licenseId = c?.license_id ?? 'YOUR-LICENSE-ID'
const natsUrl = c?.nats_url ?? 'nats://nats.corrosionmgmt.com:4222'
const natsUser = c?.nats_user ?? 'YOUR-LICENSE-ID'
const natsPassword = c?.nats_password ?? 'YOUR-AGENT-TOKEN'
return `[agent]
license_id = "${licenseId}"
nats_url = "${natsUrl}"
nats_user = "${natsUser}"
nats_password = "${natsPassword}"
heartbeat_seconds = 60
[[instance]]
id = "rust-main"
game = "rust"
root = "/opt/rustserver"
label = "My Server"`
})
async function copySetupCommands() { async function copySetupCommands() {
try { try {
@@ -133,6 +181,16 @@ async function copySetupCommands() {
} }
} }
async function copyTomlConfig() {
try {
await navigator.clipboard.writeText(agentTomlConfigRaw.value)
tomlCopied.value = true
setTimeout(() => { tomlCopied.value = false }, 2000)
} catch {
// Clipboard API unavailable
}
}
async function startDeploy() { async function startDeploy() {
if (!deployForm.value.rcon_password || deployForm.value.rcon_password.length < 6) return if (!deployForm.value.rcon_password || deployForm.value.rcon_password.length < 6) return
deployLoading.value = true deployLoading.value = true
@@ -297,6 +355,14 @@ onMounted(async () => {
await server.fetchServer() await server.fetchServer()
loadFormFromConfig() loadFormFromConfig()
// Fetch agent credentials for the TOML config block (leave null on error — honest fallback)
try {
const creds = await useApi().get<AgentCreds | null>('/servers/agent-credentials')
agentCreds.value = creds
} catch {
agentCreds.value = null
}
const ws = useWebSocket() const ws = useWebSocket()
ws.subscribe((msg) => { ws.subscribe((msg) => {
if (msg.type === 'event' && msg.event === 'deploy_status') { if (msg.type === 'event' && msg.event === 'deploy_status') {
@@ -463,10 +529,9 @@ onMounted(async () => {
<p class="sv__cmt"># Download the agent</p> <p class="sv__cmt"># Download the agent</p>
<p>curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64</p> <p>curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64</p>
<p>chmod +x corrosion-host-agent-linux-amd64</p> <p>chmod +x corrosion-host-agent-linux-amd64</p>
<p class="sv__cmt sv__mt">&#x23; Start with your license key</p> <p class="sv__cmt sv__mt">&#x23; Write /etc/corrosion/agent.toml (see config block below), then run:</p>
<p>export LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p> <p>sudo mkdir -p /etc/corrosion</p>
<p>export NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p> <p>sudo ./corrosion-host-agent-linux-amd64 --config <span class="sv__accent">/etc/corrosion/agent.toml</span></p>
<p>./corrosion-host-agent-linux-amd64</p>
</div> </div>
<!-- Windows commands --> <!-- Windows commands -->
@@ -474,11 +539,38 @@ onMounted(async () => {
<p class="sv__cmt"># Requires PowerShell (not Command Prompt)</p> <p class="sv__cmt"># Requires PowerShell (not Command Prompt)</p>
<p class="sv__cmt"># Download the agent</p> <p class="sv__cmt"># Download the agent</p>
<p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-host-agent-windows-amd64.exe"</span></p> <p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-host-agent-windows-amd64.exe"</span></p>
<p class="sv__cmt sv__mt">&#x23; Start with your license key</p> <p class="sv__cmt sv__mt">&#x23; Write C:\ProgramData\Corrosion\agent.toml (see config block below), then run:</p>
<p>$env:LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p> <p>New-Item -ItemType Directory -Force -Path <span class="sv__accent">"C:\ProgramData\Corrosion"</span></p>
<p>$env:NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p> <p>.\corrosion-host-agent-windows-amd64.exe --config <span class="sv__accent">"C:\ProgramData\Corrosion\agent.toml"</span></p>
<p>.\corrosion-host-agent-windows-amd64.exe</p>
</div> </div>
<!-- Agent configuration (agent.toml) -->
<div class="sv__section-head sv__mt">
<Icon name="file-text" :size="14" />
<span>Agent configuration (agent.toml)</span>
</div>
<div class="sv__setup-head">
<div class="sv__toml-reveal">
<Button
variant="ghost"
size="sm"
:icon="showCreds ? 'eye-off' : 'eye'"
@click="showCreds = !showCreds"
>{{ showCreds ? 'Hide credentials' : 'Reveal credentials' }}</Button>
</div>
<Button
variant="secondary"
size="sm"
:icon="tomlCopied ? 'check' : 'copy'"
@click="copyTomlConfig"
>{{ tomlCopied ? 'Copied' : 'Copy' }}</Button>
</div>
<div class="sv__codeblock">
<pre class="sv__pre">{{ agentTomlConfig }}</pre>
</div>
<Alert v-if="!agentCreds" tone="warn" class="sv__mt">
Could not load credentials from server. Copy this config and replace the placeholders with values from your Corrosion dashboard settings.
</Alert>
</Panel> </Panel>
<!-- Deploy Server Rust only (SteamCMD path). Other games use docker-compose or external tooling. --> <!-- Deploy Server Rust only (SteamCMD path). Other games use docker-compose or external tooling. -->
@@ -778,15 +870,21 @@ onMounted(async () => {
<div class="sv__field-label">Max players</div> <div class="sv__field-label">Max players</div>
<div class="sv__field-val sv__field-val--mono">{{ server.config?.max_players ?? '—' }}</div> <div class="sv__field-val sv__field-val--mono">{{ server.config?.max_players ?? '—' }}</div>
</div> </div>
<div class="sv__field"> <!-- Rust-only: world size and seed are Facepunch/procgen concepts -->
<div v-if="isRust" class="sv__field">
<div class="sv__field-label">World size</div> <div class="sv__field-label">World size</div>
<div class="sv__field-val sv__field-val--mono">{{ server.config?.world_size ?? '—' }}</div> <div class="sv__field-val sv__field-val--mono">{{ server.config?.world_size ?? '—' }}</div>
</div> </div>
<div class="sv__field"> <div v-if="isRust" class="sv__field">
<div class="sv__field-label">Current seed</div> <div class="sv__field-label">Current seed</div>
<div class="sv__field-val sv__field-val--mono">{{ server.config?.current_seed ?? '—' }}</div> <div class="sv__field-val sv__field-val--mono">{{ server.config?.current_seed ?? '—' }}</div>
</div> </div>
</div> </div>
<!-- Non-Rust: game-specific settings live in config files on the host -->
<Alert v-if="!editMode && !isRust" tone="neutral" class="sv__mt">
Game-specific settings for {{ profile.label }} live in config files on the host — manage them in the
<Button variant="ghost" size="sm" icon="folder" @click="$router.push('/files')">File Manager</Button>
</Alert>
<!-- Edit mode --> <!-- Edit mode -->
<form v-else @submit.prevent="saveConfig" class="sv__form"> <form v-else @submit.prevent="saveConfig" class="sv__form">
@@ -803,7 +901,9 @@ onMounted(async () => {
type="number" type="number"
:mono="true" :mono="true"
/> />
<!-- Rust-only: world size and seed are Facepunch/procgen concepts -->
<Input <Input
v-if="isRust"
:model-value="String(form.world_size)" :model-value="String(form.world_size)"
@update:model-value="v => { form.world_size = Number(v) }" @update:model-value="v => { form.world_size = Number(v) }"
label="World size" label="World size"
@@ -811,6 +911,7 @@ onMounted(async () => {
:mono="true" :mono="true"
/> />
<Input <Input
v-if="isRust"
:model-value="String(form.current_seed)" :model-value="String(form.current_seed)"
@update:model-value="v => { form.current_seed = Number(v) }" @update:model-value="v => { form.current_seed = Number(v) }"
label="Current seed" label="Current seed"
@@ -818,6 +919,11 @@ onMounted(async () => {
:mono="true" :mono="true"
class="sv__col-span2" class="sv__col-span2"
/> />
<!-- Non-Rust: redirect to file manager for game-specific config -->
<Alert v-if="!isRust" tone="neutral" class="sv__col-span2">
Game-specific settings for {{ profile.label }} live in config files on the host — manage them in the
<Button variant="ghost" size="sm" icon="folder" @click="$router.push('/files')">File Manager</Button>
</Alert>
</div> </div>
</form> </form>
</Panel> </Panel>
@@ -931,6 +1037,12 @@ onMounted(async () => {
/* Setup head */ /* Setup head */
.sv__setup-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .sv__setup-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* TOML reveal row */
.sv__toml-reveal { display: flex; align-items: center; }
/* Pre inside codeblock — preserve whitespace, no extra margin */
.sv__pre { margin: 0; white-space: pre; }
/* Code block */ /* Code block */
.sv__codeblock { .sv__codeblock {
background: var(--surface-inset); border-radius: var(--radius-md); background: var(--surface-inset); border-radius: var(--radius-md);