Sandbox and Test Flow Guide
This document describes the public sandbox API, the global ConfigManager under
AgentENV/src/cfg.rs, and what AgentENV/tests/integration/fc.rs,
AgentENV/tests/integration/process.rs, AgentENV/tests/integration/snapshot.rs, and
AgentENV/tests/integration/orchestrator.rs validate.
1) Sandbox Public Interface
Types and responsibilities
-
FirecrackerSandboxConfig(AgentENV/src/sandbox/firecracker/config.rs)- Purpose: describes a fresh VM boot (kernel + tools drive + user image).
- Key fields
firecracker_binary: path to thefirecrackerexecutable.kernel_image: path tovmlinux.bin.tools_drive_version: immutable tools drive identity. The node resolves it to<deps_path>/tools/<version>/tools.ext4before mounting it as/dev/vda.user_image_config: overlaybd config for the writable user image (/dev/vdb).boot_args: kernel boot args (e.g.console=ttyS0 ... init=/init).vcpu_count,mem_size_mib: VM size.runtime_policy: socket/envd timeouts and poll intervals.firecracker_stdout_path,firecracker_stderr_path: capture FC logs. If unset, logs default towork_dir/logs/firecracker-stdout.logandwork_dir/logs/firecracker-stderr.log.envd_version: expected envd version for the guest image.env_vars: optional default environment variables injected after envd init.ublk_config: optional ublk-backed rootfs configuration.
- Convenience
from_global_config()builds a config fromConfigManagerusing the internal default template rootfs image. The image must already be resolved under<image-cache-root>/configs.from_global_config_with_user_image(...)builds a config from an already resolved overlaybd image config.- Runtime launches from committed snapshots are built through
FirecrackerSandbox::from_snapshot(&RunnableSnapshot, &SandboxLaunchConfig). uid/gid: File ownership (default0).
-
ProcessOpts(AgentENV/src/sandbox/process.rs)- Purpose: Options for starting a process inside the sandbox.
- Key fields
envs:HashMap<String, String>for process environment variables.cwd: Optional working directory.timeout: Optional max time to wait for completion.
- Builder methods:
with_envs(),with_cwd(),with_timeout().
-
ProcessOutput(AgentENV/src/sandbox/process.rs)- Purpose: Result of a completed process execution.
- Fields:
stdout,stderr,exit_code.
-
ProcessHandle(AgentENV/src/sandbox/process.rs)- Purpose: Handle to a running process inside the sandbox.
- Key methods
pid(): Returns the PID inside the guest VM.wait().await: Waits for exit and collects all output.send_stdin(data).await: Sends bytes to stdin.send_signal(signal).await: Sends a signal (e.g. SIGTERM).kill().await: Kills the process withSIGKILL.
-
FirecrackerSnapshotConfig(AgentENV/src/sandbox/firecracker/config.rs)- Purpose: describes how to resume a VM from snapshot + memory + base disk.
- Key fields
vm_state_path: Firecracker VM state file.mem_overlaybd_config: runtime memory overlay image config.base_rootfs_path: writable disk image or overlaybd rootfs image config paired with that snapshot.firecracker_binary: FC binary to use for resuming.runtime_policy: socket/envd timeouts and poll intervals.envd_version: guest envd version metadata.env_vars: optional default environment variables restored on start.
- Notes
FirecrackerSnapshotConfigreturned bypause()owns a tempdir (keeps files alive).get_rootfs_path()resolves the snapshot rootfs path used by runtime tests.
-
SandboxBackendandSandboxExecutor(AgentENV/src/sandbox/backend.rs)- Purpose: public traits for sandbox lifecycle and process execution.
- Key methods
SandboxBackend:start,start_nowait,wait_for_ready,pause,resume,stop.SandboxExecutor:run_command,run_command_with_opts,start_process.
-
FirecrackerSandbox(AgentENV/src/sandbox/firecracker/sandbox.rs)- Purpose: lifecycle controller for a single Firecracker instance.
- Main methods (detailed)
FirecrackerSandbox::new(FirecrackerSandboxConfig)- Creates a sandbox handle for a fresh boot.
- Does not start Firecracker; it only prepares the object.
- Each sandbox gets its own temporary work directory.
FirecrackerSandbox::resume_from_snapshot_config(&FirecrackerSnapshotConfig)- Creates a new sandbox handle and immediately starts it from a snapshot config.
- Uses
vm_state_path, the memory overlay image config, andbase_rootfs_pathfrom the config. - Returns a running sandbox after Firecracker and envd are ready.
FirecrackerSandbox::from_snapshot(&RunnableSnapshot, &SandboxLaunchConfig)- Creates a sandbox handle from committed snapshot state resolved for a node.
- Uses snapshot
vm_state.binplus node-local materializedmemory/image.jsonandrootfs/image.json. - Does not start Firecracker; call
start().awaitafterward.
start().await- Spawns the Firecracker process, waits for the API socket, starts the VM, then waits for guest-level envd readiness.
pause().await- Sends
PATCH /vmto pause the VM. - Creates snapshot artifacts including
vm_state.bin,mem.bin, the packaged memory image config, and rootfs snapshot state. - Returns a
FirecrackerSnapshotConfigthat owns the tempdir holding these files.
- Sends
resume().await- Resumes a paused VM in-place by sending
PATCH /vmwithResumed. - Use this when you want to keep the same sandbox instance.
- Resumes a paused VM in-place by sending
stop().await- Stops Firecracker, releases network resources, and cleans up optional ublk state.
run_command(cmd, args).await- Runs a command inside the sandbox via envd gRPC and waits for it to
complete. Returns
ProcessOutputwith stdout, stderr, and exit code.
- Runs a command inside the sandbox via envd gRPC and waits for it to
complete. Returns
run_command_with_opts(cmd, args, opts).await- Same as
run_commandbut acceptsProcessOptsfor setting env vars, working directory, and execution timeout.
- Same as
start_process(cmd, args, opts).await- Starts a long-running process inside the sandbox. Returns a
ProcessHandlethat can stream output, send stdin, or kill the process.
- Starts a long-running process inside the sandbox. Returns a
firecracker_stdout_path()/firecracker_stderr_path()- Resolved log paths used by this sandbox (including defaults).
work_rootfs_path()- Returns the path to the per-instance writable rootfs inside the work directory. Useful for inspecting or copying disk state after a run.
What happens under the hood (important for correct usage)
- Each
FirecrackerSandboxuses its own temp work directory. - Kernel is used via the configured host path.
- When resuming, the writable rootfs and memory image are materialized into the work dir so each instance can mutate independently. For overlaybd-backed snapshot-backed template launches, this means recreating runtime image configs and fresh writable uppers from the committed layer stack.
- Rootfs copies use copy-on-write helpers where available.
- Firecracker runs with
current_dir = work_dir, so relative paths work. - Firecracker logs default to
work_dir/logs/*unless explicit log paths are configured. start()waits for both the API socket and envd readiness.- Although the Firecracker process will be killed on drop, explicit
stop()is recommended.
Minimal usage example
#![allow(unused)]
fn main() {
use agentenv::sandbox::{FirecrackerSandbox, FirecrackerSandboxConfig, SandboxExecutor};
let mut cfg = FirecrackerSandboxConfig::new(
"/path/to/firecracker".into(),
"/path/to/vmlinux.bin".into(),
"/path/to/rootfs.ext4".into(),
);
cfg.boot_args = Some("console=ttyS0 reboot=k panic=1 pci=off init=/init".into());
let mut sandbox = FirecrackerSandbox::new(cfg)?;
sandbox.start().await?;
// Run a command inside the VM
let output = sandbox.run_command("echo", &["hello"]).await?;
assert_eq!(output.exit_code, 0);
println!("{}", output.stdout);
let snapshot = sandbox.pause().await?;
sandbox.stop().await?;
let mut resumed_sandbox = FirecrackerSandbox::resume_from_snapshot_config(&snapshot).await?;
resumed_sandbox.stop().await?;
}
Runtime prerequisites
- Linux host
/dev/kvmaccessible by current userdebugfs(e2fsprogs) if you use init injection or disk-inspection helpers
2) Global Config Manager (src/cfg.rs)
The tests and benchmarks use a shared global config manager based on
config/default.toml.
Config file format
AgentENV/config/default.toml (override via AENV_CONFIG_PATH or --config)
[firecracker]
boot_args = "your-kernel-boot-args"
binary_path = "/path/to/firecracker"
socket_timeout_secs = 3
socket_poll_ms = 20
# Optional override; defaults to $AENV_HOME/firecracker-work.
# work_dir = "/path/to/firecracker-work"
# Optional override; defaults to $AENV_HOME/logs/serial.
# serial_dir = "/path/to/serial"
# Optional; enables Firecracker's own logging when set to a non-empty level.
# log_level = "Info"
[kernel]
image_path = "/path/to/vmlinux.bin"
[tools]
version = "0.1.0-custom.1"
drive_path = "/path/to/tools.ext4"
[machine]
mem_size_mib = 128
vcpu_count = 1
[envd]
version = "0.5.15"
init_timeout_secs = 30
poll_ms = 10
[orchestrator]
auto_evict_interval_ms = 1000
default_sandbox_timeout_secs = 15
[snapshot]
repository_backend = "posix_fs"
local_cache_path = "$AENV_HOME/snapshot-local-cache"
[backend.posix_fs]
snapshot_store = "$AENV_HOME/snapshot-store"
[ublk]
enabled = false
TOML parameter reference
-
[firecracker]boot_args: Kernel command line. The runtime appendsinit=/initif not already present (tools drive provides the init script).binary_path: Absolute or relative path tofirecracker.socket_timeout_secs: Max time to wait for the Firecracker API socket.socket_poll_ms: Poll interval for checking socket existence.work_dir: Optional override for the per-sandbox Firecracker working directories. These directories include runtime sockets, symlinks, local logs, and writable OverlayBD upper layer data (overlaybd/upper.dataandoverlaybd/upper.index). Defaults to$AENV_HOME/firecracker-work.serial_dir: Optional override for the persistent output directory. Serial output is written under a per-sandbox subdirectory and not removed by the sandbox. Defaults to$AENV_HOME/logs/serial.log_level: Optional Firecracker log level (Error,Warning,Info,Debug,Trace, case-insensitive). When set to a non-empty value, Firecracker’s own logging is enabled and written to afirecracker.logfile in the sandbox’s log directory (the same directory used for serial output). If omitted or empty, Firecracker logging is disabled.
-
[kernel]image_path: Optional local kernel image path (vmlinux.bin). If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.
-
[tools]drive_path: Optional local tools drive source. When set, an explicitversionis required and setup imports it into the immutable version directory.
-
Template rootfs image
- User-visible rootfs images are selected when starting template builds through the optional
fromImagefield. - If
fromImageis omitted, AgentENV uses[image.resolver].default_image. - Standard Docker Hub shortnames such as
ubuntu:24.04andnode:20are normalized before image resolution. - Resolved image configs are cached under
<image-cache-root>/configs/<slug>-<hash>-image.json; local layer products are cached under<image-cache-root>/layers/; overlaybd-native remote block reads are cached under<image-cache-root>/remote-blocks/. - Registry authentication uses docker credentials from
~/.docker/config.jsonor$DOCKER_CONFIG/config.json.
- User-visible rootfs images are selected when starting template builds through the optional
-
[machine]mem_size_mib: Guest RAM size (default: 128).vcpu_count: Number of vCPUs (default: 1).
-
[envd]version: Expected envd version baked into the runtime image.init_timeout_secs: Max time to wait for the in-guest envd daemon to become ready after VM start (default: 30).poll_ms: Poll interval for envd health-check retries (default: 10).
-
[orchestrator]auto_evict_interval_ms: Poll interval for background timeout eviction.default_sandbox_timeout_secs: Default keep-alive timeout used by the orchestrator.auto_resume_min_sandbox_timeout_secs: When a data-plane request targets a non-running sandbox, automatically resume it (if auto-resume is enabled) and refresh its timeout for no-less than this duration.
-
[pool]low_watermark: Shared lower bound for warm resources. Defaults to2.high_watermark: Shared upper bound target for warm resources. Defaults to64.[pool.network].maintenance_enabled: Enables the background network-slot maintenance worker. Defaults totrue.[pool.block].enabled: Enables the ublk overlaybd warm-device pool.[pool.firecracker].enabled: Enables pre-spawned Firecracker processes for snapshot resume.
-
[snapshot]local_cache_path: Manager-owned node-local snapshot artifact/cache root. Defaults to$AENV_HOME/snapshot-local-cache.repository_backend: Snapshot repository backend. Defaults toposix_fs.
-
[backend.posix_fs]snapshot_store: Committed snapshot store root. Defaults to$AENV_HOME/snapshot-store.
-
[ublk]enabled: Enables ublk-backed rootfs handling.daemon_binary_path: Optional path touvm-ublk-daemon.daemon_socket_path: Optional unix socket path for daemon RPCs.daemon_log_path: Optional log file path for the daemon process.device_type:coworoverlaybd.
-
[ublk.overlaybd]global_config_path: path to the generated overlaybd runtime config. Required foroverlaybddevice_type. Per-image configs are derived from template buildfromImagevalues and live under<image.cache.root_dir>/configs.
Defaults and optional fields
[firecracker].binary_path: Optional. If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.[firecracker].boot_args: Optional. Defaults toconsole=ttyS0 reboot=k panic=1 pci=off.[firecracker].socket_timeout_secs: Optional. Defaults to 3 seconds.[firecracker].socket_poll_ms: Optional. Defaults to 20 ms.[firecracker].work_dir: Optional override. Defaults to$AENV_HOME/firecracker-work.[firecracker].serial_dir: Optional override. Defaults to$AENV_HOME/logs/serial.[kernel].image_path: Optional. If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.[tools].drive_path: Optional local source override; requires an explicit[tools].versionwhen set.[tools].url: Optional registry override; requires an explicit[tools].versionwhen set.[machine]: Optional. If omitted, defaults are 128 MiB RAM and 1 vCPU.[envd]: Optional, but template and sandbox flows expect a validversion.[envd].init_timeout_secs: Optional. Defaults to 30 seconds.[envd].poll_ms: Optional. Defaults to 10 ms.[orchestrator]: Optional. If omitted, orchestrator runtime defaults are used.[pool]: Optional. If omitted, network pool maintenance is enabled with low watermark 2 and high watermark 64; block and Firecracker process pools are disabled unless enabled in their component subsections.[snapshot]: Optional. If omitted,repository_backenddefaults toposix_fsand the local cache root derives fromAENV_HOME.[backend.posix_fs]: Optional. If omitted, the POSIX snapshot store defaults to$AENV_HOME/snapshot-store.[ublk]: Optional. If omitted, ublk is disabled.
Environment-based configuration
The config loader reads the full TOML file from the following sources:
AENV_CONFIG_PATH--config <path>config/default.toml
Runtime image and binary paths are configured in the TOML file itself.
Required fields
[kernel].image_path[tools].versionwhen[tools].drive_pathor[tools].urlis overridden[envd].version
If the [orchestrator] section is present, it must include
default_sandbox_timeout_secs.
ConfigManager (recommended entry point)
The helper is strict. If requirements are not met (missing files, invalid config), it returns an error and the test fails.
Path resolution order
AENV_CONFIG_PATHenvironment variable--configCLI flagconfig/default.toml(default)
Helper methods
ConfigManager::global()->Result<&'static ConfigManager>- Lazily initializes and returns a process-wide singleton.
ConfigManager::new()->Result<ConfigManager>- Loads config using the default resolution order.
ConfigManager::new_from_path(path)->Result<ConfigManager>- Loads a specific TOML file.
ConfigManager::config()->&AppConfig- Returns the parsed application config.
ConfigManager::global_config()->Result<&'static AppConfig>- Returns the global parsed config directly.
ConfigManager::get_orchestrator_config()->Option<OrchestratorConfig>- Returns orchestrator settings when configured.
AppConfig::resolved_snapshot_store()->PathBuf- Resolves the effective committed snapshot store root.
AppConfig::resolved_snapshot_local_cache_path()->PathBuf- Resolves the effective manager-owned node-local snapshot cache root.
Typical test setup flow
#![allow(unused)]
fn main() {
use agentenv::sandbox::{FirecrackerSandbox, FirecrackerSandboxConfig, SandboxExecutor};
let sandbox_config = FirecrackerSandboxConfig::from_global_config()?;
let mut sandbox = FirecrackerSandbox::new(sandbox_config)?;
sandbox.start().await?;
// Run commands via envd gRPC
let output = sandbox.run_command("echo", &["hello"]).await?;
assert_eq!(output.exit_code, 0);
let snapshot = sandbox.pause().await?;
sandbox.stop().await?;
let mut resumed_sandbox = FirecrackerSandbox::resume_from_snapshot_config(&snapshot).await?;
resumed_sandbox.stop().await?;
}
3) What the Integration Tests Validate
integration/fc.rs
-
microvm_startup_moves_to_runningBoots a fresh VM and verifies that:- Firecracker starts and accepts API calls.
- Kernel + rootfs can be configured without error.
- The VM can be cleanly shut down afterward.
-
microvm_pause_transitions_to_pausedValidates pause + snapshot creation by:- Pausing a running VM via FC API.
- Creating
vm_state.binandmem.bin. - Ensuring those files exist on disk.
-
microvm_resume_from_pause_transitions_to_runningValidates in-place resume by:- Pausing a VM.
- Resuming it in the same sandbox instance.
- Confirming the control flow completes without error.
-
microvm_resume_transitions_to_runningValidates resume into a new sandbox by:- Pausing and snapshotting a VM.
- Shutting down the original sandbox.
- Resuming from the snapshot in a new sandbox instance.
-
snapshot_preserves_disk_state_on_resumeConfirms disk state is preserved by:- Writing a marker file via
run_command. - Pausing and resuming from the snapshot.
- Reading the marker file after resume and asserting it persists.
- Writing a marker file via
-
snapshot_chain_survives_after_parent_snapshot_handle_is_droppedValidates multi-level snapshot chains by:- Creating a snapshot, resuming, creating a second snapshot.
- Dropping the first snapshot handle.
- Resuming from the second snapshot and verifying disk state.
- Also tests
pause_to_dirwith snapshot-owned inherited layer adoption.
-
microvm_can_access_internetConfirms guest networking works before and after snapshot resume. -
multiple_resumes_have_independent_disk_stateConfirms multiple resumptions are isolated by:- Resuming multiple VMs from a single snapshot sequentially.
- Each resumed VM verifies the shared marker and writes its own file.
- Verifying each snapshot has independent state.
integration/process.rs
-
run_command_captures_stdoutRunsecho hello worldand verifies stdout contains the expected string. -
run_command_captures_stderrRuns a shell command that writes to stderr and verifies stderr capture. -
run_command_reports_exit_codeRunsexit 42and verifies the exit code is reported correctly. -
run_command_with_opts_sets_env_and_cwdVerifies that environment variables and working directory options are honoured. -
run_command_handles_timeoutVerifies long-running commands fail when a timeout is configured. -
run_command_with_unbounded_outputVerifies excessive output is rejected instead of buffering forever. -
start_process_interactive_stdinStartscat, sends data via stdin, kills the process, and verifies the echoed output. -
start_process_send_signalStartssleep 300, sends SIGTERM, and verifies the process terminates with a non-zero exit code. -
run_command_after_snapshot_resumePauses a sandbox, resumes from a snapshot config, and verifiesrun_commandworks on the resumed instance.
integration/snapshot.rs
-
built_snapshot_can_be_loaded_by_alias_and_launchedBuilds a template-backed snapshot, loads it by alias, launches a sandbox from the resolved runnable snapshot, and verifies captured runtime state. -
listed_committed_snapshot_can_be_loaded_and_resolvedVerifiesSnapshotManager::list_committed()returns usable committed snapshots that can be loaded and resolved into runnable paths. -
loaded_runnable_snapshot_by_alias_can_be_resolvedVerifiesSnapshotManager::load_runnable()resolves alias-based lookups into runnable snapshot state in one step. -
delete_committed_snapshot_by_alias_removes_it_from_repositoryVerifies deleting a committed snapshot by alias removes the durable record. -
snapshot_rebuild_from_committed_snapshot_preserves_base_stateVerifies rebuilding from a committed snapshot preserves the base filesystem state while adding new changes. -
rebuild_merges_env_metadata_and_list_filter_can_select_by_idVerifies rebuilds merge environment metadata correctly and that committed snapshot listing can filter by snapshot id.
integration/orchestrator.rs
orchestrator_lifecycleVerifies the orchestrator can create, fetch, list, filter, keep alive, and delete template-backed sandboxes while keeping proxy lookup state in sync.
4) End-to-End Execution Checklist
- Provide Firecracker binary, kernel, tools drive, overlaybd runtime, and ublk daemon. These are automatically downloaded when the server starts, or you can run
cargo run --bin server -- --setup-onlyto provision runtime dependencies independently. - Provision host access once as root with
server --setup-host --runtime-user <user> --runtime-group <group>. The group is the runtime service group: it owns AgentENV state and receives ublk device access. Normal server startup performs validation only and never invokessudo. - Host setup installs a udev rule for
/dev/ublk-control,/dev/ublkc*, and/dev/ublkb*, so the runtime group can access the control and dynamic device nodes. - Update
config/default.tomlpaths, or pointAENV_CONFIG_PATHto a custom config file. - Ensure
/dev/kvmis accessible by the runtime user. - If you run template tests, ensure the host can run
regctl(server setup installs it automatically from the[regclient]manifest entry) and access the registry for templatefromImageresolution. - Run
scripts/tests/e2e/run_e2e.shfor API-level E2E coverage. The runner exportsE2E_TEMPLATE_USER_IMAGE. Suite05_template_lifecycle.shalso creates a template build withE2E_SHORT_USER_IMAGEto verify short-name image resolution. - Run
make test-agent-integrationto run theagentenvintegration test modules intests/integration/as a non-root user with the required capabilities, plus the Docker/MinIO-backed OSS snapshot repository test (tests/snapshot_oss_e2e_test.rs). - Run
make test-ublkto run theuvm-ublkandoverlaybdstorage tests, including the Docker/MinIO-backed OSS backend test (storage/overlaybd/tests/oss_backend_minio.rs).
4.1) Firecracker Client and Runtime Upgrade Checklist
When bumping AgentENV to a patched Firecracker runtime, update both the generated client API and the binary that setup downloads:
-
Replace
thirdparty/firecracker-client/firecracker.yamlwith the target Firecracker OpenAPI spec. -
Run
make firecracker-clientfrom the repository root and review generated changes underthirdparty/firecracker-client/. -
Build the patched Firecracker release binary on a Linux host. Package it as a gzip tar archive named
firecracker-{version}-{arch}.tgzcontaining afirecrackerexecutable. -
Update the
[firecracker]entry inconfig/deps_manifest.tomlwith the new version and a download URL template that supports{version}and{arch}. -
Before relying on the URL in tests, verify it is a direct download:
curl -L "<url>" -o /tmp/firecracker.tgz tar -tzf /tmp/firecracker.tgzThe archive listing should include
firecracker. -
Run setup against a clean or temporary Firecracker dependency directory so it really downloads the archive:
cargo run --bin server -- --config /path/to/config.toml --setup-only -
Validate the Rust side after code generation:
cargo fmt --check -p firecracker_client -p agentenv cargo check -p firecracker_client cargo check -p agentenv cargo test -p agentenv --lib
5) Running Custom Scripts
Run commands via envd
Once the sandbox is started (with a rootfs that includes the envd daemon), you can execute commands inside the VM using the process API:
#![allow(unused)]
fn main() {
// Simple command
let output = sandbox.run_command("ls", &["-la", "/tmp"]).await?;
println!("exit={} stdout={}", output.exit_code, output.stdout);
// Command with options
use agentenv::sandbox::ProcessOpts;
let opts = ProcessOpts::new()
.with_cwd("/tmp")
.with_envs([("KEY".into(), "value".into())].into());
let output = sandbox.run_command_with_opts("env", &[], opts).await?;
// Interactive / long-running process
let mut handle = sandbox.start_process("cat", &[], ProcessOpts::default()).await?;
handle.send_stdin(b"hello\n").await?;
handle.kill().await?;
let output = handle.wait().await?;
}
The process API uses envd’s gRPC ProcessClient under the hood. It requires
the envd daemon to be running inside the guest VM.
Run different scripts for multiple snapshot resumes
/init is not re-executed after resume, so per-instance behavior must come
from per-instance disk contents.
A typical pattern:
- Start a base VM and
pause()to create a snapshot. - For each instance, copy the snapshot rootfs and write a per-instance file
into it (for example using
debugfs). - Create a
FirecrackerSnapshotConfigthat points to that per-instance rootfs and resume.
The guest must keep a long-running dispatcher (started during the initial boot) that watches for these per-instance files and executes them after resume.