atlas-iac/testing/tests/test_hermes_runtime_access.py

277 lines
10 KiB
Python

"""Contracts for Hermes' Vault-only runtime access boundary."""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).parents[2]
HERMES = ROOT / "services" / "hermes"
SCRIPTS = HERMES / "scripts"
def _load(name: str):
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_agent_runtime_stage_keeps_credentials_in_memory(tmp_path: Path, monkeypatch):
stage = _load("stage_runtime_access")
vault = tmp_path / "vault"
runtime = tmp_path / "runtime"
home = tmp_path / "home"
vault.mkdir()
(home / ".claude").mkdir(parents=True)
(home / ".claude" / "backups").mkdir()
(home / ".codex" / "skills").mkdir(parents=True)
(home / ".codex" / "sessions").mkdir()
(home / ".claude" / "settings.json").write_text("{}\n", encoding="utf-8")
(home / ".claude" / "backups" / "credentials.json").write_text(
"do-not-link\n", encoding="utf-8"
)
values = {
"agent-api-key": "agent-key",
"chat-relay-key": "relay-key",
"gitea-token": "gitea-key",
"gitea-username": "hermes-automation",
"node-ssh-private-key": "private-key",
"node-ssh-config": "host-config",
"node-ssh-known-hosts": "known-hosts",
"claude-credentials": json.dumps(
{"claudeAiOauth": {"refreshToken": "claude-refresh"}}
),
"codex-auth": json.dumps({"tokens": {"refresh_token": "codex-refresh"}}),
}
for name, value in values.items():
(vault / name).write_text(value + "\n", encoding="utf-8")
monkeypatch.setattr(stage, "VAULT_ROOT", vault)
monkeypatch.setattr(stage, "RUNTIME_ROOT", runtime)
monkeypatch.setattr(stage, "PERSISTENT_HOME", home)
monkeypatch.setattr(stage.os, "chown", lambda *_args: None)
stage.stage_agent()
assert (runtime / "claude/.credentials.json").stat().st_mode & 0o777 == 0o600
assert (runtime / "codex/auth.json").stat().st_mode & 0o777 == 0o600
assert (runtime / "claude/settings.json").is_symlink()
assert (runtime / "codex/skills").is_symlink()
assert not (runtime / "claude/backups").exists()
assert not (runtime / "codex/sessions").exists()
auth = json.loads((runtime / "hermes-auth.json").read_text(encoding="utf-8"))
assert auth == {"version": 1, "providers": {}, "credential_pool": {}}
def test_invalid_runtime_json_is_removed(tmp_path: Path, monkeypatch):
stage = _load("stage_runtime_access")
vault = tmp_path / "vault"
runtime = tmp_path / "runtime"
vault.mkdir()
runtime.mkdir()
(vault / "credential").write_text("not-json\n", encoding="utf-8")
monkeypatch.setattr(stage, "VAULT_ROOT", vault)
monkeypatch.setattr(stage.os, "chown", lambda *_args: None)
destination = runtime / "credential.json"
try:
stage._validated_json("credential", destination, ("token",))
except json.JSONDecodeError:
pass
else:
raise AssertionError("invalid credential JSON should fail staging")
assert not destination.exists()
def test_runtime_refresh_sync_uses_cas_and_preserves_other_fields(
tmp_path: Path, monkeypatch
):
sync = _load("sync_runtime_credentials")
claude = tmp_path / "claude.json"
codex = tmp_path / "codex.json"
claude.write_text(
json.dumps({"claudeAiOauth": {"refreshToken": "new-claude"}}),
encoding="utf-8",
)
codex.write_text(
json.dumps({"tokens": {"refresh_token": "new-codex"}}),
encoding="utf-8",
)
monkeypatch.setattr(
sync,
"CREDENTIALS",
{
"claude_credentials_json": (
claude,
("claudeAiOauth", "refreshToken"),
),
"codex_auth_json": (codex, ("tokens", "refresh_token")),
},
)
writes = []
def request(method, path, payload=None, *, token=""):
assert token == "vault-token"
if method == "GET":
return {
"data": {
"data": {
"claude_credentials_json": "old",
"codex_auth_json": "old",
"agent_api_key": "preserve-me",
},
"metadata": {"version": 7},
}
}
writes.append((path, payload))
return {}
monkeypatch.setattr(sync, "_request", request)
assert sync.sync_once("vault-token") == [
"claude_credentials_json",
"codex_auth_json",
]
assert writes[0][1]["options"] == {"cas": 7}
assert writes[0][1]["data"]["agent_api_key"] == "preserve-me"
def test_subprocess_patches_strip_and_redact_runtime_credentials(tmp_path: Path):
boundary = _load("patch_subprocess_secret_boundary")
process = _load("patch_process_output_redaction")
local_source = tmp_path / "local.py"
local_output = tmp_path / "patched-local.py"
local_source.write_text("prefix\n" + boundary.BEFORE + "suffix\n", encoding="utf-8")
boundary.patch(local_source, local_output)
patched_local = local_output.read_text(encoding="utf-8")
for name in (
"API_SERVER_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"GITEA_TOKEN",
"HERMES_IMAGE_BROKER_KEY",
):
assert f'"{name}"' in patched_local
process_source = tmp_path / "process.py"
process_output = tmp_path / "patched-process.py"
process_source.write_text(
"prefix\n" + process.BEFORE + "suffix\n", encoding="utf-8"
)
process.patch(process_source, process_output)
assert "code_file=False" in process_output.read_text(encoding="utf-8")
def test_manifests_never_seed_access_material_into_persistent_env():
agent = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
chat = yaml.safe_load((HERMES / "chat-statefulset.yaml").read_text())
triage = yaml.safe_load((HERMES / "deployment.yaml").read_text())
for workload in (agent, chat, triage):
pod = workload["spec"]["template"]["spec"]
runtime = next(item for item in pod["volumes"] if item["name"] == "runtime-access")
assert runtime["emptyDir"]["medium"] == "Memory"
assert not any(item["name"] == "provider-auth" for item in pod["volumes"])
init = next(item for item in pod["initContainers"] if item["name"] == "init-config")
command = init["command"][2]
for key in (
"ANTHROPIC_API_KEY",
"API_SERVER_KEY",
"CLAUDE_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"GITEA_TOKEN",
"HERMES_IMAGE_BROKER_KEY",
"OPENAI_API_KEY",
):
assert f"printf '{key}=" not in command
assert f"upsert_env {key}" not in command
annotations = agent["spec"]["template"]["metadata"]["annotations"]
agent_runtime = next(
item
for item in agent["spec"]["template"]["spec"]["volumes"]
if item["name"] == "runtime-access"
)
assert agent_runtime["emptyDir"] == {
"medium": "Memory",
"sizeLimit": "128Mi",
}
assert "vault.hashicorp.com/agent-inject-secret-anthropic-token" not in annotations
assert annotations["vault.hashicorp.com/agent-inject-secret-claude-credentials"] == (
"kv/data/atlas/hermes/agent-tokens"
)
assert annotations["vault.hashicorp.com/agent-inject-secret-codex-auth"] == (
"kv/data/atlas/hermes/agent-tokens"
)
credential_sync = next(
item
for item in agent["spec"]["template"]["spec"]["containers"]
if item["name"] == "credential-sync"
)
assert {
(item["mountPath"], item.get("subPath"))
for item in credential_sync["volumeMounts"]
if item["name"] == "runtime-access"
} == {
("/runtime-access/claude", "claude"),
("/runtime-access/codex", "codex"),
}
triage_annotations = triage["spec"]["template"]["metadata"]["annotations"]
assert "vault.hashicorp.com/agent-inject-secret-anthropic-token" not in (
triage_annotations
)
assert triage_annotations[
"vault.hashicorp.com/agent-inject-secret-triage-api-key"
] == "kv/data/atlas/hermes/triage-api"
askpass = (SCRIPTS / "gitea_askpass.sh").read_text(encoding="utf-8")
assert "/runtime-access/gitea-token" in askpass
assert "GITEA_TOKEN" not in askpass
def test_chat_media_reads_one_raw_runtime_secret(tmp_path: Path):
module = _load("telegram_media_server")
secret = tmp_path / "relay"
secret.write_text("relay-value\n", encoding="utf-8")
assert module.read_relay_key(secret) == "relay-value"
def test_ariadne_receives_the_triage_key_directly_from_vault():
maintenance = ROOT / "services" / "maintenance" / "apps" / "ariadne-deployment.yaml"
deployment = yaml.safe_load(maintenance.read_text(encoding="utf-8"))
annotations = deployment["spec"]["template"]["metadata"]["annotations"]
template = annotations["vault.hashicorp.com/agent-inject-template-ariadne-env.sh"]
assert 'secret "kv/data/atlas/hermes/triage-api"' in template
assert 'export ARIADNE_HERMES_API_KEY="{{ .Data.data.api_key }}"' in template
assert 'secret "kv/data/atlas/hermes/developer-gitea"' in template
assert 'export ARIADNE_HERMES_GITEA_TOKEN="{{ .Data.data.token }}"' in template
container = deployment["spec"]["template"]["spec"]["containers"][0]
assert not any(item["name"] == "ARIADNE_HERMES_API_KEY" for item in container["env"])
assert not any(
item["name"] == "ARIADNE_HERMES_GITEA_TOKEN" for item in container["env"]
)
vault_policy = (
ROOT / "services" / "vault" / "scripts" / "vault_k8s_auth_configure.sh"
).read_text(encoding="utf-8")
assert '"hermes/triage-oidc hermes/agent-tokens hermes/triage-api"' in vault_policy
assert 'hermes-credential-sync' in vault_policy
assert 'hermes/triage-api hermes/developer-gitea' in vault_policy
def test_gitea_bootstrap_reuses_valid_vault_tokens():
bootstrap = (
ROOT / "services" / "gitea" / "scripts" / "gitea_atlas_identity_ensure.sh"
).read_text(encoding="utf-8")
assert "vault_login\n\nreconciler_token=$(vault_read_field" in bootstrap
assert 'if ! token_is_valid "${reconciler_user}"' in bootstrap
assert 'if ! token_is_valid "${hermes_user}"' in bootstrap
assert bootstrap.count('generate_token "${reconciler_user}"') == 1
assert bootstrap.count('generate_token "${hermes_user}"') == 1