Skip to content
Open
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
20 changes: 20 additions & 0 deletions examples/createos/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@sandbox-agent/example-createos",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@nodeops-createos/sandbox": "latest",
"@sandbox-agent/example-shared": "workspace:*",
"sandbox-agent": "workspace:*"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest",
"vitest": "^3.0.0"
}
}
35 changes: 35 additions & 0 deletions examples/createos/src/createos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SandboxAgent } from "sandbox-agent";
import { createos } from "sandbox-agent/createos";

function collectEnvVars(): Record<string, string> {
const envs: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
return envs;
}

function inspectorUrlToBaseUrl(inspectorUrl: string): string {
return inspectorUrl.replace(/\/ui\/$/, "");
}

export async function setupCreateosSandboxAgent(): Promise<{
baseUrl: string;
token?: string;
cleanup: () => Promise<void>;
}> {
const client = await SandboxAgent.start({
sandbox: createos({
client: { apiKey: process.env.CREATEOS_API_KEY },
...(process.env.CREATEOS_SHAPE ? { shape: process.env.CREATEOS_SHAPE } : {}),
...(process.env.CREATEOS_ROOTFS ? { rootfs: process.env.CREATEOS_ROOTFS } : {}),
create: { envs: collectEnvVars() },
}),
});

return {
baseUrl: inspectorUrlToBaseUrl(client.inspectorUrl),
cleanup: async () => {
await client.killSandbox();
},
};
}
31 changes: 31 additions & 0 deletions examples/createos/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { detectAgent } from "@sandbox-agent/example-shared";
import { SandboxAgent } from "sandbox-agent";
import { createos } from "sandbox-agent/createos";

const envs: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;

const client = await SandboxAgent.start({
// ✨ NEW ✨
sandbox: createos({
client: { apiKey: process.env.CREATEOS_API_KEY },
...(process.env.CREATEOS_SHAPE ? { shape: process.env.CREATEOS_SHAPE } : {}),
create: { envs },
}),
});

const session = await client.createSession({
agent: detectAgent(),
});

session.onEvent((event) => {
console.log(`[${event.sender}]`, JSON.stringify(event.payload));
});

session.prompt([{ type: "text", text: "Say hello from createos in one sentence." }]);

process.once("SIGINT", async () => {
await client.destroySandbox();
process.exit(0);
});
28 changes: 28 additions & 0 deletions examples/createos/tests/createos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { buildHeaders } from "@sandbox-agent/example-shared";
import { describe, expect, it } from "vitest";
import { setupCreateosSandboxAgent } from "../src/createos.ts";

const shouldRun = Boolean(process.env.CREATEOS_API_KEY);
const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 300_000;

const testFn = shouldRun ? it : it.skip;

describe("createos example", () => {
testFn(
"starts sandbox-agent and responds to /v1/health",
async () => {
const { baseUrl, token, cleanup } = await setupCreateosSandboxAgent();
try {
const response = await fetch(`${baseUrl}/v1/health`, {
headers: buildHeaders({ token }),
});
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.status).toBe("ok");
} finally {
await cleanup();
}
},
timeoutMs,
);
});
17 changes: 17 additions & 0 deletions examples/createos/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}
9 changes: 9 additions & 0 deletions sdks/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,14 @@
"./sprites": {
"types": "./dist/providers/sprites.d.ts",
"import": "./dist/providers/sprites.js"
},
"./createos": {
"types": "./dist/providers/createos.d.ts",
"import": "./dist/providers/createos.js"
}
},
"peerDependencies": {
"@nodeops-createos/sandbox": ">=0.6.0",
"@cloudflare/sandbox": ">=0.1.0",
"@daytonaio/sdk": ">=0.12.0",
"@e2b/code-interpreter": ">=1.0.0",
Expand All @@ -68,6 +73,9 @@
"computesdk": ">=0.1.0"
},
"peerDependenciesMeta": {
"@nodeops-createos/sandbox": {
"optional": true
},
"@cloudflare/sandbox": {
"optional": true
},
Expand Down Expand Up @@ -113,6 +121,7 @@
"test:watch": "vitest"
},
"devDependencies": {
"@nodeops-createos/sandbox": ">=0.6.0",
"@cloudflare/sandbox": ">=0.1.0",
"@daytonaio/sdk": ">=0.12.0",
"@e2b/code-interpreter": ">=1.0.0",
Expand Down
125 changes: 125 additions & 0 deletions sdks/typescript/src/providers/createos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { type CreateosSandboxClientOptions, CreateosSandboxNotFoundError, type CreateSandboxRequest, Sandbox } from "@nodeops-createos/sandbox";
import { SandboxDestroyedError } from "../client.ts";
import { buildServerStartCommand, DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
import type { SandboxProvider } from "./types.ts";

const DEFAULT_AGENT_PORT = 3000;
// createos requires a shape at create time; this is a sane default for running
// sandbox-agent plus a coding agent. Override via `shape` / `create`.
const DEFAULT_SHAPE = "s-1vcpu-1gb";
const DEFAULT_CWD = "/root";
// sandbox-agent installs into /usr/local/bin or ~/.local/bin; a login shell
// won't always have the latter on PATH, so export it before every command.
const SANDBOX_AGENT_PATH_EXPORT = 'export PATH="/usr/local/bin:$HOME/.local/bin:$PATH"';

type CreateOverrides = Omit<Partial<CreateSandboxRequest>, "shape"> & { shape?: string };
type CreateOverridesInput = CreateOverrides | (() => CreateOverrides | Promise<CreateOverrides>);

export interface CreateosProviderOptions {
/** Client options forwarded to the createos SDK (apiKey, baseUrl, …). */
client?: CreateosSandboxClientOptions;
/** Shape id from `listShapes()`. Defaults to {@link DEFAULT_SHAPE}. */
shape?: string;
/** Rootfs catalog name or template id. Empty = host default. */
rootfs?: string;
/** Extra create-request overrides, or a factory returning them. */
create?: CreateOverridesInput;
/** In-guest port the sandbox-agent server listens on. */
agentPort?: number;
/** Default working directory for sessions. */
defaultCwd?: string;
/** URL scheme for the ingress preview URL. Defaults to "https"; pass
* "http" when the ingress TLS cert has not been provisioned yet. */
scheme?: "http" | "https";
}

async function resolveCreate(value: CreateOverridesInput | undefined): Promise<CreateOverrides> {
if (!value) return {};
return typeof value === "function" ? await value() : value;
}

function shScript(command: string): string {
return `${SANDBOX_AGENT_PATH_EXPORT}; ${command}`;
}

export function createos(options: CreateosProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const scheme = options.scheme ?? "https";
const clientOptions = options.client ?? {};

const connect = (sandboxId: string) => Sandbox.connect(sandboxId, { ...clientOptions });

return {
name: "createos",
defaultCwd: options.defaultCwd ?? DEFAULT_CWD,
async create(): Promise<string> {
const overrides = await resolveCreate(options.create);
const request: CreateSandboxRequest = {
shape: options.shape ?? overrides.shape ?? DEFAULT_SHAPE,
...(options.rootfs !== undefined ? { rootfs: options.rootfs } : {}),
...overrides,
// Ingress is how we reach the agent server from outside the sandbox.
ingress_enabled: true,
};

const sandbox = await Sandbox.create(request, { ...clientOptions });

await sandbox.sh(shScript(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`), {
label: "createos install",
});
for (const agent of DEFAULT_AGENTS) {
await sandbox.sh(shScript(`sandbox-agent install-agent ${agent}`), {
label: `createos install-agent ${agent}`,
});
}
await sandbox.sh(shScript(buildServerStartCommand(agentPort)), {
label: "createos server start",
});

return sandbox.id;
},
async destroy(sandboxId: string): Promise<void> {
const sandbox = await connect(sandboxId);
await sandbox.destroy();
},
async reconnect(sandboxId: string): Promise<void> {
let sandbox: Sandbox;
try {
sandbox = await connect(sandboxId);
} catch (error) {
if (error instanceof CreateosSandboxNotFoundError) {
throw new SandboxDestroyedError(sandboxId, "createos", { cause: error });
}
throw error;
}
if (sandbox.status === "paused") {
await sandbox.resume();
}
},
async pause(sandboxId: string): Promise<void> {
const sandbox = await connect(sandboxId);
await sandbox.pause();
},
async kill(sandboxId: string): Promise<void> {
const sandbox = await connect(sandboxId);
await sandbox.destroy();
},
async getUrl(sandboxId: string): Promise<string> {
const sandbox = await connect(sandboxId);
if (!sandbox.data.ingress_enabled) {
await sandbox.setIngress(true);
}
return sandbox.previewUrl(agentPort, { scheme });
},
async ensureServer(sandboxId: string): Promise<void> {
const sandbox = await connect(sandboxId);
if (sandbox.status === "paused") {
await sandbox.resume();
}
// Idempotent: a duplicate server exits on port conflict.
await sandbox.sh(shScript(buildServerStartCommand(agentPort)), {
label: "createos ensure server",
});
},
};
}
Loading