数据截至 (上游 commit ac864a6fe3bd)
客户端 SDK:Go / Python 的 Sandbox 生命周期与连接模型
30 秒导读: 前四章讲的是集群里的"服务端"(CRD、控制器、扩展、router)。本章换到调用方视角:一个 agent / 应用怎么用几行代码创建一个沙箱、等它就绪、往里跑命令读写文件、用完删掉。主线是 Go SDK(
clients/go/sandbox/),它把这件事拆成两条清晰的线——控制面(和 Kubernetes API 打交道,管 Claim/Sandbox 生命周期)与数据面(和沙箱里的 HTTP 服务打交道,发命令/传文件),中间用一层"连接策略"决定网络怎么通。Python SDK 是同一模型的另一种实现,用来横向印证。
1. 这是什么(零基础也能懂)
一句话定义: 客户端 SDK 是一个编程库,让你的程序不必手写 kubectl 和 HTTP,就能"要一个沙箱 → 在里面执行代码 → 收拾干净"。
给谁用、解决什么问题: 假设你在写一个 AI agent,它需要一个隔离的 Linux 环境来跑模型生成的代码。你不想关心"怎么创建 CRD、怎么等 Pod 起来、Pod 的 IP 是多少、请求怎么路由进去"。SDK 把这些全包了,只暴露四个动作:
| 动作 | Go 方法 | 干什么 |
|---|---|---|
| 要一个沙箱 | client.CreateSandbox(...) | 创建 Claim,阻塞到 Sandbox Ready,建好连接 |
| 跑命令 | sb.Run(ctx, "echo hi") | 在沙箱里执行 shell,拿回 stdout/stderr/exit code |
| 读写文件 | sb.Write / sb.Read / sb.List / sb.Exists | 上传、下载、列目录、判存在 |
| 收拾 | sb.Close(ctx) / client.DeleteAll(ctx) | 删 Claim、断连接、释放本地资源 |
用起来什么样(Go,真实示意):
client, _ := sandbox.NewClient(ctx, sandbox.Options{Namespace: "default"})
defer client.DeleteAll(ctx)
sb, _ := client.CreateSandbox(ctx, "my-warmpool", "default") // 阻塞到就绪
result, _ := sb.Run(ctx, "echo 'Hello from Go!'")
fmt.Println(result.Stdout, result.ExitCode)
一句话直觉: 把 SDK 想成"云主机的 SDK",只不过"主机"是一个 Kubernetes 沙箱 Pod:CreateSandbox ≈ 开机并拿到 SSH,Run/Read/Write ≈ 在里面敲命令,Close ≈ 关机销毁。
本章不重复 CRD 字段(见 01-sandbox-api-model.md)和 router 内部机制(见 04-sandbox-router.md),只讲客户端怎么消费它们。
2. 顶层全景(SDK 大概怎么转)
Go SDK 的 sandbox 包里,一个请求会穿过这几层。先看图,再看每层职责。
怎么读这张图: 上半是控制面(和 K8s API 交互,一次性的生命周期动作),下半是数据面(和沙箱 HTTP 交互,每次 Run/Read 都走)。connector 是数据面的枢纽,它把"URL 怎么发现"这件事甩给可替换的 strategy。
你的 agent 代码
│
┌────────────┴─────────────┐
│ Client (注册表 + 批量清理) │ client.go
└────────────┬─────────────┘
│ 持有 / 创建
┌────────────▼─────────────┐
│ Sandbox 句柄 │ sandbox.go
│ Open/Close/Disconnect │
└───┬──────────────────┬───┘
控制面 ↑ │ │ ↓ 数据面
┌────────▼───────┐ ┌──────▼──────────┐
│ K8sHelper │ │ connector │ connector.go
│ 建Claim/等Ready │ │ 身份头+重试+reqID │
│ 取 podIP/注解 │ └──────┬──────────┘
└────────┬───────┘ │ 委托 URL 发现
k8s.go ┌────▼────────────────┐
│ │ ConnectionStrategy │ strategy.go
┌────────▼───────┐ │ gateway / tunnel / │
│ 生成 clientset │ │ direct │
│ (typed CRUD) │ └────┬─────────┬───────┘
└────────────────┘ gateway.go tunnel.go
│ │
Gateway 外部IP SPDY 端口转发
└────┬────┘
▼
sandbox-router → 沙箱 Pod
各部件一句话职责:
| 部件 | 干什么 | 文件 |
|---|---|---|
Client | 可选的注册表层:按 (namespace, claimName) 缓存句柄、批量删除、信号自动清理 | clients/go/sandbox/client.go |
Sandbox | 单个沙箱句柄:生命周期(Open/Close/Disconnect)+ 优雅关闭状态机 | clients/go/sandbox/sandbox.go |
K8sHelper | 控制面:建/删 SandboxClaim、watch 到 Ready、抽取 Pod IP 与注解 | clients/go/sandbox/k8s.go |
connector | 数据面枢纽:拼 URL、加 X-Sandbox-* 头、带重试和 requestID 发 HTTP | clients/go/sandbox/connector.go |
ConnectionStrategy | 可插拔选路:URL 从哪来(网关地址 / 本地隧道 / 直连) | strategy.go gateway.go tunnel.go |
Commands / Files | 数据面操作面:Run 打 execute,文件打 upload/download/list/exists | commands.go files.go |
| 生成的 clientset | code-gen 出来的 typed client,K8sHelper 底层用它做 CRUD | clients/k8s/clientset/versioned/... |
主线走一遍(高层): CreateSandbox → K8sHelper 建 Claim、watch 到 Sandbox Ready、拿到 sandboxName + podIP → connector 记下身份、让 strategy 发现 base URL、Connect → 之后每次 Run 都由 connector 拼 baseURL + /execute、加头、发请求、按需重试。
3. 生命周期:New / Open / Close / Disconnect
这节讲一个 Sandbox 句柄从生到死的四个动作,以及它们怎么在并发下不打架。
3.1 四个动作各管什么
| 方法 | 做什么 | 服务端影响 |
|---|---|---|
New | 只构造句柄:装配 strategy / connector / Commands / Files,不碰网络 | 无 |
Open | 建 Claim → 等 Sandbox Ready → 建传输连接 | 创建 Claim/Sandbox |
Disconnect | 断开传输,但保留 Claim(沙箱还活着,可再 Open 重连) | 无(沙箱存活) |
Close | 排空在途操作 → 删 Claim → 关传输 | 删除 Claim |
New 在 sandbox.go:64-170 里根据 Options 选连接策略:给了 APIURL 用 DirectStrategy,给了 GatewayName 用 gatewayStrategy,都没给就默认 tunnelStrategy(端口转发)——见 sandbox.go:83-110 的 switch。
Open(sandbox.go:176-277)是主流程:createClaim → resolveSandboxName(热池下 Sandbox 名 ≠ Claim 名)→ waitForSandboxReady → connector.SetPodIP → connector.Connect。任何一步失败都会 rollbackOpen(sandbox.go:341)自动删掉刚建的 Claim,避免留下孤儿。
3.2 优雅关闭:draining + inflightOps + lifecycleSem
要解决的小问题: 你在另一个 goroutine 里正 sb.Run(...),同时主线程调了 sb.Close()。如果 Close 直接删 Claim,那条正在跑的请求就会 打到一个正在消失的沙箱上。SDK 要先排空在途操作,再删。
三个字段各司其职(sandbox.go:46-49):
| 字段 | 类型 | 作用 |
|---|---|---|
lifecycleSem | chan struct{}(容量 1) | 串行化所有生命周期动作:Open/Close/Disconnect 互斥,同一时刻只有一个在跑 |
inflightOps | *sync.WaitGroup | 数一数有多少个 Run/Read/Write 正在飞 |
draining | bool | Close 已开始排空的标志位 |
排空的关键舞步(Close,sandbox.go:371-468):
- 先
connector.Close()拆传输——让新操作立刻拿到ErrNotReady快速失败,而不是排队。 - 在
s.mu锁内原子地置draining = true并换掉 WaitGroup(s.inflightOps = &sync.WaitGroup{}),把旧的那个交给 drain goroutine 去Wait。这样trackOp之后再进来只会看到draining而空转,不会再往旧 WG 加人。 - 给排空一半的时间预算,另一半留给删 Claim(
drainBudget := time.Until(closeDeadline)/2)。
每个数据面操作开头都 defer c.trackOp()()(见 commands.go:60、files.go:130)。trackOp(sandbox.go:590-600)在锁内检查:若 draining 就返回空函数,否则 wg.Add(1) 并返回 wg.Done。这套"先拆传输、再换 WG"的顺序,专门堵住了"操作刚好从 trackOp 溜过去(看到 draining=false)又摸到活传输"的 竞态——注释写在 sandbox.go:585-589。
一句话记忆: lifecycleSem 管"生命周期动作之间"的互斥,inflightOps+draining 管"生命周期动作 vs 在途数据请求"的排空。
3.3 Client:注册表层(可选的更高一层)
Client(client.go:37-48)是 Sandbox 之上的便利层,内部维护 registry map[Key]*Sandbox。它多给了三样东西:
CreateSandbox(client.go:80)=New+Open+ 登记进注册表。GetSandbox(client.go:112)= 按 Claim 名重连:缓存里有且IsReady就直接返回,否则重建句柄、注入身份走Open的 reconnect 分支。EnableAutoCleanup(client.go:235)= 挂 SIGINT/SIGTERM 处理器,进程被 Ctrl-C 时自动DeleteAll再重新抛信号退出——避免跑测试时留下一堆沙箱。
examples/basic/main.go 用的就是这一层;examples/gateway/main.go 则直接用底层 sandbox.New + Open。
4. K8sHelper:控制面(和 Kubernetes 打交道的那一半)
这节讲"要沙箱"的具体步骤。K8sHelper(k8s.go:55-64)聚合了五个 Kubernetes 客户端,可在多个 Sandbox 间共享。
4.1 客户端从哪来
NewK8sHelper(k8s.go:69-124)的配置解析顺序很实用:传入的 restConfig 优先 → 否则试 in-cluster config(SDK 跑在 Pod 里)→ 再退回 ~/.kube/config。然后用同一个 HTTP client 构造 agents / extensions / dynamic / core / discovery 五个客户端。
注意:即使用 Direct URL 模式,SDK 仍然需要 kubeconfig,因为 Claim 的生命周期(建/删)始终走 Kubernetes API,只有数据面才走 HTTP。(
clients/go/README.md:24)
4.2 建 Claim → 等 Ready → 取状态
三步都在 k8s.go:
createClaim(k8s.go:142):用GenerateName: "sandbox-claim-"创建SandboxClaim,打上created-by: go-client标签,并把当前 trace context 塞进注解做分布式追踪。resolveSandboxName(k8s.go:201):watch Claim 的Status.SandboxStatus.Name——因为热池领取下,拿到的 Sandbox 名和 Claim 名不同(见 03-extensions-warmpool-claim.md)。带指数退避、watch 断了自动重建。waitForSandboxReady(k8s.go:294):watch Sandbox 直到Ready条件为 True(isSandboxReadyk8s.go:428),然后extractState(k8s.go:437)抽出这份"就绪快照":
sandboxState 字段 | 来源 |
|---|---|
SandboxName | sb.Name |
PodName | 注解 agents.x-k8s.io/pod-name,缺省回退到 Sandbox 名 |
PodIP | sb.Status.PodIPs,经 selectPodIP 择优 |
Annotations | Sandbox 全量注解的拷贝 |
这份 state 回填进 Sandbox(setState sandbox.go:602),PodIP 同时喂给 connector 走"直连 Pod 快路"(见 §6.3)。
4.3 重连路径
Open 若发现句柄已有 claimName(比如经 GetSandbox 注入),就走 reconnect(sandbox.go:279):先 verifyClaimExists 再 verifySandboxAlive,确认服务端还活着,只重建传输而不重建沙箱。这让"客户端重启后重新接管一个存活沙箱"变得廉价。
5. connector:数据面枢纽(拼 URL、加头、重试)
这节是客户端"发一次请求"的核心。connector(connector.go:58-88)拥有 HTTP client、重试逻辑和身份状态,把"URL 从哪发现"委托给 strategy。
5.1 三个状态设置器
| 方法 | 干什么 | 位置 |
|---|---|---|
SetIdentity(name) | 记下 Sandbox 名,作为 X-Sandbox-ID 头 | connector.go:139 |
SetPodIP(ip) | 记下 Pod IP,作为 X-Sandbox-Pod-IP 头;先校验防止 header 注入 | connector.go:147 |
Connect(ctx) | 调 strategy 发现 base URL 并存下 | connector.go:161 |
5.2 SendRequest:重试 + requestID + 身份头
SendRequest(connector.go:285)是所有数据面操作的底座。它做的事:
- 每次请求生成一个 requestID(
generateRequestIDconnector.go:481,8 字节随机 hex),写进X-Request-ID头并贴到 trace span 上——这样一次请求 在客户端日志、router 日志、trace 里能用同一个 ID 串起来。 - 每个 attempt 前重读
baseURL(connector.go:307):若为空(比如隧道刚死),立刻返回ErrNotReady,不做无谓重试。 - 注入五个头(
connector.go:352-370,常量在types.go:33-38):X-Sandbox-ID、X-Sandbox-Namespace、X-Sandbox-Port、X-Request-ID,以及可选的X-Sandbox-Pod-IP;还会把剩余 deadline 换算成X-Sandbox-Timeout传给 router。注意X-Sandbox-*这族头由routerHeaders门控(connector.go:352)——只对 router 传输注入,sandboxd pod 隧道直连 pod,不带它们。 - 只对瞬时失败重试:重试状态码限定 500/502/503/504(
retryableStatusCodesconnector.go:49),指数退避带抖动(backoffconnector.go:459,base 500ms、cap 8s、最多 6 次)。 - 不可重试的 body 要能重放:若 body 实现了
io.Seeker就 Seek 回 0 重发,否则第二次起直接报错(connector.go:326-338)——这也是为什么文件上传要先整个缓存进内存。 - 安全细节:关掉自动重定向(
CheckRedirect返回ErrUseLastResponse,connector.go:128),把 3xx 原样交回调用方,防客户端 SSRF。
命令执行默认不重试(
Run里maxAttempts缺省为 1,commands.go:63),因为execute非幂等;想让幂等命令重试要显式sandbox.WithMaxAttempts(6)。文件操作则默认走 6 次重试。
6. 两种连通方式对照:gateway vs port-forward
同一个 connector,靠不同的 ConnectionStrategy(接口在 strategy.go:20-23,只有 Connect/Close 两个方法)通向 router。README 把它们叫三种"模式"。
6.1 三种策略一览
| 模式 | 策略类型 | URL 怎么来 | 典型场景 |
|---|---|---|---|
| 生产 / Gateway | gatewayStrategy(gateway.go) | watch Gateway 资源拿外部 IP/域名 | 集群有公网 Gateway LB |
| 开发 / Port-Forward | tunnelStrategy(tunnel.go) | 起 SPDY 端口转发,URL 是 127.0.0.1:随机端口 | 本地开发 / CI |
| 高级 / Direct | DirectStrategy(strategy.go:26) | 直接用给定 APIURL | in-cluster 或自定义域名 |
选哪个由 New 里的 switch(sandbox.go:83-110)按 Options 决定:APIURL > GatewayName > 默认隧道。
6.2 Gateway 策略:watch 到外部地址,还要防 SSRF
gatewayStrategy.Connect(gateway.go:52)watch gateway.networking.k8s.io/v1 的 Gateway 资源,等 status.addresses[0].value 出现。拿到地址后不是直接拼 URL——extractGatewayAddress(gateway.go:178)会校验:含 /?#@ 一律拒绝,且必须是合法 IP 或 isValidGatewayHostname(gateway.go:210,只允许 DNS 字符)通过的主机名。这是防"被篡改的 Gateway 资源把路径/userinfo 注进 URL"的 SSRF 防线。
6.3 Port-Forward 策略:原生 SPDY 隧道 + 死亡监控
tunnelStrategy.Connect(tunnel.go:129)不 fork kubectl,而是用 client-go 原生 SPDY建隧道:
resolveRouterPod(tunnel.go:315):按kubernetes.io/service-name=sandbox-router-svc查 EndpointSlices,挑一个 Ready 的 router Pod。spdy.RoundTripperFor+portforward.NewForStreaming,转发规格"0:8080"(本地 0 = 随机端口 → router 的 8080),成功后 baseURL =http://127.0.0.1:<随机端口>。- 起一个
monitorPortForward(tunnel.go:261)goroutine 守着:隧道一死就connector.SetLastError(ErrPortForwardDied)——于是下一个SendRequest读到空baseURL+ 这个错,立刻返回ErrNotReady并带死因,而不是傻等超时。
trackingDialer(tunnel.go:74)包住 SPDY 连接,保证 stop channel 不足以解阻塞 ForwardPorts 时能强制关掉底层连接,避免 goroutine 泄漏。
6.4 选路里的 Pod-IP 快路(ip.go)
无论哪种策略,只要 K8sHelper 拿到了 Pod IP,就通过 X-Sandbox-Pod-IP 头带给 router,让 router 走"Pod-IP 缓存快路"(见 04-sandbox-router.md)。selectPodIP(ip.go:28)在双栈环境显式偏好 IPv4:扫描 PodIPs,命中第一个合法 IPv4 就返回,否则回退第一个合法 IP。SetPodIP(connector.go:147)会先跑一遍这个校验,非法 IP 直接丢弃并降级日志,防 header 注入。
7. 命令与文件操作面
数据面对外只有两个子对象:Commands 和 Files,都通过 sb.Commands() / sb.Files() 取,或用 Sandbox 上的便利别名(sb.Run/sb.Read/... sandbox.go:521-537)。
7.1 Commands.Run
Run(commands.go:59)POST 到 execute 端点,body 是 {"command": "..."},响应 JSON 解成 ExecutionResult{Stdout, Stderr, ExitCode}(types.go:129)。要点:响应体限 16MB(超了返回 ErrResponseTooLarge);默认单次尝试(非幂等);还会把命令的可执行名抽出来贴到 span 属性(commandExecutable commands.go:147)。
7.2 Files:四个操作 + 端点映射
| 方法 | HTTP | 端点 | 备注 |
|---|---|---|---|
Write(files.go:129) | POST | upload | 只收纯文件名(不含目录);整体缓存成 multipart 以支持重试;默认限 256MB |
Read(files.go:212) | GET | download/<路径> | 路径 percent-encode;下载超 MaxDownloadSize 报错 |
List(files.go:258) | GET | list/<路径> | 解成 []FileEntry,过滤掉非 file/directory 的项 |
Exists(files.go:333) | GET | exists/<路径> | 解 {"exists": bool} |
路径编码值得一提:percentEncode(files.go:43)对 RFC 3986 非保留集之外的所有字节(含 /)编码,. / .. 还会被特判成 %2E(encodeFilePath files.go:60),防路径穿越。
8. Python SDK 横向印证 + 生成 clientset + KEP-359
8.1 同一模型,另一种实现
Python SDK(clients/python/agentic-sandbox-client/k8s_agent_sandbox/)和 Go 是同构的,便于交叉验证:
| 概念 | Go | Python |
|---|---|---|
| 注册表客户端 | Client(client.go) | SandboxClient(sandbox_client.py:47) |
| 单沙箱句柄 | Sandbox(sandbox.go) | Sandbox(sandbox.py:31) |
| 数据面枢纽 | connector(connector.go) | SandboxConnector(connector.py:400) |
| 连接策略 | ConnectionStrategy | ConnectionStrategy(connector.py:61) |
| 控制面 | K8sHelper(typed clientset) | K8sHelper(k8s_helper.py:38,用 CustomObjectsApi 动态客户端) |
| 命令/文件 | Commands/Files | CommandExecutor/Filesystem |
对齐点很清楚:send_request(connector.py:514)注入的头和 Go 一字不差(X-Sandbox-ID/Namespace/Port/Timeout/Pod-IP,connector.py:564-585),同样关重定向防 SSRF(allow_redirects=False,connector.py:595),同样用 urllib3 的 Retry 对 500/502/503/504 重试(connector.py:431)。生命周期也一致:create_sandbox(sandbox_client.py:93)= 建 Claim → resolve_sandbox_name → wait_for_sandbox_ready,失败即删孤儿 Claim。
两点差异值得记:
- 端口转发方式不同。 Python 的
LocalTunnelConnectionStrategy(connector.py:137)直接subprocess.Popen(["kubectl", "port-forward", ...])fork 外部进程;Go 用 client-go 原生 SPDY(无需 kubectl 二进制)。 - Python 多一种策略。
InClusterConnectionStrategy(connector.py:355)绕过 router,直接打http://<sandbox>.<ns>.svc.cluster.local:<port>,且should_inject_router_headers()返回 False——SDK 跑在集群内时的直连模式。
Python 还提供全套异步版(async_sandbox_client.py、async_sandbox.py、async_connector.py、async_k8s_helper.py,如 AsyncSandboxClient async_sandbox_client.py:42),把同一模型搬到 asyncio 上。
8.2 生成的 clientset:只是底层 typed CRUD
clients/k8s/clientset/versioned/... 是 code-gen 出来的 typed client。它不是 SDK 的对外面,只是 Go 版 K8sHelper 的底层齿轮:SandboxInterface(typed/api/v1beta1/sandbox.go:37)提供 Create/Get/List/Watch/Update/UpdateStatus/Delete/Patch 标准 CRUD,实现委托给 gentype.ClientWithList。你一般不直接用它;K8sHelper 已把 watch-到-Ready 之类的高层语义封好。
8.3 KEP-359 背景
sandbox 这个包的 import 路径是 sigs.k8s.io/agent-sandbox,即 Kubernetes 官方增强提案 KEP-359 "Agent Sandbox" 的参考实现。本章讲的 Go/Python SDK 正是该提案定义的客户端消费层:把"Claim 生命周期(控制面)"与"经 router 的数据面 HTTP"清晰分离,再用可插拔策略统一 gateway / port-forward / 直连三种连通方式。(仓库源码本身未直接引用 KEP 编号,此背景为项目归属信息,inferred)
9. 最小 Go 使用示意
下面这段综合了 examples/basic/main.go 与 examples/gateway/main.go 的真实用法,演示"注册表层"的完整闭环。
// 示意,非源码(结构参照 clients/go/examples/basic/main.go)
ctx := context.Background()
client, err := sandbox.NewClient(ctx, sandbox.Options{Namespace: "default"})
if err != nil { log.Fatal(err) }
stop := client.EnableAutoCleanup() // Ctrl-C 时自动删所有沙箱
defer stop()
defer client.DeleteAll(ctx)
// 建沙箱:阻塞到 Ready,内部走 New+Open
sb, err := client.CreateSandbox(ctx, "my-warmpool", "default")
if err != nil { log.Fatal(err) }
// 跑命令 / 读写文件
res, _ := sb.Run(ctx, "echo 'Hello from Go!'")
_ = sb.Write(ctx, "hello.txt", []byte("Hello, world!"))
data, _ := sb.Read(ctx, "hello.txt")
fmt.Printf("%s -> exit=%d, file=%q\n", res.Stdout, res.ExitCode, data)
// 按 Claim 名重连(比如进程重启后接管同一个沙箱)
sb2, _ := client.GetSandbox(ctx, sb.ClaimName(), "default")
_, _ = sb2.Run(ctx, "echo 're-attached!'")
要用底层、走 Gateway 模式(examples/gateway/main.go 的真实写法)则是:sandbox.New(ctx, Options{WarmPoolName, Namespace, GatewayName, GatewayNamespace}) → client.Open(ctx) → client.Run(...) → defer client.Close(ctx)。
10. 代码地图(导航索引)
用符号名 grep 比行号更抗漂移。
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 单沙箱句柄 / 生命周期 | clients/go/sandbox/sandbox.go | Sandbox、New、Open、Close、Disconnect、reconnect、rollbackOpen |
| 优雅关闭状态机 | clients/go/sandbox/sandbox.go | trackOp、inflightOps、draining、lifecycleSem、setState |
| 注册表客户端 | clients/go/sandbox/client.go | Client、CreateSandbox、GetSandbox、DeleteAll、EnableAutoCleanup |
| 控制面 / K8s | clients/go/sandbox/k8s.go | K8sHelper、NewK8sHelper、createClaim、resolveSandboxName、waitForSandboxReady、extractState |
| 数据面枢纽 | clients/go/sandbox/connector.go | connector、SetIdentity、SetPodIP、Connect、SendRequest、backoff、generateRequestID |
| 请求头常量 / 错误 / 类型 | clients/go/sandbox/types.go | headerSandboxID、ExecutionResult、FileEntry、Handle、Info |
| 连接策略接口 / 直连 | clients/go/sandbox/strategy.go | ConnectionStrategy、DirectStrategy |
| Gateway 选路 | clients/go/sandbox/gateway.go | gatewayStrategy、extractGatewayAddress、isValidGatewayHostname |
| 端口转发隧道 | clients/go/sandbox/tunnel.go | tunnelStrategy、resolveRouterPod、monitorPortForward、trackingDialer |
| Pod-IP 择优 | clients/go/sandbox/ip.go | selectPodIP |
| 命令 / 文件操作 | clients/go/sandbox/commands.go、files.go | Commands.Run、Files.Write/Read/List/Exists、percentEncode、encodeFilePath |
| 配置 / 默认值 | clients/go/sandbox/options.go | Options、setDefaults、validate、WithTimeout、WithMaxAttempts |
| Go 示例 | clients/go/examples/basic/main.go、gateway/main.go | main |
| Python 客户端 | .../k8s_agent_sandbox/sandbox_client.py、sandbox.py | SandboxClient、Sandbox、create_sandbox、get_sandbox、terminate |
| Python 数据面 / 策略 | .../k8s_agent_sandbox/connector.py | SandboxConnector.send_request、GatewayConnectionStrategy、LocalTunnelConnectionStrategy、InClusterConnectionStrategy |
| Python 控制面 / 异步 | .../k8s_agent_sandbox/k8s_helper.py、async_*.py | K8sHelper、AsyncSandboxClient |
| 生成的 typed clientset | clients/k8s/clientset/versioned/typed/api/v1beta1/sandbox.go | SandboxInterface、newSandboxes |