drukbox-python-sdk 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drukbox-python-sdk
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Async Python client for Drukbox.
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.28
@@ -38,9 +38,13 @@ try:
38
38
  env={"FOO": "bar"},
39
39
  idempotency_key="agent-run-42",
40
40
  )
41
- # Use host.external_ssh_host (or host.internal_ssh_host when the
42
- # service runs with Tailscale enabled), host.external_ssh_port,
43
- # and host.known_hosts with asyncssh or another SSH client.
41
+ # Dial whichever reachable address fits your network: host.external_ssh_host
42
+ # for the provider's public path (may be empty when the service runs an
43
+ # AWS-with-Tailscale-on deployment) or host.internal_ssh_host for the
44
+ # tailnet MagicDNS name (only when Tailscale is enabled). Use host.known_hosts
45
+ # for SSH host-key verification. If the provider mints a per-VM keypair
46
+ # (AWS with Tailscale off), host.private_key carries the private half —
47
+ # returned exactly once at create time; subsequent get_host returns None.
44
48
  finally:
45
49
  await sandbox.delete_host(host.id)
46
50
  await sandbox.aclose()
@@ -30,9 +30,13 @@ try:
30
30
  env={"FOO": "bar"},
31
31
  idempotency_key="agent-run-42",
32
32
  )
33
- # Use host.external_ssh_host (or host.internal_ssh_host when the
34
- # service runs with Tailscale enabled), host.external_ssh_port,
35
- # and host.known_hosts with asyncssh or another SSH client.
33
+ # Dial whichever reachable address fits your network: host.external_ssh_host
34
+ # for the provider's public path (may be empty when the service runs an
35
+ # AWS-with-Tailscale-on deployment) or host.internal_ssh_host for the
36
+ # tailnet MagicDNS name (only when Tailscale is enabled). Use host.known_hosts
37
+ # for SSH host-key verification. If the provider mints a per-VM keypair
38
+ # (AWS with Tailscale off), host.private_key carries the private half —
39
+ # returned exactly once at create time; subsequent get_host returns None.
36
40
  finally:
37
41
  await sandbox.delete_host(host.id)
38
42
  await sandbox.aclose()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "drukbox-python-sdk"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "Async Python client for Drukbox."
9
9
  readme = "README.md"
10
10
  # Tracks the lowest Python any current consumer supports. Bump only
@@ -45,5 +45,10 @@ testpaths = ["tests"]
45
45
  [tool.pyright]
46
46
  include = ["src", "tests"]
47
47
  pythonVersion = "3.11"
48
- typeCheckingMode = "strict"
48
+ typeCheckingMode = "standard"
49
+ venvPath = "."
50
+ venv = ".venv"
49
51
  reportMissingTypeStubs = false
52
+ reportUnknownMemberType = false
53
+ reportUnknownVariableType = false
54
+ reportUnknownArgumentType = false
@@ -61,13 +61,22 @@ _SANDBOX_HTTP_LIMITS = httpx.Limits(
61
61
  class SandboxHost:
62
62
  """Snapshot of a provisioned host as returned by the service.
63
63
 
64
- The shape mirrors the Drukbox ``Host`` schema. ``external_ssh_host``
65
- is always populated by the VM provider; ``internal_ssh_host`` is
66
- populated only when the service runs with Tailscale enabled (MagicDNS
67
- name on the tailnet). The internal path is always reached on port 22
68
- by Tailscale convention, so there is no ``internal_ssh_port``.
69
- Callers pick whichever path they can reach and dial it themselves —
70
- this SDK doesn't speak SSH.
64
+ The shape mirrors the Drukbox ``Host`` schema. Either reachable
65
+ address may be empty/None depending on the provider and deployment:
66
+ ``external_ssh_host`` carries the provider's public path (always
67
+ populated by exe.dev; empty for an AWS host with Tailscale on);
68
+ ``internal_ssh_host`` carries the tailnet MagicDNS form, populated
69
+ only when the service runs with Tailscale enabled. The internal
70
+ path is always reached on port 22 by Tailscale convention, so
71
+ there is no ``internal_ssh_port``. Callers pick whichever path
72
+ they can reach and dial it themselves — this SDK doesn't speak SSH.
73
+
74
+ ``private_key`` carries per-VM SSH private key material returned
75
+ exactly once at create time, when the provider mints fresh material
76
+ per instance (AWS with Tailscale off). Providers that use a
77
+ different SSH auth model (exe.dev's edge proxy, Tailscale ACLs
78
+ via tailscaled-SSH) return None here. A later ``get_host`` call
79
+ always returns None — the key is not persisted server-side.
71
80
  """
72
81
 
73
82
  id: str
@@ -80,6 +89,7 @@ class SandboxHost:
80
89
  internal_ssh_host: str | None
81
90
  known_hosts: str
82
91
  tailscale_device_id: str | None
92
+ private_key: str | None
83
93
  last_error: str
84
94
  created_at: str
85
95
  updated_at: str
@@ -49,6 +49,7 @@ def _host_payload(**overrides: Any) -> dict[str, Any]:
49
49
  "internal_ssh_host": "host-abc.example.ts.net",
50
50
  "known_hosts": "ssh-ed25519 AAAA...\n",
51
51
  "tailscale_device_id": None,
52
+ "private_key": None,
52
53
  "last_error": "",
53
54
  "created_at": "2026-05-28T12:00:00+00:00",
54
55
  "updated_at": "2026-05-28T12:00:00+00:00",
@@ -199,6 +200,37 @@ async def test_unknown_fields_in_host_payload_are_ignored(api: SandboxAPI):
199
200
  assert not hasattr(host, "future_field") # New field silently dropped.
200
201
 
201
202
 
203
+ @respx.mock
204
+ async def test_create_host_returns_private_key_when_provider_mints_one(api: SandboxAPI):
205
+ """AWS with Tailscale-off mints a per-VM ed25519 keypair and returns
206
+ the private half exactly once at create time. The SDK must round-trip
207
+ it through to the caller."""
208
+
209
+ pem = "-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n-----END OPENSSH PRIVATE KEY-----\n"
210
+ payload = _host_payload(provider="aws", private_key=pem, external_ssh_host="ec2-x.example")
211
+ respx.post(f"{BASE_URL}/hosts").mock(return_value=httpx.Response(201, json=payload))
212
+
213
+ host = await api.create_host()
214
+
215
+ assert host.provider == "aws"
216
+ assert host.private_key == pem
217
+
218
+
219
+ @respx.mock
220
+ async def test_get_host_returns_none_private_key_after_create(api: SandboxAPI):
221
+ """drukbox doesn't persist the key, so a subsequent GET should
222
+ return None regardless of what was returned at create time."""
223
+
224
+ payload = _host_payload(private_key=None)
225
+ respx.get(f"{BASE_URL}/hosts/{payload['id']}").mock(
226
+ return_value=httpx.Response(200, json=payload),
227
+ )
228
+
229
+ host = await api.get_host(payload["id"])
230
+
231
+ assert host.private_key is None
232
+
233
+
202
234
  # ---------------------------------------------------------------------------
203
235
  # Error classification
204
236
  # ---------------------------------------------------------------------------