Add the deployment path: agent systemd unit and container image
The README referenced `deploy/install-agent.sh` and a `Containerfile` that did not exist, so a fresh clone had no way to actually run this anywhere. * agent: `--token-file` (and AGENT_TOKEN_FILE). The token guards hostname, mounts, disk and GPU models and process names, and argv is world-readable through `ps`, so a file has to win over the flag. Missing or empty token files are fatal — falling back to "no token" would expose the host. * deploy/install-agent.sh: creates a system user, generates the token once (re-runs reuse it, --rotate-token for a new one), stores it 0600 root-owned, and runs the agent under ProtectSystem=strict with no capabilities; --status and --uninstall included. * Containerfile: two-stage, uv.lock resolved in the builder and slim Python in the runtime. Verified with podman — the image serves /api/health as an unprivileged user and writes its history under /var/lib/dashboards. UV_PROJECT_ENVIRONMENT and --no-editable are what make the venv survive being copied into the runtime stage. * .containerignore keeps .git, .venv, data/ and .env out of the build context. 258 tests, 94% coverage; ruff and pyright clean. LLM-Generated: true
This commit is contained in:
@@ -282,3 +282,37 @@ def test_agent_is_dependency_free() -> None:
|
||||
assert top <= {"__future__", "argparse", "collections", "dataclasses", "http", "json",
|
||||
"os", "pathlib", "re", "socket", "subprocess", "threading", "time",
|
||||
"typing"}, f"agent import footprint grew: {sorted(top)}"
|
||||
|
||||
|
||||
# ---- how the token is supplied ------------------------------------------- #
|
||||
|
||||
def test_token_file_wins_over_the_command_line(tmp_path) -> None:
|
||||
path = tmp_path / "token"
|
||||
path.write_text("from-file\n", encoding="utf-8")
|
||||
token, error = host_agent.resolve_token("from-flag", str(path))
|
||||
assert error is None
|
||||
assert token == "from-file", "argv is world-readable in ps; the file has to win"
|
||||
|
||||
|
||||
def test_a_missing_token_file_is_fatal_and_says_which_one(tmp_path) -> None:
|
||||
missing = tmp_path / "nope"
|
||||
token, error = host_agent.resolve_token("", str(missing))
|
||||
assert token is None
|
||||
assert error and str(missing) in error, "a silent fallback to no-token would expose the host"
|
||||
|
||||
|
||||
def test_an_empty_token_file_is_fatal(tmp_path) -> None:
|
||||
path = tmp_path / "token"
|
||||
path.write_text(" \n", encoding="utf-8")
|
||||
token, error = host_agent.resolve_token("ignored", str(path))
|
||||
assert token is None and error is not None
|
||||
|
||||
|
||||
def test_no_token_at_all_is_allowed_on_loopback(tmp_path) -> None:
|
||||
token, error = host_agent.resolve_token("", "")
|
||||
assert (token, error) == (None, None)
|
||||
|
||||
|
||||
def test_main_refuses_to_bind_off_loopback_without_a_token(tmp_path) -> None:
|
||||
code = host_agent.main(["--host", "0.0.0.0", "--token-file", str(tmp_path / "missing")])
|
||||
assert code == 2, "the agent must not start exposed and unauthenticated"
|
||||
|
||||
Reference in New Issue
Block a user