Files
PaperClipAI/packages/plugins/sandbox-providers/boxd/src/plugin.live.test.ts
T
MichielMAnalyticsandClaude Opus 5.5 644937c931 feat(sandbox-providers): add boxd sandbox provider (#17284)
<p align="center">
<img
src="https://raw.githubusercontent.com/MichielMAnalytics/paperclip/08f1cdda04443ed10fc43e7097ae0b2d14b5cc19/boxd-x-paperclip.png"
alt="boxd × Paperclip" width="700">
</p>

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent work runs in sandboxes that provider plugins supply
> - An operator selects a provider for each environment, and a reused
lease keeps one sandbox for an issue across heartbeats
> - The time to resume a reused sandbox delays every heartbeat on that
issue (see #14684)
> - boxd runs each sandbox as an isolated microVM that suspends and
resumes in milliseconds, with disk, memory and processes kept
> - This pull request adds a boxd sandbox provider plugin
> - The benefit is that operators get one more provider, with fast
resume for reused leases

## Linked Issues or Issue Description

Refs #14684. That pull request keys the remote session identity on the
provider lease ID. This provider keeps the provider lease ID equal to
the boxd machine ID, so the ID stays the same across suspend and resume.

No issue exists for boxd. The request is described below with the fields
of `adapter_request.yml`.

**Agent or provider**

boxd (https://boxd.sh). boxd runs each sandbox as a KVM microVM. The
plugin uses the public TypeScript SDK, `@boxd-sh/sdk` 0.2.20 or later.

**Why this adapter is useful**

boxd suspends a machine in about 17 ms and resumes it in about 0.2 s,
with the disk, the memory and the running processes kept. This fits
`reuseLease`: the next heartbeat on an issue continues on the same
machine. Each machine is created with `isolated: true`. An isolated
machine has no metadata endpoint, no in-VM `boxd` CLI and no boxd
integrations, so sandbox code cannot reach the provider management
interface.

**How the agent is invoked**

Build and install the plugin by local path, as the package README
describes. Open Instance Settings, then Environments. Select the `boxd`
provider and supply a boxd API key. Agents that use the environment run
their adapter commands in a boxd machine.

**Are you willing to implement it?**

Yes. This pull request is the implementation.

## What Changed

- Adds the `boxd` sandbox provider under
`packages/plugins/sandbox-providers/boxd`. Its structure follows the E2B
and exe.dev plugins. The plugin has no code comments, to follow
`CONTRIBUTING.md`.
- Implements the validate, probe, acquire, resume, release, stop,
destroy, inspect, realize-workspace and execute hooks. The manifest
declares `sandboxCapabilities: { reusableLeases: true }`.
- With `reuseLease`, release suspends the machine and resume wakes it. A
suspended, hibernated or stopped machine is woken by the matching boxd
call. A release that cancels active work stops the machine and keeps its
disk.
- Release and destroy return a `destroyed` receipt only after boxd no
longer lists the machine. Stop returns a `stopped` receipt only after
boxd reports the `stopped` state.
- When the host sets `requestedExpiresAt`, the plugin sets a boxd delete
timer at or before that time and returns `expiresAt` on the lease. The
timer is set after the create call returns, so a failure while setting
it cannot lose the machine ID.
- Commands run through `sh` with quoted arguments and per-command
environment. Stdin is staged with boxd's file API in a temporary file
that only the machine user can read, and removed afterwards; the
workspace directory is created the same way. A machine whose boxd agent
predates that API, such as one restored from an older snapshot, falls
back to the older upload and `mkdir` calls. A command timeout ends the
remote process group and returns `timedOut: true`. A command on a
stopped machine starts the machine and runs one more time.
- A failed acquisition deletes its machine. If boxd cannot confirm the
deletion, the plugin throws `PluginEnvironmentCreationCleanupError` with
the machine ID, so the host records it and retries the teardown; the
destroy hook checks the account and the machine labels before it deletes
a failed creation. A probe whose machine cannot be deleted reports a
failure, and probe machines carry a 15-minute boxd delete timer.
- The plugin keeps one SDK client for each API key. This prevents a
session token exchange for each command.
- Machines carry the labels `paperclip-provider`,
`paperclip-environment` and `paperclip-run`, so an operator can find
them with `boxd machine list --label paperclip-provider=boxd`.
- Registers the optional bundled catalog key `boxd` and the trusted
`BOXD_API_KEY` host fallback, with tests that mirror the CreateOS cases.
- Adds the package to the release manifest with `publishFromCi: false`
until a maintainer bootstraps the first npm publish.
- Adds unit tests with a mocked SDK (40 tests) and opt-in live tests
(`BOXD_LIVE_TEST=1`, 9 tests) that run every hook against boxd.
- Adds a section to `doc/plugins/PLUGIN_AUTHORING_GUIDE.md` and adds
boxd to the README provider list.

## Verification

Provider package (`packages/plugins/sandbox-providers/boxd`):

- `pnpm install --ignore-workspace --no-lockfile && pnpm typecheck`:
passes.
- `pnpm test`: 40 passed, 9 live tests skipped.
- `BOXD_LIVE_TEST=1 BOXD_API_KEY=... pnpm test`: 49 passed against
production boxd on 2026-10-28 with `@boxd-sh/sdk` 0.2.20. The live tests
cover:
- validation, and a probe at 2 vCPU that deletes its machine (a bad key
returns a failed probe);
  - the labels, the isolation and the org of an acquired machine;
- stdin, env, argument quoting, the exit code and stderr, and the
removal of the staged stdin file;
- an empty stdin, and a staged stdin file that only the machine user can
read (mode 600);
- a 5 MiB round trip through the base64-over-exec path (the SHA-256
matches);
- a command timeout, which returns `timedOut` and ends the remote
processes;
- suspend and resume (a file and a running process stay), stop and
resume (the file stays), and inspect in each state;
  - a command on a stopped machine, which starts the machine and runs;
- resume and stop of a hibernated machine, stop of a suspended machine,
and release with `cancelActiveWork` and with `stop_and_retain`;
  - release without reuse, which deletes the machine;
- a `requestedExpiresAt` 45 s away, which boxd enforces by deleting the
machine;
- a run restored from a snapshot, and a fresh machine at 2 vCPU / 8 GiB;
- resume after destroy, which returns an expired lease, and a final
check that no test machines remain.
- Measured times: probe 1.5 s, acquire 1.4 s, release with suspend 17
ms, resume from suspend 0.13 s, resume from hibernate 0.18 s, stop 14
ms, resume from stop 3.2 s, acquire from snapshot 1.6 s, confirmed
destroy 30 ms.
- On a machine started before boxd's file API (the test host), the
plugin's hooks fell back to the older calls: the workspace was created,
stdin was staged (mode 644, the upload path), an empty stdin worked, and
no staged file was left.

Host:

- `npx vitest run src/__tests__/bundled-plugins.test.ts
src/__tests__/plugin-database.test.ts` in `server`: 73 passed, 6 of them
new (re-run on 2026-10-28).
- `npx vitest run src/__tests__/cli-invocation-safety.test.ts` in
`server`: 40 passed. The README install commands now use the direct-exec
CLI form.
- `npx tsc --noEmit` in `server`: passes.
- `node --test scripts/release-package-map.test.mjs
scripts/check-release-package-bootstrap.test.mjs`: 18 passed.

Full stack, on a Linux host with `pnpm dev:once` in `local_trusted`
mode:

- I installed the plugin with `POST /api/plugins/install` and a local
path, then created boxd environments with a secret reference for the API
key.
- Ephemeral environment: the environment probe passed. A `claude_local`
agent (`claude-sonnet-5-5`, legacy runner) ran a task in a boxd machine.
It reported Ubuntu 24.04.5 and the workspace
`/home/boxd/paperclip-workspace`, and it marked the task done. The lease
was released and the machine was deleted.
- Reused-lease environment: the first run wrote a file and started a
background loop. After release, the machine was in the `suspended`
state. A comment with `reopen: true` started a second run. That run used
the same machine, so the provider lease ID was the same on both lease
rows. The file was there and the background loop was still running.
- Validation: a config with `vcpu: 3`, and a config with both `vcpu` and
`snapshot`, return 422 with the plugin error. The unsaved-config probe
(`/environments/probe-config`) passed in 2.0 s and deleted its machine.
A saved probe with `vcpu: 4` created a 4 vCPU / 16 GiB isolated machine.
- Cancel: cancelling the first run on a reused lease marked the lease
`expired`, and the host destroyed the machine through the plugin about
12 s later.
- Delete: deleting an environment that holds a reused lease returns 409.
With `?destroyReusableSandboxLeases=true` it returns 200, and the host
destroys the suspended machine through the plugin.
- I re-ran this flow on 2026-10-26 and again on 2026-10-28 with
`@boxd-sh/sdk` 0.2.20, from a script, with each step checked. On
2026-10-28 the ephemeral run posted its proof and both of its machines
were deleted. The reused-lease follow-up ran on the same machine (both
lease rows have the same provider lease ID), and found the file and a
background loop that had kept running. The validation errors returned
422, the unsaved-config probe passed in 1.8 s, and deleting the
environments left no machines. Every check passed except the host gap
below.
- After the runs, `boxd machine list` showed no machines with the test
prefix.

Not run: `pnpm -r typecheck`, `pnpm test:run` and `pnpm build` at the
root. The test host has no Rust toolchain for the runner binary. On that
host, `scripts/check-forbidden-tokens.mjs` adds the local username
(`boxd`) as a token. It then flags every `boxd` substring, and it also
fails on `master` there (`InboxDismissal`). A CI runner uses a different
username.

## Risks

- Host gap, not in this plugin: when a run that **resumed** a reused
lease is cancelled, the host marks the run `cancelled` but does not stop
the agent in the sandbox. The adapter keeps polling the file bridge, and
the host calls no release, stop or destroy hook. The lease stays
`active` until the environment is deleted. A plugin-side trace showed
only bridge-polling `execute` calls after the cancel. boxd is the only
provider that declares `reusableLeases`, so this path may not have been
exercised before. A cancel on the first run of a lease works as
expected.

- Low risk to existing behavior. The plugin is opt-in and is not
installed by default. The host changes add one catalog entry, one
credential passthrough entry and their tests.
- boxd machine creation has no idempotency key. If a create response is
lost, a machine can stay behind. The labels above help an operator find
it.
- A suspended machine keeps its memory image until boxd hibernates it
after a period of idle time. An operator who keeps many reused leases
pays for that storage.
- boxd stops a non-PTY command that has no output for 30 minutes. Agent
CLIs write output during a run, so this does not affect normal runs.
- boxd has one region (EU), amd64 machines only, and no GPUs. Machine
sizes are 1/4 GB, 2/8 GB, 4/16 GB and 8/32 GB.
- The new `package.json` needs code-owner approval.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
and shell execution on a boxd machine. The session does not expose the
context window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (focused checks; the limits
are listed under Verification)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01HNWtBYxLByQmmNKjuaou1P

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:34:21 -07:00

324 lines
17 KiB
TypeScript

import { createHash, randomBytes, randomUUID } from "node:crypto";
import { Boxd, NotFoundError } from "@boxd-sh/sdk";
import { afterAll, describe, expect, it } from "vitest";
import plugin from "./plugin.js";
const live = process.env.BOXD_LIVE_TEST === "1";
const hooks = plugin.definition;
const timings: Record<string, number> = {};
const params = {
driverKey: "boxd",
companyId: "boxd-plugin-smoke",
environmentId: "boxd-plugin-smoke",
config: {
...(process.env.BOXD_ORG ? { org: process.env.BOXD_ORG } : {}),
namePrefix: "paperclip-e2e",
reuseLease: true,
timeoutMs: 120_000,
},
};
const ephemeral = { ...params, config: { ...params.config, reuseLease: false } };
let sdkClient: Boxd | null = null;
function sdk(): Boxd {
sdkClient ??= new Boxd({ apiKey: process.env.BOXD_API_KEY });
return sdkClient;
}
async function timed<T>(label: string, work: () => Promise<T>): Promise<T> {
const startedAt = performance.now();
try {
return await work();
} finally {
timings[label] = Math.round(performance.now() - startedAt);
}
}
async function acquire(input: typeof params = params, extra: Record<string, unknown> = {}) {
return await hooks.onEnvironmentAcquireLease!({ ...input, runId: `smoke-${randomUUID().slice(0, 8)}`, ...extra });
}
async function cleanup(providerLeaseId: string | null | undefined) {
if (!providerLeaseId) return;
await hooks.onEnvironmentDestroyLease!({ ...params, providerLeaseId }).catch(() => undefined);
}
async function machineOrNull(id: string) {
try {
return await sdk().machines.get(id);
} catch (error) {
if (error instanceof NotFoundError) return null;
throw error;
}
}
async function waitFor(description: string, timeoutMs: number, check: () => Promise<boolean>) {
const deadline = Date.now() + timeoutMs;
while (!(await check())) {
if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${description}.`);
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
}
describe.skipIf(!live)("boxd live", () => {
afterAll(async () => {
await hooks.onShutdown!();
await sdkClient?.close();
console.log(`boxd live timings (ms): ${JSON.stringify(timings)}`);
});
it("validates config before the API is called", async () => {
if (!process.env.BOXD_API_KEY) throw new Error("Set BOXD_API_KEY for the live tests.");
expect(await hooks.onEnvironmentValidateConfig!({ ...params, config: { vcpu: 3 } })).toMatchObject({ ok: false });
expect(await hooks.onEnvironmentValidateConfig!({ ...params, config: { vcpu: 2, snapshot: "x" } })).toMatchObject({ ok: false });
expect(await hooks.onEnvironmentValidateConfig!(params)).toMatchObject({ ok: true });
});
it("probes with a sized throwaway machine and deletes it", async () => {
const before = new Set((await sdk().machines.list()).map((machine) => machine.id));
const probe = await timed("probe", () => hooks.onEnvironmentProbe!({ ...params, config: { ...params.config, vcpu: 2 } }));
expect(probe).toMatchObject({ ok: true, metadata: { remoteCwd: "/home/boxd/paperclip-workspace" } });
const name = String(probe.metadata?.machineName);
expect(name).toMatch(/^paperclip-e2e-probe-/);
const leftover = (await sdk().machines.list()).filter((machine) => !before.has(machine.id) && machine.name === name);
expect(leftover).toEqual([]);
const failed = await hooks.onEnvironmentProbe!({ ...params, config: { ...params.config, apiKey: "bxd_invalid" } });
expect(failed).toMatchObject({ ok: false, metadata: { provider: "boxd" } });
expect(String(failed.metadata?.error)).not.toBe("");
}, 120_000);
it("runs create/exec/stdin/timeout/suspend/resume/stop/destroy", async () => {
const lease = await timed("acquire", () => acquire());
const providerLeaseId = lease.providerLeaseId!;
try {
const machine = await sdk().machines.get(providerLeaseId);
expect(machine.labels).toMatchObject({
"paperclip-provider": "boxd",
"paperclip-environment": "boxd-plugin-smoke",
});
expect(machine.labels["paperclip-run"]).toMatch(/^smoke-/);
expect(machine.networking.isolated).toBe(true);
if (process.env.BOXD_ORG) expect([machine.org?.id, machine.org?.name]).toContain(process.env.BOXD_ORG);
const workspace = await hooks.onEnvironmentRealizeWorkspace!({ ...params, lease, workspace: {} });
expect(workspace.cwd).toBe("/home/boxd/paperclip-workspace");
const tools = await hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "command -v node && command -v tar && command -v claude"],
});
expect(tools).toMatchObject({ exitCode: 0, timedOut: false });
const streams = await timed("execute", () => hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "cat > note.txt; printf '%s' \"$SMOKE\" >&2; exit 7"],
stdin: "persisted\n", env: { SMOKE: "stderr works" }, cwd: workspace.cwd,
}));
expect(streams).toEqual({ exitCode: 7, timedOut: false, stdout: "", stderr: "stderr works" });
const staged = await hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "ls /tmp | grep -c '^paperclip-stdin-' || true"],
});
expect(staged.stdout.trim()).toBe("0");
const emptyStdin = await hooks.onEnvironmentExecute!({ ...params, lease, command: "wc", args: ["-c"], stdin: "" });
expect(emptyStdin).toMatchObject({ exitCode: 0, stdout: "0\n" });
const stdinMode = await hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "stat -c %a \"$(readlink /proc/self/fd/0)\""], stdin: "private",
});
expect(stdinMode.stdout.trim()).toBe("600");
const quoted = await hooks.onEnvironmentExecute!({
...params, lease, command: "printf", args: ["%s|", "it's", "$HOME", "a b"],
});
expect(quoted.stdout).toBe("it's|$HOME|a b|");
const payload = randomBytes(5 * 1024 * 1024);
const digest = createHash("sha256").update(payload).digest("hex");
const upload = await timed("upload5MiB", () => hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "base64 -d > big.bin && sha256sum big.bin | cut -d' ' -f1"],
stdin: payload.toString("base64"), cwd: workspace.cwd,
}));
expect(upload.stdout.trim()).toBe(digest);
const download = await timed("download5MiB", () => hooks.onEnvironmentExecute!({
...params, lease, command: "base64", args: ["-w0", "big.bin"], cwd: workspace.cwd,
}));
expect(createHash("sha256").update(Buffer.from(download.stdout, "base64")).digest("hex")).toBe(digest);
const timeout = await timed("timeout", () => hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "sleep 47 & sleep 48; wait"], timeoutMs: 2_000,
}));
expect(timeout).toMatchObject({ exitCode: null, timedOut: true });
await waitFor("the timed out command to end", 15_000, async () => {
const left = await hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "pgrep -fc 'sleep 4[78]' || true"],
});
return left.stdout.trim() === "0";
});
await hooks.onEnvironmentExecute!({
...params, lease, command: "sh", args: ["-c", "setsid -f sh -c 'while true; do date +%s >> ticks; sleep 1; done' >/dev/null 2>&1 < /dev/null"], cwd: workspace.cwd,
});
await new Promise((resolve) => setTimeout(resolve, 2_000));
const released = await timed("releaseSuspend", () => hooks.onEnvironmentReleaseLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }));
expect(released).toBeUndefined();
expect((await sdk().machines.get(providerLeaseId)).status).toBe("suspended");
expect(await hooks.onEnvironmentInspectLease!({ ...params, providerLeaseId })).toEqual({ providerLeaseId, state: "stopped" });
const resumed = await timed("resumeSuspended", () => hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }));
expect(resumed.providerLeaseId).toBe(providerLeaseId);
expect(resumed.metadata).toMatchObject({ resumedLease: true, remoteCwd: workspace.cwd });
expect(await hooks.onEnvironmentInspectLease!({ ...params, providerLeaseId })).toEqual({ providerLeaseId, state: "active" });
const kept = await hooks.onEnvironmentExecute!({
...params, lease: resumed, command: "sh", args: ["-c", "cat note.txt; a=$(wc -l < ticks); sleep 2; b=$(wc -l < ticks); [ \"$b\" -gt \"$a\" ] && echo process-kept"], cwd: workspace.cwd,
});
expect(kept.stdout).toBe("persisted\nprocess-kept\n");
const stopped = await timed("stop", () => hooks.onEnvironmentStopLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }));
expect(stopped).toEqual({ providerLeaseId, state: "stopped" });
const restarted = await timed("resumeStopped", () => hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }));
const afterStop = await hooks.onEnvironmentExecute!({ ...params, lease: restarted, command: "cat", args: ["note.txt"], cwd: workspace.cwd });
expect(afterStop).toMatchObject({ exitCode: 0, stdout: "persisted\n" });
} finally {
const receipt = await timed("destroy", () => hooks.onEnvironmentDestroyLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }));
expect(receipt).toEqual({ providerLeaseId, state: "destroyed" });
expect(await hooks.onEnvironmentInspectLease!({ ...params, providerLeaseId })).toEqual({ providerLeaseId, state: "destroyed" });
expect(await hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata: lease.metadata }))
.toEqual({ providerLeaseId: null, metadata: { expired: true } });
}
}, 300_000);
it("starts a stopped machine when a command arrives, and deletes it on release without reuse", async () => {
const lease = await acquire(ephemeral, { requestedCwd: "/home/boxd/custom-cwd" });
const providerLeaseId = lease.providerLeaseId!;
try {
expect(lease.metadata).toMatchObject({ remoteCwd: "/home/boxd/custom-cwd", reuseLease: false });
await hooks.onEnvironmentExecute!({ ...ephemeral, lease, command: "sh", args: ["-c", "echo kept > note.txt"] });
await sdk().machines.stop(providerLeaseId);
await waitFor("the machine to stop", 60_000, async () => (await sdk().machines.get(providerLeaseId)).status === "stopped");
const result = await timed("executeOnStopped", () => hooks.onEnvironmentExecute!({ ...ephemeral, lease, command: "cat", args: ["note.txt"] }));
expect(result).toMatchObject({ exitCode: 0, stdout: "kept\n" });
await sdk().machines.stop(providerLeaseId);
await waitFor("the machine to stop", 60_000, async () => (await sdk().machines.get(providerLeaseId)).status === "stopped");
const realized = await hooks.onEnvironmentRealizeWorkspace!({ ...ephemeral, lease, workspace: {} });
expect(realized.cwd).toBe("/home/boxd/custom-cwd");
const receipt = await timed("releaseDestroy", () => hooks.onEnvironmentReleaseLease!({ ...ephemeral, providerLeaseId, leaseMetadata: lease.metadata }));
expect(receipt).toEqual({ providerLeaseId, state: "destroyed" });
expect(await machineOrNull(providerLeaseId)).toBeNull();
} finally {
await cleanup(providerLeaseId);
}
}, 300_000);
it("wakes and stops hibernated and suspended machines, and stops on cancel or stop_and_retain", async () => {
const lease = await acquire();
const providerLeaseId = lease.providerLeaseId!;
const leaseMetadata = lease.metadata;
const readNote = async (current: typeof lease) =>
await hooks.onEnvironmentExecute!({ ...params, lease: current, command: "cat", args: ["note.txt"] });
const hibernate = async () => {
await sdk().machines.hibernate(providerLeaseId);
await waitFor("the machine to hibernate", 120_000, async () => (await sdk().machines.get(providerLeaseId)).status === "hibernated");
};
try {
await hooks.onEnvironmentExecute!({ ...params, lease, command: "sh", args: ["-c", "echo kept > note.txt"] });
await hibernate();
expect(await hooks.onEnvironmentInspectLease!({ ...params, providerLeaseId })).toEqual({ providerLeaseId, state: "stopped" });
const woken = await timed("resumeHibernated", () => hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata }));
expect(await readNote(woken)).toMatchObject({ exitCode: 0, stdout: "kept\n" });
await hibernate();
expect(await timed("stopHibernated", () => hooks.onEnvironmentStopLease!({ ...params, providerLeaseId, leaseMetadata })))
.toEqual({ providerLeaseId, state: "stopped" });
expect((await sdk().machines.get(providerLeaseId)).status).toBe("stopped");
let current = await hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata });
await hooks.onEnvironmentReleaseLease!({ ...params, providerLeaseId, leaseMetadata });
expect((await sdk().machines.get(providerLeaseId)).status).toBe("suspended");
expect(await timed("stopSuspended", () => hooks.onEnvironmentStopLease!({ ...params, providerLeaseId, leaseMetadata })))
.toEqual({ providerLeaseId, state: "stopped" });
current = await hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata });
expect(await hooks.onEnvironmentReleaseLease!({ ...params, providerLeaseId, leaseMetadata, cancelActiveWork: true }))
.toEqual({ providerLeaseId, state: "stopped" });
current = await hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata });
expect(await hooks.onEnvironmentReleaseLease!({ ...ephemeral, providerLeaseId, leaseMetadata, resourceDisposition: "stop_and_retain" }))
.toEqual({ providerLeaseId, state: "stopped" });
current = await hooks.onEnvironmentResumeLease!({ ...params, providerLeaseId, leaseMetadata });
expect(await readNote(current)).toMatchObject({ exitCode: 0, stdout: "kept\n" });
} finally {
await cleanup(providerLeaseId);
}
}, 600_000);
it("bounds the machine lifetime at a requested expiry", async () => {
const requestedExpiresAt = new Date(Date.now() + 45_000).toISOString();
const lease = await acquire(ephemeral, { requestedExpiresAt });
const providerLeaseId = lease.providerLeaseId!;
try {
expect(Date.parse(lease.expiresAt!)).toBeLessThanOrEqual(Date.parse(requestedExpiresAt));
const machine = await sdk().machines.get(providerLeaseId);
expect(machine.deleteAt).not.toBeNull();
expect(machine.deleteAt!.getTime()).toBeLessThanOrEqual(Date.parse(requestedExpiresAt));
await timed("expiry", () => waitFor("boxd to delete the expired machine", 180_000, async () => {
const current = await machineOrNull(providerLeaseId);
return current === null || current.status === "destroyed";
}));
expect(await hooks.onEnvironmentInspectLease!({ ...ephemeral, providerLeaseId })).toEqual({ providerLeaseId, state: "destroyed" });
} finally {
await cleanup(providerLeaseId);
}
}, 300_000);
it("restores each run from a snapshot", async () => {
const source = await acquire(ephemeral);
const snapshotName = `paperclip-e2e-${randomUUID().slice(0, 8)}`;
let restoredId: string | null = null;
try {
await hooks.onEnvironmentExecute!({ ...ephemeral, lease: source, command: "sh", args: ["-c", "echo from-snapshot > /home/boxd/golden.txt"] });
await timed("snapshotSave", () => sdk().snapshots.create(source.providerLeaseId!, snapshotName));
await waitFor("the snapshot to be ready", 300_000, async () => {
const snapshot = await sdk().snapshots.get(snapshotName).catch(() => null);
return snapshot?.status === "ready";
});
const snapshotParams = { ...ephemeral, config: { ...ephemeral.config, snapshot: snapshotName } };
const restored = await timed("acquireFromSnapshot", () => acquire(snapshotParams));
restoredId = restored.providerLeaseId!;
expect(restored.metadata).toMatchObject({ snapshot: snapshotName });
expect((await sdk().machines.get(restoredId)).networking.isolated).toBe(true);
const golden = await hooks.onEnvironmentExecute!({ ...snapshotParams, lease: restored, command: "cat", args: ["/home/boxd/golden.txt"] });
expect(golden).toMatchObject({ exitCode: 0, stdout: "from-snapshot\n" });
} finally {
await cleanup(restoredId);
await cleanup(source.providerLeaseId);
await sdk().snapshots.delete(snapshotName).catch(() => undefined);
}
}, 600_000);
it("creates fresh machines at the configured size", async () => {
const sized = { ...ephemeral, config: { ...ephemeral.config, vcpu: 2 } };
const lease = await acquire(sized);
try {
const machine = await sdk().machines.get(lease.providerLeaseId!);
expect(machine.resources.vcpu).toBe(2);
expect(machine.resources.memoryBytes).toBe(8 * 1024 ** 3);
const nproc = await hooks.onEnvironmentExecute!({ ...sized, lease, command: "nproc" });
expect(nproc.stdout.trim()).toBe("2");
} finally {
await cleanup(lease.providerLeaseId);
}
}, 180_000);
it("leaves no machines behind", async () => {
const leftover = (await sdk().machines.list())
.filter((machine) => machine.name.startsWith("paperclip-e2e-") && machine.status !== "destroyed");
expect(leftover.map((machine) => machine.name)).toEqual([]);
});
});