@kanyun-ai-infra/sandbox
v0.3.8
Published
TypeScript SDK for Kanyun Sandbox v2
Readme
@kanyun-ai-infra/sandbox
TypeScript SDK for the Kanyun Sandbox v2 control plane.
Install
npm install @kanyun-ai-infra/sandboxInstall a specific release:
npm install @kanyun-ai-infra/[email protected]Quick Start
import { Client } from "@kanyun-ai-infra/sandbox";
const client = new Client({
controlPlaneUrl: "http://127.0.0.1:8080",
apiKey: "kanyun_xxx",
});
// Create a sandbox from an existing template
const handle = await client.createSandbox({
templateName: "my-devbox",
ttlSeconds: 3600,
});
console.log(`Sandbox: ${handle.info.claimName}`);
console.log(`Status: ${handle.info.status}`);
// When done
await handle.destroy();Typical Workflow
A common usage flow looks like this:
准备镜像 → 查看可用资源 → 创建模板 → (可选) 创建预热池 → 创建 Sandbox → 使用 → 销毁Step 1: Explore Available Resources
Before creating resources, check what clusters and runtime profiles are available:
// List clusters
const clusters = await client.listClusters();
for (const c of clusters) {
console.log(` id=${c.id}, name=${c.displayName}, status=${c.status}`);
}
// List runtime profiles (defines where sandboxes run)
const profiles = await client.listRuntimeProfiles();
for (const p of profiles) {
console.log(` id=${p.id}, cluster=${p.clusterId}, ns=${p.namespace}`);
}Step 2: Create a Template
You need a container image first (built by yourself or via the platform's image build system). Then create a template that references it.
Before creating a template, you need to know which runtime profile to use. A runtime profile defines which cluster and namespace your sandboxes will run in:
// List available runtime profiles
const profiles = await client.listRuntimeProfiles();
for (const p of profiles) {
console.log(` id=${p.id}, cluster=${p.clusterId}, ns=${p.namespace}`);
}
// Use the first available profile
const runtimeProfileId = profiles[0].id;Then create the template:
const template = await client.applyTemplate("my-devbox", {
runtimeProfileId,
displayName: "My Dev Box",
description: "Custom development environment",
defaultTTLSeconds: 3600,
maxTTLSeconds: 86400,
enabled: true,
podTemplate: {
spec: {
containers: [{
name: "main",
image: "registry.example.com/my-org/my-devbox:latest",
resources: {
requests: { cpu: "1", memory: "2Gi" },
limits: { cpu: "2", memory: "4Gi" },
},
}],
},
},
moxt: {
enabled: true,
apiKey: "moxt_api_key",
},
});
console.log(`Template created: ${template.name}`);When Moxt mount is enabled, the platform injects the sidecar and the main container can access the mounted content at /moxt/data.
Step 3: Create a Warm Pool (Optional)
If you need sandboxes to start quickly, pre-warm a pool of standby instances:
const warmPool = await client.applyWarmPool("my-devbox-pool", {
templateName: "my-devbox",
desiredReplicas: 3,
});
console.log(`Warm pool: ${warmPool.name}, ready: ${warmPool.readyReplicas}/${warmPool.desiredReplicas}`);Step 4: Create a Sandbox
const handle = await client.createSandbox({
templateName: "my-devbox",
ttlSeconds: 3600,
metadata: { user: "alice", purpose: "code-review" },
});
console.log(`Sandbox: ${handle.info.claimName}`);
console.log(`IP: ${handle.info.ip}`);
console.log(`Expires: ${handle.info.expiresAt}`);If you did not create a warm pool, the sandbox starts cold (Pod is created on-demand). You may need to wait for it to become Running:
const handle = await client.createSandbox({
templateName: "my-devbox",
ttlSeconds: 3600,
});
// Cold start: poll until Running
while (handle.info.status.toLowerCase() !== "running") {
await new Promise((r) => setTimeout(r, 3000));
await handle.refresh();
console.log(` status=${handle.info.status}`);
}
console.log(`Sandbox ready! IP: ${handle.info.ip}`);With a warm pool, createSandbox typically returns immediately with status=Running since a pre-warmed Pod is adopted instantly.
Step 5: Manage Sandbox Lifecycle
// Extend TTL by another 30 minutes
await handle.extend(1800);
// Refresh to get latest status
await handle.refresh();
console.log(`Status: ${handle.info.status}`);
// Get detailed info (includes metadata)
const detail = await handle.detail();
console.log(`Metadata:`, detail.metadata);
// List events
const events = await handle.events();
for (const event of events) {
console.log(` ${event.eventType} at ${event.occurredAt}`);
}
// Destroy when done
await handle.destroy();Client Options
const client = new Client({
controlPlaneUrl: "http://127.0.0.1:8080",
apiKey: "kanyun_xxx",
timeoutMs: 15_000, // HTTP timeout (default: 15s)
});API Reference
Sandbox
| Method | Description |
|--------|-------------|
| createSandbox(request) | Create a sandbox, returns SandboxHandle |
| getSandbox(id) | Get sandbox info by ID |
| listSandboxes() | List all running sandboxes |
| getSandboxDetail(id) | Get detailed sandbox info (includes metadata) |
| listSandboxEvents(id) | List sandbox lifecycle events |
| extendSandbox(id, ttlSeconds) | Extend a running sandbox's TTL |
| deleteSandbox(id) | Delete a sandbox |
| connectSandbox(id) | Connect to an existing sandbox, returns SandboxHandle |
Template
| Method | Description |
|--------|-------------|
| listTemplates() | List all templates |
| getTemplate(name) | Get template by name |
| applyTemplate(name, request) | Create or update a template |
| deleteTemplate(name) | Delete a template |
| listTemplateMaterializations(name) | List template materializations across clusters |
| resyncTemplateMaterialization(name) | Trigger re-sync of a template materialization |
Warm Pool
| Method | Description |
|--------|-------------|
| listWarmPools() | List all warm pools |
| getWarmPool(name) | Get warm pool by name |
| applyWarmPool(name, request) | Create or update a warm pool |
| deleteWarmPool(name) | Delete a warm pool |
SandboxHandle
| Method | Description |
|--------|-------------|
| handle.info | Current SandboxInfo snapshot |
| handle.daemon | Direct-connect daemon client for process, file, git, and info APIs |
| handle.extend(ttlSeconds) | Extend sandbox TTL from now |
| handle.refreshActivity() | Explicitly refresh sandbox activity while using direct-connect daemon APIs |
| handle.refresh() | Refresh sandbox info from server |
| handle.detail() | Get sandbox detail |
| handle.events() | List sandbox events |
| handle.destroy() | Delete the sandbox |
Direct-Connect Daemon
When daemon injection is enabled, sandbox responses include daemonEndpoint and
daemonAuthToken. The SDK wraps those credentials in handle.daemon, so callers
do not need to build daemon URLs or authorization headers by hand.
Since 0.3.6, handle.daemon covers:
- info:
version(),health(),workDir() - process: one-shot command execution plus persistent shell sessions
- files: list/info/download/upload, binary-safe
Uint8Array/Buffer/Blobupload, bulk upload/download, folder/delete/move/search/find/replace/permissions - git: clone/status/add/commit/push/pull
In 0.3.7, streamSessionCommandLogs() adds true chunk-by-chunk log
following for asynchronous session commands.
Prefer await handle.daemon.info.workDir() over hard-coding /workspace; the
control plane can override the daemon work directory with
KANYUN_TOOLBOX_DAEMON_WORK_DIR.
const handle = await client.createSandbox({
templateName: "aio-devbox",
ttlSeconds: 3600,
});
const workDir = await handle.daemon.info.workDir();
const result = await handle.daemon.process.executeCommand({
command: "pwd && whoami",
cwd: workDir,
timeout: 10,
});
console.log(result.stdout);
await handle.daemon.files.upload(`${workDir}/hello.txt`, "Hello from SDK!\n");
await handle.daemon.files.upload(`${workDir}/blob.bin`, new Uint8Array([0, 1, 2, 255]));
await handle.daemon.files.bulkUpload([
{ path: `${workDir}/a.txt`, content: "alpha" },
{ path: `${workDir}/b.txt`, content: new Blob(["beta"]) },
]);
const downloaded = await handle.daemon.files.bulkDownload([`${workDir}/a.txt`, `${workDir}/b.txt`]);
console.log(await downloaded.get(`${workDir}/a.txt`)?.text());
const files = await handle.daemon.files.list(workDir);
console.log(files.map((file) => file.name));
const status = await handle.daemon.git.status(workDir);
console.log(status.branch);
// Direct daemon calls do not pass through the control plane, so refresh activity explicitly.
await handle.refreshActivity();Persistent sessions keep shell state between commands:
const session = await handle.daemon.process.createSession({ cwd: workDir });
await handle.daemon.process.executeInSession(session.id!, {
command: "export DEMO=direct && mkdir -p demo",
});
const asyncCommand = await handle.daemon.process.executeInSession(session.id!, {
command: "printf \"$DEMO\\n\" && pwd",
async: true,
});
if (!("commandId" in asyncCommand) || !asyncCommand.commandId) {
throw new Error("async command did not return a command id");
}
for await (const chunk of handle.daemon.process.streamSessionCommandLogs(session.id!, asyncCommand.commandId)) {
process.stdout.write(chunk);
}
await handle.daemon.process.deleteSession(session.id!);handle.daemon throws SandboxDaemonUnavailableError when the sandbox does not
have daemonEndpoint or daemonAuthToken. Create the sandbox with daemon
injection enabled and wait until it is running before using protected daemon APIs.
Session
const session = await client.newSession({
templateName: "my-devbox",
ttlSeconds: 600,
});
try {
console.log(session.info.ip);
} finally {
await session.close(); // sandbox auto-destroyed
}Or with a callback:
const result = await client.runSession(
{ templateName: "my-devbox", ttlSeconds: 600 },
async (session) => {
return session.info.ip;
},
);History
| Method | Description |
|--------|-------------|
| listSandboxHistory(params) | Query sandbox history (paginated) |
| getSandboxHistory(id) | Get historical sandbox detail with events |
Platform Profiles
| Method | Description |
|--------|-------------|
| listClusters() / getCluster(id) / createCluster(req) / upsertCluster(id, req) / deleteCluster(id) | Cluster management |
| listRuntimeProfiles() / getRuntimeProfile(id) / createRuntimeProfile(req) / upsertRuntimeProfile(id, req) / deleteRuntimeProfile(id) | Runtime profile management |
| listBuildProfiles() / getBuildProfile(id) / createBuildProfile(req) / upsertBuildProfile(id, req) / deleteBuildProfile(id) | Build profile management |
| listRegistryProfiles() / getRegistryProfile(id) / createRegistryProfile(req) / upsertRegistryProfile(id, req) / deleteRegistryProfile(id) | Registry profile management |
| listSecretProfiles() / getSecretProfile(id) / createSecretProfile(req) / upsertSecretProfile(id, req) / deleteSecretProfile(id) | Secret profile management |
| listSecretMaterializations(profileId) / resyncSecretMaterialization(profileId, matId) / rotateSecretProfile(profileId, req) | Secret materialization |
Image Build
| Method | Description |
|--------|-------------|
| createImageBuild(req) / listImageBuilds() / getImageBuild(id) | Image build management |
| listImageBuildLogs(buildId) / cancelImageBuild(buildId) | Build logs and cancellation |
| listImageAssets() / createImageAsset(req) / getImageAsset(id) / upsertImageAsset(id, req) / deleteImageAsset(id) | Reusable image assets |
Error Handling
| Error | Cause |
|-------|-------|
| ValidationError | Invalid request (400) |
| AuthenticationError | API key invalid (401) |
| NotFoundError | Resource not found (404) |
| ConflictError | State conflict (409) |
| APIError | Other control-plane errors |
| SandboxDaemonUnavailableError | .daemon accessed without daemon direct-connect credentials |
Authentication
All control-plane requests use X-API-Key header.
