fix(ssh): persist sandbox host identities (#4094)

* fix(ssh): persist sandbox host identities

Store each sandbox's Ed25519 host key in the gateway credential store and
deliver it only to the supervisor. Preserve identity across restarts,
delete owned credentials with the sandbox, and expose the public SHA256
fingerprint through sandbox and SSH-session APIs and client SDKs.

Cover credential ownership, cancellation, deletion retries, client
compatibility, and pinned SSH connections through lifecycle transitions.

Closes #3835

Signed-off-by: Mike Nguyen <miken@nvidia.com>

* fix(compute): clean up failed sandbox SSH identity creation

Signed-off-by: Mike Nguyen <miken@nvidia.com>

* test(ssh): wait for sandbox deletion before name reuse

Signed-off-by: Mike Nguyen <miken@nvidia.com>

---------

Signed-off-by: Mike Nguyen <miken@nvidia.com>
This commit is contained in:
Mike Nguyen
2026-10-06 20:09:20 +00:00
committed by GitHub
parent 5601d71b01
commit 9fd41e6bfd
38 changed files with 1808 additions and 186 deletions
+3
View File
@@ -521,6 +521,8 @@ class SandboxRef:
service_urls: Mapping[str, str] = field(
default_factory=_ImmutableLabels, compare=False
)
# Stable SSH host identity; absent on older runtimes.
host_key_fingerprint: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "labels", _ImmutableLabels(self.labels))
@@ -1796,6 +1798,7 @@ def _sandbox_ref(
labels=sandbox.metadata.labels if sandbox.metadata else {},
created_from_workload_template=provenance,
service_urls=service_urls or {},
host_key_fingerprint=sandbox.host_key_fingerprint or None,
)
+7
View File
@@ -2728,6 +2728,13 @@ def test_sandbox_ref_retains_gateway_labels() -> None:
assert dict(ref.labels) == {"aiq": "deep-research", "env": "dev"}
def test_sandbox_ref_exposes_host_identity_and_accepts_legacy_response() -> None:
proto = _make_sandbox_proto("sandbox-1", "job-1")
assert _sandbox_ref(proto).host_key_fingerprint is None
proto.host_key_fingerprint = "SHA256:expected"
assert _sandbox_ref(proto).host_key_fingerprint == "SHA256:expected"
def test_sandbox_ref_includes_main_process_result() -> None:
proto = _make_sandbox_proto("sandbox-1", "job-1")
proto.status.exit_code = 0