feat(host-agent): Phase 1c — SteamCMD update + jailed file manager

steam_update func runs SteamCMD per game (rust/conan/soulmask app-ids;
dune rejected), streaming stdout to {instance}.steam_status. Jailed
file manager on {instance}.files.cmd: list/read/write/delete/rename/
mkdir/mkfile/move/copy, all confined to instance root via two-stage
lexical-normalize + canonicalize (defeats ../ traversal AND symlink
escape — incl chained symlinks). Replaces the Go agent's UNJAILED
legacy files API (retired, not ported). 5MiB read cap.

42/42 tests green: 24 filemanager incl 7 jail-escape attempts
(dotdot, deep dotdot, absolute, symlink-inside, direct symlink,
chained symlink), 5 steamcmd app-id (cfg-gated win/linux soulmask).
Jail logic reviewed line-by-line: Path::starts_with is component-wise
(no sibling-prefix bypass), non-existent suffix components can't be
symlinks, leading .. normalizes to / and fails the prefix check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 11:51:46 -04:00
parent 9e5e828c8d
commit 18f978dde1
14 changed files with 1508 additions and 10 deletions

View File

@@ -110,9 +110,29 @@ conan/soulmask; explicit `kind` override available in the instance's
Errors reply `{ "status": "error", "message": ... }` — including start on an
unmanaged instance, double start, missing rcon config, and unknown funcs.
Planned funcs: `steam_update`, `oxide_install` (rust), plus
game-adapter-specific commands (Dune: docker lifecycle, RabbitMQ bus
commands, Coriolis reset).
Also implemented: `steam_update` `{ "func": "steam_update" }` runs
SteamCMD for the instance's game (app ids: rust 258550, conan 443030,
soulmask 3017310/3017300; dune rejects — Docker images, no SteamCMD),
streaming progress lines to `corrosion.{license}.{instance}.steam_status`
and replying on completion.
Planned funcs: `oxide_install` (rust), plus game-adapter-specific
commands (Dune: docker lifecycle, RabbitMQ bus commands, Coriolis reset).
### `corrosion.{license_id}.{instance_id}.steam_status` (agent → backend, publish) — LIVE
Per-line SteamCMD stdout during a `steam_update`, so the panel can show
live update progress. Payload: `{ "timestamp", "instance_id", "line" }`.
### `corrosion.{license_id}.{instance_id}.files.cmd` (backend → agent, request-reply) — LIVE
Jailed file manager, confined to the instance `root` (two-stage check:
lexical normalize + canonicalize, defeating `../` traversal and symlink
escape). Request `{ "op": "list|read|write|delete|rename|mkdir|mkfile|move|copy",
"path": "rel/path", "dest"?, "content"?, "name"? }`; reply
`{ "status": "success", "data": ... }` or `{ "status": "error", "message": ... }`.
`read` caps at 5 MiB. Replaces the Go agent's UNJAILED legacy files API,
which is retired and will not be ported.
### `corrosion.{license_id}.{instance_id}.status` (agent → backend, publish) — LIVE