Skip to content

feat(inspect-ai): add inspect-capsem VM-mode SandboxEnvironment integration - #338

Open
tholop wants to merge 1 commit into
feat/sdk-typed-helpersfrom
feat/inspect-capsem
Open

tholop wants to merge 1 commit into
feat/sdk-typed-helpersfrom
feat/inspect-capsem

Conversation

@tholop

@tholop tholop commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Ports the inspect-capsem Inspect AI SandboxEnvironment integration (@sandboxenv(name="capsem") in integrations/inspect-ai, distribution inspect-capsem-sandbox, module inspect_capsem) onto the 0.7 Python SDK surface in VM-only mode (superseding the monolithic feat(inspect-ai): add inspect-capsem SandboxEnvironment integration #291 PR; upstream tracking issue 0.7: Refactor and land Inspect VM/workload support #310).
  • Splits the implementation by responsibility across focused modules under integrations/inspect-ai/inspect_capsem/ (__init__.py, config.py, _controller.py, _exec.py, _files.py, _lifecycle.py, _registry.py, _tools.py, _transfer.py, sandbox.py), with every module under 250 lines at 100 columns.
  • VM-mode behaviour in this PR:
    • Ephemeral labeled sample VMs: creates one fresh ephemeral VM per sample (persistent=False, labeled managed-by=inspect-capsem + optional inspect-capsem-prefix / inspect-capsem-task) and scopes task_cleanup / cli_cleanup strictly to matching labels.
    • Typed SDK surface (capsem>=0.7.0): uses Hypervisor.connect / discover_gateway, typed VmNotFoundError / CreateTimeoutError / ExecTimeoutError (cleaning up unnamed VMs on HTTP 504 CreateTimeoutError via CreateTimeoutError.vm_id), and VmLifecycleState.
    • Bounded memory & streaming transfers: derives staged transfer chunk sizes from MAX_REQUEST_BODY_BYTES, enforces SandboxEnvironmentLimits.MAX_EXEC_OUTPUT_SIZE (10 MiB per stream), requires [ -f ] and caps guest reads at limit + 1 bytes while streaming (head -c + per-part accounting in _transfer._staged_download), after a stat pre-check against SandboxEnvironmentLimits.MAX_READ_FILE_SIZE.
    • Interactive shell connection: connection() returns SandboxConnection(type="capsem", command="capsem shell <vm_id>").
    • Gate & live acceptance: adds integrations/inspect-ai to [boundary.scripts].roots (300-line ceiling), wires the package into capsem-gate (fast.integrations.inspect-ai.{lint,types,tests,build}), CI scope routing, and the ironbank live VM lane (tests/ironbank/test_sdk_live.py runs integrations/inspect-ai/tests/live_acceptance.py, asserting INSPECT_CAPSEM_VM_ACCEPTANCE_OK).

Mapping to Elie's #291 Review

Every item from the #291 review, and where it is resolved in the stack:

# #291 item Where Notes
B1 300-line ceiling (integrations/ not in [boundary.scripts].roots; 6 files over) PR 2b (+ PR 3 / PR 4 for container modules) integrations/inspect-ai added to roots; VM-mode modules split by responsibility (largest: _lifecycle.py 238, sandbox.py 231). PR 3 / PR 4 modules also stay under the ceiling (compose_yaml.py 253, image_build.py 295, oci_registry.py 294).
S1 Host memory bound on downloads (_staged_download no byte cap, /dev/zero, _save_built_image_to_cache) PR 2b; _save_built_image_to_cache half in PR 3 / PR 4 _files.read_guest_file requires a regular file ([ ! -f ] -> NOT_REGULAR), _staged_download runs [ -f ] && head -c <limit+1> | split and stops at limit + 1 while joining parts. The in-guest image cache that owned _save_built_image_to_cache is deleted with the 0.6 dockerd backend (PR 3); PR 4's host-side ingestion writes docker image save -o <tmp>/image.tar to disk and streams blobs in 256 KiB chunks (oci_registry.ingest_docker_save_tar), never reading the archive into memory.
S2 Ephemeral by default (named sample VMs were persistent; "add an SDK/service way to label or find ephemeral VMs. Happy to do that side.") PR 1 (service + SDK labels) + PR 2b We built the SDK/service side ourselves in PR 1 rather than taking Elie's offer; PR 2b creates unnamed persistent=False VMs with managed-by=inspect-capsem.
S3 Cleanup scope ("require the inspect-capsem- prefix") PR 2b Deliberate adaptation: sample VMs no longer have names (S2), so scoping is by label instead of name prefix — cli_cleanup and task_cleanup only stop VMs with managed-by=inspect-capsem (and the matching inspect-capsem-prefix when CAPSEM_VM_PREFIX is set); inspect sandbox cleanup capsem <id> on any other VM logs a warning and leaves it alone.
S4 Host environment and paths (Compose interpolation, bare environment: [KEY], bind volumes) PR 3 (+ PR 4 for build contexts) Compose parsing only exists from PR 3 on, so this item is addressed there: default-deny allowlists CAPSEM_INSPECT_ALLOWED_HOST_ENV / CAPSEM_INSPECT_ALLOWED_HOST_PATHS (operator env vars, not task config), os.path.realpath containment of bind sources. Not claimed by this PR.
S5 SDK floor (capsem>=0.6.3 while using newer APIs) PR 2b integrations/inspect-ai/pyproject.toml declares capsem>=0.7.0 — the first release that will contain labels, Hypervisor.connect, ErrorCode, and image=. In-repo resolution uses [tool.uv.sources] (editable ../../sdk/python); the sdk/python/pyproject.toml manifest itself still reads 0.6.3 only because it is outside the [[versions.stamped]] release cohort (see PR 2a notes; bump tracked with #309).
S6 Move shared logic into the SDK (gateway discovery, copied sanitize_file_path, error-text parsing) PR 2a + PR 2b PR 2a adds discover_gateway / Hypervisor.connect, structured ErrorResponse.code + typed exceptions; PR 2b consumes them. No sanitize_file_path copy remains — guest paths are validated server-side by 0.7's files.read/write(..., exact=...). No re.search over error text in inspect_capsem/.
S7 A VM-backed test in the gate PR 2b tests/ironbank/test_sdk_live.py runs integrations/inspect-ai/tests/live_acceptance.py in the VM lane without opt-in flags.
N1 Nit: drop X as X re-exports in buildschema.py Superseded upstream The gate refactor now lives in Elie's refactor/0.7-qualification-schemas-draft (bb29556b, kept under Pierre's authorship); his follow-up ba76171f (test_qualification_schema_reexports_keep_one_model_identity) asserts buildschema.X is qualifyschema.X, i.e. the re-exports are now pinned by upstream's own test. Not changed in this stack — say the word if you still want them dropped and we will update that test alongside.
N2 Nit: 8a146e35 also adds SourcePackageConfig, only the next commit uses it Superseded upstream Same draft branch: bb29556b carries SourcePackageConfig and ba76171f tests it (_source_package_type). PR 2b is the first consumer ([integrations_inspect_ai] in config/gate.toml).
N3 Nit: pass the settings explicitly in sdkchecks.py instead of isinstance / fallback branches PR 2b sdkchecks.py takes settings: SourcePackageConfig explicitly; no isinstance fallback remains.
N4 Nit: avoid assert in production code (sandbox.py) PR 2b No assert statements in inspect_capsem/.
SC Scope suggestion: 3 PRs (gate refactor / VM-mode sandbox / container mode) Whole stack Gate refactor -> Elie's refactor/0.7-qualification-schemas-draft; VM-mode sandbox -> PR 2b; container mode -> PR 3, with host-side Dockerfile / build: split further into PR 4 because it runs docker build on the host (its own trust-boundary caveat). SDK prerequisites -> PR 1 / PR 2a.
— ModulesConfig.transition: TransitionSettings conflict in qualifyschema.py n/a on 0.7 Present at qualifyschema.py:70 on this stack (the stack is based on integration/0.7-clients-inspect + the two draft branches), so there is no conflict to resolve.

Stack Overview (Supersedes #291)

  1. PR 1 (feat/sdk-vm-labels) — VM labels on create, fork, and list
  2. PR 2a (feat/sdk-typed-helpers) — structured ErrorCode enum, timeout/lookup exceptions, and gateway discovery
  3. PR 2b (feat/inspect-capsem) (this PR) — VM-mode inspect-capsem SandboxEnvironment integration
  4. PR 3 (feat/inspect-capsem-containers) — OCI container execution mode and Compose parser
  5. PR 4 (feat/inspect-capsem-host-build) — host-side Dockerfile and Compose build: image support

@codecov-commenter

codecov-commenter commented Oct 7, 2026 •

Copy link
Copy Markdown

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
6016 5 6011 0
View the top 3 failed test(s) by shortest run time
capsem-assets::oci::cache::inventory::tests::reused_materialization_controls_invalidate_inventory_on_acquire_and_after_release
Stack Traces | 0.23s run time
thread 'oci::cache::inventory::tests::reused_materialization_controls_invalidate_inventory_on_acquire_and_after_release' (134637) panicked at .../cache/inventory/tests.rs:130:39:
called `Result::unwrap()` on an `Err` value: cache changed during inventory observation
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
capsem-assets::oci::pull::readiness::tests::reconciling_another_image_preserves_readiness_and_retained_inode_facts
Stack Traces | 0.321s run time
thread 'oci::pull::readiness::tests::reconciling_another_image_preserves_readiness_and_retained_inode_facts' (136951) panicked at .../pull/readiness/tests.rs:105:65:
called `Result::unwrap()` on an `Err` value: cache changed during readiness verification
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
capsem-assets::oci::cache::inventory::tests::cancelled_partial_materialization_releases_its_barrier_before_invalidating_inventory
Stack Traces | 0.499s run time
thread 'oci::cache::inventory::tests::cancelled_partial_materialization_releases_its_barrier_before_invalidating_inventory' (154392) panicked at .../cache/inventory/tests.rs:205:39:
called `Result::unwrap()` on an `Err` value: cache changed during inventory observation
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
capsem-assets::oci::worker::tests::inventory_worker_publishes_only_quiet_bounded_observations_and_tracks_nested_changes
Stack Traces | 2.38s run time
thread 'oci::worker::tests::inventory_worker_publishes_only_quiet_bounded_observations_and_tracks_nested_changes' (139094) panicked at .../oci/worker/tests.rs:45:6:
called `Result::unwrap()` on an `Err` value: TimedOut { label: "linked-inventory", attempts: 8, timeout: 2s }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
capsem-assets::oci::worker::tests::incompatible_receipts_are_observed_once_until_their_metadata_changes
Stack Traces | 2.65s run time
thread 'oci::worker::tests::incompatible_receipts_are_observed_once_until_their_metadata_changes' (139024) panicked at .../oci/worker/tests.rs:221:10:
called `Result::unwrap()` on an `Err` value: TimedOut { label: "foreign-cache-observed", attempts: 8, timeout: 2s }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
capsem-core::fs_monitor::tests::a_workspace_swapped_for_a_host_link_is_never_walked_or_read
Stack Traces | 360s run time
No failure message available

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

ebursztein added a commit that referenced this pull request Oct 7, 2026
Link assigned issue #342 and source PRs #338, #339 and #340. Keep completed parser components available while removing Inspect VM/workload and Dockerfile/Compose phases from this agent sprint. Shared 0.7 includes all carried code and latest main; complete integration and runtime/package qualification remain Pierre’s work.
…ration

Port the inspect-capsem Inspect AI SandboxEnvironment integration onto
the 0.7 Python SDK surface in VM-only mode (container/image execution
deferred to the follow-up container commit):

- Split the implementation across focused modules under
  integrations/inspect-ai/inspect_capsem/ (config.py, _controller.py,
  _exec.py, _files.py, _lifecycle.py, _registry.py, _tools.py,
  _transfer.py, sandbox.py) with every module under 300 lines at 100
  columns.
- Encapsulate private CapsemSandboxEnvironment state inside sandbox.py,
  return SandboxConnection(type="capsem", command="capsem shell <id>"),
  and bound process-owned VM teardown at interpreter exit.
- Derive staged file transfer part sizes from MAX_REQUEST_BODY_BYTES,
  scope VM cleanup to exact managed-by + prefix labels, and clean up
  unnamed VMs on 504 CreateTimeoutError via CreateTimeoutError.vm_id.
- Harden non-root user environment reset when id -un prints numeric UID
  to stdout and exits 1 or pwd.getpwuid raises KeyError in minimal
  containers.
- Wire integrations/inspect-ai into capsem-gate, CI scope routing,
  installed wheel/sdist entry-point proof (image_package_acceptance.py),
  and live VM ironbank acceptance (live_acceptance.py + test_sdk_live.py).

Proves #310 / #342 acceptance criteria:
- [x] `inspect_capsem` registers cleanly as an `inspect_ai`
  `SandboxEnvironment` entry point (`@sandboxenv(name="capsem")`) from
  an installed `inspect-capsem-sandbox` wheel and sdist in an isolated
  prefix (`integrations/inspect-ai/tests/image_package_acceptance.py`).
- [x] `sample_init`, `exec` (`ExecTarget.VM` with non-zero exit, signal,
  and timeout), `read_file`/`write_file` (text, binary, non-workspace),
  Inspect's `self_check` suite, `eval_async` with `SandboxEnvironmentSpec("capsem", ...)`,
  `sample_cleanup`, `task_cleanup`, and prefix-scoped `cli_cleanup` run
  against the live service with session ledger (`history(layer=EXEC)` +
  `session.db` `exec_events`) and zero leaked VMs.
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.

2 participants