Skip to content

SDK 第一个请求

多语言 SDK(talon-sandbox 系列)统一表层 — 装包、写 5 行、跑起来。本页演示 完整的"创建 → 跑命令 → 暴露端口 → 销毁"流程,Python / TypeScript / Go / Rust / C# 并列。

前置

  • 一个能访问的 Talon Sandbox 实例。测试环境直接用官方托管地址 https://api.sandbox.talon.net.cn;本机 Docker 部署是 http://localhost:18080 (见 Docker quickstart / systemd quickstart)
  • 一个 API Key,形如 ask_...(login 后生成,或 bootstrap 时打印)
  • 网络:network="allowlist" 默认放 DNS + 配置好的白名单域;"open" 全 开放出口;"sealed" 完全隔离(只有 lo)

SDK 默认从环境变量读取服务地址与鉴权,先导出:

bash
# 测试环境
export TALON_SANDBOX_SERVER="https://api.sandbox.talon.net.cn"
export TALON_SANDBOX_API_KEY="ask_xxxxxxxxxxxxxxxx"

TALON_SANDBOX_SERVER 只填到域名,不要带 /v1/api —— SDK 内部自动拼 /v1/* 路径。不设这个变量时 SDK 默认连本机 http://localhost:18080,这是接 入测试环境最常见的失败原因。

安装

bash
pip install talon-sandbox
bash
npm install talon-sandbox
bash
go get github.com/talon-org/talon-sandbox-sdk-go
toml
# Cargo.toml
[dependencies]
talon-sandbox = { git = "https://github.com/talon-org/talon-sandbox-sdk-rust" }
tokio = { version = "1", features = ["full"] }
bash
dotnet add package TalonSandbox.Sdk
bash
# Homebrew(规划中)
brew install talon-sandbox

# 源码安装(立即可用)
go install github.com/talon-org/talon-sandbox-cli@latest
# 安装后两个名字都能用:talon-sandbox / tsb(短名)

Hero 示例 — 拉起一个 dev server 并拿到 preview URL

python
import asyncio
import os
from talon_sandbox import Sandbox

async def main():
    async with Sandbox.create(
        image="talon-alpine",
        resources={"cpu": 2, "memory": "4GiB"},
        network="allowlist",
        timeout="30m",
        api_key=os.environ["TALON_SANDBOX_API_KEY"],
        base_url=os.environ["TALON_SANDBOX_SERVER"],
    ) as sb:
        await sb.fs.write_text("/workspace/app.py",
            "print('hello from sandbox')\n")
        result = await sb.run("python3 /workspace/app.py")
        print(result.stdout)  # → hello from sandbox

        proc = await sb.spawn("python3 -m http.server 8000")
        url = await sb.expose(8000)
        print(f"Preview: {url}")
        # → http://sb-xxx-8000.preview.example.com

        # async with 退出时自动 kill,sandbox 被销毁
        await asyncio.sleep(60)

asyncio.run(main())
typescript
import { Sandbox } from "talon-sandbox";

const sb = await Sandbox.create({
  image: "talon-alpine",
  resources: { cpu: 2, memory: "4GiB" },
  network: "allowlist",
  timeout: "30m",
  apiKey: process.env.TALON_SANDBOX_API_KEY!,
  baseUrl: process.env.TALON_SANDBOX_SERVER!,
});

try {
  await sb.fs.writeText("/workspace/app.js",
    "console.log('hello from sandbox')\n");
  const result = await sb.run("node /workspace/app.js");
  console.log(result.stdout);  // → hello from sandbox

  const proc = await sb.spawn("npx http-server -p 8000");
  const url = await sb.expose(8000);
  console.log(`Preview: ${url}`);
  // → http://sb-xxx-8000.preview.example.com

  await new Promise(r => setTimeout(r, 60_000));
} finally {
  await sb.kill();
}
go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    talonsandbox "github.com/talon-org/talon-sandbox-sdk-go"
)

func main() {
    ctx := context.Background()

    sb, err := talonsandbox.Create(ctx,
        talonsandbox.WithImage("talon-alpine"),
        talonsandbox.WithResources(talonsandbox.Resources{CPU: 2, Memory: "4GiB"}),
        talonsandbox.WithNetwork("allowlist"),
        talonsandbox.WithTimeout("30m"),
        talonsandbox.WithAPIKey(os.Getenv("TALON_SANDBOX_API_KEY")),
        talonsandbox.WithBaseURL(os.Getenv("TALON_SANDBOX_SERVER")),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer sb.Kill(ctx)

    if err := sb.Files.WriteText(ctx, "/workspace/app.js",
        "console.log('hello from sandbox')\n"); err != nil {
        log.Fatal(err)
    }

    result, err := sb.Run(ctx, "node /workspace/app.js")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result.Stdout)  // → hello from sandbox

    _, err = sb.Spawn(ctx, "npx http-server -p 8000")
    if err != nil {
        log.Fatal(err)
    }

    exposed, err := sb.Expose(ctx, 8000)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Preview: %s\n", exposed.URL)

    time.Sleep(60 * time.Second)
}
rust
// server / api_key 默认从 TALON_SANDBOX_SERVER / TALON_SANDBOX_API_KEY 读取
use talon_sandbox::{CreateOpts, ExposeOpts, Resources, Sandbox};

#[tokio::main]
async fn main() -> talon_sandbox::Result<()> {
    let sb = Sandbox::create(CreateOpts {
        image: Some("talon-alpine".into()),
        resources: Resources {
            cpu: 2.0,
            memory: Some("4GiB".into()),
            ..Default::default()
        },
        network: Some("allowlist".into()),
        timeout: Some("30m".into()),
        ..Default::default()
    })
    .await?;

    sb.fs()
        .write_text("/workspace/app.js", "console.log('hello from sandbox')\n")
        .await?;
    let result = sb.run("node /workspace/app.js").await?;
    print!("{}", result.combined); // → hello from sandbox

    sb.spawn("npx http-server -p 8000").await?;
    let url = sb.expose(8000, ExposeOpts::default()).await?;
    println!("Preview: {url}");
    // → http://sb-xxx-8000.preview.example.com

    tokio::time::sleep(std::time::Duration::from_secs(60)).await;

    sb.kill().await?; // 用完销毁,释放配额
    Ok(())
}
csharp
using TalonSandbox.Sdk;

await using var sb = await Sandbox.CreateAsync(new CreateOptions {
    Image = "talon-alpine",
    Resources = new Resources { Cpu = 2, Memory = "4GiB" },
    Network = "allowlist",
    Timeout = "30m",
    ApiKey = Environment.GetEnvironmentVariable("TALON_SANDBOX_API_KEY"),
    BaseUrl = Environment.GetEnvironmentVariable("TALON_SANDBOX_SERVER"),
});

await sb.Files.WriteTextAsync("/workspace/app.js",
    "console.log('hello from sandbox')\n");
var result = await sb.RunAsync("node /workspace/app.js");
Console.WriteLine(result.Stdout);  // → hello from sandbox

await sb.SpawnAsync("npx http-server -p 8000");
var url = await sb.ExposeAsync(8000);
Console.WriteLine($"Preview: {url.Url}");

await Task.Delay(TimeSpan.FromMinutes(1));
// await using 退出时自动 kill
bash
# 一行起跑 + 暴露:
SBX=$(tsb create \
    --image talon-alpine \
    --resources cpu=2,memory=4GiB \
    --network allowlist \
    --wait running \
    -o id)

tsb run $SBX "python3 -c 'print(\"hello from sandbox\")'"
tsb spawn $SBX "npx http-server -p 8000"
tsb expose $SBX 8000
# → http://sb-xxx-8000.preview.example.com

# 收尾
tsb rm $SBX

设计原则

四语言 SDK 表层一致,遵守同一份设计规范。

  • 概念扁平sb.run / sb.spawn / sb.expose / sb.fs / sb.terminal, 没有嵌套的 sb.processes.create() 这种 RPC 风格
  • 字符串单位memory="4GiB",timeout="30m",ttl="6h"(不是 memory_bytes / idle_timeout_seconds)
  • async first-class — 所有 IO 都是 async/await
  • 资源管理async with / await using / defer sb.Kill() 自动清理
  • 命名抄 unix-docker-Pitcherrun(同步,unix system())/ spawn(异步, fork+exec)/ kill(销毁)

常用操作速查

文件系统

python
await sb.fs.write_text("/workspace/main.py", "print('hi')")
text = await sb.fs.read_text("/workspace/main.py")
entries = await sb.fs.list("/workspace")
await sb.fs.remove("/workspace/old.py")
exists = await sb.fs.exists("/workspace/main.py")
typescript
await sb.fs.writeText("/workspace/main.py", "print('hi')");
const text = await sb.fs.readText("/workspace/main.py");
const entries = await sb.fs.list("/workspace");
await sb.fs.remove("/workspace/old.py");
const exists = await sb.fs.exists("/workspace/main.py");
go
sb.Files.WriteText(ctx, "/workspace/main.py", "print('hi')")
text, _ := sb.Files.ReadText(ctx, "/workspace/main.py")
entries, _ := sb.Files.List(ctx, "/workspace")
sb.Files.Remove(ctx, "/workspace/old.py")
exists, _ := sb.Files.Exists(ctx, "/workspace/main.py")
rust
sb.fs().write_text("/workspace/main.py", "print('hi')").await?;
let text = sb.fs().read_text("/workspace/main.py").await?;
let entries = sb.fs().list("/workspace").await?;
sb.fs().remove("/workspace/old.py").await?;
csharp
await sb.Files.WriteTextAsync("/workspace/main.py", "print('hi')");
var text = await sb.Files.ReadTextAsync("/workspace/main.py");
var entries = await sb.Files.ListAsync("/workspace");
await sb.Files.RemoveAsync("/workspace/old.py");
var exists = await sb.Files.ExistsAsync("/workspace/main.py");
bash
echo "print('hi')" | tsb cp - $SBX:/workspace/main.py
tsb cp $SBX:/workspace/main.py ./main.py

交互式终端(PTY)

python
async with sb.terminal.open(cmd="/bin/bash") as pty:
    await pty.write("ls -la /workspace\n")
    async for chunk in pty:
        print(chunk, end="")
typescript
const pty = await sb.terminal.open({ cmd: "/bin/bash" });
pty.on("data", chunk => process.stdout.write(chunk));
await pty.write("ls -la /workspace\n");
await pty.close();
go
import "github.com/talon-org/talon-sandbox-sdk-go/terminal"

pty, _ := terminal.Open(ctx, sb, "/bin/bash")
defer pty.Close(ctx)
pty.Write(ctx, []byte("ls -la /workspace\n"))
// 读 pty.Output() channel 获取输出
rust
let mut pty = sb.terminal().open().await?;
pty.write(b"ls -la /workspace\n").await?;
while let Some(chunk) = pty.recv().await? {
    print!("{}", String::from_utf8_lossy(&chunk));
}
pty.close().await?;
csharp
await using var pty = await sb.Terminal.OpenAsync("/bin/bash");
pty.OnData += chunk => Console.Write(chunk);
await pty.WriteAsync("ls -la /workspace\n");
bash
tsb pty $SBX --cmd /bin/bash
# 当前终端进入 raw 模式,Ctrl-D 退出

端口暴露 — 签名 URL(给第三方)

python
exposed = await sb.expose(8000, sign=True, ttl="1h")
print(exposed.url)
# → http://sb-xxx-8000.preview.example.com/?token=eyJ...
typescript
const exposed = await sb.expose(8000, { sign: true, ttl: "1h" });
console.log(exposed.url);
go
exposed, _ := sb.Expose(ctx, 8000,
    talonsandbox.WithSign(true),
    talonsandbox.WithTTL("1h"))
fmt.Println(exposed.URL)
rust
let url = sb.expose(8000, ExposeOpts {
    sign: true,
    ttl: Some("1h".into()),
    ..Default::default()
}).await?;
println!("{url}");
csharp
var exposed = await sb.ExposeAsync(8000, new ExposeOptions {
    Sign = true, Ttl = "1h"
});
Console.WriteLine(exposed.Url);
bash
tsb expose $SBX 8000 --sign --ttl 1h

完整 expose 模型(显式 vs 动态、签名、自定义 subdomain)见 端口暴露

暂停 / 恢复

python
await sb.pause()    # 软暂停,进程冻结,workspace 保留
await sb.resume()   # 毫秒级恢复
typescript
await sb.pause();
await sb.resume();
go
sb.Pause(ctx)
sb.Resume(ctx)
rust
sb.pause().await?;   // 软暂停,进程冻结,workspace 保留
sb.resume().await?;  // 毫秒级恢复
csharp
await sb.PauseAsync();
await sb.ResumeAsync();
bash
tsb pause $SBX
tsb resume $SBX

鉴权与配置

SDK 默认从环境变量读取服务地址与 API key:

环境变量用途
TALON_SANDBOX_SERVERAPI base URL。测试环境 https://api.sandbox.talon.net.cn;本机 http://localhost:18080
TALON_SANDBOX_API_KEYAPI key,形如 ask_...。SDK 自动加 Bearer 前缀,原样填入即可

各语言 Sandbox.create()api_key / base_url 参数会覆盖环境变量。Rust 还 可以用进程级 configure(不依赖环境变量):

rust
use talon_sandbox::{configure, Config};

configure(
    Config::new("https://api.sandbox.talon.net.cn")
        .api_key("ask_xxxxxxxxxxxxxxxx"),
);
// 之后 Sandbox::create(...) 不传 client 即用这套默认

CLI 还支持 ~/.config/talon-sandbox/config.yaml 多 context 配置:

bash
tsb login --server https://api.sandbox.talon.net.cn
tsb whoami

资源配额

创建参数受账号套餐限制,超限返回 422 quota exceeded。不传 resources / image 时由服务端兜底默认值,最省事。各套餐上限(单 sandbox + 并发数)以 [控制台 → 套餐]为准;不确定时先用默认值跑通,再按需调大。用完的 sandbox 务必 kill()(或 async with / defer / await using 自动清理),否则并发数占满会 阻塞后续创建。

偏好 raw HTTP?

如果你想直接拼 curl / fetch 看 wire 契约,跳到 curl 30 秒上手 — 那篇覆盖了同样的流程但全程 raw HTTP。

完整 OpenAPI 规格在 API 参考

下一步

基于 MIT License 发布