A3S Box TypeScript SDK

August 11, 2026 ยท View on GitHub

@a3s-lab/box is a local-first TypeScript SDK with native Sandbox, commands, and files APIs. It controls the A3S Box runtime installed on the same machine.

Local use

Install the A3S Box runtime and the TypeScript package:

brew install a3s-lab/tap/a3s-box
npm install @a3s-lab/box

No endpoint or API key is required:

import { Sandbox } from '@a3s-lab/box'

const sandbox = await Sandbox.create('python:3.12-alpine')

try {
  const result = await sandbox.commands.run(
    'python -c "print(6 * 7)"'
  )
  console.log(result.stdout)

  await sandbox.files.write('/workspace/note.txt', 'hello')
  console.log(await sandbox.files.read('/workspace/note.txt'))
} finally {
  await sandbox.kill()
}

Export one bounded build or test artifact, optionally writing it to an exact host destination:

const artifactSandbox = await Sandbox.create('alpine:3.20')
try {
  const artifact = await artifactSandbox.files.export('/workspace/report.json', {
    maxBytes: 8 * 1024 * 1024,
    destination: 'artifacts/report.json',
  })
  console.log(artifact.size, artifact.sha256)
} finally {
  await artifactSandbox.kill()
}

Artifact export has a hard 8 MiB ceiling. It checks the file type and size before reading, rejects declared-size mismatches or stat/read size changes, and returns the bytes with a lowercase SHA-256 digest. A destination is created exclusively; an existing host file is never overwritten.

Sandbox.create() defaults to alpine:3.20 and MicroVM isolation. The first argument is an OCI image reference in local mode. Select the shared-kernel Sandbox backend explicitly on a certified Linux host:

const sandbox = await Sandbox.create('python:3.12-alpine', {
  isolation: 'sandbox',
  cpus: 2,
  memoryMb: 1024,
})

By default, creation replaces the image command with a long-running keepalive process. Set command and entrypoint to configure the initial OCI process, including on Windows/WHPX where post-boot command execution is unavailable:

const sandbox = await Sandbox.create('alpine:3.20', {
  entrypoint: ['/bin/sh', '-c'],
  command: ['echo ready; exec httpd -f -p 8080'],
})

Lifecycle and inspection

Local Sandbox lifecycle calls are generation-fenced. stop() preserves the durable Sandbox, restart() advances its generation under a caller-supplied idempotency identity, remove() deletes a terminal Sandbox, and kill() performs stop plus removal. Reuse the same operationId when retrying a restart whose outcome is not yet known.

import { A3SBoxClient, Sandbox } from '@a3s-lab/box'

const client = new A3SBoxClient()
const sandbox = await Sandbox.create('alpine:3.20')

try {
  const logs = await sandbox.logs({ tail: 100 })
  const stats = await sandbox.stats()
  console.log(logs.length, stats?.memoryPercent)

  const processes = await sandbox.processes()
  const runtimeStats = await sandbox.runtimeStats()
  const events = await sandbox.events({
    afterSequence: 0,
    limit: 256,
    waitTimeoutMs: 1_000,
  })
  const eventStream = sandbox
    .streamEvents({ afterSequence: events.nextSequence })
    [Symbol.asyncIterator]()
  console.log(processes.processes.length, runtimeStats.memory.usageBytes)
  console.log(events.nextSequence)
  await sandbox.updateResources(
    { cpuShares: 512 },
    { operationId: 'ci-resources-1' }
  )
  console.log((await eventStream.next()).value?.kind)
  await eventStream.return()

  await sandbox.stop()
  await sandbox.restart({
    operationId: 'ci-restart-1',
    stopTimeoutSeconds: 10,
  })
  console.log(await client.getSandbox(sandbox.id))
} finally {
  await sandbox.kill()
}

Log snapshots contain structured stream, message, and timestamp values, and accept tails from 1 through 10,000 entries. The runtime client also exposes listSandboxes(), getSandbox(), runtimeDiagnostics(), runtimeDiskUsage(), listFilesystemSnapshots(), and getFilesystemSnapshot().

processes(), runtimeStats(), events(), and streamEvents() accept a running or paused Sandbox and preserve its exact generation; updateResources() requires a running Sandbox. Event polls and stream batches default to 256 items and accept at most 4,096. streamEvents() is an AsyncIterable; pass an AbortSignal to cancel an active local bridge process. Streams terminate on generation drift instead of following a restart. Reuse an explicit resource-update operationId when retrying an outcome that is not yet known.

Builder-style programmable CI/CD

The direct Sandbox API remains available for execution. For build and CI tooling, A3SBoxClient adds fluent builders over the same local runtime and bridge:

import { A3SBoxClient } from '@a3s-lab/box'

const client = new A3SBoxClient()

const image = await client
  .image('./ci')
  .dockerfile('Dockerfile')
  .tag('local/ci-base:latest')
  .buildArg('NODE_VERSION', '24')
  .build()
const cache = await client
  .volume('npm-cache')
  .label('purpose', 'ci-cache')
  .sizeLimit(10 * 1024 * 1024 * 1024)
  .create()
const network = await client
  .network('ci-net')
  .subnet('10.89.40.0/24')
  .create()

const box = await client
  .sandbox(image.reference)
  .cpus(4)
  .memoryMb(4096)
  .entrypoint('/usr/bin/env', 'sh')
  .command('-c', 'npm test && sleep 3600')
  .mountNamed(cache.name, '/root/.npm')
  .network(network.name)
  .publishTcp(8080, 8080)
  .workdir('/workspace')
  .start()

try {
  const result = await box
    .script('npm ci\nnpm test\n')
    .interpreter('/bin/sh', '-se')
    .env('CI', 'true')
    .run()
  if (result.exitCode !== 0) throw new Error(result.stderr)
} finally {
  await box.kill()
}

Named volumes and networks must be created explicitly before they are mounted or selected. Builder scripts are sent through standard input to the selected interpreter, so their contents are not interpolated into a host shell command. Initial command and entrypoint argument vectors are validated before the runtime is invoked.

Named bridge networks and published ports are currently MicroVM-only. A shared-kernel Sandbox request that selects either fails before runtime mutation; use .disableNetwork() or the default TSI-compatible configuration for supported Sandbox workloads.

The package invokes the versioned machine bridge built into the installed a3s-box executable. It does not parse human CLI output. Protocol v3 performs one shared, complete capability handshake before the first normal operation, including when callers start concurrently. Typed values, standard Base64, and Sandbox identity, generation, state, and isolation are validated fail-closed; malformed responses use bridge_protocol_error, a missing binary uses binary_not_found, and a local bridge deadline uses bridge_timeout. Set A3S_BOX_BINARY only when the executable is not on PATH, or inject a typed A3SLocalRuntime object in application tests.

Host resources use the same typed client:

import {
  A3SBoxClient,
  RegistryCredentials,
  SignaturePolicy,
} from '@a3s-lab/box'

const client = new A3SBoxClient()
const password = process.env.REGISTRY_PASSWORD
if (!password) throw new Error('REGISTRY_PASSWORD is required')
const credentials = new RegistryCredentials('builder', password)
const image = await client.pullImage('registry.example/ci/base:latest', {
  credentials,
  signaturePolicy: SignaturePolicy.cosignKey('/keys/cosign.pub'),
})
const metadata = await client.inspectImage(image.reference)
const history = await client.imageHistory(image.reference)
const tagged = await client.tagImage(image.reference, 'local/ci-base:tested')
await client.pushImage(
  tagged.reference,
  'registry.example/ci/base:tested',
  { credentials }
)
await client.pruneVolumes()
await client.pruneNetworks()

client.capabilities() returns bridge protocol version 3 and the exact 52 supported operation names. Registry passwords are passed only to the local runtime process.