SecondBox

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT

README

SecondBox

Durable, isolated development sandboxes — as a service you run yourself.

CI License

SecondBox runs untrusted workloads — AI agents, user code, plugins, CI jobs, long-lived dev environments — inside Firecracker microVMs whose filesystems survive between sessions.

  • Hardware isolation. Every Sandbox is a Firecracker microVM, not a container.
  • Durable workspaces. A Sandbox keeps its disk across stops, restarts, and generations. Snapshot it and restore in place.
  • Real terminals. A genuine PTY with raw mode, resize forwarding, and bounded reconnect — not a line-buffered exec loop.
  • Multi-tenant by construction. Every row is scoped to an opaque tenant and subject reference. Application tokens carry fixed scopes and explicit Profile grants.
  • Immutable Profiles. Operators fix image, resources, lifecycle, network, and port policy. Each Sandbox pins the revision resolved at creation.
  • Self-hosted. One unprivileged control plane, PostgreSQL, S3-compatible storage, and one or more privileged runners you place yourself.

[!NOTE] SecondBox is a networked control plane, not an embeddable library. Sandboxes run on separately deployed runners, and a client only ever talks to the control plane over HTTPS. There is no daemonless mode.

How it works

A Sandbox is the durable public resource; the Instance running it is replaceable compute fenced to one Sandbox generation. Each Sandbox is placed at creation on one home Runner, whose reflink-capable filesystem owns that Sandbox's Workspace and local Snapshots. Ordinary lifecycle and automatic recovery never relocate it. An operator may relocate a stopped Sandbox with no retained Snapshots through the explicit asynchronous relocation operation.

secondboxd stores desired state in PostgreSQL and immutable Artifacts in S3-compatible storage. Workspace bytes stay on the owning Runner except while secondboxd forwards a bounded, in-memory stream for an explicit stopped-Sandbox relocation; it never persists those bytes.

Getting started

Prerequisites

Executing a Sandbox requires:

  • a Linux Runner host with KVM and TUN;
  • cgroup v2;
  • a workspace root on an XFS or Btrfs filesystem with reflink support; ext4 and ZFS do not work;
  • a signed microVM bundle built, verified, and materialized as described by the microVM image pipeline;
  • an enrolled Runner and a Profile that pins that bundle's runtime and toolchain digests.

These requirements are inherent to running hardware-isolated Firecracker microVMs. The development topology starts the control plane, PostgreSQL, and object storage. It does not start a Runner, so secondbox run will not succeed until a Runner is enrolled in a ready RunnerPool.

Shortest same-host path

On a qualified host, the shortest existing deployment path uses deploy/compose.same-host-runner.yml. secondbox-deploy selects that overlay when the manifest contains one Runner with placement = "same-host" and preflights its host directories, dedicated workspace filesystem, and enrolled identity before Compose starts it.

  1. Obtain and independently verify the signed microVM bundle. Materialize its files and trusted public key in the Runner artifact host directory, construct the verified signed-asset catalog, and record the runtime and toolchain component digests.

  2. Initialize the development deployment:

    just deploy-init-development .tmp/secondbox-development
    

    Replace the development bootstrap catalog and artifact manifest in secondbox.toml with verified release artifacts. Review the explicit [standard_resources] bundle and typed RunnerPool selections. Keep runners = [] for the first start.

  3. Start the control plane, PostgreSQL, and object storage:

    just deploy-development-up .tmp/secondbox-development
    
  4. Install the CLI and log in with the generated platform authority. secondbox-deploy ... up creates the selected RunnerPool and standard Profile lineages idempotently. Its name must match the Runner's pool_id; its architecture and capabilities must admit the selected amd64 bundles.

  5. Add one explicit [[runners]] entry with placement = "same-host" to secondbox.toml. Supply every identity, artifact, state, workspace, Firecracker, network, capacity, and data-plane value; create the declared artifact, state, and workspace host directories, but leave the identity target absent. The workspace host directory must be on the dedicated XFS or Btrfs filesystem.

  6. Build the declared Runner image and issue the declared Runner identity:

    docker build --file runner/Dockerfile --tag secondbox-runner:development .
    secondbox-deploy runner-init \
      .tmp/secondbox-development/secondbox.toml \
      <runner-id> \
      <identity-host-directory>
    
  7. Validate and apply the expanded topology. This selects the privileged same-host overlay and starts the Runner only after the host preflight passes:

    secondbox-deploy validate .tmp/secondbox-development/secondbox.toml
    just deploy-up .tmp/secondbox-development/secondbox.toml
    
  8. Confirm the Runner is ready with secondbox runners get --path runnerId=<runner-id>, then run the pinned standard Profile:

    secondbox run durable-coding -- python3 -c 'print("hello from a microVM")'
    

The generated development artifact identity is synthetic and must not be used as an execution asset. Production selects a verified release artifact manifest, signed-asset catalog, standard bundles, RunnerPool inventory, and Runner gateway mappings explicitly. See deployment and runtime operations, declarative resources, and the Firecracker runtime.

Control-plane-only start
just deploy-development-up .tmp/secondbox-development

This creates one private, versioned secondbox.toml, generates unique referenced secrets, compiles a protected environment transport, and starts the reviewed loopback PostgreSQL, object-store, and control-plane topology. The generated environment is never operator input. This topology is useful for control-plane development and API work, but it cannot execute a Sandbox. Read deployment and runtime operations before exposing the API, configuring production, or enrolling a Runner.

Install the CLI
go build -o ./dist/secondbox ./cmd/secondbox
Log in once
secondbox login \
  --url https://secondbox.example.com \
  --token "$SECONDBOX_PLATFORM_TOKEN" \
  --tenant-ref acme \
  --subject-ref alice

Credentials are verified against the deployment before anything is written, then stored at mode 0600. Every later command resolves them from the first source that has them: an explicit flag, then SECONDBOX_URL / SECONDBOX_TOKEN / SECONDBOX_TENANT_REF / SECONDBOX_SUBJECT_REF, then that file. secondbox whoami shows what resolved and from where; it never prints the token.

Run something
secondbox run durable-coding -- python3 -c 'print("hello from a microVM")'

Using the CLI

One-off commands

run creates a Sandbox, waits for it, runs one command, and deletes it:

secondbox run durable-coding -- python3 -c 'print("hello")'
secondbox run durable-coding --shell -- 'ls -la /workspace && whoami'
echo 'piped in' | secondbox run durable-coding --stdin -- cat

The guest's stdout and stderr land on your two streams, unmerged, and its exit status becomes the CLI's exit status — so secondbox run … -- false exits 1 and prints nothing of its own, exactly like a local command.

Named Sandboxes
# Create one and keep it
secondbox run durable-coding --name my-box --keep -- true

# Address it by name from any machine
secondbox exec my-box -- go test ./...
secondbox exec my-box --shell -- 'cd /workspace && make build'
secondbox shell my-box

Names are the reserved metadata key secondbox.dev/name, unique per tenant and subject and resolved server-side — so the same name works from anywhere, with nothing cached locally. A deleted Sandbox releases its name.

run, exec, and shell accept a name or an opaque sbx_… identifier, telling them apart by the identifier prefix. The transport-level commands below take the identifier only; secondbox sandboxes list shows both.

Interactive shell
secondbox run durable-coding --tty              # throwaway shell, deleted on exit
secondbox run durable-coding --tty -- /bin/bash # choose the shell
secondbox shell my-box                              # attach to one that already exists
secondbox shell my-box --command /bin/bash --detachable

run --tty is the docker run -it --rm shape: it creates a Sandbox, waits for it, drops you into a terminal, and deletes it when you disconnect — including on a dropped connection, since the Sandbox exists only for that session. Add --keep to retain it and reconnect later with secondbox shell.

shell resolves the name, applies the Sandbox's current generation, acquires and renews a Lease for the session, and releases it on exit. You get a real PTY: raw mode, local dimensions, SIGWINCH forwarding, byte-exact binary I/O, and your terminal restored on exit, cancellation, or transport failure.

Every value it supplies is an overridable default — pass --lease, --generation, or --session and yours wins.

Everything else

The remaining commands are thin transport over the published API — repeatable --path, --query, and --header pairs, and --body taking a file or -:

secondbox sandboxes list

secondbox files read --path sandboxId=sbx_123 --query path=/workspace/out.txt \
  --header SecondBox-Generation=4

secondbox snapshots create --path sandboxId=sbx_123 \
  --header 'If-Match="revision-5"' --header Idempotency-Key=$(uuidgen) \
  --body ./snapshot.json

secondbox exec stream --sandbox sbx_123 --generation 4 \
  --idempotency-key $(uuidgen) --request ./stream.json

Routes that mutate a Sandbox require both Idempotency-Key and an If-Match revision validator; secondbox sandboxes get reports the current revision.

secondbox operation <operationId> reaches any route in the table directly. Local operator commands — logs tail, logs follow, diagnostics bundle, timings summary — need no API credentials for the log routes.

Full reference: SDK, CLI, and Flue quick starts.

SDKs

Go and TypeScript share one handwritten composition layer over generated transports and wire types: idempotency keys, bounded-wait looping, lease keepers that renew in the background, outcome decoding, and run.

client, _ := secondboxclient.NewSecondBoxSubjectClient(
    "https://secondbox.example.com", token, "acme", "alice", http.DefaultClient)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

handle, outcome, err := client.Run(ctx, secondboxclient.RunRequest{
    Profile: "durable-coding",
    Command: secondboxclient.Command{ArgvCommand: &secondboxclient.ArgvCommand{
        Mode: "argv", Executable: "python3", Arguments: []string{"-c", "print('hello')"},
    }},
    DeadlineMilliseconds: 30_000,
    MaximumOutputBytes:   1 << 20,
})
fmt.Print(string(outcome.Result.Stdout))
_ = handle // Run never deletes; disposal is yours
TypeScript
import { SecondBox, SecondBoxClient } from "@secondstack-ai/secondbox";

const api = new SecondBox(
  new SecondBoxClient("https://secondbox.example.com", token, fetch, "acme", "alice"),
);

const { handle, result } = await api.run({
  profile: "durable-coding",
  command: "python3 -c 'print(\"hello\")'",
  deadlineMilliseconds: 30_000,
  maximumOutputBytes: 1_048_576,
  readyTimeoutMilliseconds: 300_000,
});

if (result.kind === "exited") process.stdout.write(result.stdout);

Run never deletes the Sandbox it created in either client — disposal stays your decision.

Repository layout

Path Contents
cmd/secondbox the CLI
cmd/secondboxd unprivileged control plane
runner privileged Firecracker runner and guest agent
contracts canonical public, runner, and guest-agent protocols
internal domain, API, scheduling, reconciliation, persistence
migrations/postgres database migration lineage
sdk Go and TypeScript clients
deploy Compose, systemd, and deployment examples
docs/design architecture and compatibility contracts
docs/operations installation, backup, diagnostics

Validation

The portable gate needs no KVM and is what CI runs:

just test-non-kvm

Firecracker validation requires a dedicated Linux host with KVM and the configured assets:

just test-firecracker

The external scenario gate joins the HTTP API, PostgreSQL, object storage, the runner protocol, and real Firecracker guests. It needs a self-hosted Linux x86-64 machine with writable KVM and TUN devices, cgroup v2, a separately verified signed microVM bundle, and an XFS or Btrfs workspace root with reflink support:

SECONDBOX_REQUIRE_QUALIFIED_SCENARIO=1 just test-scenario

See scenario qualification for setup, evidence, and timing budgets. Every commit admitted to main must pass the GitHub-hosted CI workflow. Release candidates are built and KVM-qualified locally on a qualified host, uploaded to a private draft Release, and published without rebuilding by a GitHub-hosted workflow. See release operator setup.

Security

Runner connections require TLS 1.3, a CA-signed certificate identifying the Runner, and a pre-shared Runner credential. The HTTP API accepts the deployment-wide platform token for operators, and explicitly configured application authorities bound to fixed tenant and subject references, exact operation scopes, and named Profile grants. None of these authorities are interchangeable.

Loss of an unbacked home-runner workspace filesystem loses that Sandbox: PostgreSQL or S3 recovery alone is not sufficient. Back up each Runner's stable identity and workspace root as one consistent unit — see backup and recovery and the threat model.

License

MIT. Third-party components and execution assets retain their own licenses; see THIRD_PARTY_NOTICES.md.

Directories

Path Synopsis
cmd
secondbox command
secondboxd command
Package deployassets provides the Compose assets compiled into secondbox-deploy.
Package deployassets provides the Compose assets compiled into secondbox-deploy.
examples
gen
internal
api
Package api exposes the versioned standalone SecondBox HTTP contract.
Package api exposes the versioned standalone SecondBox HTTP contract.
assetcatalog
Package assetcatalog owns the immutable signed execution-asset catalog.
Package assetcatalog owns the immutable signed execution-asset catalog.
config
Package config loads required standalone SecondBox process configuration.
Package config loads required standalone SecondBox process configuration.
deployconfig
Package deployconfig compiles the sole operator-edited SecondBox deployment manifest into explicit process environment artifacts.
Package deployconfig compiles the sole operator-edited SecondBox deployment manifest into explicit process environment artifacts.
lifecycle
Package lifecycle computes durable desired-state reconciliation actions.
Package lifecycle computes durable desired-state reconciliation actions.
objectstore
Package objectstore provides provider-neutral immutable object publication.
Package objectstore provides provider-neutral immutable object publication.
observability
Package observability owns fixed-cardinality in-process timing observations.
Package observability owns fixed-cardinality in-process timing observations.
pagination
Package pagination defines provider-neutral opaque traversal cursors.
Package pagination defines provider-neutral opaque traversal cursors.
ports
Package ports defines SecondBox control-plane persistence boundaries.
Package ports defines SecondBox control-plane persistence boundaries.
reconcile
Package reconcile defines restart-safe assignment recovery decisions.
Package reconcile defines restart-safe assignment recovery decisions.
runnercontrol
Package runnercontrol authenticates and validates runner-initiated protocol sessions.
Package runnercontrol authenticates and validates runner-initiated protocol sessions.
runnercontrol/conformance
Package conformance provides reusable runner protocol state-machine qualification.
Package conformance provides reusable runner protocol state-machine qualification.
scheduler
Package scheduler selects compatible runners and persists fenced assignment authority.
Package scheduler selects compatible runners and persists fenced assignment authority.
service
Package service validates and coordinates standalone SecondBox authority.
Package service validates and coordinates standalone SecondBox authority.
store
Package store implements PostgreSQL-backed SecondBox control-plane authority.
Package store implements PostgreSQL-backed SecondBox control-plane authority.
store/lifecycleprojection
Package lifecycleprojection owns PostgreSQL projections shared by durable lifecycle transitions and the runner evidence transactions that establish those transitions' prerequisites.
Package lifecycleprojection owns PostgreSQL projections shared by durable lifecycle transitions and the runner evidence transactions that establish those transitions' prerequisites.
store/rowlock
Package rowlock owns the invariant PostgreSQL lock order for local Workspace mutations.
Package rowlock owns the invariant PostgreSQL lock order for local Workspace mutations.
worknotify
Package worknotify turns PostgreSQL commit notifications into process-local, coalesced wakeup hints.
Package worknotify turns PostgreSQL commit notifications into process-local, coalesced wakeup hints.
migrations
postgres
Package postgresmigrations applies the embedded, immutable SecondBox schema lineage.
Package postgresmigrations applies the embedded, immutable SecondBox schema lineage.
pkg
buildinfo
Package buildinfo contains immutable release identity injected at link time.
Package buildinfo contains immutable release identity injected at link time.
contracts
Package contracts defines the public and persisted SecondBox domain language.
Package contracts defines the public and persisted SecondBox domain language.
portdirect
Package portdirect defines the bounded caller-facing handshake that precedes every byte of a direct SecondBox data-plane connection.
Package portdirect defines the bounded caller-facing handshake that precedes every byte of a direct SecondBox data-plane connection.
releasecontract
Package releasecontract defines the public, provider-neutral identity chain for a coordinated SecondBox release.
Package releasecontract defines the public, provider-neutral identity chain for a coordinated SecondBox release.
releaseverify
Package releaseverify retrieves and independently verifies public coordinated release authority without a repository checkout.
Package releaseverify retrieves and independently verifies public coordinated release authority without a repository checkout.
resourceapply
Package resourceapply checks and converges declarative SecondBox resources.
Package resourceapply checks and converges declarative SecondBox resources.
standardresources
Package standardresources materializes release-owned resource bundles from a verified release artifact manifest and explicit deployment bindings.
Package standardresources materializes release-owned resource bundles from a verified release artifact manifest and explicit deployment bindings.
sdk
tests
openapicheck
Package openapicheck validates live HTTP responses against the canonical OpenAPI contract.
Package openapicheck validates live HTTP responses against the canonical OpenAPI contract.
scenario/stress command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL