feat: reachability and consumer profiles (SAND-WP-0011)

Add reachability enrichment (tunnel metadata, ops-bridge pointer),
secret_refs boundary resolution, profile.agent-dev and profile.build,
CLI reachability show, API endpoint, consumer smoke scripts, and tests.
This commit is contained in:
2026-06-24 12:54:27 +02:00
parent 7cabf77fb6
commit 1f87be4c6b
20 changed files with 522 additions and 34 deletions

View File

@@ -0,0 +1,20 @@
"""Consumer profile loader smoke tests."""
from sandboxer.profiles.loader import load_profile
def test_profile_agent_dev_loads() -> None:
profile = load_profile("profile.agent-dev")
assert profile.id == "profile.agent-dev"
assert profile.extension == "ext.compose-ssh"
assert profile.scope_default == "agent"
assert profile.route is not None
assert profile.ttl.default == "8h"
def test_profile_build_loads() -> None:
profile = load_profile("profile.build")
assert profile.id == "profile.build"
assert profile.extension == "ext.vm-packer"
assert "build-registry-token" in profile.setup.secret_refs
assert profile.reachability.tunnel == "ops-bridge"

View File

@@ -0,0 +1,78 @@
"""Reachability enrichment tests."""
from __future__ import annotations
from datetime import UTC, datetime
from sandboxer.models import (
ActorType,
Consumer,
Profile,
Reachability,
ReachabilitySpec,
SandboxState,
SandboxStatus,
)
from sandboxer.reachability.enrich import (
build_reachability_report,
enrich_reachability,
ssh_one_liner,
)
def _profile() -> Profile:
return Profile.model_validate(
{
"id": "profile.agent-dev",
"version": "1.0.0",
"extension": "ext.compose-ssh",
"reachability": ReachabilitySpec(
tunnel="ops-bridge", identity="ops-warden"
).model_dump(),
}
)
def test_enrich_adds_tunnel_and_identity(monkeypatch) -> None:
monkeypatch.setenv("SANDBOXER_TUNNEL_PORT", "12222")
reach = enrich_reachability(
{"ssh": "build@localhost", "remote_dir": "/build/sbx-1", "host": "localhost"},
_profile(),
{"ssh_port": "12222"},
)
assert reach["identity"] == "ops-warden"
assert reach["tunnel"] == "localhost:12222"
assert reach["tunnel_via"] == "ops-bridge"
def test_ssh_one_liner() -> None:
reach = Reachability(ssh="user@host", remote_dir="/tmp/ws")
line = ssh_one_liner(reach)
assert line is not None
assert "user@host" in line
assert "/tmp/ws" in line
def test_build_reachability_report() -> None:
now = datetime.now(UTC)
status = SandboxStatus(
sandbox_id="abc12345",
profile_id="profile.agent-dev",
extension_id="ext.compose-ssh",
state=SandboxState.READY,
consumer=Consumer(actor=ActorType.AGT, project="glas-harness"),
host="coulombcore",
reachability=Reachability(
ssh="root@coulombcore",
remote_dir="/tmp/sandboxer/abc12345",
tunnel="localhost:22",
tunnel_via="ops-bridge",
identity="ops-warden",
),
created_at=now,
updated_at=now,
)
report = build_reachability_report(status)
assert report["sandbox_id"] == "abc12345"
assert report["ssh_one_liner"] is not None
assert "ops_bridge" in report

51
tests/test_secrets.py Normal file
View File

@@ -0,0 +1,51 @@
"""Setup secret resolution tests."""
from __future__ import annotations
import pytest
from sandboxer.models import Profile, SetupSpec
from sandboxer.secrets.resolver import resolve_secret_ref, resolve_setup_secrets
def test_resolve_secret_ref_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SANDBOXER_SECRET_BUILD_REGISTRY_TOKEN", "tok123")
assert resolve_secret_ref("build-registry-token") == "tok123"
def test_resolve_setup_secrets_success(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SANDBOXER_SECRET_BUILD_REGISTRY_TOKEN", "tok123")
profile = Profile.model_validate(
{
"id": "profile.build",
"version": "1.0.0",
"extension": "ext.vm-packer",
"setup": SetupSpec(secret_refs=["build-registry-token"]).model_dump(),
}
)
secrets = resolve_setup_secrets(profile)
assert secrets["build-registry-token"] == "tok123"
def test_resolve_setup_secrets_missing_raises() -> None:
profile = Profile.model_validate(
{
"id": "profile.build",
"version": "1.0.0",
"extension": "ext.vm-packer",
"setup": SetupSpec(secret_refs=["missing-ref"]).model_dump(),
}
)
with pytest.raises(ValueError, match="Unresolved secret_refs"):
resolve_setup_secrets(profile)
def test_empty_secret_refs() -> None:
profile = Profile.model_validate(
{
"id": "profile.compose-e2e",
"version": "1.0.0",
"extension": "ext.compose-ssh",
}
)
assert resolve_setup_secrets(profile) == {}