Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sdk/typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
coverage/
96 changes: 96 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# @arcbox/sandbox

TypeScript SDK for ArcBox sandboxes: isolated microVMs on your Mac,
driven over the local daemon's Unix socket with the Connect protocol.
Requires Node ≥ 22.

```sh
npm install @arcbox/sandbox
```

## Hello world

With the daemon running (`abctl daemon start`):

```ts
import { Sandbox } from '@arcbox/sandbox';

// Local daemon over ~/.arcbox/run/arcbox.sock — zero config.
await using sandbox = await Sandbox.create('', { ttlMs: 300_000 });

await sandbox.files.writeText('/tmp/hello.txt', 'hello from arcbox\n');

const check = await sandbox.commands.run(['/bin/cat', '/tmp/hello.txt']);
console.log(check.expect().stdout, '→ exit', check.exitCode);

const job = await sandbox.commands.run('for i in 1 2 3; do echo line$i; done', {
background: true,
});
for await (const chunk of job.output) {
process.stdout.write(chunk.data);
}
const done = await job.waitForExit();
console.log('background job exited', done.exitCode);
// scope exit: await using → sandbox killed, nothing leaked
```

Non-zero exit is data (`result.exitCode`), never an exception —
`result.expect()` is the opt-in throw. Every daemon error maps to a
typed class (`SandboxNotFoundError`, `CapabilityError`,
`ConnectionFailedError`, ...) carrying a machine-readable `code`, an
actionable `suggestion`, and the failed `operation`.

## Connection

Resolution order: explicit option > environment > default.

| Environment | Meaning |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `ARCBOX_SOCKET` | daemon Unix socket (default `$ARCBOX_DATA_DIR/run/arcbox.sock`; data dir default `~/.arcbox`, or `~/.arcbox-dev` under `ARCBOX_PROFILE=development`) |
| `ARCBOX_API_URL` | remote daemon / cloud front door; setting it selects the remote tier (reserved, CORE-63) |
| `ARCBOX_API_KEY` | bearer credential, attached as `Authorization` when set; unused by the local daemon |

Every entry point takes a `connection` options slot
(`socketPath` / `apiUrl` / `apiKey` / `requestTimeoutMs` / injected
`transport` for mocking).

## Development

Inside the arcbox repo (`sdk/typescript`):

```sh
npm install
npm run generate # regenerate src/gen from ../../rpc/arcbox-protocol/proto (buf + protoc-gen-es)
npm run lint # eslint (eslint-config-sukka)
npm run format:check # biome formatter check (biome format --write . fixes)
npm test # vitest unit tests (no daemon needed)
npm run typecheck # tsc --noEmit
npm run build # bunchee → dist/
```

Generated code under `src/gen/` is committed and is **never** exported
from the package entry point — public shapes are hand-written and mapped
at the transport boundary.

The end-to-end hello-world loop runs only against a live daemon and is
opt-in:

```sh
ARCBOX_SDK_E2E=1 npm test -- test/e2e.test.ts
```

TODO(CI): wire `npm run lint`, `npm run format:check`, `npm test`, and
`npm run typecheck` into `.github/workflows` as an `sdk-typescript` job
(follow-up; workflow changes are intentionally not part of this branch).

## Status

Phase 1 of CORE-58 — the hello-world closed loop: `Sandbox`
create/connect/list, `kill`/`pause`/`info` (`pause` and the
paused-sandbox reconnect path are wire-complete but reject with
Unimplemented until the daemon's CORE-21 lands — the daemon serves
Pause/Resume as contract-only stubs today), `commands.run` (foreground
result + background handle with streamed output, `waitForExit`, `kill`),
and whole-file `files` read/write. Deferred: PTY, `ports`,
`waitForPort`/`waitForLog`, filesystem path verbs (stat/list/mkdir/…),
`Template` statics, `events()`, `setLifecycle`, capabilities handshake.
27 changes: 27 additions & 0 deletions sdk/typescript/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!src/gen", "!package-lock.json"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": false
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
19 changes: 19 additions & 0 deletions sdk/typescript/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Codegen for the sandbox API (`npm run generate`).
#
# Input is the repo's proto tree; only the public `arcbox.sandbox.v1`
# package is generated (it is self-contained: imports only well-known
# types, which protoc-gen-es maps to @bufbuild/protobuf/wkt). Output in
# src/gen/ is committed, never exported from the package entry point —
# public shapes are hand-written and mapped at the transport boundary.
version: v2
inputs:
- directory: ../../rpc/arcbox-protocol/proto
paths:
- ../../rpc/arcbox-protocol/proto/arcbox/sandbox/v1
plugins:
- local: protoc-gen-es
out: src/gen
opt:
- target=ts
- import_extension=js
clean: true
47 changes: 47 additions & 0 deletions sdk/typescript/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-check
import { sukka } from "eslint-config-sukka";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";

export default sukka(
{
ignores: {
// Generated code is lint-exempt: protoc-gen-es owns its style.
customGlobs: ["src/gen/**"],
},
// Biome owns formatting; no stylistic rules.
stylistic: false,
// npm-managed package inside a Rust repo — no pnpm workspace to lint.
pnpm: false,
},
{
settings: {
"import-x/resolver-next": [
createTypeScriptImportResolver({
project: ["tsconfig.json"],
}),
],
},
},
{
files: ["package.json"],
rules: {
// npm "files" negations must FOLLOW the pattern they negate
// (last match wins); sorting would put "!dist/**" first and
// re-include the excluded files.
"jsonc/sort-array-values": "off",
},
},
{
files: ["src/sandbox.ts"],
rules: {
// ArcBox ⇄ Sandbox are a deliberately mutually recursive pair
// (entry point mints handles, handle statics are sugar over the
// entry point), so no source order can satisfy `classes: true`;
// the references run at call time, never in the TDZ.
"@typescript-eslint/no-use-before-define": [
"error",
{ functions: false, classes: false, variables: true },
],
},
},
);
Loading
Loading