Skip to content

DRU-379 - Druks sandbox client: set_expiry / release unchanged - #409

Open
druks-operator-treadstone[bot] wants to merge 1 commit into
mainfrom
agent/DRU-379
Open

DRU-379 - Druks sandbox client: set_expiry / release unchanged#409
druks-operator-treadstone[bot] wants to merge 1 commit into
mainfrom
agent/DRU-379

Conversation

@druks-operator-treadstone

Copy link
Copy Markdown
Contributor

Linear ticket: DRU-379

Plan

DRU-379 — Druks sandbox Client.set_expiry; release unchanged

Understanding

Idle-hold needs to clip a sandbox host's lease down without deleting the VM. The SDK
already exposes SandboxAPI.renew_host(host_id, *, expires_at=None)POST /hosts/{id}/renew
(DRU-378, Done). This ticket adds a thin Druks-client wrapper. Scope is one method plus tests;
no callers are wired here (that's DRU-380).

Change (all in backend/druks/sandbox/)

  • client.py — add Client.set_expiry(host_id, expires_at) next to release. It creates
    its own SandboxAPI via self._api(), calls api.renew_host(host_id, expires_at=expires_at),
    and aclose()s in a finally — mirroring the lifecycle idiom of release/list_hosts.
    Follow the repo's keyword-only idiom for the params. SandboxNotFoundError is already imported.
  • Exception behaviorset_expiry does not catch SandboxNotFoundError: a missing host
    propagates it unchanged. Do not map it to HostGone (that mapping is attach's, for stale
    reattach refs; the ticket pins SandboxNotFoundError as the surfaced "gone" signal here).
  • Docstring — document that clipping expiry is how idle-hold ends, Drukbox's janitor still
    reaps the host at expiry, and release() stays a hard delete_host.

Do not

  • No host-id column on durable_runs; reattach stays {workflow_id}:sandbox (no migration).
  • No Drukbox-side max invented. A client-side clip to now + SANDBOX_HOST_LEASE_SECONDS is
    explicitly optional and left out — pass expires_at straight through (see rejected).
  • No SDK change — renew_host already exists.

Tests (backend/tests/test_sandbox_lifecycle.py)

Extend _FakeAPI with a renew_host recording hook (+ optional renew_raises). Cover:
set_expiry forwards the expires_at to renew_host; a SandboxNotFoundError from
renew_host surfaces out of set_expiry; existing release delete behavior is untouched.

Out of scope

Wiring set_expiry into an idle-hold caller (DRU-380) and any lease-policy/clip-extend logic
(DRU-377).

Acceptance Criteria

  • AC1: Client.set_expiry is added in backend/druks/sandbox/client.py, accepts a host_id and an expires_at, and calls api.renew_host(host_id, expires_at=expires_at) on a SandboxAPI it obtains via self._api() and closes with aclose() in a finally (matching sibling methods).
    • Verification: Read the diff: method present with that body and lifecycle.
  • AC2: When renew_host raises SandboxNotFoundError, set_expiry propagates it unchanged — it is not swallowed and not remapped to HostGone.
    • Verification: A test asserts set_expiry on a fake API whose renew_host raises SandboxNotFoundError re-raises SandboxNotFoundError.
  • AC3: set_expiry forwards the caller-supplied expires_at to renew_host (no client-side clamp/override).
    • Verification: A test records the expires_at passed into the fake renew_host and asserts it equals the value passed to set_expiry.
  • AC4: release() is unchanged — still calls delete_host and swallows already-gone/failed deletes; no host-id column is added to durable_runs and no new migration is introduced.
    • Verification: Read the diff: release body and reattach key untouched; no file under backend/migrations/ added; existing test_release_calls_sdk_delete still passes.
  • AC5: set_expiry carries a docstring stating that clipping expiry is how idle-hold ends, that Drukbox's janitor still reaps at expiry, and that release() remains a hard delete.
    • Verification: Read the diff: method docstring present conveying that intent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@druks-operator-treadstone druks-operator-treadstone Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: approve

Verification

All five acceptance criteria pass, verified directly against the diff (git diff 6b350ce591652c72b91a7254bd40985508bcca0c...16c4b2607774b9378fe2d9bdcbe56fae34c382c2), which touches exactly two files: backend/druks/sandbox/client.py (+17) and backend/tests/test_sandbox_lifecycle.py (+47/-1).

  • AC1 (method shape/lifecycle) — pass. client.py:238-253: set_expiry(self, *, host_id: str, expires_at: datetime) -> None obtains api = self._api(), calls await api.renew_host(host_id, expires_at=expires_at), and closes via finally: await api.aclose() — identical lifecycle idiom to release/list_hosts.
  • AC2 (SandboxNotFoundError propagates) — pass. test_set_expiry_surfaces_missing_host sets renew_raises to a SandboxNotFoundError instance and asserts pytest.raises(SandboxNotFoundError) with excinfo.value is missing — an identity check confirming unmodified propagation, not swallowed or remapped to HostGone.
  • AC3 (expires_at forwarded verbatim) — pass. test_set_expiry_forwards_expires_at_to_sdk_renew asserts api.renewed == [("host-xyz", expires_at)] with no clamp/override in the method body.
  • AC4 (release unchanged, no migration/durable_runs change) — pass. release() body is byte-identical to base; no file under backend/migrations/ was added; reattach key logic untouched.
  • AC5 (docstring) — pass. The docstring states clipping is how idle-hold ends, that drukbox's janitor still reaps at expiry, and that release remains the hard delete.

CI: both underlying checks (backend, frontend) that the verification profile's "checks" name covers are green for this head SHA, so ruff check backend, frontend lint, frontend build, pytest backend/, and frontend test are all recorded as passing via CI.

No blocking findings, no open findings carried forward (round 1).

Code review

Advisory-only pass, clean-room (no ticket/AC visibility). One medium finding:

  • set_expiry currently has zero callers outside its own tests (grep -rn "set_expiry" backend outside the test file returns only the definition at client.py:238). The plan's own scope note confirms this is deliberate — wiring an idle-hold caller is explicitly deferred to a follow-up ticket — but flagging it so the primitive doesn't linger unused if that follow-up slips. Filed as DRU-412, a child of DRU-379 (Linear auto-linked it to the existing DRU-380 follow-up, which already covers wiring the caller).

Everything else checked clean: the method mirrors the established _api()/try-finally aclose() pattern used by sibling methods, SandboxNotFoundError handling is consistent with how release (swallows) and attach (remaps to HostGone) each own their respective error semantics, the SDK's renew_host signature matches the fake's signature exactly, both new tests assert on behavior rather than implementation/prose, and no out-of-scope files were touched.

@druks-operator-treadstone
druks-operator-treadstone Bot marked this pull request as ready for review September 3, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants